diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index 93218100c..7527aefdd 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -38,6 +38,7 @@ add_library(qbt_base STATIC bittorrent/speedmonitor.h bittorrent/sslparameters.h bittorrent/torrent.h + bittorrent/torrentannouncestatus.h bittorrent/torrentcontenthandler.h bittorrent/torrentcontentlayout.h bittorrent/torrentcontentremoveoption.h diff --git a/src/base/bittorrent/session.h b/src/base/bittorrent/session.h index cae03ba83..7d024d5db 100644 --- a/src/base/bittorrent/session.h +++ b/src/base/bittorrent/session.h @@ -518,7 +518,7 @@ namespace BitTorrent void torrentTagRemoved(Torrent *torrent, const Tag &tag); void trackerError(Torrent *torrent, const QString &tracker); void trackersAdded(Torrent *torrent, const QList &trackers); - void trackersChanged(Torrent *torrent); + void trackersReset(Torrent *torrent, const QList &oldEntries, const QList &newEntries); void trackersRemoved(Torrent *torrent, const QStringList &trackers); void trackerSuccess(Torrent *torrent, const QString &tracker); void trackerWarning(Torrent *torrent, const QString &tracker); diff --git a/src/base/bittorrent/sessionimpl.cpp b/src/base/bittorrent/sessionimpl.cpp index 708258e93..72f6e6a2a 100644 --- a/src/base/bittorrent/sessionimpl.cpp +++ b/src/base/bittorrent/sessionimpl.cpp @@ -5279,9 +5279,9 @@ void SessionImpl::handleTorrentTrackersRemoved(TorrentImpl *const torrent, const emit trackersRemoved(torrent, deletedTrackers); } -void SessionImpl::handleTorrentTrackersChanged(TorrentImpl *const torrent) +void SessionImpl::handleTorrentTrackersReset(TorrentImpl *const torrent, const QList &oldEntries, const QList &newEntries) { - emit trackersChanged(torrent); + emit trackersReset(torrent, oldEntries, newEntries); } void SessionImpl::handleTorrentUrlSeedsAdded(TorrentImpl *const torrent, const QList &newUrlSeeds) diff --git a/src/base/bittorrent/sessionimpl.h b/src/base/bittorrent/sessionimpl.h index bd2c836ca..b40622a7f 100644 --- a/src/base/bittorrent/sessionimpl.h +++ b/src/base/bittorrent/sessionimpl.h @@ -474,7 +474,7 @@ namespace BitTorrent void handleTorrentFinished(TorrentImpl *torrent); void handleTorrentTrackersAdded(TorrentImpl *torrent, const QList &newTrackers); void handleTorrentTrackersRemoved(TorrentImpl *torrent, const QStringList &deletedTrackers); - void handleTorrentTrackersChanged(TorrentImpl *torrent); + void handleTorrentTrackersReset(TorrentImpl *torrent, const QList &oldEntries, const QList &newEntries); void handleTorrentUrlSeedsAdded(TorrentImpl *torrent, const QList &newUrlSeeds); void handleTorrentUrlSeedsRemoved(TorrentImpl *torrent, const QList &urlSeeds); void handleTorrentResumeDataReady(TorrentImpl *torrent, LoadTorrentParams data); diff --git a/src/base/bittorrent/torrent.h b/src/base/bittorrent/torrent.h index c47036147..8a2d0bc76 100644 --- a/src/base/bittorrent/torrent.h +++ b/src/base/bittorrent/torrent.h @@ -38,6 +38,7 @@ #include "base/pathfwd.h" #include "base/tagset.h" #include "sharelimitaction.h" +#include "torrentannouncestatus.h" #include "torrentcontenthandler.h" class QBitArray; @@ -290,6 +291,7 @@ namespace BitTorrent virtual int connectionsCount() const = 0; virtual int connectionsLimit() const = 0; virtual qlonglong nextAnnounce() const = 0; + virtual TorrentAnnounceStatus announceStatus() const = 0; virtual void setName(const QString &name) = 0; virtual void setSequentialDownload(bool enable) = 0; diff --git a/src/base/bittorrent/torrentannouncestatus.h b/src/base/bittorrent/torrentannouncestatus.h new file mode 100644 index 000000000..9f2d996ca --- /dev/null +++ b/src/base/bittorrent/torrentannouncestatus.h @@ -0,0 +1,47 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2025 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 + +namespace BitTorrent +{ + enum class TorrentAnnounceStatusFlag + { + HasNoProblem = 0, + + HasWarning = 1, + HasTrackerError = 2, + HasOtherError = 4 + }; + + Q_DECLARE_FLAGS(TorrentAnnounceStatus, TorrentAnnounceStatusFlag); +} + +Q_DECLARE_OPERATORS_FOR_FLAGS(BitTorrent::TorrentAnnounceStatus); diff --git a/src/base/bittorrent/torrentimpl.cpp b/src/base/bittorrent/torrentimpl.cpp index 191bf9053..84450fe4f 100644 --- a/src/base/bittorrent/torrentimpl.cpp +++ b/src/base/bittorrent/torrentimpl.cpp @@ -718,9 +718,9 @@ void TorrentImpl::replaceTrackers(QList trackers) std::vector nativeTrackers; nativeTrackers.reserve(trackers.size()); - m_trackerEntryStatuses.clear(); + const auto oldEntries = std::exchange(m_trackerEntryStatuses, {}); - for (const TrackerEntry &tracker : trackers) + for (const TrackerEntry &tracker : asConst(trackers)) { nativeTrackers.emplace_back(makeNativeAnnounceEntry(tracker.url, tracker.tier)); m_trackerEntryStatuses.append({tracker.url, tracker.tier}); @@ -734,7 +734,7 @@ void TorrentImpl::replaceTrackers(QList trackers) clearPeers(); deferredRequestResumeData(); - m_session->handleTorrentTrackersChanged(this); + m_session->handleTorrentTrackersReset(this, oldEntries, trackers); } QList TorrentImpl::urlSeeds() const @@ -1557,6 +1557,46 @@ qlonglong TorrentImpl::nextAnnounce() const return lt::total_seconds(m_nativeStatus.next_announce); } +TorrentAnnounceStatus TorrentImpl::announceStatus() const +{ + if (m_announceStatus) + return *m_announceStatus; + + TorrentAnnounceStatus announceStatus = TorrentAnnounceStatusFlag::HasNoProblem; + for (const TrackerEntryStatus &trackerEntryStatus : asConst(m_trackerEntryStatuses)) + { + switch (trackerEntryStatus.state) + { + case BitTorrent::TrackerEndpointState::Working: + if (!announceStatus.testFlag(TorrentAnnounceStatusFlag::HasWarning)) + { + const bool hasWarningMessage = std::ranges::any_of(trackerEntryStatus.endpoints + , [](const TrackerEndpointStatus &endpointEntry) + { + return !endpointEntry.message.isEmpty() && (endpointEntry.state == BitTorrent::TrackerEndpointState::Working); + }); + announceStatus.setFlag(TorrentAnnounceStatusFlag::HasWarning, hasWarningMessage); + } + break; + + case BitTorrent::TrackerEndpointState::NotWorking: + case BitTorrent::TrackerEndpointState::Unreachable: + announceStatus.setFlag(TorrentAnnounceStatusFlag::HasOtherError); + break; + + case BitTorrent::TrackerEndpointState::TrackerError: + announceStatus.setFlag(TorrentAnnounceStatusFlag::HasTrackerError); + break; + + case BitTorrent::TrackerEndpointState::NotContacted: + break; + }; + } + + m_announceStatus = announceStatus; + return *m_announceStatus; +} + qreal TorrentImpl::popularity() const { // in order to produce floating-point numbers using `std::chrono::duration_cast`, @@ -1743,6 +1783,7 @@ TrackerEntryStatus TorrentImpl::updateTrackerEntryStatus(const lt::announce_entr #endif ::updateTrackerEntryStatus(*it, announceEntry, btProtocols, updateInfo); + m_announceStatus.reset(); return *it; } @@ -1758,6 +1799,8 @@ void TorrentImpl::resetTrackerEntryStatuses() status.url = tempUrl; status.tier = tempTier; } + + m_announceStatus = TorrentAnnounceStatusFlag::HasNoProblem; } std::shared_ptr TorrentImpl::nativeTorrentInfo() const diff --git a/src/base/bittorrent/torrentimpl.h b/src/base/bittorrent/torrentimpl.h index e44d875c9..f10c53177 100644 --- a/src/base/bittorrent/torrentimpl.h +++ b/src/base/bittorrent/torrentimpl.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015-2023 Vladimir Golovnev + * Copyright (C) 2015-2025 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -217,6 +217,7 @@ namespace BitTorrent int connectionsCount() const override; int connectionsLimit() const override; qlonglong nextAnnounce() const override; + TorrentAnnounceStatus announceStatus() const override; void setName(const QString &name) override; void setSequentialDownload(bool enable) override; @@ -349,6 +350,7 @@ namespace BitTorrent MaintenanceJob m_maintenanceJob = MaintenanceJob::None; QList m_trackerEntryStatuses; + mutable std::optional m_announceStatus; QList m_urlSeeds; FileErrorInfo m_lastFileError; diff --git a/src/base/preferences.cpp b/src/base/preferences.cpp index 0760184ce..b0d70db5a 100644 --- a/src/base/preferences.cpp +++ b/src/base/preferences.cpp @@ -1904,6 +1904,32 @@ void Preferences::setTrackerFilterState(const bool checked) setValue(u"TransferListFilters/trackerFilterState"_s, checked); } +bool Preferences::getTrackerStatusFilterState() const +{ + return value(u"TransferListFilters/TrackerStatusFilterState"_s, true); +} + +void Preferences::setTrackerStatusFilterState(const bool checked) +{ + if (checked == getTrackerStatusFilterState()) + return; + + setValue(u"TransferListFilters/TrackerStatusFilterState"_s, checked); +} + +bool Preferences::useSeparateTrackerStatusFilter() const +{ + return value(u"TransferListFilters/SeparateTrackerStatusFilter"_s, false); +} + +void Preferences::setUseSeparateTrackerStatusFilter(const bool value) +{ + if (value == useSeparateTrackerStatusFilter()) + return; + + setValue(u"TransferListFilters/SeparateTrackerStatusFilter"_s, value); +} + int Preferences::getTransSelFilter() const { return value(u"TransferListFilters/selectedFilterIndex"_s, 0); diff --git a/src/base/preferences.h b/src/base/preferences.h index dc22f26b5..5877e6749 100644 --- a/src/base/preferences.h +++ b/src/base/preferences.h @@ -402,6 +402,9 @@ public: bool getCategoryFilterState() const; bool getTagFilterState() const; bool getTrackerFilterState() const; + bool getTrackerStatusFilterState() const; + bool useSeparateTrackerStatusFilter() const; + void setUseSeparateTrackerStatusFilter(bool value); int getTransSelFilter() const; void setTransSelFilter(int index); bool getHideZeroStatusFilters() const; @@ -451,6 +454,7 @@ public slots: void setCategoryFilterState(bool checked); void setTagFilterState(bool checked); void setTrackerFilterState(bool checked); + void setTrackerStatusFilterState(bool checked); void apply(); diff --git a/src/base/torrentfilter.cpp b/src/base/torrentfilter.cpp index e1a6243b1..81843c54a 100644 --- a/src/base/torrentfilter.cpp +++ b/src/base/torrentfilter.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2014 Vladimir Golovnev + * Copyright (C) 2014-2025 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -28,94 +28,57 @@ #include "torrentfilter.h" -#include "bittorrent/infohash.h" -#include "bittorrent/torrent.h" +#include + +#include + +#include "base/bittorrent/infohash.h" +#include "base/bittorrent/torrent.h" +#include "base/bittorrent/trackerentrystatus.h" +#include "base/global.h" + +using namespace BitTorrent; -const std::optional TorrentFilter::AnyCategory; const std::optional TorrentFilter::AnyID; +const std::optional TorrentFilter::AnyCategory; const std::optional TorrentFilter::AnyTag; +const std::optional TorrentFilter::AnyTrackerHost; +const std::optional TorrentFilter::AnyAnnounceStatus; -const TorrentFilter TorrentFilter::DownloadingTorrent(TorrentFilter::Downloading); -const TorrentFilter TorrentFilter::SeedingTorrent(TorrentFilter::Seeding); -const TorrentFilter TorrentFilter::CompletedTorrent(TorrentFilter::Completed); -const TorrentFilter TorrentFilter::StoppedTorrent(TorrentFilter::Stopped); -const TorrentFilter TorrentFilter::RunningTorrent(TorrentFilter::Running); -const TorrentFilter TorrentFilter::ActiveTorrent(TorrentFilter::Active); -const TorrentFilter TorrentFilter::InactiveTorrent(TorrentFilter::Inactive); -const TorrentFilter TorrentFilter::StalledTorrent(TorrentFilter::Stalled); -const TorrentFilter TorrentFilter::StalledUploadingTorrent(TorrentFilter::StalledUploading); -const TorrentFilter TorrentFilter::StalledDownloadingTorrent(TorrentFilter::StalledDownloading); -const TorrentFilter TorrentFilter::CheckingTorrent(TorrentFilter::Checking); -const TorrentFilter TorrentFilter::MovingTorrent(TorrentFilter::Moving); -const TorrentFilter TorrentFilter::ErroredTorrent(TorrentFilter::Errored); +QString getTrackerHost(const QString &url) +{ + // We want the hostname. + if (const QString host = QUrl(url).host(); !host.isEmpty()) + return host; -using BitTorrent::Torrent; + // If failed to parse the domain, original input should be returned + return url; +} -TorrentFilter::TorrentFilter(const Type type, const std::optional &idSet - , const std::optional &category, const std::optional &tag, const std::optional isPrivate) - : m_type {type} +TorrentFilter::TorrentFilter(const Status status, const std::optional &idSet, const std::optional &category + , const std::optional &tag, const std::optional &isPrivate, const std::optional &trackerHost + , const std::optional &announceStatus) + : m_status {status} , m_category {category} , m_tag {tag} , m_idSet {idSet} , m_private {isPrivate} + , m_trackerHost {trackerHost} + , m_announceStatus {announceStatus} { } -TorrentFilter::TorrentFilter(const QString &filter, const std::optional &idSet - , const std::optional &category, const std::optional &tag, const std::optional isPrivate) - : m_category {category} - , m_tag {tag} - , m_idSet {idSet} - , m_private {isPrivate} +bool TorrentFilter::setStatus(const Status status) { - setTypeByName(filter); -} - -bool TorrentFilter::setType(Type type) -{ - if (m_type != type) + if (m_status != status) { - m_type = type; + m_status = status; return true; } return false; } -bool TorrentFilter::setTypeByName(const QString &filter) -{ - Type type = All; - - if (filter == u"downloading") - type = Downloading; - else if (filter == u"seeding") - type = Seeding; - else if (filter == u"completed") - type = Completed; - else if (filter == u"stopped") - type = Stopped; - else if (filter == u"running") - type = Running; - else if (filter == u"active") - type = Active; - else if (filter == u"inactive") - type = Inactive; - else if (filter == u"stalled") - type = Stalled; - else if (filter == u"stalled_uploading") - type = StalledUploading; - else if (filter == u"stalled_downloading") - type = StalledDownloading; - else if (filter == u"checking") - type = Checking; - else if (filter == u"moving") - type = Moving; - else if (filter == u"errored") - type = Errored; - - return setType(type); -} - bool TorrentFilter::setTorrentIDSet(const std::optional &idSet) { if (m_idSet != idSet) @@ -160,18 +123,43 @@ bool TorrentFilter::setPrivate(const std::optional isPrivate) return false; } -bool TorrentFilter::match(const Torrent *const torrent) const +bool TorrentFilter::setTrackerHost(const std::optional &trackerHost) { - if (!torrent) return false; + if (m_trackerHost != trackerHost) + { + m_trackerHost = trackerHost; + return true; + } - return (matchState(torrent) && matchHash(torrent) && matchCategory(torrent) && matchTag(torrent) && matchPrivate(torrent)); + return false; } -bool TorrentFilter::matchState(const BitTorrent::Torrent *const torrent) const +bool TorrentFilter::setAnnounceStatus(const std::optional &announceStatus) { - const BitTorrent::TorrentState state = torrent->state(); + if (m_announceStatus != announceStatus) + { + m_announceStatus = announceStatus; + return true; + } - switch (m_type) + return false; +} + +bool TorrentFilter::match(const Torrent *const torrent) const +{ + Q_ASSERT(torrent); + if (!torrent) [[unlikely]] + return false; + + return (matchStatus(torrent) && matchHash(torrent) && matchCategory(torrent) + && matchTag(torrent) && matchPrivate(torrent) && matchTracker(torrent)); +} + +bool TorrentFilter::matchStatus(const Torrent *const torrent) const +{ + const TorrentState state = torrent->state(); + + switch (m_status) { case All: return true; @@ -190,16 +178,16 @@ bool TorrentFilter::matchState(const BitTorrent::Torrent *const torrent) const case Inactive: return torrent->isInactive(); case Stalled: - return (state == BitTorrent::TorrentState::StalledUploading) - || (state == BitTorrent::TorrentState::StalledDownloading); + return (state == TorrentState::StalledUploading) + || (state == TorrentState::StalledDownloading); case StalledUploading: - return state == BitTorrent::TorrentState::StalledUploading; + return state == TorrentState::StalledUploading; case StalledDownloading: - return state == BitTorrent::TorrentState::StalledDownloading; + return state == TorrentState::StalledDownloading; case Checking: - return (state == BitTorrent::TorrentState::CheckingUploading) - || (state == BitTorrent::TorrentState::CheckingDownloading) - || (state == BitTorrent::TorrentState::CheckingResumeData); + return (state == TorrentState::CheckingUploading) + || (state == TorrentState::CheckingDownloading) + || (state == TorrentState::CheckingResumeData); case Moving: return torrent->isMoving(); case Errored: @@ -212,7 +200,7 @@ bool TorrentFilter::matchState(const BitTorrent::Torrent *const torrent) const return false; } -bool TorrentFilter::matchHash(const BitTorrent::Torrent *const torrent) const +bool TorrentFilter::matchHash(const Torrent *const torrent) const { if (!m_idSet) return true; @@ -220,7 +208,7 @@ bool TorrentFilter::matchHash(const BitTorrent::Torrent *const torrent) const return m_idSet->contains(torrent->id()); } -bool TorrentFilter::matchCategory(const BitTorrent::Torrent *const torrent) const +bool TorrentFilter::matchCategory(const Torrent *const torrent) const { if (!m_category) return true; @@ -228,7 +216,7 @@ bool TorrentFilter::matchCategory(const BitTorrent::Torrent *const torrent) cons return (torrent->belongsToCategory(*m_category)); } -bool TorrentFilter::matchTag(const BitTorrent::Torrent *const torrent) const +bool TorrentFilter::matchTag(const Torrent *const torrent) const { if (!m_tag) return true; @@ -240,10 +228,65 @@ bool TorrentFilter::matchTag(const BitTorrent::Torrent *const torrent) const return torrent->hasTag(*m_tag); } -bool TorrentFilter::matchPrivate(const BitTorrent::Torrent *const torrent) const +bool TorrentFilter::matchPrivate(const Torrent *const torrent) const { if (!m_private) return true; return m_private == torrent->isPrivate(); } + +bool TorrentFilter::matchTracker(const Torrent *torrent) const +{ + if (!m_trackerHost) + { + if (!m_announceStatus) + return true; + + const TorrentAnnounceStatus announceStatus = torrent->announceStatus(); + const TorrentAnnounceStatus &testAnnounceStatus = *m_announceStatus; + if (!testAnnounceStatus) + return !announceStatus; + + return announceStatus.testAnyFlags(testAnnounceStatus); + } + + // Trackerless torrent + if (m_trackerHost->isEmpty()) + return torrent->trackers().isEmpty() && !m_announceStatus; + + return std::ranges::any_of(asConst(torrent->trackers()) + , [trackerHost = m_trackerHost, announceStatus = m_announceStatus](const TrackerEntryStatus &trackerEntryStatus) + { + if (getTrackerHost(trackerEntryStatus.url) != trackerHost) + return false; + + if (!announceStatus) + return true; + + switch (trackerEntryStatus.state) + { + case TrackerEndpointState::Working: + { + const bool hasWarningMessage = std::ranges::any_of(trackerEntryStatus.endpoints + , [](const TrackerEndpointStatus &endpointEntry) + { + return !endpointEntry.message.isEmpty() && (endpointEntry.state == TrackerEndpointState::Working); + }); + return hasWarningMessage ? announceStatus->testFlag(TorrentAnnounceStatusFlag::HasWarning) : !*announceStatus; + } + + case TrackerEndpointState::NotWorking: + case TrackerEndpointState::Unreachable: + return announceStatus->testFlag(TorrentAnnounceStatusFlag::HasOtherError); + + case TrackerEndpointState::TrackerError: + return announceStatus->testFlag(TorrentAnnounceStatusFlag::HasTrackerError); + + case TrackerEndpointState::NotContacted: + return false; + }; + + return false; + }); +} diff --git a/src/base/torrentfilter.h b/src/base/torrentfilter.h index 39fd3e06f..8bff9dc8b 100644 --- a/src/base/torrentfilter.h +++ b/src/base/torrentfilter.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2014 Vladimir Golovnev + * Copyright (C) 2014-2025 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -34,6 +34,7 @@ #include #include "base/bittorrent/infohash.h" +#include "base/bittorrent/torrentannouncestatus.h" #include "base/tag.h" namespace BitTorrent @@ -46,7 +47,7 @@ using TorrentIDSet = QSet; class TorrentFilter { public: - enum Type + enum Status { All, Downloading, @@ -67,57 +68,47 @@ public: }; // These mean any permutation, including no category / tag. - static const std::optional AnyCategory; static const std::optional AnyID; + static const std::optional AnyCategory; static const std::optional AnyTag; - - static const TorrentFilter DownloadingTorrent; - static const TorrentFilter SeedingTorrent; - static const TorrentFilter CompletedTorrent; - static const TorrentFilter StoppedTorrent; - static const TorrentFilter RunningTorrent; - static const TorrentFilter ActiveTorrent; - static const TorrentFilter InactiveTorrent; - static const TorrentFilter StalledTorrent; - static const TorrentFilter StalledUploadingTorrent; - static const TorrentFilter StalledDownloadingTorrent; - static const TorrentFilter CheckingTorrent; - static const TorrentFilter MovingTorrent; - static const TorrentFilter ErroredTorrent; + static const std::optional AnyTrackerHost; + static const std::optional AnyAnnounceStatus; TorrentFilter() = default; // category & tags: pass empty string for uncategorized / untagged torrents. - TorrentFilter(Type type + TorrentFilter(Status status , const std::optional &idSet = AnyID , const std::optional &category = AnyCategory , const std::optional &tag = AnyTag - , std::optional isPrivate = {}); - TorrentFilter(const QString &filter - , const std::optional &idSet = AnyID - , const std::optional &category = AnyCategory - , const std::optional &tags = AnyTag - , std::optional isPrivate = {}); + , const std::optional &isPrivate = {} + , const std::optional &trackerHost = AnyTrackerHost + , const std::optional &announceStatus = AnyAnnounceStatus); - - bool setType(Type type); - bool setTypeByName(const QString &filter); + bool setStatus(Status status); bool setTorrentIDSet(const std::optional &idSet); bool setCategory(const std::optional &category); bool setTag(const std::optional &tag); bool setPrivate(std::optional isPrivate); + bool setTrackerHost(const std::optional &trackerHost); + bool setAnnounceStatus(const std::optional &announceStatus); bool match(const BitTorrent::Torrent *torrent) const; private: - bool matchState(const BitTorrent::Torrent *torrent) const; + bool matchStatus(const BitTorrent::Torrent *torrent) const; bool matchHash(const BitTorrent::Torrent *torrent) const; bool matchCategory(const BitTorrent::Torrent *torrent) const; bool matchTag(const BitTorrent::Torrent *torrent) const; bool matchPrivate(const BitTorrent::Torrent *torrent) const; + bool matchTracker(const BitTorrent::Torrent *torrent) const; - Type m_type {All}; + Status m_status {All}; std::optional m_category; std::optional m_tag; std::optional m_idSet; std::optional m_private; + std::optional m_trackerHost; + std::optional m_announceStatus; }; + +QString getTrackerHost(const QString &url); diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 94a6f065a..a0625dc31 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -129,7 +129,9 @@ add_library(qbt_gui STATIC transferlistfilters/tagfilterproxymodel.h transferlistfilters/tagfilterwidget.h transferlistfilters/trackersfilterwidget.h + transferlistfilters/trackerstatusfilterwidget.h transferlistfilterswidget.h + transferlistfilterswidgetitem.h transferlistmodel.h transferlistsortmodel.h transferlistwidget.h @@ -230,7 +232,9 @@ add_library(qbt_gui STATIC transferlistfilters/tagfilterproxymodel.cpp transferlistfilters/tagfilterwidget.cpp transferlistfilters/trackersfilterwidget.cpp + transferlistfilters/trackerstatusfilterwidget.cpp transferlistfilterswidget.cpp + transferlistfilterswidgetitem.cpp transferlistmodel.cpp transferlistsortmodel.cpp transferlistwidget.cpp diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp index 0310fedcd..2e4bd6198 100644 --- a/src/gui/mainwindow.cpp +++ b/src/gui/mainwindow.cpp @@ -492,7 +492,7 @@ MainWindow::MainWindow(IGUIApplication *app, const WindowState initialState, con m_transferListWidget->applyStatusFilter(pref->getTransSelFilter()); m_transferListWidget->applyCategoryFilter(QString()); m_transferListWidget->applyTagFilter(std::nullopt); - m_transferListWidget->applyTrackerFilterAll(); + m_transferListWidget->applyTrackerFilter({}); } // Start watching the executable for updates @@ -1361,11 +1361,6 @@ void MainWindow::showFiltersSidebar(const bool show) if (show && !m_transferListFiltersWidget) { m_transferListFiltersWidget = new TransferListFiltersWidget(m_splitter, m_transferListWidget, isDownloadTrackerFavicon()); - connect(BitTorrent::Session::instance(), &BitTorrent::Session::trackersAdded, m_transferListFiltersWidget, &TransferListFiltersWidget::addTrackers); - connect(BitTorrent::Session::instance(), &BitTorrent::Session::trackersRemoved, m_transferListFiltersWidget, &TransferListFiltersWidget::removeTrackers); - connect(BitTorrent::Session::instance(), &BitTorrent::Session::trackersChanged, m_transferListFiltersWidget, &TransferListFiltersWidget::refreshTrackers); - connect(BitTorrent::Session::instance(), &BitTorrent::Session::trackerEntryStatusesUpdated, m_transferListFiltersWidget, &TransferListFiltersWidget::trackerEntryStatusesUpdated); - m_splitter->insertWidget(0, m_transferListFiltersWidget); m_splitter->setCollapsible(0, true); // From https://doc.qt.io/qt-5/qsplitter.html#setSizes: diff --git a/src/gui/optionsdialog.cpp b/src/gui/optionsdialog.cpp index a2249e4fa..f1c3cca62 100644 --- a/src/gui/optionsdialog.cpp +++ b/src/gui/optionsdialog.cpp @@ -297,6 +297,7 @@ void OptionsDialog::loadBehaviorTabOptions() m_ui->actionTorrentFnOnDblClBox->setCurrentIndex(m_ui->actionTorrentFnOnDblClBox->findData(actionSeeding)); m_ui->checkBoxHideZeroStatusFilters->setChecked(pref->getHideZeroStatusFilters()); + m_ui->checkBoxUseSeparateTrackerStatusFilter->setChecked(pref->useSeparateTrackerStatusFilter()); m_ui->checkTorrentContentDrag->setChecked(pref->isTorrentContentDragEnabled()); @@ -407,6 +408,7 @@ void OptionsDialog::loadBehaviorTabOptions() connect(m_ui->actionTorrentDlOnDblClBox, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton); connect(m_ui->actionTorrentFnOnDblClBox, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton); connect(m_ui->checkBoxHideZeroStatusFilters, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); + connect(m_ui->checkBoxUseSeparateTrackerStatusFilter, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); connect(m_ui->checkTorrentContentDrag, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); @@ -504,6 +506,7 @@ void OptionsDialog::saveBehaviorTabOptions() const pref->setActionOnDblClOnTorrentFn(m_ui->actionTorrentFnOnDblClBox->currentData().toInt()); pref->setHideZeroStatusFilters(m_ui->checkBoxHideZeroStatusFilters->isChecked()); + pref->setUseSeparateTrackerStatusFilter(m_ui->checkBoxUseSeparateTrackerStatusFilter->isChecked()); pref->setTorrentContentDragEnabled(m_ui->checkTorrentContentDrag->isChecked()); diff --git a/src/gui/optionsdialog.ui b/src/gui/optionsdialog.ui index fe427909f..21435d809 100644 --- a/src/gui/optionsdialog.ui +++ b/src/gui/optionsdialog.ui @@ -456,6 +456,16 @@ + + + + Use separate "Tracker status" filter. Otherwise it gets merged with "Trackers" filter. + + + Use separate "Tracker status" filter + + + diff --git a/src/gui/trackerlist/trackerlistmodel.cpp b/src/gui/trackerlist/trackerlistmodel.cpp index 6c52a0bf2..eff284db2 100644 --- a/src/gui/trackerlist/trackerlistmodel.cpp +++ b/src/gui/trackerlist/trackerlistmodel.cpp @@ -267,7 +267,7 @@ TrackerListModel::TrackerListModel(BitTorrent::Session *btSession, QObject *pare if (torrent == m_torrent) onTrackersRemoved(deletedTrackers); }); - connect(m_btSession, &BitTorrent::Session::trackersChanged, this + connect(m_btSession, &BitTorrent::Session::trackersReset, this , [this](BitTorrent::Torrent *torrent) { if (torrent == m_torrent) diff --git a/src/gui/transferlistfilters/statusfilterwidget.cpp b/src/gui/transferlistfilters/statusfilterwidget.cpp index 92f9d8c3e..2b73a5bc3 100644 --- a/src/gui/transferlistfilters/statusfilterwidget.cpp +++ b/src/gui/transferlistfilters/statusfilterwidget.cpp @@ -99,8 +99,6 @@ StatusFilterWidget::StatusFilterWidget(QWidget *parent, TransferListWidget *tran setCurrentRow(TorrentFilter::All, QItemSelectionModel::SelectCurrent); else setCurrentRow(storedRow, QItemSelectionModel::SelectCurrent); - - toggleFilter(pref->getStatusFilterState()); } StatusFilterWidget::~StatusFilterWidget() @@ -128,7 +126,7 @@ void StatusFilterWidget::updateTorrentStatus(const BitTorrent::Torrent *torrent) { TorrentFilterBitset &torrentStatus = m_torrentsStatus[torrent]; - const auto update = [torrent, &torrentStatus](const TorrentFilter::Type status, int &counter) + const auto update = [torrent, &torrentStatus](const TorrentFilter::Status status, int &counter) { const bool hasStatus = torrentStatus[status]; const bool needStatus = TorrentFilter(status).match(torrent); diff --git a/src/gui/transferlistfilters/trackersfilterwidget.cpp b/src/gui/transferlistfilters/trackersfilterwidget.cpp index 65e76ee01..f7cd9e4fe 100644 --- a/src/gui/transferlistfilters/trackersfilterwidget.cpp +++ b/src/gui/transferlistfilters/trackersfilterwidget.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2023 Vladimir Golovnev + * Copyright (C) 2023-2025 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -36,13 +36,13 @@ #include #include -#include "base/algorithm.h" #include "base/bittorrent/session.h" #include "base/bittorrent/trackerentry.h" #include "base/bittorrent/trackerentrystatus.h" #include "base/global.h" #include "base/net/downloadmanager.h" #include "base/preferences.h" +#include "base/torrentfilter.h" #include "base/utils/compare.h" #include "base/utils/fs.h" #include "gui/transferlistwidget.h" @@ -69,16 +69,35 @@ namespace return !scheme.isEmpty() ? scheme : u"http"_s; } - QString getHost(const QString &url) + template + concept HasUrlMember = requires (T t) { { t.url } -> std::convertible_to; }; + + template + QString getTrackerHost(const T &t) { - // We want the hostname. - // If failed to parse the domain, original input should be returned + return getTrackerHost(t.url); + } - const QString host = QUrl(url).host(); - if (host.isEmpty()) - return url; + template + QSet extractTrackerHosts(const T &trackerEntries) + { + QSet trackerHosts; + trackerHosts.reserve(trackerEntries.size()); + for (const auto &trackerEntry : trackerEntries) + trackerHosts.insert(getTrackerHost(trackerEntry)); - return host; + return trackerHosts; + } + + template + QSet extractTrackerURLs(const T &trackerEntries) + { + QSet trackerURLs; + trackerURLs.reserve(trackerEntries.size()); + for (const auto &trackerEntry : trackerEntries) + trackerURLs.insert(trackerEntry.url); + + return trackerURLs; } QString getFaviconHost(const QString &trackerHost) @@ -123,7 +142,7 @@ namespace TrackersFilterWidget::TrackersFilterWidget(QWidget *parent, TransferListWidget *transferList, const bool downloadFavicon) : BaseFilterWidget(parent, transferList) - , m_downloadTrackerFavicon(downloadFavicon) + , m_downloadTrackerFavicon {downloadFavicon} { auto *allTrackersItem = new QListWidgetItem(this); allTrackersItem->setData(Qt::DisplayRole, formatItemText(ALL_ROW, 0)); @@ -131,22 +150,34 @@ TrackersFilterWidget::TrackersFilterWidget(QWidget *parent, TransferListWidget * auto *trackerlessItem = new QListWidgetItem(this); trackerlessItem->setData(Qt::DisplayRole, formatItemText(TRACKERLESS_ROW, 0)); trackerlessItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"trackerless"_s, u"network-server"_s)); - auto *trackerErrorItem = new QListWidgetItem(this); - trackerErrorItem->setData(Qt::DisplayRole, formatItemText(TRACKERERROR_ROW, 0)); - trackerErrorItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"tracker-error"_s, u"dialog-error"_s)); - auto *otherErrorItem = new QListWidgetItem(this); - otherErrorItem->setData(Qt::DisplayRole, formatItemText(OTHERERROR_ROW, 0)); - otherErrorItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"tracker-error"_s, u"dialog-error"_s)); - auto *warningItem = new QListWidgetItem(this); - warningItem->setData(Qt::DisplayRole, formatItemText(WARNING_ROW, 0)); - warningItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"tracker-warning"_s, u"dialog-warning"_s)); - m_trackers[NULL_HOST] = {{}, trackerlessItem}; + m_trackers[NULL_HOST] = {0, trackerlessItem}; - handleTorrentsLoaded(BitTorrent::Session::instance()->torrents()); + const auto *pref = Preferences::instance(); + const bool useSeparateTrackerStatusFilter = pref->useSeparateTrackerStatusFilter(); + if (useSeparateTrackerStatusFilter == m_handleTrackerStatuses) + enableTrackerStatusItems(!useSeparateTrackerStatusFilter); + connect(pref, &Preferences::changed, this, [this, pref] + { + const bool useSeparateTrackerStatusFilter = pref->useSeparateTrackerStatusFilter(); + if (useSeparateTrackerStatusFilter == m_handleTrackerStatuses) + { + enableTrackerStatusItems(!useSeparateTrackerStatusFilter); + updateGeometry(); + if (m_handleTrackerStatuses) + applyFilter(currentRow()); + } + }); + + const auto *btSession = BitTorrent::Session::instance(); + handleTorrentsLoaded(btSession->torrents()); + + connect(btSession, &BitTorrent::Session::trackersAdded, this, &TrackersFilterWidget::handleTorrentTrackersAdded); + connect(btSession, &BitTorrent::Session::trackersRemoved, this, &TrackersFilterWidget::handleTorrentTrackersRemoved); + connect(btSession, &BitTorrent::Session::trackersReset, this, &TrackersFilterWidget::handleTorrentTrackersReset); + connect(btSession, &BitTorrent::Session::trackerEntryStatusesUpdated, this, &TrackersFilterWidget::handleTorrentTrackerStatusesUpdated); setCurrentRow(0, QItemSelectionModel::SelectCurrent); - toggleFilter(Preferences::instance()->getTrackerFilterState()); } TrackersFilterWidget::~TrackersFilterWidget() @@ -155,83 +186,64 @@ TrackersFilterWidget::~TrackersFilterWidget() Utils::Fs::removeFile(iconPath); } -void TrackersFilterWidget::addTrackers(const BitTorrent::Torrent *torrent, const QList &trackers) +void TrackersFilterWidget::handleTorrentTrackersAdded(const BitTorrent::Torrent *torrent, const QList &trackers) { - const BitTorrent::TorrentID torrentID = torrent->id(); + const QSet prevTrackerURLs = extractTrackerURLs(torrent->trackers()).subtract(extractTrackerURLs(trackers)); + const QSet addedTrackerHosts = extractTrackerHosts(trackers).subtract(extractTrackerHosts(prevTrackerURLs)); - for (const BitTorrent::TrackerEntry &tracker : trackers) - addItems(tracker.url, {torrentID}); + for (const QString &trackerHost : addedTrackerHosts) + increaseTorrentsCount(trackerHost, 1); - removeItem(NULL_HOST, torrentID); + if (prevTrackerURLs.isEmpty()) + decreaseTorrentsCount(NULL_HOST); // torrent was trackerless previously } -void TrackersFilterWidget::removeTrackers(const BitTorrent::Torrent *torrent, const QStringList &trackers) +void TrackersFilterWidget::handleTorrentTrackersRemoved(const BitTorrent::Torrent *torrent, const QStringList &trackers) { - const BitTorrent::TorrentID torrentID = torrent->id(); + const QList currentTrackerEntries = torrent->trackers(); + const QSet removedTrackerHosts = extractTrackerHosts(trackers).subtract(extractTrackerHosts(currentTrackerEntries)); + for (const QString &trackerHost : removedTrackerHosts) + decreaseTorrentsCount(trackerHost); - for (const QString &tracker : trackers) - removeItem(tracker, torrentID); + if (currentTrackerEntries.isEmpty()) + increaseTorrentsCount(NULL_HOST, 1); - if (torrent->trackers().isEmpty()) - addItems(NULL_HOST, {torrentID}); + if (m_handleTrackerStatuses) + refreshStatusItems(torrent); } -void TrackersFilterWidget::refreshTrackers(const BitTorrent::Torrent *torrent) +void TrackersFilterWidget::handleTorrentTrackersReset(const BitTorrent::Torrent *torrent + , const QList &oldEntries, const QList &newEntries) { - const BitTorrent::TorrentID torrentID = torrent->id(); - - m_errors.remove(torrentID); - m_trackerErrors.remove(torrentID); - m_warnings.remove(torrentID); - - Algorithm::removeIf(m_trackers, [this, &torrentID](const QString &host, TrackerData &trackerData) + if (oldEntries.isEmpty()) { - QSet &torrentIDs = trackerData.torrents; - if (!torrentIDs.remove(torrentID)) - return false; - - QListWidgetItem *trackerItem = trackerData.item; - - if (!host.isEmpty() && torrentIDs.isEmpty()) - { - if (currentItem() == trackerItem) - setCurrentRow(0, QItemSelectionModel::SelectCurrent); - delete trackerItem; - return true; - } - - trackerItem->setText(formatItemText(host, torrentIDs.size())); - return false; - }); - - const QList trackers = torrent->trackers(); - if (trackers.isEmpty()) - { - addItems(NULL_HOST, {torrentID}); + decreaseTorrentsCount(NULL_HOST); } else { - for (const BitTorrent::TrackerEntryStatus &status : trackers) - addItems(status.url, {torrentID}); + for (const QString &trackerHost : asConst(extractTrackerHosts(oldEntries))) + decreaseTorrentsCount(trackerHost); } - item(OTHERERROR_ROW)->setText(formatItemText(OTHERERROR_ROW, m_errors.size())); - item(TRACKERERROR_ROW)->setText(formatItemText(TRACKERERROR_ROW, m_trackerErrors.size())); - item(WARNING_ROW)->setText(formatItemText(WARNING_ROW, m_warnings.size())); - - if (const int row = currentRow(); (row == OTHERERROR_ROW) - || (row == TRACKERERROR_ROW) || (row == WARNING_ROW)) + if (newEntries.isEmpty()) { - applyFilter(row); + increaseTorrentsCount(NULL_HOST, 1); } + else + { + for (const QString &trackerHost : asConst(extractTrackerHosts(newEntries))) + increaseTorrentsCount(trackerHost, 1); + } + + if (m_handleTrackerStatuses) + refreshStatusItems(torrent); updateGeometry(); } -void TrackersFilterWidget::addItems(const QString &trackerURL, const QList &torrents) +void TrackersFilterWidget::increaseTorrentsCount(const QString &trackerHost, const qsizetype torrentsCount) { - const QString host = getHost(trackerURL); - auto trackersIt = m_trackers.find(host); + auto trackersIt = m_trackers.find(trackerHost); const bool exists = (trackersIt != m_trackers.end()); QListWidgetItem *trackerItem = nullptr; @@ -244,33 +256,27 @@ void TrackersFilterWidget::addItems(const QString &trackerURL, const QListsetData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"trackers"_s, u"network-server"_s)); - const TrackerData trackerData {{}, trackerItem}; - trackersIt = m_trackers.insert(host, trackerData); + const TrackerData trackerData {0, trackerItem}; + trackersIt = m_trackers.insert(trackerHost, trackerData); - const QString scheme = getScheme(trackerURL); - downloadFavicon(host, u"%1://%2/favicon.ico"_s.arg((scheme.startsWith(u"http") ? scheme : u"http"_s), getFaviconHost(host))); + const QString scheme = getScheme(trackerHost); + downloadFavicon(trackerHost, u"%1://%2/favicon.ico"_s.arg((scheme.startsWith(u"http") ? scheme : u"http"_s), getFaviconHost(trackerHost))); } Q_ASSERT(trackerItem); - QSet &torrentIDs = trackersIt->torrents; - for (const BitTorrent::TorrentID &torrentID : torrents) - torrentIDs.insert(torrentID); + trackersIt->torrentsCount += torrentsCount; - trackerItem->setText(formatItemText(host, torrentIDs.size())); + trackerItem->setText(formatItemText(trackerHost, trackersIt->torrentsCount)); if (exists) - { - if (item(currentRow()) == trackerItem) - applyFilter(currentRow()); return; - } - Q_ASSERT(count() >= NUM_SPECIAL_ROWS); + Q_ASSERT(count() >= numSpecialRows()); const Utils::Compare::NaturalLessThan naturalLessThan {}; int insPos = count(); - for (int i = NUM_SPECIAL_ROWS; i < count(); ++i) + for (int i = numSpecialRows(); i < count(); ++i) { - if (naturalLessThan(host, item(i)->text())) + if (naturalLessThan(trackerHost, item(i)->text())) { insPos = i; break; @@ -280,88 +286,59 @@ void TrackersFilterWidget::addItems(const QString &trackerURL, const QList torrentIDs = m_trackers.value(host).torrents; - torrentIDs.remove(id); + TrackerData &trackerData = iter.value(); + Q_ASSERT(trackerData.torrentsCount > 0); + if (trackerData.torrentsCount <= 0) [[unlikely]] + return; - QListWidgetItem *trackerItem = nullptr; + --trackerData.torrentsCount; - if (!host.isEmpty()) + if (trackerData.torrentsCount == 0) { - // Remove from 'Error', 'Tracker error' and 'Warning' view - if (const auto errorHashesIt = m_errors.find(id) - ; errorHashesIt != m_errors.end()) - { - QSet &errored = *errorHashesIt; - errored.remove(trackerURL); - if (errored.isEmpty()) - { - m_errors.erase(errorHashesIt); - item(OTHERERROR_ROW)->setText(formatItemText(OTHERERROR_ROW, m_errors.size())); - if (currentRow() == OTHERERROR_ROW) - applyFilter(OTHERERROR_ROW); - } - } - - if (const auto trackerErrorHashesIt = m_trackerErrors.find(id) - ; trackerErrorHashesIt != m_trackerErrors.end()) - { - QSet &errored = *trackerErrorHashesIt; - errored.remove(trackerURL); - if (errored.isEmpty()) - { - m_trackerErrors.erase(trackerErrorHashesIt); - item(TRACKERERROR_ROW)->setText(formatItemText(TRACKERERROR_ROW, m_trackerErrors.size())); - if (currentRow() == TRACKERERROR_ROW) - applyFilter(TRACKERERROR_ROW); - } - } - - if (const auto warningHashesIt = m_warnings.find(id) - ; warningHashesIt != m_warnings.end()) - { - QSet &warned = *warningHashesIt; - warned.remove(trackerURL); - if (warned.isEmpty()) - { - m_warnings.erase(warningHashesIt); - item(WARNING_ROW)->setText(formatItemText(WARNING_ROW, m_warnings.size())); - if (currentRow() == WARNING_ROW) - applyFilter(WARNING_ROW); - } - } - - trackerItem = m_trackers.value(host).item; - - if (torrentIDs.isEmpty()) - { - if (currentItem() == trackerItem) - setCurrentRow(0, QItemSelectionModel::SelectCurrent); - delete trackerItem; - m_trackers.remove(host); - updateGeometry(); - return; - } - - if (trackerItem) - trackerItem->setText(u"%1 (%2)"_s.arg(host, QString::number(torrentIDs.size()))); + if (currentItem() == trackerData.item) + setCurrentRow(0, QItemSelectionModel::SelectCurrent); + delete trackerData.item; + m_trackers.erase(iter); + updateGeometry(); } else { - trackerItem = item(TRACKERLESS_ROW); - trackerItem->setText(formatItemText(TRACKERLESS_ROW, torrentIDs.size())); + trackerData.item->setText(formatItemText(trackerHost, trackerData.torrentsCount)); } - - m_trackers.insert(host, {torrentIDs, trackerItem}); - - if (currentItem() == trackerItem) - applyFilter(currentRow()); } -void TrackersFilterWidget::setDownloadTrackerFavicon(bool value) +void TrackersFilterWidget::refreshStatusItems(const BitTorrent::Torrent *torrent) +{ + const BitTorrent::TorrentAnnounceStatus announceStatus = torrent->announceStatus(); + + if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasWarning)) + m_warnings.insert(torrent); + else + m_warnings.remove(torrent); + + if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasTrackerError)) + m_trackerErrors.insert(torrent); + else + m_trackerErrors.remove(torrent); + + if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasOtherError)) + m_errors.insert(torrent); + else + m_errors.remove(torrent); + + item(OTHERERROR_ROW)->setText(formatItemText(OTHERERROR_ROW, m_errors.size())); + item(TRACKERERROR_ROW)->setText(formatItemText(TRACKERERROR_ROW, m_trackerErrors.size())); + item(WARNING_ROW)->setText(formatItemText(WARNING_ROW, m_warnings.size())); +} + +void TrackersFilterWidget::setDownloadTrackerFavicon(const bool value) { if (value == m_downloadTrackerFavicon) return; m_downloadTrackerFavicon = value; @@ -381,107 +358,11 @@ void TrackersFilterWidget::setDownloadTrackerFavicon(bool value) } } -void TrackersFilterWidget::handleTrackerStatusesUpdated(const BitTorrent::Torrent *torrent - , const QHash &updatedTrackers) +void TrackersFilterWidget::handleTorrentTrackerStatusesUpdated(const BitTorrent::Torrent *torrent + , [[maybe_unused]] const QHash &updatedTrackers) { - const BitTorrent::TorrentID id = torrent->id(); - - auto errorHashesIt = m_errors.find(id); - auto trackerErrorHashesIt = m_trackerErrors.find(id); - auto warningHashesIt = m_warnings.find(id); - - for (const BitTorrent::TrackerEntryStatus &trackerEntryStatus : updatedTrackers) - { - switch (trackerEntryStatus.state) - { - case BitTorrent::TrackerEndpointState::Working: - { - // remove tracker from "error" and "tracker error" categories - if (errorHashesIt != m_errors.end()) - errorHashesIt->remove(trackerEntryStatus.url); - if (trackerErrorHashesIt != m_trackerErrors.end()) - trackerErrorHashesIt->remove(trackerEntryStatus.url); - - const bool hasNoWarningMessages = std::ranges::all_of(trackerEntryStatus.endpoints - , [](const BitTorrent::TrackerEndpointStatus &endpointEntry) - { - return endpointEntry.message.isEmpty() || (endpointEntry.state != BitTorrent::TrackerEndpointState::Working); - }); - if (hasNoWarningMessages) - { - if (warningHashesIt != m_warnings.end()) - { - warningHashesIt->remove(trackerEntryStatus.url); - } - } - else - { - if (warningHashesIt == m_warnings.end()) - warningHashesIt = m_warnings.insert(id, {}); - warningHashesIt->insert(trackerEntryStatus.url); - } - } - break; - - case BitTorrent::TrackerEndpointState::NotWorking: - case BitTorrent::TrackerEndpointState::Unreachable: - { - // remove tracker from "tracker error" and "warning" categories - if (warningHashesIt != m_warnings.end()) - warningHashesIt->remove(trackerEntryStatus.url); - if (trackerErrorHashesIt != m_trackerErrors.end()) - trackerErrorHashesIt->remove(trackerEntryStatus.url); - - if (errorHashesIt == m_errors.end()) - errorHashesIt = m_errors.insert(id, {}); - errorHashesIt->insert(trackerEntryStatus.url); - } - break; - - case BitTorrent::TrackerEndpointState::TrackerError: - { - // remove tracker from "error" and "warning" categories - if (warningHashesIt != m_warnings.end()) - warningHashesIt->remove(trackerEntryStatus.url); - if (errorHashesIt != m_errors.end()) - errorHashesIt->remove(trackerEntryStatus.url); - - if (trackerErrorHashesIt == m_trackerErrors.end()) - trackerErrorHashesIt = m_trackerErrors.insert(id, {}); - trackerErrorHashesIt->insert(trackerEntryStatus.url); - } - break; - - case BitTorrent::TrackerEndpointState::NotContacted: - { - // remove tracker from "error", "tracker error" and "warning" categories - if (warningHashesIt != m_warnings.end()) - warningHashesIt->remove(trackerEntryStatus.url); - if (errorHashesIt != m_errors.end()) - errorHashesIt->remove(trackerEntryStatus.url); - if (trackerErrorHashesIt != m_trackerErrors.end()) - trackerErrorHashesIt->remove(trackerEntryStatus.url); - } - break; - }; - } - - if ((errorHashesIt != m_errors.end()) && errorHashesIt->isEmpty()) - m_errors.erase(errorHashesIt); - if ((trackerErrorHashesIt != m_trackerErrors.end()) && trackerErrorHashesIt->isEmpty()) - m_trackerErrors.erase(trackerErrorHashesIt); - if ((warningHashesIt != m_warnings.end()) && warningHashesIt->isEmpty()) - m_warnings.erase(warningHashesIt); - - item(OTHERERROR_ROW)->setText(formatItemText(OTHERERROR_ROW, m_errors.size())); - item(TRACKERERROR_ROW)->setText(formatItemText(TRACKERERROR_ROW, m_trackerErrors.size())); - item(WARNING_ROW)->setText(formatItemText(WARNING_ROW, m_warnings.size())); - - if (const int row = currentRow(); (row == OTHERERROR_ROW) - || (row == TRACKERERROR_ROW) || (row == WARNING_ROW)) - { - applyFilter(row); - } + if (m_handleTrackerStatuses) + refreshStatusItems(torrent); } void TrackersFilterWidget::downloadFavicon(const QString &trackerHost, const QString &faviconURL) @@ -500,19 +381,14 @@ void TrackersFilterWidget::downloadFavicon(const QString &trackerHost, const QSt downloadingFaviconNode.insert(trackerHost); } -void TrackersFilterWidget::removeTracker(const QString &tracker) +void TrackersFilterWidget::removeTracker(const QString &trackerHost) { - for (const BitTorrent::TorrentID &torrentID : asConst(m_trackers.value(tracker).torrents)) + for (BitTorrent::Torrent *torrent : asConst(BitTorrent::Session::instance()->torrents())) { - auto *torrent = BitTorrent::Session::instance()->getTorrent(torrentID); - Q_ASSERT(torrent); - if (!torrent) [[unlikely]] - continue; - QStringList trackersToRemove; for (const BitTorrent::TrackerEntryStatus &trackerEntryStatus : asConst(torrent->trackers())) { - if ((trackerEntryStatus.url == tracker) || (QUrl(trackerEntryStatus.url).host() == tracker)) + if (getTrackerHost(trackerEntryStatus) == trackerHost) trackersToRemove.append(trackerEntryStatus.url); } @@ -522,6 +398,72 @@ void TrackersFilterWidget::removeTracker(const QString &tracker) updateGeometry(); } +void TrackersFilterWidget::enableTrackerStatusItems(const bool value) +{ + m_handleTrackerStatuses = value; + if (m_handleTrackerStatuses) + { + auto *trackerErrorItem = new QListWidgetItem; + trackerErrorItem->setData(Qt::DisplayRole, formatItemText(TRACKERERROR_ROW, 0)); + trackerErrorItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"tracker-error"_s, u"dialog-error"_s)); + insertItem(TRACKERERROR_ROW, trackerErrorItem); + + auto *otherErrorItem = new QListWidgetItem; + otherErrorItem->setData(Qt::DisplayRole, formatItemText(OTHERERROR_ROW, 0)); + otherErrorItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"tracker-error"_s, u"dialog-error"_s)); + insertItem(OTHERERROR_ROW, otherErrorItem); + + auto *warningItem = new QListWidgetItem; + warningItem->setData(Qt::DisplayRole, formatItemText(WARNING_ROW, 0)); + warningItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"tracker-warning"_s, u"dialog-warning"_s)); + insertItem(WARNING_ROW, warningItem); + + const QList torrents = BitTorrent::Session::instance()->torrents(); + for (const BitTorrent::Torrent *torrent : torrents) + { + const BitTorrent::TorrentAnnounceStatus announceStatus = torrent->announceStatus(); + + if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasWarning)) + m_warnings.insert(torrent); + + if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasTrackerError)) + m_trackerErrors.insert(torrent); + + if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasOtherError)) + m_errors.insert(torrent); + } + + warningItem->setText(formatItemText(WARNING_ROW, m_warnings.size())); + trackerErrorItem->setText(formatItemText(TRACKERERROR_ROW, m_trackerErrors.size())); + otherErrorItem->setText(formatItemText(OTHERERROR_ROW, m_errors.size())); + } + else + { + if (const int row = currentRow(); + (row == WARNING_ROW) || (row == TRACKERERROR_ROW) || (row == OTHERERROR_ROW)) + { + setCurrentRow(0, QItemSelectionModel::ClearAndSelect); + } + + // Need to be removed in reversed order + takeItem(WARNING_ROW); + takeItem(OTHERERROR_ROW); + takeItem(TRACKERERROR_ROW); + + m_warnings.clear(); + m_trackerErrors.clear(); + m_errors.clear(); + } +} + +qsizetype TrackersFilterWidget::numSpecialRows() const +{ + if (m_handleTrackerStatuses) + return NUM_SPECIAL_ROWS; + + return NUM_SPECIAL_ROWS - 3; +} + void TrackersFilterWidget::handleFavicoDownloadFinished(const Net::DownloadResult &result) { const QSet trackerHosts = m_downloadingFavicons.take(result.url); @@ -590,7 +532,7 @@ void TrackersFilterWidget::showMenu() QMenu *menu = new QMenu(this); menu->setAttribute(Qt::WA_DeleteOnClose); - if (currentRow() >= NUM_SPECIAL_ROWS) + if (currentRow() >= numSpecialRows()) { menu->addAction(UIThemeManager::instance()->getIcon(u"edit-clear"_s, u"list-remove"_s), tr("Remove tracker") , this, &TrackersFilterWidget::onRemoveTrackerTriggered); @@ -609,30 +551,80 @@ void TrackersFilterWidget::showMenu() void TrackersFilterWidget::applyFilter(const int row) { - if (row == ALL_ROW) - transferList()->applyTrackerFilterAll(); - else if (isVisible()) - transferList()->applyTrackerFilter(getTorrentIDs(row)); + if (m_handleTrackerStatuses) + { + switch (row) + { + case ALL_ROW: + transferList()->applyTrackerFilter(std::nullopt); + transferList()->applyAnnounceStatusFilter(std::nullopt); + break; + + case TRACKERLESS_ROW: + transferList()->applyTrackerFilter(NULL_HOST); + transferList()->applyAnnounceStatusFilter(std::nullopt); + break; + + case OTHERERROR_ROW: + transferList()->applyAnnounceStatusFilter(BitTorrent::TorrentAnnounceStatusFlag::HasOtherError); + transferList()->applyTrackerFilter(std::nullopt); + break; + + case TRACKERERROR_ROW: + transferList()->applyAnnounceStatusFilter(BitTorrent::TorrentAnnounceStatusFlag::HasTrackerError); + transferList()->applyTrackerFilter(std::nullopt); + break; + + case WARNING_ROW: + transferList()->applyAnnounceStatusFilter(BitTorrent::TorrentAnnounceStatusFlag::HasWarning); + transferList()->applyTrackerFilter(std::nullopt); + break; + + default: + transferList()->applyTrackerFilter(trackerFromRow(row)); + transferList()->applyAnnounceStatusFilter(std::nullopt); + break; + } + } + else + { + switch (row) + { + case ALL_ROW: + transferList()->applyTrackerFilter(std::nullopt); + break; + + case TRACKERLESS_ROW: + transferList()->applyTrackerFilter(NULL_HOST); + break; + + default: + transferList()->applyTrackerFilter(trackerFromRow(row)); + break; + } + } } void TrackersFilterWidget::handleTorrentsLoaded(const QList &torrents) { - QHash> torrentsPerTracker; + QHash torrentsPerTrackerHost; for (const BitTorrent::Torrent *torrent : torrents) { - const BitTorrent::TorrentID torrentID = torrent->id(); - const QList trackers = torrent->trackers(); - for (const BitTorrent::TrackerEntryStatus &tracker : trackers) - torrentsPerTracker[tracker.url].append(torrentID); - // Check for trackerless torrent - if (trackers.isEmpty()) - torrentsPerTracker[NULL_HOST].append(torrentID); + if (const QList trackers = torrent->trackers(); trackers.isEmpty()) + { + ++torrentsPerTrackerHost[NULL_HOST]; + } + else + { + for (const QString &trackerHost : asConst(extractTrackerHosts(trackers))) + ++torrentsPerTrackerHost[trackerHost]; + } } - for (const auto &[trackerURL, torrents] : asConst(torrentsPerTracker).asKeyValueRange()) + for (const auto &[trackerHost, torrentsCount] : asConst(torrentsPerTrackerHost).asKeyValueRange()) { - addItems(trackerURL, torrents); + increaseTorrentsCount(trackerHost, torrentsCount); } m_totalTorrents += torrents.count(); @@ -641,22 +633,35 @@ void TrackersFilterWidget::handleTorrentsLoaded(const QListid(); - const QList trackers = torrent->trackers(); - for (const BitTorrent::TrackerEntryStatus &tracker : trackers) - removeItem(tracker.url, torrentID); - // Check for trackerless torrent - if (trackers.isEmpty()) - removeItem(NULL_HOST, torrentID); + if (const QList trackers = torrent->trackers(); trackers.isEmpty()) + { + decreaseTorrentsCount(NULL_HOST); + } + else + { + for (const QString &trackerHost : asConst(extractTrackerHosts(trackers))) + decreaseTorrentsCount(trackerHost); + } item(ALL_ROW)->setText(formatItemText(ALL_ROW, --m_totalTorrents)); + + if (m_handleTrackerStatuses) + { + m_warnings.remove(torrent); + m_trackerErrors.remove(torrent); + m_errors.remove(torrent); + + item(WARNING_ROW)->setText(formatItemText(WARNING_ROW, m_warnings.size())); + item(TRACKERERROR_ROW)->setText(formatItemText(TRACKERERROR_ROW, m_trackerErrors.size())); + item(OTHERERROR_ROW)->setText(formatItemText(OTHERERROR_ROW, m_errors.size())); + } } void TrackersFilterWidget::onRemoveTrackerTriggered() { const int row = currentRow(); - if (row < NUM_SPECIAL_ROWS) + if (row < numSpecialRows()) return; const QString &tracker = trackerFromRow(row); @@ -694,27 +699,10 @@ QString TrackersFilterWidget::trackerFromRow(int row) const int TrackersFilterWidget::rowFromTracker(const QString &tracker) const { Q_ASSERT(!tracker.isEmpty()); - for (int i = NUM_SPECIAL_ROWS; i < count(); ++i) + for (int i = numSpecialRows(); i < count(); ++i) { if (tracker == trackerFromRow(i)) return i; } return -1; } - -QSet TrackersFilterWidget::getTorrentIDs(const int row) const -{ - switch (row) - { - case TRACKERLESS_ROW: - return m_trackers.value(NULL_HOST).torrents; - case OTHERERROR_ROW: - return {m_errors.keyBegin(), m_errors.keyEnd()}; - case TRACKERERROR_ROW: - return {m_trackerErrors.keyBegin(), m_trackerErrors.keyEnd()}; - case WARNING_ROW: - return {m_warnings.keyBegin(), m_warnings.keyEnd()}; - default: - return m_trackers.value(trackerFromRow(row)).torrents; - } -} diff --git a/src/gui/transferlistfilters/trackersfilterwidget.h b/src/gui/transferlistfilters/trackersfilterwidget.h index 0b7e82731..1f153ab23 100644 --- a/src/gui/transferlistfilters/trackersfilterwidget.h +++ b/src/gui/transferlistfilters/trackersfilterwidget.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2023 Vladimir Golovnev + * Copyright (C) 2023-2025 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -57,11 +57,6 @@ public: TrackersFilterWidget(QWidget *parent, TransferListWidget *transferList, bool downloadFavicon); ~TrackersFilterWidget() override; - void addTrackers(const BitTorrent::Torrent *torrent, const QList &trackers); - void removeTrackers(const BitTorrent::Torrent *torrent, const QStringList &trackers); - void refreshTrackers(const BitTorrent::Torrent *torrent); - void handleTrackerStatusesUpdated(const BitTorrent::Torrent *torrent - , const QHash &updatedTrackers); void setDownloadTrackerFavicon(bool value); private slots: @@ -75,28 +70,40 @@ private: void handleTorrentsLoaded(const QList &torrents) override; void torrentAboutToBeDeleted(BitTorrent::Torrent *torrent) override; + void handleTorrentTrackersAdded(const BitTorrent::Torrent *torrent, const QList &trackers); + void handleTorrentTrackersRemoved(const BitTorrent::Torrent *torrent, const QStringList &trackers); + void handleTorrentTrackersReset(const BitTorrent::Torrent *torrent, const QList &oldEntries + , const QList &newEntries); + void handleTorrentTrackerStatusesUpdated(const BitTorrent::Torrent *torrent + , const QHash &updatedTrackers); + void onRemoveTrackerTriggered(); - void addItems(const QString &trackerURL, const QList &torrents); - void removeItem(const QString &trackerURL, const BitTorrent::TorrentID &id); + void increaseTorrentsCount(const QString &trackerHost, qsizetype torrentsCount); + void decreaseTorrentsCount(const QString &trackerHost); + void refreshStatusItems(const BitTorrent::Torrent *torrent); QString trackerFromRow(int row) const; int rowFromTracker(const QString &tracker) const; - QSet getTorrentIDs(int row) const; void downloadFavicon(const QString &trackerHost, const QString &faviconURL); - void removeTracker(const QString &tracker); + void removeTracker(const QString &trackerHost); + + void enableTrackerStatusItems(bool value); + + qsizetype numSpecialRows() const; struct TrackerData { - QSet torrents; + qsizetype torrentsCount = 0; QListWidgetItem *item = nullptr; }; QHash m_trackers; // - QHash> m_errors; // - QHash> m_trackerErrors; // - QHash> m_warnings; // + QSet m_errors; + QSet m_trackerErrors; + QSet m_warnings; PathList m_iconPaths; int m_totalTorrents = 0; bool m_downloadTrackerFavicon = false; + bool m_handleTrackerStatuses = false; QHash> m_downloadingFavicons; // }; diff --git a/src/gui/transferlistfilters/trackerstatusfilterwidget.cpp b/src/gui/transferlistfilters/trackerstatusfilterwidget.cpp new file mode 100644 index 000000000..279ad9847 --- /dev/null +++ b/src/gui/transferlistfilters/trackerstatusfilterwidget.cpp @@ -0,0 +1,218 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023-2025 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 "trackerstatusfilterwidget.h" + +#include +#include +#include +#include + +#include "base/bittorrent/session.h" +#include "base/global.h" +#include "base/preferences.h" +#include "gui/transferlistwidget.h" +#include "gui/uithememanager.h" + +namespace +{ + enum TRACKERSTATUS_FILTER_ROW + { + ANY_ROW, + WARNING_ROW, + TRACKERERROR_ROW, + OTHERERROR_ROW, + + NUM_SPECIAL_ROWS + }; + + QString getFormatStringForRow(const int row) + { + switch (row) + { + case ANY_ROW: + return TrackerStatusFilterWidget::tr("All (%1)", "this is for the tracker filter"); + case WARNING_ROW: + return TrackerStatusFilterWidget::tr("Warning (%1)"); + case TRACKERERROR_ROW: + return TrackerStatusFilterWidget::tr("Tracker error (%1)"); + case OTHERERROR_ROW: + return TrackerStatusFilterWidget::tr("Other error (%1)"); + default: + return {}; + } + } + + QString formatItemText(const int row, const int torrentsCount) + { + return getFormatStringForRow(row).arg(torrentsCount); + } +} + +TrackerStatusFilterWidget::TrackerStatusFilterWidget(QWidget *parent, TransferListWidget *transferList) + : BaseFilterWidget(parent, transferList) +{ + auto *anyStatusItem = new QListWidgetItem(this); + anyStatusItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"trackers"_s, u"network-server"_s)); + + auto *warningItem = new QListWidgetItem(this); + warningItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"tracker-warning"_s, u"dialog-warning"_s)); + + auto *trackerErrorItem = new QListWidgetItem(this); + trackerErrorItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"tracker-error"_s, u"dialog-error"_s)); + + auto *otherErrorItem = new QListWidgetItem(this); + otherErrorItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"tracker-error"_s, u"dialog-error"_s)); + + const auto *btSession = BitTorrent::Session::instance(); + + const QList torrents = btSession->torrents(); + m_totalTorrents += torrents.count(); + + for (const BitTorrent::Torrent *torrent : torrents) + { + const BitTorrent::TorrentAnnounceStatus announceStatus = torrent->announceStatus(); + + if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasWarning)) + m_warnings.insert(torrent); + + if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasTrackerError)) + m_trackerErrors.insert(torrent); + + if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasOtherError)) + m_errors.insert(torrent); + } + + connect(btSession, &BitTorrent::Session::trackersRemoved, this, &TrackerStatusFilterWidget::handleTorrentTrackersRemoved); + connect(btSession, &BitTorrent::Session::trackersReset, this, &TrackerStatusFilterWidget::handleTorrentTrackersReset); + connect(btSession, &BitTorrent::Session::trackerEntryStatusesUpdated, this, &TrackerStatusFilterWidget::handleTorrentTrackerStatusesUpdated); + + anyStatusItem->setText(formatItemText(ANY_ROW, m_totalTorrents)); + warningItem->setText(formatItemText(WARNING_ROW, m_warnings.size())); + trackerErrorItem->setText(formatItemText(TRACKERERROR_ROW, m_trackerErrors.size())); + otherErrorItem->setText(formatItemText(OTHERERROR_ROW, m_errors.size())); + + setCurrentRow(0, QItemSelectionModel::SelectCurrent); + setVisible(Preferences::instance()->getTrackerStatusFilterState()); +} + +void TrackerStatusFilterWidget::handleTorrentTrackersRemoved(const BitTorrent::Torrent *torrent) +{ + refreshItems(torrent); +} + +void TrackerStatusFilterWidget::handleTorrentTrackersReset(const BitTorrent::Torrent *torrent + , [[maybe_unused]] const QList &oldEntries, [[maybe_unused]] const QList &newEntries) +{ + refreshItems(torrent); +} + +void TrackerStatusFilterWidget::handleTorrentTrackerStatusesUpdated(const BitTorrent::Torrent *torrent) +{ + refreshItems(torrent); +} + +void TrackerStatusFilterWidget::showMenu() +{ + QMenu *menu = new QMenu(this); + menu->setAttribute(Qt::WA_DeleteOnClose); + + menu->addAction(UIThemeManager::instance()->getIcon(u"torrent-start"_s, u"media-playback-start"_s), tr("Start torrents") + , transferList(), &TransferListWidget::startVisibleTorrents); + menu->addAction(UIThemeManager::instance()->getIcon(u"torrent-stop"_s, u"media-playback-pause"_s), tr("Stop torrents") + , transferList(), &TransferListWidget::stopVisibleTorrents); + menu->addAction(UIThemeManager::instance()->getIcon(u"list-remove"_s), tr("Remove torrents") + , transferList(), &TransferListWidget::deleteVisibleTorrents); + + menu->popup(QCursor::pos()); +} + +void TrackerStatusFilterWidget::applyFilter(const int row) +{ + switch (row) + { + case ANY_ROW: + transferList()->applyAnnounceStatusFilter(std::nullopt); + break; + + case WARNING_ROW: + transferList()->applyAnnounceStatusFilter(BitTorrent::TorrentAnnounceStatusFlag::HasWarning); + break; + + case TRACKERERROR_ROW: + transferList()->applyAnnounceStatusFilter(BitTorrent::TorrentAnnounceStatusFlag::HasTrackerError); + break; + + case OTHERERROR_ROW: + transferList()->applyAnnounceStatusFilter(BitTorrent::TorrentAnnounceStatusFlag::HasOtherError); + break; + } +} + +void TrackerStatusFilterWidget::handleTorrentsLoaded(const QList &torrents) +{ + m_totalTorrents += torrents.count(); + item(ANY_ROW)->setText(formatItemText(ANY_ROW, m_totalTorrents)); +} + +void TrackerStatusFilterWidget::refreshItems(const BitTorrent::Torrent *torrent) +{ + const BitTorrent::TorrentAnnounceStatus announceStatus = torrent->announceStatus(); + + if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasWarning)) + m_warnings.insert(torrent); + else + m_warnings.remove(torrent); + + if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasTrackerError)) + m_trackerErrors.insert(torrent); + else + m_trackerErrors.remove(torrent); + + if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasOtherError)) + m_errors.insert(torrent); + else + m_errors.remove(torrent); + + item(OTHERERROR_ROW)->setText(formatItemText(OTHERERROR_ROW, m_errors.size())); + item(TRACKERERROR_ROW)->setText(formatItemText(TRACKERERROR_ROW, m_trackerErrors.size())); + item(WARNING_ROW)->setText(formatItemText(WARNING_ROW, m_warnings.size())); +} + +void TrackerStatusFilterWidget::torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent) +{ + m_warnings.remove(torrent); + m_trackerErrors.remove(torrent); + m_errors.remove(torrent); + + item(ANY_ROW)->setText(formatItemText(ANY_ROW, --m_totalTorrents)); + item(WARNING_ROW)->setText(formatItemText(WARNING_ROW, m_warnings.size())); + item(TRACKERERROR_ROW)->setText(formatItemText(TRACKERERROR_ROW, m_trackerErrors.size())); + item(OTHERERROR_ROW)->setText(formatItemText(OTHERERROR_ROW, m_errors.size())); +} diff --git a/src/gui/transferlistfilters/trackerstatusfilterwidget.h b/src/gui/transferlistfilters/trackerstatusfilterwidget.h new file mode 100644 index 000000000..aea9066b2 --- /dev/null +++ b/src/gui/transferlistfilters/trackerstatusfilterwidget.h @@ -0,0 +1,66 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023-2025 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 "basefilterwidget.h" + +class TransferListWidget; + +class TrackerStatusFilterWidget final : public BaseFilterWidget +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(TrackerStatusFilterWidget) + +public: + TrackerStatusFilterWidget(QWidget *parent, TransferListWidget *transferList); + +private: + // These 4 methods are virtual slots in the base class. + // No need to redeclare them here as slots. + void showMenu() override; + void applyFilter(int row) override; + void handleTorrentsLoaded(const QList &torrents) override; + void torrentAboutToBeDeleted(BitTorrent::Torrent *torrent) override; + + void handleTorrentTrackersRemoved(const BitTorrent::Torrent *torrent); + void handleTorrentTrackersReset(const BitTorrent::Torrent *torrent, const QList &oldEntries + , const QList &newEntries); + void handleTorrentTrackerStatusesUpdated(const BitTorrent::Torrent *torrent); + + void refreshItems(const BitTorrent::Torrent *torrent); + + QSet m_errors; + QSet m_trackerErrors; + QSet m_warnings; + int m_totalTorrents = 0; +}; diff --git a/src/gui/transferlistfilterswidget.cpp b/src/gui/transferlistfilterswidget.cpp index e9d301212..4dd47b0ab 100644 --- a/src/gui/transferlistfilterswidget.cpp +++ b/src/gui/transferlistfilterswidget.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2023 Vladimir Golovnev + * Copyright (C) 2023-2025 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -29,58 +29,35 @@ #include "transferlistfilterswidget.h" -#include #include #include #include -#include #include -#include #include #include #include "base/bittorrent/session.h" -#include "base/bittorrent/torrent.h" -#include "base/bittorrent/trackerentrystatus.h" #include "base/global.h" -#include "base/logger.h" -#include "base/net/downloadmanager.h" #include "base/preferences.h" -#include "base/torrentfilter.h" #include "base/utils/compare.h" -#include "base/utils/fs.h" #include "transferlistfilters/categoryfilterwidget.h" #include "transferlistfilters/statusfilterwidget.h" #include "transferlistfilters/tagfilterwidget.h" #include "transferlistfilters/trackersfilterwidget.h" +#include "transferlistfilters/trackerstatusfilterwidget.h" +#include "transferlistfilterswidgetitem.h" #include "transferlistwidget.h" -#include "uithememanager.h" #include "utils.h" namespace { - class ArrowCheckBox final : public QCheckBox + enum ItemPos { - public: - using QCheckBox::QCheckBox; - - private: - void paintEvent(QPaintEvent *) override - { - QPainter painter(this); - - QStyleOptionViewItem indicatorOption; - indicatorOption.initFrom(this); - indicatorOption.rect = style()->subElementRect(QStyle::SE_CheckBoxIndicator, &indicatorOption, this); - indicatorOption.state |= (QStyle::State_Children - | (isChecked() ? QStyle::State_Open : QStyle::State_None)); - style()->drawPrimitive(QStyle::PE_IndicatorBranch, &indicatorOption, &painter, this); - - QStyleOptionButton labelOption; - initStyleOption(&labelOption); - labelOption.rect = style()->subElementRect(QStyle::SE_CheckBoxContents, &labelOption, this); - style()->drawControl(QStyle::CE_CheckBoxLabel, &labelOption, &painter, this); - } + StatusItemPos, + CategoryItemPos, + TagItemPos, + TrackerStatusItemPos, + TrackersItemPos }; } @@ -99,66 +76,63 @@ TransferListFiltersWidget::TransferListFiltersWidget(QWidget *parent, TransferLi mainWidgetLayout->setSpacing(2); mainWidgetLayout->setAlignment(Qt::AlignLeft | Qt::AlignTop); - QFont font; - font.setBold(true); - font.setCapitalization(QFont::AllUppercase); + { + auto *item = new TransferListFiltersWidgetItem(tr("Status"), new StatusFilterWidget(this, transferList), this); + item->setChecked(pref->getStatusFilterState()); + connect(item, &TransferListFiltersWidgetItem::toggled, pref, &Preferences::setStatusFilterState); + mainWidgetLayout->insertWidget(StatusItemPos, item); + } - QCheckBox *statusLabel = new ArrowCheckBox(tr("Status"), this); - statusLabel->setChecked(pref->getStatusFilterState()); - statusLabel->setFont(font); - connect(statusLabel, &QCheckBox::toggled, pref, &Preferences::setStatusFilterState); - mainWidgetLayout->addWidget(statusLabel); + { + auto *categoryFilterWidget = new CategoryFilterWidget(this); + connect(categoryFilterWidget, &CategoryFilterWidget::actionDeleteTorrentsTriggered + , transferList, &TransferListWidget::deleteVisibleTorrents); + connect(categoryFilterWidget, &CategoryFilterWidget::actionStopTorrentsTriggered + , transferList, &TransferListWidget::stopVisibleTorrents); + connect(categoryFilterWidget, &CategoryFilterWidget::actionStartTorrentsTriggered + , transferList, &TransferListWidget::startVisibleTorrents); + connect(categoryFilterWidget, &CategoryFilterWidget::categoryChanged + , transferList, &TransferListWidget::applyCategoryFilter); - auto *statusFilters = new StatusFilterWidget(this, transferList); - connect(statusLabel, &QCheckBox::toggled, statusFilters, &StatusFilterWidget::toggleFilter); - mainWidgetLayout->addWidget(statusFilters); + auto *item = new TransferListFiltersWidgetItem(tr("Categories"), categoryFilterWidget, this); + item->setChecked(pref->getCategoryFilterState()); + connect(item, &TransferListFiltersWidgetItem::toggled, this, [this, categoryFilterWidget](const bool enabled) + { + m_transferList->applyCategoryFilter(enabled ? categoryFilterWidget->currentCategory() : QString()); + }); + connect(item, &TransferListFiltersWidgetItem::toggled, pref, &Preferences::setCategoryFilterState); + mainWidgetLayout->insertWidget(CategoryItemPos, item); + } - QCheckBox *categoryLabel = new ArrowCheckBox(tr("Categories"), this); - categoryLabel->setChecked(pref->getCategoryFilterState()); - categoryLabel->setFont(font); - connect(categoryLabel, &QCheckBox::toggled, this - , &TransferListFiltersWidget::onCategoryFilterStateChanged); - mainWidgetLayout->addWidget(categoryLabel); + { + auto *tagFilterWidget = new TagFilterWidget(this); + connect(tagFilterWidget, &TagFilterWidget::actionDeleteTorrentsTriggered + , transferList, &TransferListWidget::deleteVisibleTorrents); + connect(tagFilterWidget, &TagFilterWidget::actionStopTorrentsTriggered + , transferList, &TransferListWidget::stopVisibleTorrents); + connect(tagFilterWidget, &TagFilterWidget::actionStartTorrentsTriggered + , transferList, &TransferListWidget::startVisibleTorrents); + connect(tagFilterWidget, &TagFilterWidget::tagChanged + , transferList, &TransferListWidget::applyTagFilter); - m_categoryFilterWidget = new CategoryFilterWidget(this); - connect(m_categoryFilterWidget, &CategoryFilterWidget::actionDeleteTorrentsTriggered - , transferList, &TransferListWidget::deleteVisibleTorrents); - connect(m_categoryFilterWidget, &CategoryFilterWidget::actionStopTorrentsTriggered - , transferList, &TransferListWidget::stopVisibleTorrents); - connect(m_categoryFilterWidget, &CategoryFilterWidget::actionStartTorrentsTriggered - , transferList, &TransferListWidget::startVisibleTorrents); - connect(m_categoryFilterWidget, &CategoryFilterWidget::categoryChanged - , transferList, &TransferListWidget::applyCategoryFilter); - toggleCategoryFilter(pref->getCategoryFilterState()); - mainWidgetLayout->addWidget(m_categoryFilterWidget); + auto *item = new TransferListFiltersWidgetItem(tr("Tags"), tagFilterWidget, this); + item->setChecked(pref->getTagFilterState()); + connect(item, &TransferListFiltersWidgetItem::toggled, this, [this, tagFilterWidget](const bool enabled) + { + m_transferList->applyTagFilter(enabled ? tagFilterWidget->currentTag() : std::nullopt); + }); + connect(item, &TransferListFiltersWidgetItem::toggled, pref, &Preferences::setTagFilterState); + mainWidgetLayout->insertWidget(TagItemPos, item); + } - QCheckBox *tagsLabel = new ArrowCheckBox(tr("Tags"), this); - tagsLabel->setChecked(pref->getTagFilterState()); - tagsLabel->setFont(font); - connect(tagsLabel, &QCheckBox::toggled, this, &TransferListFiltersWidget::onTagFilterStateChanged); - mainWidgetLayout->addWidget(tagsLabel); + { + m_trackersFilterWidget = new TrackersFilterWidget(this, transferList, downloadFavicon); - m_tagFilterWidget = new TagFilterWidget(this); - connect(m_tagFilterWidget, &TagFilterWidget::actionDeleteTorrentsTriggered - , transferList, &TransferListWidget::deleteVisibleTorrents); - connect(m_tagFilterWidget, &TagFilterWidget::actionStopTorrentsTriggered - , transferList, &TransferListWidget::stopVisibleTorrents); - connect(m_tagFilterWidget, &TagFilterWidget::actionStartTorrentsTriggered - , transferList, &TransferListWidget::startVisibleTorrents); - connect(m_tagFilterWidget, &TagFilterWidget::tagChanged - , transferList, &TransferListWidget::applyTagFilter); - toggleTagFilter(pref->getTagFilterState()); - mainWidgetLayout->addWidget(m_tagFilterWidget); - - QCheckBox *trackerLabel = new ArrowCheckBox(tr("Trackers"), this); - trackerLabel->setChecked(pref->getTrackerFilterState()); - trackerLabel->setFont(font); - connect(trackerLabel, &QCheckBox::toggled, pref, &Preferences::setTrackerFilterState); - mainWidgetLayout->addWidget(trackerLabel); - - m_trackersFilterWidget = new TrackersFilterWidget(this, transferList, downloadFavicon); - connect(trackerLabel, &QCheckBox::toggled, m_trackersFilterWidget, &TrackersFilterWidget::toggleFilter); - mainWidgetLayout->addWidget(m_trackersFilterWidget); + auto *item = new TransferListFiltersWidgetItem(tr("Trackers"), m_trackersFilterWidget, this); + item->setChecked(pref->getTrackerFilterState()); + connect(item, &TransferListFiltersWidgetItem::toggled, pref, &Preferences::setTrackerFilterState); + mainWidgetLayout->insertWidget(TrackersItemPos, item); + } auto *scroll = new QScrollArea(this); scroll->setWidgetResizable(true); @@ -169,54 +143,35 @@ TransferListFiltersWidget::TransferListFiltersWidget(QWidget *parent, TransferLi auto *vLayout = new QVBoxLayout(this); vLayout->setContentsMargins(0, 0, 0, 0); vLayout->addWidget(scroll); + + const auto createTrackerStatusItem = [this, mainWidgetLayout, pref] + { + auto *item = new TransferListFiltersWidgetItem(tr("Tracker status"), new TrackerStatusFilterWidget(this, m_transferList), this); + item->setChecked(pref->getTrackerStatusFilterState()); + connect(item, &TransferListFiltersWidgetItem::toggled, pref, &Preferences::setTrackerStatusFilterState); + mainWidgetLayout->insertWidget(TrackerStatusItemPos, item); + }; + + const auto removeTrackerStatusItem = [mainWidgetLayout] + { + QLayoutItem *layoutItem = mainWidgetLayout->takeAt(TrackerStatusItemPos); + delete layoutItem->widget(); + delete layoutItem; + }; + + if (pref->useSeparateTrackerStatusFilter()) + createTrackerStatusItem(); + + connect(pref, &Preferences::changed, this, [pref, createTrackerStatusItem, removeTrackerStatusItem] + { + if (pref->useSeparateTrackerStatusFilter()) + createTrackerStatusItem(); + else + removeTrackerStatusItem(); + }); } void TransferListFiltersWidget::setDownloadTrackerFavicon(bool value) { m_trackersFilterWidget->setDownloadTrackerFavicon(value); } - -void TransferListFiltersWidget::addTrackers(const BitTorrent::Torrent *torrent, const QList &trackers) -{ - m_trackersFilterWidget->addTrackers(torrent, trackers); -} - -void TransferListFiltersWidget::removeTrackers(const BitTorrent::Torrent *torrent, const QStringList &trackers) -{ - m_trackersFilterWidget->removeTrackers(torrent, trackers); -} - -void TransferListFiltersWidget::refreshTrackers(const BitTorrent::Torrent *torrent) -{ - m_trackersFilterWidget->refreshTrackers(torrent); -} - -void TransferListFiltersWidget::trackerEntryStatusesUpdated(const BitTorrent::Torrent *torrent - , const QHash &updatedTrackers) -{ - m_trackersFilterWidget->handleTrackerStatusesUpdated(torrent, updatedTrackers); -} - -void TransferListFiltersWidget::onCategoryFilterStateChanged(bool enabled) -{ - toggleCategoryFilter(enabled); - Preferences::instance()->setCategoryFilterState(enabled); -} - -void TransferListFiltersWidget::toggleCategoryFilter(bool enabled) -{ - m_categoryFilterWidget->setVisible(enabled); - m_transferList->applyCategoryFilter(enabled ? m_categoryFilterWidget->currentCategory() : QString()); -} - -void TransferListFiltersWidget::onTagFilterStateChanged(bool enabled) -{ - toggleTagFilter(enabled); - Preferences::instance()->setTagFilterState(enabled); -} - -void TransferListFiltersWidget::toggleTagFilter(bool enabled) -{ - m_tagFilterWidget->setVisible(enabled); - m_transferList->applyTagFilter(enabled ? m_tagFilterWidget->currentTag() : std::nullopt); -} diff --git a/src/gui/transferlistfilterswidget.h b/src/gui/transferlistfilterswidget.h index b6afbb9ee..1d69d8e9f 100644 --- a/src/gui/transferlistfilterswidget.h +++ b/src/gui/transferlistfilterswidget.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2023 Vladimir Golovnev + * Copyright (C) 2023-2025 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -32,11 +32,6 @@ #include #include -#include "base/bittorrent/trackerentry.h" - -class CategoryFilterWidget; -class StatusFilterWidget; -class TagFilterWidget; class TrackersFilterWidget; class TransferListWidget; @@ -55,23 +50,7 @@ public: TransferListFiltersWidget(QWidget *parent, TransferListWidget *transferList, bool downloadFavicon); void setDownloadTrackerFavicon(bool value); -public slots: - void addTrackers(const BitTorrent::Torrent *torrent, const QList &trackers); - void removeTrackers(const BitTorrent::Torrent *torrent, const QStringList &trackers); - void refreshTrackers(const BitTorrent::Torrent *torrent); - void trackerEntryStatusesUpdated(const BitTorrent::Torrent *torrent - , const QHash &updatedTrackers); - -private slots: - void onCategoryFilterStateChanged(bool enabled); - void onTagFilterStateChanged(bool enabled); - private: - void toggleCategoryFilter(bool enabled); - void toggleTagFilter(bool enabled); - TransferListWidget *m_transferList = nullptr; TrackersFilterWidget *m_trackersFilterWidget = nullptr; - CategoryFilterWidget *m_categoryFilterWidget = nullptr; - TagFilterWidget *m_tagFilterWidget = nullptr; }; diff --git a/src/gui/transferlistfilterswidgetitem.cpp b/src/gui/transferlistfilterswidgetitem.cpp new file mode 100644 index 000000000..37acb6925 --- /dev/null +++ b/src/gui/transferlistfilterswidgetitem.cpp @@ -0,0 +1,96 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2025 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 "transferlistfilterswidgetitem.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ + class ArrowCheckBox final : public QCheckBox + { + public: + using QCheckBox::QCheckBox; + + private: + void paintEvent(QPaintEvent *) override + { + QPainter painter {this}; + + QStyleOptionViewItem indicatorOption; + indicatorOption.initFrom(this); + indicatorOption.rect = style()->subElementRect(QStyle::SE_CheckBoxIndicator, &indicatorOption, this); + indicatorOption.state |= (QStyle::State_Children + | (isChecked() ? QStyle::State_Open : QStyle::State_None)); + style()->drawPrimitive(QStyle::PE_IndicatorBranch, &indicatorOption, &painter, this); + + QStyleOptionButton labelOption; + initStyleOption(&labelOption); + labelOption.rect = style()->subElementRect(QStyle::SE_CheckBoxContents, &labelOption, this); + style()->drawControl(QStyle::CE_CheckBoxLabel, &labelOption, &painter, this); + } + }; +} + +TransferListFiltersWidgetItem::TransferListFiltersWidgetItem(const QString &caption, QWidget *filterWidget, QWidget *parent) + : QWidget(parent) + , m_caption {new ArrowCheckBox(caption, this)} + , m_filterWidget {filterWidget} +{ + QFont font; + font.setBold(true); + font.setCapitalization(QFont::AllUppercase); + m_caption->setFont(font); + + auto *layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 2, 0, 0); + layout->setSpacing(2); + layout->setAlignment(Qt::AlignLeft | Qt::AlignTop); + layout->addWidget(m_caption); + layout->addWidget(m_filterWidget); + + m_filterWidget->setVisible(m_caption->isChecked()); + + connect(m_caption, &QCheckBox::toggled, m_filterWidget, &QWidget::setVisible); + connect(m_caption, &QCheckBox::toggled, this, &TransferListFiltersWidgetItem::toggled); +} + +bool TransferListFiltersWidgetItem::isChecked() const +{ + return m_caption->isChecked(); +} + +void TransferListFiltersWidgetItem::setChecked(const bool value) +{ + m_caption->setChecked(value); +} diff --git a/src/gui/transferlistfilterswidgetitem.h b/src/gui/transferlistfilterswidgetitem.h new file mode 100644 index 000000000..692bf3bc6 --- /dev/null +++ b/src/gui/transferlistfilterswidgetitem.h @@ -0,0 +1,53 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2025 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 QCheckBox; +class QString; + +class TransferListFiltersWidgetItem final : public QWidget +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(TransferListFiltersWidgetItem) + +public: + TransferListFiltersWidgetItem(const QString &caption, QWidget *filterWidget, QWidget *parent = nullptr); + + bool isChecked() const; + void setChecked(bool value); + +signals: + void toggled(bool checked); + +private: + QCheckBox *m_caption = nullptr; + QWidget *m_filterWidget = nullptr; +}; diff --git a/src/gui/transferlistmodel.cpp b/src/gui/transferlistmodel.cpp index f72443a7e..756046137 100644 --- a/src/gui/transferlistmodel.cpp +++ b/src/gui/transferlistmodel.cpp @@ -91,27 +91,25 @@ namespace TransferListModel::TransferListModel(QObject *parent) : QAbstractListModel {parent} - , m_statusStrings - { - {BitTorrent::TorrentState::Downloading, tr("Downloading")}, - {BitTorrent::TorrentState::StalledDownloading, tr("Stalled", "Torrent is waiting for download to begin")}, - {BitTorrent::TorrentState::DownloadingMetadata, tr("Downloading metadata", "Used when loading a magnet link")}, - {BitTorrent::TorrentState::ForcedDownloadingMetadata, tr("[F] Downloading metadata", "Used when forced to load a magnet link. You probably shouldn't translate the F.")}, - {BitTorrent::TorrentState::ForcedDownloading, tr("[F] Downloading", "Used when the torrent is forced started. You probably shouldn't translate the F.")}, - {BitTorrent::TorrentState::Uploading, tr("Seeding", "Torrent is complete and in upload-only mode")}, - {BitTorrent::TorrentState::StalledUploading, tr("Seeding", "Torrent is complete and in upload-only mode")}, - {BitTorrent::TorrentState::ForcedUploading, tr("[F] Seeding", "Used when the torrent is forced started. You probably shouldn't translate the F.")}, - {BitTorrent::TorrentState::QueuedDownloading, tr("Queued", "Torrent is queued")}, - {BitTorrent::TorrentState::QueuedUploading, tr("Queued", "Torrent is queued")}, - {BitTorrent::TorrentState::CheckingDownloading, tr("Checking", "Torrent local data is being checked")}, - {BitTorrent::TorrentState::CheckingUploading, tr("Checking", "Torrent local data is being checked")}, - {BitTorrent::TorrentState::CheckingResumeData, tr("Checking resume data", "Used when loading the torrents from disk after qbt is launched. It checks the correctness of the .fastresume file. Normally it is completed in a fraction of a second, unless loading many many torrents.")}, - {BitTorrent::TorrentState::StoppedDownloading, tr("Stopped")}, - {BitTorrent::TorrentState::StoppedUploading, tr("Completed")}, - {BitTorrent::TorrentState::Moving, tr("Moving", "Torrent local data are being moved/relocated")}, - {BitTorrent::TorrentState::MissingFiles, tr("Missing Files")}, - {BitTorrent::TorrentState::Error, tr("Errored", "Torrent status, the torrent has an error")} - } + , m_statusStrings { + {BitTorrent::TorrentState::Downloading, tr("Downloading")}, + {BitTorrent::TorrentState::StalledDownloading, tr("Stalled", "Torrent is waiting for download to begin")}, + {BitTorrent::TorrentState::DownloadingMetadata, tr("Downloading metadata", "Used when loading a magnet link")}, + {BitTorrent::TorrentState::ForcedDownloadingMetadata, tr("[F] Downloading metadata", "Used when forced to load a magnet link. You probably shouldn't translate the F.")}, + {BitTorrent::TorrentState::ForcedDownloading, tr("[F] Downloading", "Used when the torrent is forced started. You probably shouldn't translate the F.")}, + {BitTorrent::TorrentState::Uploading, tr("Seeding", "Torrent is complete and in upload-only mode")}, + {BitTorrent::TorrentState::StalledUploading, tr("Seeding", "Torrent is complete and in upload-only mode")}, + {BitTorrent::TorrentState::ForcedUploading, tr("[F] Seeding", "Used when the torrent is forced started. You probably shouldn't translate the F.")}, + {BitTorrent::TorrentState::QueuedDownloading, tr("Queued", "Torrent is queued")}, + {BitTorrent::TorrentState::QueuedUploading, tr("Queued", "Torrent is queued")}, + {BitTorrent::TorrentState::CheckingDownloading, tr("Checking", "Torrent local data is being checked")}, + {BitTorrent::TorrentState::CheckingUploading, tr("Checking", "Torrent local data is being checked")}, + {BitTorrent::TorrentState::CheckingResumeData, tr("Checking resume data", "Used when loading the torrents from disk after qbt is launched. It checks the correctness of the .fastresume file. Normally it is completed in a fraction of a second, unless loading many many torrents.")}, + {BitTorrent::TorrentState::StoppedDownloading, tr("Stopped")}, + {BitTorrent::TorrentState::StoppedUploading, tr("Completed")}, + {BitTorrent::TorrentState::Moving, tr("Moving", "Torrent local data are being moved/relocated")}, + {BitTorrent::TorrentState::MissingFiles, tr("Missing Files")}, + {BitTorrent::TorrentState::Error, tr("Errored", "Torrent status, the torrent has an error")}} { configure(); connect(Preferences::instance(), &Preferences::changed, this, &TransferListModel::configure); @@ -137,6 +135,8 @@ TransferListModel::TransferListModel(QObject *parent) connect(Session::instance(), &Session::torrentStarted, this, &TransferListModel::handleTorrentStatusUpdated); connect(Session::instance(), &Session::torrentStopped, this, &TransferListModel::handleTorrentStatusUpdated); connect(Session::instance(), &Session::torrentFinishedChecking, this, &TransferListModel::handleTorrentStatusUpdated); + + connect(Session::instance(), &Session::trackerEntryStatusesUpdated, this, &TransferListModel::handleTorrentStatusUpdated); } int TransferListModel::rowCount(const QModelIndex &) const diff --git a/src/gui/transferlistsortmodel.cpp b/src/gui/transferlistsortmodel.cpp index ca222dd8e..442900321 100644 --- a/src/gui/transferlistsortmodel.cpp +++ b/src/gui/transferlistsortmodel.cpp @@ -124,14 +124,14 @@ void TransferListSortModel::sort(const int column, const Qt::SortOrder order) QSortFilterProxyModel::sort(column, order); } -void TransferListSortModel::setStatusFilter(const TorrentFilter::Type filter) +void TransferListSortModel::setStatusFilter(const TorrentFilter::Status status) { #if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0) beginFilterChange(); - m_filter.setType(filter); + m_filter.setStatus(status); endFilterChange(Direction::Rows); #else - if (m_filter.setType(filter)) + if (m_filter.setStatus(status)) invalidateRowsFilter(); #endif } @@ -184,26 +184,26 @@ void TransferListSortModel::disableTagFilter() #endif } -void TransferListSortModel::setTrackerFilter(const QSet &torrentIDs) +void TransferListSortModel::setTrackerFilter(const std::optional &trackerHost) { #if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0) beginFilterChange(); - m_filter.setTorrentIDSet(torrentIDs); + m_filter.setTrackerHost(trackerHost); endFilterChange(Direction::Rows); #else - if (m_filter.setTorrentIDSet(torrentIDs)) + if (m_filter.setTrackerHost(trackerHost)) invalidateRowsFilter(); #endif } -void TransferListSortModel::disableTrackerFilter() +void TransferListSortModel::setAnnounceStatusFilter(const std::optional &announceStatus) { #if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0) beginFilterChange(); - m_filter.setTorrentIDSet(TorrentFilter::AnyID); + m_filter.setAnnounceStatus(announceStatus); endFilterChange(Direction::Rows); #else - if (m_filter.setTorrentIDSet(TorrentFilter::AnyID)) + if (m_filter.setAnnounceStatus(announceStatus)) invalidateRowsFilter(); #endif } diff --git a/src/gui/transferlistsortmodel.h b/src/gui/transferlistsortmodel.h index 3b3a59694..158c7e1ec 100644 --- a/src/gui/transferlistsortmodel.h +++ b/src/gui/transferlistsortmodel.h @@ -49,13 +49,13 @@ public: void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override; - void setStatusFilter(TorrentFilter::Type filter); + void setStatusFilter(TorrentFilter::Status status); void setCategoryFilter(const QString &category); void disableCategoryFilter(); void setTagFilter(const Tag &tag); void disableTagFilter(); - void setTrackerFilter(const QSet &torrentIDs); - void disableTrackerFilter(); + void setTrackerFilter(const std::optional &trackerHost); + void setAnnounceStatusFilter(const std::optional &announceStatus); private: int compare(const QModelIndex &left, const QModelIndex &right) const; diff --git a/src/gui/transferlistwidget.cpp b/src/gui/transferlistwidget.cpp index 35c53e252..2d1cca04d 100644 --- a/src/gui/transferlistwidget.cpp +++ b/src/gui/transferlistwidget.cpp @@ -1342,14 +1342,14 @@ void TransferListWidget::applyTagFilter(const std::optional &tag) m_sortFilterModel->setTagFilter(*tag); } -void TransferListWidget::applyTrackerFilterAll() +void TransferListWidget::applyTrackerFilter(const std::optional &trackerHost) { - m_sortFilterModel->disableTrackerFilter(); + m_sortFilterModel->setTrackerFilter(trackerHost); } -void TransferListWidget::applyTrackerFilter(const QSet &torrentIDs) +void TransferListWidget::applyAnnounceStatusFilter(const std::optional &announceStatus) { - m_sortFilterModel->setTrackerFilter(torrentIDs); + m_sortFilterModel->setAnnounceStatusFilter(announceStatus); } void TransferListWidget::applyFilter(const QString &name, const TransferListModel::Column &type) @@ -1362,7 +1362,7 @@ void TransferListWidget::applyFilter(const QString &name, const TransferListMode void TransferListWidget::applyStatusFilter(const int filterIndex) { - const auto filterType = static_cast(filterIndex); + const auto filterType = static_cast(filterIndex); m_sortFilterModel->setStatusFilter(((filterType >= TorrentFilter::All) && (filterType < TorrentFilter::_Count)) ? filterType : TorrentFilter::All); // Select first item if nothing is selected if (selectionModel()->selectedRows(0).empty() && (m_sortFilterModel->rowCount() > 0)) diff --git a/src/gui/transferlistwidget.h b/src/gui/transferlistwidget.h index 28d85477e..e49684b09 100644 --- a/src/gui/transferlistwidget.h +++ b/src/gui/transferlistwidget.h @@ -100,8 +100,8 @@ public slots: void applyStatusFilter(int filterIndex); void applyCategoryFilter(const QString &category); void applyTagFilter(const std::optional &tag); - void applyTrackerFilterAll(); - void applyTrackerFilter(const QSet &torrentIDs); + void applyTrackerFilter(const std::optional &trackerHost); + void applyAnnounceStatusFilter(const std::optional &announceStatus); void previewFile(const Path &filePath); void renameSelectedTorrent(); diff --git a/src/webui/api/synccontroller.cpp b/src/webui/api/synccontroller.cpp index a68a7fce2..244b21b2f 100644 --- a/src/webui/api/synccontroller.cpp +++ b/src/webui/api/synccontroller.cpp @@ -547,7 +547,7 @@ void SyncController::maindataAction() 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); + connect(btSession, &BitTorrent::Session::trackersReset, this, &SyncController::onTorrentTrackersChanged); connect(btSession, &BitTorrent::Session::trackerEntryStatusesUpdated, this, &SyncController::onTorrentTrackerEntryStatusesUpdated); } diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index 99186ac9b..b91f5c1da 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -31,7 +31,6 @@ #include #include #include -#include #include #include @@ -506,6 +505,50 @@ namespace return nonstd::make_unexpected(TorrentsController::tr("Priority is not valid")); return priority; } + + TorrentFilter::Status parseTorrentStatus(const QString &statusStr) + { + if (statusStr == u"downloading") + return TorrentFilter::Downloading; + + if (statusStr == u"seeding") + return TorrentFilter::Seeding; + + if (statusStr == u"completed") + return TorrentFilter::Completed; + + if (statusStr == u"stopped") + return TorrentFilter::Stopped; + + if (statusStr == u"running") + return TorrentFilter::Running; + + if (statusStr == u"active") + return TorrentFilter::Active; + + if (statusStr == u"inactive") + return TorrentFilter::Inactive; + + if (statusStr == u"stalled") + return TorrentFilter::Stalled; + + if (statusStr == u"stalled_uploading") + return TorrentFilter::StalledUploading; + + if (statusStr == u"stalled_downloading") + return TorrentFilter::StalledDownloading; + + if (statusStr == u"checking") + return TorrentFilter::Checking; + + if (statusStr == u"moving") + return TorrentFilter::Moving; + + if (statusStr == u"errored") + return TorrentFilter::Errored; + + return TorrentFilter::All; + } } TorrentsController::TorrentsController(IApplication *app, QObject *parent) @@ -574,7 +617,7 @@ void TorrentsController::infoAction() idSet->insert(BitTorrent::TorrentID::fromString(hash)); } - const TorrentFilter torrentFilter {filter, idSet, category, tag, isPrivate}; + const TorrentFilter torrentFilter {parseTorrentStatus(filter), idSet, category, tag, isPrivate}; QVariantList torrentList; for (const BitTorrent::Torrent *torrent : asConst(BitTorrent::Session::instance()->torrents())) {