Implement separate (advanced) "Tracker status" filter

PR #23452.
This commit is contained in:
Vladimir Golovnev 2025-12-03 10:09:35 +03:00 committed by GitHub
parent f68bc3fef9
commit c45dfb6662
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 1226 additions and 652 deletions

View File

@ -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

View File

@ -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<TrackerEntry> &trackers);
void trackersChanged(Torrent *torrent);
void trackersReset(Torrent *torrent, const QList<TrackerEntryStatus> &oldEntries, const QList<TrackerEntry> &newEntries);
void trackersRemoved(Torrent *torrent, const QStringList &trackers);
void trackerSuccess(Torrent *torrent, const QString &tracker);
void trackerWarning(Torrent *torrent, const QString &tracker);

View File

@ -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<TrackerEntryStatus> &oldEntries, const QList<TrackerEntry> &newEntries)
{
emit trackersChanged(torrent);
emit trackersReset(torrent, oldEntries, newEntries);
}
void SessionImpl::handleTorrentUrlSeedsAdded(TorrentImpl *const torrent, const QList<QUrl> &newUrlSeeds)

View File

@ -474,7 +474,7 @@ namespace BitTorrent
void handleTorrentFinished(TorrentImpl *torrent);
void handleTorrentTrackersAdded(TorrentImpl *torrent, const QList<TrackerEntry> &newTrackers);
void handleTorrentTrackersRemoved(TorrentImpl *torrent, const QStringList &deletedTrackers);
void handleTorrentTrackersChanged(TorrentImpl *torrent);
void handleTorrentTrackersReset(TorrentImpl *torrent, const QList<TrackerEntryStatus> &oldEntries, const QList<TrackerEntry> &newEntries);
void handleTorrentUrlSeedsAdded(TorrentImpl *torrent, const QList<QUrl> &newUrlSeeds);
void handleTorrentUrlSeedsRemoved(TorrentImpl *torrent, const QList<QUrl> &urlSeeds);
void handleTorrentResumeDataReady(TorrentImpl *torrent, LoadTorrentParams data);

View File

@ -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;

View File

@ -0,0 +1,47 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#pragma once
#include <QFlags>
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);

View File

@ -718,9 +718,9 @@ void TorrentImpl::replaceTrackers(QList<TrackerEntry> trackers)
std::vector<lt::announce_entry> 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<TrackerEntry> trackers)
clearPeers();
deferredRequestResumeData();
m_session->handleTorrentTrackersChanged(this);
m_session->handleTorrentTrackersReset(this, oldEntries, trackers);
}
QList<QUrl> 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<const libtorrent::torrent_info> TorrentImpl::nativeTorrentInfo() const

View File

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* 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<TrackerEntryStatus> m_trackerEntryStatuses;
mutable std::optional<TorrentAnnounceStatus> m_announceStatus;
QList<QUrl> m_urlSeeds;
FileErrorInfo m_lastFileError;

View File

@ -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<int>(u"TransferListFilters/selectedFilterIndex"_s, 0);

View File

@ -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();

View File

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2014 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2014-2025 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -28,94 +28,57 @@
#include "torrentfilter.h"
#include "bittorrent/infohash.h"
#include "bittorrent/torrent.h"
#include <algorithm>
#include <QUrl>
#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<QString> TorrentFilter::AnyCategory;
const std::optional<TorrentIDSet> TorrentFilter::AnyID;
const std::optional<QString> TorrentFilter::AnyCategory;
const std::optional<Tag> TorrentFilter::AnyTag;
const std::optional<QString> TorrentFilter::AnyTrackerHost;
const std::optional<TorrentAnnounceStatus> 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<TorrentIDSet> &idSet
, const std::optional<QString> &category, const std::optional<Tag> &tag, const std::optional<bool> isPrivate)
: m_type {type}
TorrentFilter::TorrentFilter(const Status status, const std::optional<TorrentIDSet> &idSet, const std::optional<QString> &category
, const std::optional<Tag> &tag, const std::optional<bool> &isPrivate, const std::optional<QString> &trackerHost
, const std::optional<TorrentAnnounceStatus> &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<TorrentIDSet> &idSet
, const std::optional<QString> &category, const std::optional<Tag> &tag, const std::optional<bool> 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<TorrentIDSet> &idSet)
{
if (m_idSet != idSet)
@ -160,18 +123,43 @@ bool TorrentFilter::setPrivate(const std::optional<bool> isPrivate)
return false;
}
bool TorrentFilter::match(const Torrent *const torrent) const
bool TorrentFilter::setTrackerHost(const std::optional<QString> &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<TorrentAnnounceStatus> &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;
});
}

View File

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2014 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2014-2025 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -34,6 +34,7 @@
#include <QString>
#include "base/bittorrent/infohash.h"
#include "base/bittorrent/torrentannouncestatus.h"
#include "base/tag.h"
namespace BitTorrent
@ -46,7 +47,7 @@ using TorrentIDSet = QSet<BitTorrent::TorrentID>;
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<QString> AnyCategory;
static const std::optional<TorrentIDSet> AnyID;
static const std::optional<QString> AnyCategory;
static const std::optional<Tag> 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<QString> AnyTrackerHost;
static const std::optional<BitTorrent::TorrentAnnounceStatus> AnyAnnounceStatus;
TorrentFilter() = default;
// category & tags: pass empty string for uncategorized / untagged torrents.
TorrentFilter(Type type
TorrentFilter(Status status
, const std::optional<TorrentIDSet> &idSet = AnyID
, const std::optional<QString> &category = AnyCategory
, const std::optional<Tag> &tag = AnyTag
, std::optional<bool> isPrivate = {});
TorrentFilter(const QString &filter
, const std::optional<TorrentIDSet> &idSet = AnyID
, const std::optional<QString> &category = AnyCategory
, const std::optional<Tag> &tags = AnyTag
, std::optional<bool> isPrivate = {});
, const std::optional<bool> &isPrivate = {}
, const std::optional<QString> &trackerHost = AnyTrackerHost
, const std::optional<BitTorrent::TorrentAnnounceStatus> &announceStatus = AnyAnnounceStatus);
bool setType(Type type);
bool setTypeByName(const QString &filter);
bool setStatus(Status status);
bool setTorrentIDSet(const std::optional<TorrentIDSet> &idSet);
bool setCategory(const std::optional<QString> &category);
bool setTag(const std::optional<Tag> &tag);
bool setPrivate(std::optional<bool> isPrivate);
bool setTrackerHost(const std::optional<QString> &trackerHost);
bool setAnnounceStatus(const std::optional<BitTorrent::TorrentAnnounceStatus> &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<QString> m_category;
std::optional<Tag> m_tag;
std::optional<TorrentIDSet> m_idSet;
std::optional<bool> m_private;
std::optional<QString> m_trackerHost;
std::optional<BitTorrent::TorrentAnnounceStatus> m_announceStatus;
};
QString getTrackerHost(const QString &url);

View File

@ -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

View File

@ -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:

View File

@ -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());

View File

@ -456,6 +456,16 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBoxUseSeparateTrackerStatusFilter">
<property name="toolTip">
<string>Use separate &quot;Tracker status&quot; filter. Otherwise it gets merged with &quot;Trackers&quot; filter.</string>
</property>
<property name="text">
<string>Use separate &quot;Tracker status&quot; filter</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@ -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)

View File

@ -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);

View File

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2023-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@ -36,13 +36,13 @@
#include <QMessageBox>
#include <QUrl>
#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 <typename T>
concept HasUrlMember = requires (T t) { { t.url } -> std::convertible_to<QString>; };
template <HasUrlMember T>
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 <typename T>
QSet<QString> extractTrackerHosts(const T &trackerEntries)
{
QSet<QString> trackerHosts;
trackerHosts.reserve(trackerEntries.size());
for (const auto &trackerEntry : trackerEntries)
trackerHosts.insert(getTrackerHost(trackerEntry));
return host;
return trackerHosts;
}
template <typename T>
QSet<QString> extractTrackerURLs(const T &trackerEntries)
{
QSet<QString> 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<BitTorrent::TrackerEntry> &trackers)
void TrackersFilterWidget::handleTorrentTrackersAdded(const BitTorrent::Torrent *torrent, const QList<BitTorrent::TrackerEntry> &trackers)
{
const BitTorrent::TorrentID torrentID = torrent->id();
const QSet<QString> prevTrackerURLs = extractTrackerURLs(torrent->trackers()).subtract(extractTrackerURLs(trackers));
const QSet<QString> 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<BitTorrent::TrackerEntryStatus> currentTrackerEntries = torrent->trackers();
const QSet<QString> 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<BitTorrent::TrackerEntryStatus> &oldEntries, const QList<BitTorrent::TrackerEntry> &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<BitTorrent::TorrentID> &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<BitTorrent::TrackerEntryStatus> 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<BitTorrent::TorrentID> &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 QList<BitTo
trackerItem = new QListWidgetItem();
trackerItem->setData(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<BitTorrent::TorrentID> &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<Qt::CaseSensitive> 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<BitTo
updateGeometry();
}
void TrackersFilterWidget::removeItem(const QString &trackerURL, const BitTorrent::TorrentID &id)
void TrackersFilterWidget::decreaseTorrentsCount(const QString &trackerHost)
{
const QString host = getHost(trackerURL);
const auto iter = m_trackers.find(trackerHost);
Q_ASSERT(iter != m_trackers.end());
if (iter == m_trackers.end()) [[unlikely]]
return;
QSet<BitTorrent::TorrentID> 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<QString> &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<QString> &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<QString> &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<QString, BitTorrent::TrackerEntryStatus> &updatedTrackers)
void TrackersFilterWidget::handleTorrentTrackerStatusesUpdated(const BitTorrent::Torrent *torrent
, [[maybe_unused]] const QHash<QString, BitTorrent::TrackerEntryStatus> &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<BitTorrent::Torrent *> 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<QString> 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<BitTorrent::Torrent *> &torrents)
{
QHash<QString, QList<BitTorrent::TorrentID>> torrentsPerTracker;
QHash<QString, qsizetype> torrentsPerTrackerHost;
for (const BitTorrent::Torrent *torrent : torrents)
{
const BitTorrent::TorrentID torrentID = torrent->id();
const QList<BitTorrent::TrackerEntryStatus> 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<BitTorrent::TrackerEntryStatus> 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 QList<BitTorrent::Torrent
void TrackersFilterWidget::torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent)
{
const BitTorrent::TorrentID torrentID = torrent->id();
const QList<BitTorrent::TrackerEntryStatus> 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<BitTorrent::TrackerEntryStatus> 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<BitTorrent::TorrentID> 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;
}
}

View File

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2023-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* 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<BitTorrent::TrackerEntry> &trackers);
void removeTrackers(const BitTorrent::Torrent *torrent, const QStringList &trackers);
void refreshTrackers(const BitTorrent::Torrent *torrent);
void handleTrackerStatusesUpdated(const BitTorrent::Torrent *torrent
, const QHash<QString, BitTorrent::TrackerEntryStatus> &updatedTrackers);
void setDownloadTrackerFavicon(bool value);
private slots:
@ -75,28 +70,40 @@ private:
void handleTorrentsLoaded(const QList<BitTorrent::Torrent *> &torrents) override;
void torrentAboutToBeDeleted(BitTorrent::Torrent *torrent) override;
void handleTorrentTrackersAdded(const BitTorrent::Torrent *torrent, const QList<BitTorrent::TrackerEntry> &trackers);
void handleTorrentTrackersRemoved(const BitTorrent::Torrent *torrent, const QStringList &trackers);
void handleTorrentTrackersReset(const BitTorrent::Torrent *torrent, const QList<BitTorrent::TrackerEntryStatus> &oldEntries
, const QList<BitTorrent::TrackerEntry> &newEntries);
void handleTorrentTrackerStatusesUpdated(const BitTorrent::Torrent *torrent
, const QHash<QString, BitTorrent::TrackerEntryStatus> &updatedTrackers);
void onRemoveTrackerTriggered();
void addItems(const QString &trackerURL, const QList<BitTorrent::TorrentID> &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<BitTorrent::TorrentID> 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<BitTorrent::TorrentID> torrents;
qsizetype torrentsCount = 0;
QListWidgetItem *item = nullptr;
};
QHash<QString, TrackerData> m_trackers; // <tracker host, tracker data>
QHash<BitTorrent::TorrentID, QSet<QString>> m_errors; // <torrent ID, tracker hosts>
QHash<BitTorrent::TorrentID, QSet<QString>> m_trackerErrors; // <torrent ID, tracker hosts>
QHash<BitTorrent::TorrentID, QSet<QString>> m_warnings; // <torrent ID, tracker hosts>
QSet<const BitTorrent::Torrent *> m_errors;
QSet<const BitTorrent::Torrent *> m_trackerErrors;
QSet<const BitTorrent::Torrent *> m_warnings;
PathList m_iconPaths;
int m_totalTorrents = 0;
bool m_downloadTrackerFavicon = false;
bool m_handleTrackerStatuses = false;
QHash<QString, QSet<QString>> m_downloadingFavicons; // <favicon URL, tracker hosts>
};

View File

@ -0,0 +1,218 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#include "trackerstatusfilterwidget.h"
#include <QCheckBox>
#include <QIcon>
#include <QListWidgetItem>
#include <QMenu>
#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<BitTorrent::Torrent *> 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<BitTorrent::TrackerEntryStatus> &oldEntries, [[maybe_unused]] const QList<BitTorrent::TrackerEntry> &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<BitTorrent::Torrent *> &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()));
}

View File

@ -0,0 +1,66 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#pragma once
#include <QtContainerFwd>
#include <QSet>
#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<BitTorrent::Torrent *> &torrents) override;
void torrentAboutToBeDeleted(BitTorrent::Torrent *torrent) override;
void handleTorrentTrackersRemoved(const BitTorrent::Torrent *torrent);
void handleTorrentTrackersReset(const BitTorrent::Torrent *torrent, const QList<BitTorrent::TrackerEntryStatus> &oldEntries
, const QList<BitTorrent::TrackerEntry> &newEntries);
void handleTorrentTrackerStatusesUpdated(const BitTorrent::Torrent *torrent);
void refreshItems(const BitTorrent::Torrent *torrent);
QSet<const BitTorrent::Torrent *> m_errors;
QSet<const BitTorrent::Torrent *> m_trackerErrors;
QSet<const BitTorrent::Torrent *> m_warnings;
int m_totalTorrents = 0;
};

View File

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2023-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@ -29,58 +29,35 @@
#include "transferlistfilterswidget.h"
#include <QCheckBox>
#include <QIcon>
#include <QListWidgetItem>
#include <QMenu>
#include <QPainter>
#include <QScrollArea>
#include <QStyleOptionButton>
#include <QUrl>
#include <QVBoxLayout>
#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<BitTorrent::TrackerEntry> &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<QString, BitTorrent::TrackerEntryStatus> &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);
}

View File

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2023-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@ -32,11 +32,6 @@
#include <QtContainerFwd>
#include <QWidget>
#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<BitTorrent::TrackerEntry> &trackers);
void removeTrackers(const BitTorrent::Torrent *torrent, const QStringList &trackers);
void refreshTrackers(const BitTorrent::Torrent *torrent);
void trackerEntryStatusesUpdated(const BitTorrent::Torrent *torrent
, const QHash<QString, BitTorrent::TrackerEntryStatus> &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;
};

View File

@ -0,0 +1,96 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#include "transferlistfilterswidgetitem.h"
#include <QCheckBox>
#include <QFont>
#include <QPainter>
#include <QString>
#include <QStyleOptionViewItem>
#include <QVBoxLayout>
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);
}

View File

@ -0,0 +1,53 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#pragma once
#include <QWidget>
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;
};

View File

@ -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

View File

@ -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<BitTorrent::TorrentID> &torrentIDs)
void TransferListSortModel::setTrackerFilter(const std::optional<QString> &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<BitTorrent::TorrentAnnounceStatus> &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
}

View File

@ -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<BitTorrent::TorrentID> &torrentIDs);
void disableTrackerFilter();
void setTrackerFilter(const std::optional<QString> &trackerHost);
void setAnnounceStatusFilter(const std::optional<BitTorrent::TorrentAnnounceStatus> &announceStatus);
private:
int compare(const QModelIndex &left, const QModelIndex &right) const;

View File

@ -1342,14 +1342,14 @@ void TransferListWidget::applyTagFilter(const std::optional<Tag> &tag)
m_sortFilterModel->setTagFilter(*tag);
}
void TransferListWidget::applyTrackerFilterAll()
void TransferListWidget::applyTrackerFilter(const std::optional<QString> &trackerHost)
{
m_sortFilterModel->disableTrackerFilter();
m_sortFilterModel->setTrackerFilter(trackerHost);
}
void TransferListWidget::applyTrackerFilter(const QSet<BitTorrent::TorrentID> &torrentIDs)
void TransferListWidget::applyAnnounceStatusFilter(const std::optional<BitTorrent::TorrentAnnounceStatus> &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<TorrentFilter::Type>(filterIndex);
const auto filterType = static_cast<TorrentFilter::Status>(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))

View File

@ -100,8 +100,8 @@ public slots:
void applyStatusFilter(int filterIndex);
void applyCategoryFilter(const QString &category);
void applyTagFilter(const std::optional<Tag> &tag);
void applyTrackerFilterAll();
void applyTrackerFilter(const QSet<BitTorrent::TorrentID> &torrentIDs);
void applyTrackerFilter(const std::optional<QString> &trackerHost);
void applyAnnounceStatusFilter(const std::optional<BitTorrent::TorrentAnnounceStatus> &announceStatus);
void previewFile(const Path &filePath);
void renameSelectedTorrent();

View File

@ -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);
}

View File

@ -31,7 +31,6 @@
#include <algorithm>
#include <chrono>
#include <concepts>
#include <functional>
#include <QBitArray>
#include <QFileInfo>
@ -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()))
{