1
mirror of https://github.com/qbittorrent/qBittorrent synced 2024-11-14 19:12:46 +01:00

Add option to auto hide zero status filters

* Extract transfer list filter classes into separate files
* Add option to auto hide zero status filters

PR #18801.
Closes #13996.
This commit is contained in:
Vladimir Golovnev 2023-04-03 10:38:08 +03:00 committed by GitHub
parent d40be79c69
commit 0dcb65bb7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1222 additions and 891 deletions

View File

@ -1551,6 +1551,16 @@ void Preferences::setTransSelFilter(const int index)
setValue(u"TransferListFilters/selectedFilterIndex"_qs, index);
}
bool Preferences::getHideZeroStatusFilters() const
{
return value<bool>(u"TransferListFilters/HideZeroStatusFilters"_qs, false);
}
void Preferences::setHideZeroStatusFilters(const bool hide)
{
setValue(u"TransferListFilters/HideZeroStatusFilters"_qs, hide);
}
QByteArray Preferences::getTransHeaderState() const
{
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))

View File

@ -382,6 +382,8 @@ public:
bool getTrackerFilterState() const;
int getTransSelFilter() const;
void setTransSelFilter(int index);
bool getHideZeroStatusFilters() const;
void setHideZeroStatusFilters(bool hide);
QByteArray getTransHeaderState() const;
void setTransHeaderState(const QByteArray &state);
bool getRegexAsFilteringPatternForTransferList() const;

View File

@ -49,9 +49,6 @@ add_library(qbt_gui STATIC
advancedsettings.h
autoexpandabledialog.h
banlistoptionsdialog.h
categoryfiltermodel.h
categoryfilterproxymodel.h
categoryfilterwidget.h
color.h
cookiesdialog.h
cookiesmodel.h
@ -103,9 +100,6 @@ add_library(qbt_gui STATIC
speedlimitdialog.h
statsdialog.h
statusbar.h
tagfiltermodel.h
tagfilterproxymodel.h
tagfilterwidget.h
torrentcategorydialog.h
torrentcontentfiltermodel.h
torrentcontentitemdelegate.h
@ -119,6 +113,15 @@ add_library(qbt_gui STATIC
torrenttagsdialog.h
trackerentriesdialog.h
transferlistdelegate.h
transferlistfilters/basefilterwidget.h
transferlistfilters/categoryfiltermodel.h
transferlistfilters/categoryfilterproxymodel.h
transferlistfilters/categoryfilterwidget.h
transferlistfilters/statusfilterwidget.h
transferlistfilters/tagfiltermodel.h
transferlistfilters/tagfilterproxymodel.h
transferlistfilters/tagfilterwidget.h
transferlistfilters/trackersfilterwidget.h
transferlistfilterswidget.h
transferlistmodel.h
transferlistsortmodel.h
@ -140,9 +143,6 @@ add_library(qbt_gui STATIC
advancedsettings.cpp
autoexpandabledialog.cpp
banlistoptionsdialog.cpp
categoryfiltermodel.cpp
categoryfilterproxymodel.cpp
categoryfilterwidget.cpp
cookiesdialog.cpp
cookiesmodel.cpp
deletionconfirmationdialog.cpp
@ -192,9 +192,6 @@ add_library(qbt_gui STATIC
speedlimitdialog.cpp
statsdialog.cpp
statusbar.cpp
tagfiltermodel.cpp
tagfilterproxymodel.cpp
tagfilterwidget.cpp
torrentcategorydialog.cpp
torrentcontentfiltermodel.cpp
torrentcontentitemdelegate.cpp
@ -208,6 +205,15 @@ add_library(qbt_gui STATIC
torrenttagsdialog.cpp
trackerentriesdialog.cpp
transferlistdelegate.cpp
transferlistfilters/basefilterwidget.cpp
transferlistfilters/categoryfiltermodel.cpp
transferlistfilters/categoryfilterproxymodel.cpp
transferlistfilters/categoryfilterwidget.cpp
transferlistfilters/statusfilterwidget.cpp
transferlistfilters/tagfiltermodel.cpp
transferlistfilters/tagfilterproxymodel.cpp
transferlistfilters/tagfilterwidget.cpp
transferlistfilters/trackersfilterwidget.cpp
transferlistfilterswidget.cpp
transferlistmodel.cpp
transferlistsortmodel.cpp

View File

@ -6,9 +6,6 @@ HEADERS += \
$$PWD/advancedsettings.h \
$$PWD/autoexpandabledialog.h \
$$PWD/banlistoptionsdialog.h \
$$PWD/categoryfiltermodel.h \
$$PWD/categoryfilterproxymodel.h \
$$PWD/categoryfilterwidget.h \
$$PWD/color.h \
$$PWD/cookiesdialog.h \
$$PWD/cookiesmodel.h \
@ -60,9 +57,6 @@ HEADERS += \
$$PWD/speedlimitdialog.h \
$$PWD/statsdialog.h \
$$PWD/statusbar.h \
$$PWD/tagfiltermodel.h \
$$PWD/tagfilterproxymodel.h \
$$PWD/tagfilterwidget.h \
$$PWD/torrentcategorydialog.h \
$$PWD/torrentcontentfiltermodel.h \
$$PWD/torrentcontentitemdelegate.h \
@ -76,6 +70,15 @@ HEADERS += \
$$PWD/torrenttagsdialog.h \
$$PWD/trackerentriesdialog.h \
$$PWD/transferlistdelegate.h \
$$PWD/transferlistfilters/basefilterwidget.h \
$$PWD/transferlistfilters/categoryfiltermodel.h \
$$PWD/transferlistfilters/categoryfilterproxymodel.h \
$$PWD/transferlistfilters/categoryfilterwidget.h \
$$PWD/transferlistfilters/statusfilterwidget.h \
$$PWD/transferlistfilters/tagfiltermodel.h \
$$PWD/transferlistfilters/tagfilterproxymodel.h \
$$PWD/transferlistfilters/tagfilterwidget.h \
$$PWD/transferlistfilters/trackersfilterwidget.h \
$$PWD/transferlistfilterswidget.h \
$$PWD/transferlistmodel.h \
$$PWD/transferlistsortmodel.h \
@ -97,9 +100,6 @@ SOURCES += \
$$PWD/advancedsettings.cpp \
$$PWD/autoexpandabledialog.cpp \
$$PWD/banlistoptionsdialog.cpp \
$$PWD/categoryfiltermodel.cpp \
$$PWD/categoryfilterproxymodel.cpp \
$$PWD/categoryfilterwidget.cpp \
$$PWD/cookiesdialog.cpp \
$$PWD/cookiesmodel.cpp \
$$PWD/deletionconfirmationdialog.cpp \
@ -149,9 +149,6 @@ SOURCES += \
$$PWD/speedlimitdialog.cpp \
$$PWD/statsdialog.cpp \
$$PWD/statusbar.cpp \
$$PWD/tagfiltermodel.cpp \
$$PWD/tagfilterproxymodel.cpp \
$$PWD/tagfilterwidget.cpp \
$$PWD/torrentcategorydialog.cpp \
$$PWD/torrentcontentfiltermodel.cpp \
$$PWD/torrentcontentitemdelegate.cpp \
@ -165,6 +162,15 @@ SOURCES += \
$$PWD/torrenttagsdialog.cpp \
$$PWD/trackerentriesdialog.cpp \
$$PWD/transferlistdelegate.cpp \
$$PWD/transferlistfilters/basefilterwidget.cpp \
$$PWD/transferlistfilters/categoryfiltermodel.cpp \
$$PWD/transferlistfilters/categoryfilterproxymodel.cpp \
$$PWD/transferlistfilters/categoryfilterwidget.cpp \
$$PWD/transferlistfilters/statusfilterwidget.cpp \
$$PWD/transferlistfilters/tagfiltermodel.cpp \
$$PWD/transferlistfilters/tagfilterproxymodel.cpp \
$$PWD/transferlistfilters/tagfilterwidget.cpp \
$$PWD/transferlistfilters/trackersfilterwidget.cpp \
$$PWD/transferlistfilterswidget.cpp \
$$PWD/transferlistmodel.cpp \
$$PWD/transferlistsortmodel.cpp \

View File

@ -245,6 +245,8 @@ void OptionsDialog::loadBehaviorTabOptions()
actionSeeding = OPEN_DEST;
m_ui->actionTorrentFnOnDblClBox->setCurrentIndex(m_ui->actionTorrentFnOnDblClBox->findData(actionSeeding));
m_ui->checkBoxHideZeroStatusFilters->setChecked(pref->getHideZeroStatusFilters());
#ifndef Q_OS_WIN
m_ui->checkStartup->setVisible(false);
#endif
@ -344,6 +346,7 @@ void OptionsDialog::loadBehaviorTabOptions()
connect(m_ui->comboHideZero, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton);
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);
#ifdef Q_OS_WIN
connect(m_ui->checkStartup, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
@ -417,6 +420,8 @@ void OptionsDialog::saveBehaviorTabOptions() const
pref->setActionOnDblClOnTorrentDl(m_ui->actionTorrentDlOnDblClBox->currentData().toInt());
pref->setActionOnDblClOnTorrentFn(m_ui->actionTorrentFnOnDblClBox->currentData().toInt());
pref->setHideZeroStatusFilters(m_ui->checkBoxHideZeroStatusFilters->isChecked());
pref->setSplashScreenDisabled(isSplashScreenDisabled());
pref->setConfirmOnExit(m_ui->checkProgramExitConfirm->isChecked());
pref->setDontConfirmAutoExit(!m_ui->checkProgramAutoExitConfirm->isChecked());

View File

@ -384,6 +384,13 @@
</layout>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBoxHideZeroStatusFilters">
<property name="text">
<string>Auto hide zero status filters</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -3761,6 +3768,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.</string>
<tabstop>checkAltRowColors</tabstop>
<tabstop>actionTorrentDlOnDblClBox</tabstop>
<tabstop>actionTorrentFnOnDblClBox</tabstop>
<tabstop>checkBoxHideZeroStatusFilters</tabstop>
<tabstop>checkStartup</tabstop>
<tabstop>checkShowSplash</tabstop>
<tabstop>windowStateComboBox</tabstop>

View File

@ -0,0 +1,92 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#include "basefilterwidget.h"
#include "base/bittorrent/session.h"
#include "gui/utils.h"
constexpr int ALL_ROW = 0;
BaseFilterWidget::BaseFilterWidget(QWidget *parent, TransferListWidget *transferList)
: QListWidget(parent)
, m_transferList {transferList}
{
setFrameShape(QFrame::NoFrame);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
setUniformItemSizes(true);
setSpacing(0);
setIconSize(Utils::Gui::smallIconSize());
#if defined(Q_OS_MACOS)
setAttribute(Qt::WA_MacShowFocusRect, false);
#endif
setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, &BaseFilterWidget::customContextMenuRequested, this, &BaseFilterWidget::showMenu);
connect(this, &BaseFilterWidget::currentRowChanged, this, &BaseFilterWidget::applyFilter);
connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentsLoaded
, this, &BaseFilterWidget::handleTorrentsLoaded);
connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentAboutToBeRemoved
, this, &BaseFilterWidget::torrentAboutToBeDeleted);
}
QSize BaseFilterWidget::sizeHint() const
{
return {
// Width should be exactly the width of the content
sizeHintForColumn(0),
// Height should be exactly the height of the content
static_cast<int>((sizeHintForRow(0) + 2 * spacing()) * (count() + 0.5))};
}
QSize BaseFilterWidget::minimumSizeHint() const
{
QSize size = sizeHint();
size.setWidth(6);
return size;
}
TransferListWidget *BaseFilterWidget::transferList() const
{
return m_transferList;
}
void BaseFilterWidget::toggleFilter(const bool checked)
{
setVisible(checked);
if (checked)
applyFilter(currentRow());
else
applyFilter(ALL_ROW);
}

View File

@ -0,0 +1,63 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#pragma once
#include <QtContainerFwd>
#include <QListWidget>
#include "base/bittorrent/torrent.h"
class TransferListWidget;
class BaseFilterWidget : public QListWidget
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(BaseFilterWidget)
public:
BaseFilterWidget(QWidget *parent, TransferListWidget *transferList);
QSize sizeHint() const override;
QSize minimumSizeHint() const override;
TransferListWidget *transferList() const;
public slots:
void toggleFilter(bool checked);
private slots:
virtual void showMenu() = 0;
virtual void applyFilter(int row) = 0;
virtual void handleTorrentsLoaded(const QVector<BitTorrent::Torrent *> &torrents) = 0;
virtual void torrentAboutToBeDeleted(BitTorrent::Torrent *const) = 0;
private:
TransferListWidget *m_transferList = nullptr;
};

View File

@ -33,7 +33,7 @@
#include "base/bittorrent/session.h"
#include "base/global.h"
#include "uithememanager.h"
#include "gui/uithememanager.h"
class CategoryModelItem
{

View File

@ -32,11 +32,11 @@
#include "base/bittorrent/session.h"
#include "base/global.h"
#include "gui/torrentcategorydialog.h"
#include "gui/uithememanager.h"
#include "gui/utils.h"
#include "categoryfiltermodel.h"
#include "categoryfilterproxymodel.h"
#include "torrentcategorydialog.h"
#include "uithememanager.h"
#include "utils.h"
namespace
{

View File

@ -0,0 +1,291 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#include "statusfilterwidget.h"
#include <QListWidgetItem>
#include <QMenu>
#include "base/bittorrent/session.h"
#include "base/global.h"
#include "base/preferences.h"
#include "base/torrentfilter.h"
#include "gui/transferlistwidget.h"
#include "gui/uithememanager.h"
StatusFilterWidget::StatusFilterWidget(QWidget *parent, TransferListWidget *transferList)
: BaseFilterWidget(parent, transferList)
{
// Add status filters
auto *all = new QListWidgetItem(this);
all->setData(Qt::DisplayRole, tr("All (0)", "this is for the status filter"));
all->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"filter-all"_qs, u"filterall"_qs));
auto *downloading = new QListWidgetItem(this);
downloading->setData(Qt::DisplayRole, tr("Downloading (0)"));
downloading->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"downloading"_qs));
auto *seeding = new QListWidgetItem(this);
seeding->setData(Qt::DisplayRole, tr("Seeding (0)"));
seeding->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"upload"_qs, u"uploading"_qs));
auto *completed = new QListWidgetItem(this);
completed->setData(Qt::DisplayRole, tr("Completed (0)"));
completed->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"checked-completed"_qs, u"completed"_qs));
auto *resumed = new QListWidgetItem(this);
resumed->setData(Qt::DisplayRole, tr("Resumed (0)"));
resumed->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"torrent-start"_qs, u"media-playback-start"_qs));
auto *paused = new QListWidgetItem(this);
paused->setData(Qt::DisplayRole, tr("Paused (0)"));
paused->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"stopped"_qs, u"media-playback-pause"_qs));
auto *active = new QListWidgetItem(this);
active->setData(Qt::DisplayRole, tr("Active (0)"));
active->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"filter-active"_qs, u"filteractive"_qs));
auto *inactive = new QListWidgetItem(this);
inactive->setData(Qt::DisplayRole, tr("Inactive (0)"));
inactive->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"filter-inactive"_qs, u"filterinactive"_qs));
auto *stalled = new QListWidgetItem(this);
stalled->setData(Qt::DisplayRole, tr("Stalled (0)"));
stalled->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"filter-stalled"_qs, u"filterstalled"_qs));
auto *stalledUploading = new QListWidgetItem(this);
stalledUploading->setData(Qt::DisplayRole, tr("Stalled Uploading (0)"));
stalledUploading->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"stalledUP"_qs));
auto *stalledDownloading = new QListWidgetItem(this);
stalledDownloading->setData(Qt::DisplayRole, tr("Stalled Downloading (0)"));
stalledDownloading->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"stalledDL"_qs));
auto *checking = new QListWidgetItem(this);
checking->setData(Qt::DisplayRole, tr("Checking (0)"));
checking->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"force-recheck"_qs, u"checking"_qs));
auto *moving = new QListWidgetItem(this);
moving->setData(Qt::DisplayRole, tr("Moving (0)"));
moving->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"set-location"_qs));
auto *errored = new QListWidgetItem(this);
errored->setData(Qt::DisplayRole, tr("Errored (0)"));
errored->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"error"_qs));
const QVector<BitTorrent::Torrent *> torrents = BitTorrent::Session::instance()->torrents();
update(torrents);
connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentsUpdated
, this, &StatusFilterWidget::update);
const Preferences *const pref = Preferences::instance();
connect(pref, &Preferences::changed, this, &StatusFilterWidget::configure);
const int storedRow = pref->getTransSelFilter();
if (item((storedRow < count()) ? storedRow : 0)->isHidden())
setCurrentRow(TorrentFilter::All, QItemSelectionModel::SelectCurrent);
else
setCurrentRow(storedRow, QItemSelectionModel::SelectCurrent);
toggleFilter(pref->getStatusFilterState());
}
StatusFilterWidget::~StatusFilterWidget()
{
Preferences::instance()->setTransSelFilter(currentRow());
}
QSize StatusFilterWidget::sizeHint() const
{
int numVisibleItems = 0;
for (int i = 0; i < count(); ++i)
{
if (!item(i)->isHidden())
++numVisibleItems;
}
return {
// Width should be exactly the width of the content
sizeHintForColumn(0),
// Height should be exactly the height of the content
static_cast<int>((sizeHintForRow(0) + 2 * spacing()) * (numVisibleItems + 0.5))};
}
void StatusFilterWidget::updateTorrentStatus(const BitTorrent::Torrent *torrent)
{
TorrentFilterBitset &torrentStatus = m_torrentsStatus[torrent];
const auto update = [torrent, &torrentStatus](const TorrentFilter::Type status, int &counter)
{
const bool hasStatus = torrentStatus[status];
const bool needStatus = TorrentFilter(status).match(torrent);
if (needStatus && !hasStatus)
{
++counter;
torrentStatus.set(status);
}
else if (!needStatus && hasStatus)
{
--counter;
torrentStatus.reset(status);
}
};
update(TorrentFilter::Downloading, m_nbDownloading);
update(TorrentFilter::Seeding, m_nbSeeding);
update(TorrentFilter::Completed, m_nbCompleted);
update(TorrentFilter::Resumed, m_nbResumed);
update(TorrentFilter::Paused, m_nbPaused);
update(TorrentFilter::Active, m_nbActive);
update(TorrentFilter::Inactive, m_nbInactive);
update(TorrentFilter::StalledUploading, m_nbStalledUploading);
update(TorrentFilter::StalledDownloading, m_nbStalledDownloading);
update(TorrentFilter::Checking, m_nbChecking);
update(TorrentFilter::Moving, m_nbMoving);
update(TorrentFilter::Errored, m_nbErrored);
m_nbStalled = m_nbStalledUploading + m_nbStalledDownloading;
}
void StatusFilterWidget::updateTexts()
{
const qsizetype torrentsCount = BitTorrent::Session::instance()->torrentsCount();
item(TorrentFilter::All)->setData(Qt::DisplayRole, tr("All (%1)").arg(torrentsCount));
item(TorrentFilter::Downloading)->setData(Qt::DisplayRole, tr("Downloading (%1)").arg(m_nbDownloading));
item(TorrentFilter::Seeding)->setData(Qt::DisplayRole, tr("Seeding (%1)").arg(m_nbSeeding));
item(TorrentFilter::Completed)->setData(Qt::DisplayRole, tr("Completed (%1)").arg(m_nbCompleted));
item(TorrentFilter::Resumed)->setData(Qt::DisplayRole, tr("Resumed (%1)").arg(m_nbResumed));
item(TorrentFilter::Paused)->setData(Qt::DisplayRole, tr("Paused (%1)").arg(m_nbPaused));
item(TorrentFilter::Active)->setData(Qt::DisplayRole, tr("Active (%1)").arg(m_nbActive));
item(TorrentFilter::Inactive)->setData(Qt::DisplayRole, tr("Inactive (%1)").arg(m_nbInactive));
item(TorrentFilter::Stalled)->setData(Qt::DisplayRole, tr("Stalled (%1)").arg(m_nbStalled));
item(TorrentFilter::StalledUploading)->setData(Qt::DisplayRole, tr("Stalled Uploading (%1)").arg(m_nbStalledUploading));
item(TorrentFilter::StalledDownloading)->setData(Qt::DisplayRole, tr("Stalled Downloading (%1)").arg(m_nbStalledDownloading));
item(TorrentFilter::Checking)->setData(Qt::DisplayRole, tr("Checking (%1)").arg(m_nbChecking));
item(TorrentFilter::Moving)->setData(Qt::DisplayRole, tr("Moving (%1)").arg(m_nbMoving));
item(TorrentFilter::Errored)->setData(Qt::DisplayRole, tr("Errored (%1)").arg(m_nbErrored));
}
void StatusFilterWidget::hideZeroItems()
{
item(TorrentFilter::Downloading)->setHidden(m_nbDownloading == 0);
item(TorrentFilter::Seeding)->setHidden(m_nbSeeding == 0);
item(TorrentFilter::Completed)->setHidden(m_nbCompleted == 0);
item(TorrentFilter::Resumed)->setHidden(m_nbResumed == 0);
item(TorrentFilter::Paused)->setHidden(m_nbPaused == 0);
item(TorrentFilter::Active)->setHidden(m_nbActive == 0);
item(TorrentFilter::Inactive)->setHidden(m_nbInactive == 0);
item(TorrentFilter::Stalled)->setHidden(m_nbStalled == 0);
item(TorrentFilter::StalledUploading)->setHidden(m_nbStalledUploading == 0);
item(TorrentFilter::StalledDownloading)->setHidden(m_nbStalledDownloading == 0);
item(TorrentFilter::Checking)->setHidden(m_nbChecking == 0);
item(TorrentFilter::Moving)->setHidden(m_nbMoving == 0);
item(TorrentFilter::Errored)->setHidden(m_nbErrored == 0);
if (currentItem() && currentItem()->isHidden())
setCurrentRow(TorrentFilter::All, QItemSelectionModel::SelectCurrent);
}
void StatusFilterWidget::update(const QVector<BitTorrent::Torrent *> torrents)
{
for (const BitTorrent::Torrent *torrent : torrents)
updateTorrentStatus(torrent);
updateTexts();
if (Preferences::instance()->getHideZeroStatusFilters())
{
hideZeroItems();
updateGeometry();
}
}
void StatusFilterWidget::showMenu()
{
QMenu *menu = new QMenu(this);
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->addAction(UIThemeManager::instance()->getIcon(u"torrent-start"_qs, u"media-playback-start"_qs), tr("Resume torrents")
, transferList(), &TransferListWidget::startVisibleTorrents);
menu->addAction(UIThemeManager::instance()->getIcon(u"torrent-stop"_qs, u"media-playback-pause"_qs), tr("Pause torrents")
, transferList(), &TransferListWidget::pauseVisibleTorrents);
menu->addAction(UIThemeManager::instance()->getIcon(u"list-remove"_qs), tr("Remove torrents")
, transferList(), &TransferListWidget::deleteVisibleTorrents);
menu->popup(QCursor::pos());
}
void StatusFilterWidget::applyFilter(int row)
{
transferList()->applyStatusFilter(row);
}
void StatusFilterWidget::handleTorrentsLoaded(const QVector<BitTorrent::Torrent *> &torrents)
{
for (const BitTorrent::Torrent *torrent : torrents)
updateTorrentStatus(torrent);
updateTexts();
}
void StatusFilterWidget::torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent)
{
const TorrentFilterBitset status = m_torrentsStatus.take(torrent);
if (status[TorrentFilter::Downloading])
--m_nbDownloading;
if (status[TorrentFilter::Seeding])
--m_nbSeeding;
if (status[TorrentFilter::Completed])
--m_nbCompleted;
if (status[TorrentFilter::Resumed])
--m_nbResumed;
if (status[TorrentFilter::Paused])
--m_nbPaused;
if (status[TorrentFilter::Active])
--m_nbActive;
if (status[TorrentFilter::Inactive])
--m_nbInactive;
if (status[TorrentFilter::StalledUploading])
--m_nbStalledUploading;
if (status[TorrentFilter::StalledDownloading])
--m_nbStalledDownloading;
if (status[TorrentFilter::Checking])
--m_nbChecking;
if (status[TorrentFilter::Moving])
--m_nbMoving;
if (status[TorrentFilter::Errored])
--m_nbErrored;
m_nbStalled = m_nbStalledUploading + m_nbStalledDownloading;
updateTexts();
}
void StatusFilterWidget::configure()
{
if (Preferences::instance()->getHideZeroStatusFilters())
{
hideZeroItems();
}
else
{
for (int i = 0; i < count(); ++i)
item(i)->setHidden(false);
}
updateGeometry();
}

View File

@ -0,0 +1,81 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#pragma once
#include <bitset>
#include <QtContainerFwd>
#include <QHash>
#include "base/torrentfilter.h"
#include "basefilterwidget.h"
class StatusFilterWidget final : public BaseFilterWidget
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(StatusFilterWidget)
public:
StatusFilterWidget(QWidget *parent, TransferListWidget *transferList);
~StatusFilterWidget() override;
private:
QSize sizeHint() const override;
// 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 QVector<BitTorrent::Torrent *> &torrents) override;
void torrentAboutToBeDeleted(BitTorrent::Torrent *const) override;
void configure();
void update(const QVector<BitTorrent::Torrent *> torrents);
void updateTorrentStatus(const BitTorrent::Torrent *torrent);
void updateTexts();
void hideZeroItems();
using TorrentFilterBitset = std::bitset<32>; // approximated size, this should be the number of TorrentFilter::Type elements
QHash<const BitTorrent::Torrent *, TorrentFilterBitset> m_torrentsStatus;
int m_nbDownloading = 0;
int m_nbSeeding = 0;
int m_nbCompleted = 0;
int m_nbResumed = 0;
int m_nbPaused = 0;
int m_nbActive = 0;
int m_nbInactive = 0;
int m_nbStalled = 0;
int m_nbStalledUploading = 0;
int m_nbStalledDownloading = 0;
int m_nbChecking = 0;
int m_nbMoving = 0;
int m_nbErrored = 0;
};

View File

@ -34,7 +34,7 @@
#include "base/bittorrent/session.h"
#include "base/global.h"
#include "uithememanager.h"
#include "gui/uithememanager.h"
namespace
{

View File

@ -33,11 +33,11 @@
#include "base/bittorrent/session.h"
#include "base/global.h"
#include "autoexpandabledialog.h"
#include "gui/autoexpandabledialog.h"
#include "gui/uithememanager.h"
#include "gui/utils.h"
#include "tagfiltermodel.h"
#include "tagfilterproxymodel.h"
#include "uithememanager.h"
#include "utils.h"
namespace
{

View File

@ -0,0 +1,513 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#include "trackersfilterwidget.h"
#include <QIcon>
#include <QListWidgetItem>
#include <QMenu>
#include <QUrl>
#include "base/algorithm.h"
#include "base/bittorrent/session.h"
#include "base/global.h"
#include "base/net/downloadmanager.h"
#include "base/preferences.h"
#include "base/utils/compare.h"
#include "base/utils/fs.h"
#include "gui/transferlistwidget.h"
#include "gui/uithememanager.h"
namespace
{
enum TRACKER_FILTER_ROW
{
ALL_ROW,
TRACKERLESS_ROW,
ERROR_ROW,
WARNING_ROW
};
QString getScheme(const QString &tracker)
{
const QString scheme = QUrl(tracker).scheme();
return !scheme.isEmpty() ? scheme : u"http"_qs;
}
QString getHost(const QString &url)
{
// We want the domain + tld. Subdomains should be disregarded
// If failed to parse the domain or IP address, original input should be returned
const QString host = QUrl(url).host();
if (host.isEmpty())
return url;
// host is in IP format
if (!QHostAddress(host).isNull())
return host;
return host.section(u'.', -2, -1);
}
const QString NULL_HOST = u""_qs;
}
TrackersFilterWidget::TrackersFilterWidget(QWidget *parent, TransferListWidget *transferList, const bool downloadFavicon)
: BaseFilterWidget(parent, transferList)
, m_downloadTrackerFavicon(downloadFavicon)
{
auto *allTrackers = new QListWidgetItem(this);
allTrackers->setData(Qt::DisplayRole, tr("All (0)", "this is for the tracker filter"));
allTrackers->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"trackers"_qs, u"network-server"_qs));
auto *noTracker = new QListWidgetItem(this);
noTracker->setData(Qt::DisplayRole, tr("Trackerless (0)"));
noTracker->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"trackerless"_qs, u"network-server"_qs));
auto *errorTracker = new QListWidgetItem(this);
errorTracker->setData(Qt::DisplayRole, tr("Error (0)"));
errorTracker->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"tracker-error"_qs, u"dialog-error"_qs));
auto *warningTracker = new QListWidgetItem(this);
warningTracker->setData(Qt::DisplayRole, tr("Warning (0)"));
warningTracker->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"tracker-warning"_qs, u"dialog-warning"_qs));
m_trackers[NULL_HOST] = {{}, noTracker};
handleTorrentsLoaded(BitTorrent::Session::instance()->torrents());
setCurrentRow(0, QItemSelectionModel::SelectCurrent);
toggleFilter(Preferences::instance()->getTrackerFilterState());
}
TrackersFilterWidget::~TrackersFilterWidget()
{
for (const Path &iconPath : asConst(m_iconPaths))
Utils::Fs::removeFile(iconPath);
}
void TrackersFilterWidget::addTrackers(const BitTorrent::Torrent *torrent, const QVector<BitTorrent::TrackerEntry> &trackers)
{
const BitTorrent::TorrentID torrentID = torrent->id();
for (const BitTorrent::TrackerEntry &tracker : trackers)
addItems(tracker.url, {torrentID});
}
void TrackersFilterWidget::removeTrackers(const BitTorrent::Torrent *torrent, const QStringList &trackers)
{
const BitTorrent::TorrentID torrentID = torrent->id();
for (const QString &tracker : trackers)
removeItem(tracker, torrentID);
}
void TrackersFilterWidget::refreshTrackers(const BitTorrent::Torrent *torrent)
{
const BitTorrent::TorrentID torrentID = torrent->id();
m_errors.remove(torrentID);
m_warnings.remove(torrentID);
Algorithm::removeIf(m_trackers, [this, &torrentID](const QString &host, TrackerData &trackerData)
{
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(u"%1 (%2)"_qs.arg((host.isEmpty() ? tr("Trackerless") : host), QString::number(torrentIDs.size())));
return false;
});
const QVector<BitTorrent::TrackerEntry> trackerEntries = torrent->trackers();
const bool isTrackerless = trackerEntries.isEmpty();
if (isTrackerless)
{
addItems(NULL_HOST, {torrentID});
}
else
{
for (const BitTorrent::TrackerEntry &trackerEntry : trackerEntries)
addItems(trackerEntry.url, {torrentID});
}
updateGeometry();
}
void TrackersFilterWidget::changeTrackerless(const BitTorrent::Torrent *torrent, const bool trackerless)
{
if (trackerless)
addItems(NULL_HOST, {torrent->id()});
else
removeItem(NULL_HOST, torrent->id());
}
void TrackersFilterWidget::addItems(const QString &trackerURL, const QVector<BitTorrent::TorrentID> &torrents)
{
const QString host = getHost(trackerURL);
auto trackersIt = m_trackers.find(host);
const bool exists = (trackersIt != m_trackers.end());
QListWidgetItem *trackerItem = nullptr;
if (exists)
{
trackerItem = trackersIt->item;
}
else
{
trackerItem = new QListWidgetItem();
trackerItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"trackers"_qs, u"network-server"_qs));
const TrackerData trackerData {{}, trackerItem};
trackersIt = m_trackers.insert(host, trackerData);
const QString scheme = getScheme(trackerURL);
downloadFavicon(u"%1://%2/favicon.ico"_qs.arg((scheme.startsWith(u"http") ? scheme : u"http"_qs), host));
}
Q_ASSERT(trackerItem);
QSet<BitTorrent::TorrentID> &torrentIDs = trackersIt->torrents;
for (const BitTorrent::TorrentID &torrentID : torrents)
torrentIDs.insert(torrentID);
trackerItem->setText(u"%1 (%2)"_qs.arg(((host == NULL_HOST) ? tr("Trackerless") : host), QString::number(torrentIDs.size())));
if (exists)
{
if (item(currentRow()) == trackerItem)
applyFilter(currentRow());
return;
}
Q_ASSERT(count() >= 4);
const Utils::Compare::NaturalLessThan<Qt::CaseSensitive> naturalLessThan {};
int insPos = count();
for (int i = 4; i < count(); ++i)
{
if (naturalLessThan(host, item(i)->text()))
{
insPos = i;
break;
}
}
QListWidget::insertItem(insPos, trackerItem);
updateGeometry();
}
void TrackersFilterWidget::removeItem(const QString &trackerURL, const BitTorrent::TorrentID &id)
{
const QString host = getHost(trackerURL);
QSet<BitTorrent::TorrentID> torrentIDs = m_trackers.value(host).torrents;
torrentIDs.remove(id);
QListWidgetItem *trackerItem = nullptr;
if (!host.isEmpty())
{
// Remove from 'Error' and 'Warning' view
const auto errorHashesIt = m_errors.find(id);
if (errorHashesIt != m_errors.end())
{
QSet<QString> &errored = errorHashesIt.value();
errored.remove(trackerURL);
if (errored.isEmpty())
{
m_errors.erase(errorHashesIt);
item(ERROR_ROW)->setText(tr("Error (%1)").arg(m_errors.size()));
if (currentRow() == ERROR_ROW)
applyFilter(ERROR_ROW);
}
}
const auto warningHashesIt = m_warnings.find(id);
if (warningHashesIt != m_warnings.end())
{
QSet<QString> &warned = *warningHashesIt;
warned.remove(trackerURL);
if (warned.isEmpty())
{
m_warnings.erase(warningHashesIt);
item(WARNING_ROW)->setText(tr("Warning (%1)").arg(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)"_qs.arg(host, QString::number(torrentIDs.size())));
}
else
{
trackerItem = item(TRACKERLESS_ROW);
trackerItem->setText(tr("Trackerless (%1)").arg(torrentIDs.size()));
}
m_trackers.insert(host, {torrentIDs, trackerItem});
if (currentItem() == trackerItem)
applyFilter(currentRow());
}
void TrackersFilterWidget::setDownloadTrackerFavicon(bool value)
{
if (value == m_downloadTrackerFavicon) return;
m_downloadTrackerFavicon = value;
if (m_downloadTrackerFavicon)
{
for (auto i = m_trackers.cbegin(); i != m_trackers.cend(); ++i)
{
const QString &tracker = i.key();
if (!tracker.isEmpty())
{
const QString scheme = getScheme(tracker);
downloadFavicon(u"%1://%2/favicon.ico"_qs
.arg((scheme.startsWith(u"http") ? scheme : u"http"_qs), getHost(tracker)));
}
}
}
}
void TrackersFilterWidget::handleTrackerEntriesUpdated(const BitTorrent::Torrent *torrent
, const QHash<QString, BitTorrent::TrackerEntry> &updatedTrackerEntries)
{
const BitTorrent::TorrentID id = torrent->id();
auto errorHashesIt = m_errors.find(id);
auto warningHashesIt = m_warnings.find(id);
for (const BitTorrent::TrackerEntry &trackerEntry : updatedTrackerEntries)
{
if (trackerEntry.status == BitTorrent::TrackerEntry::Working)
{
if (errorHashesIt != m_errors.end())
{
QSet<QString> &errored = errorHashesIt.value();
errored.remove(trackerEntry.url);
}
if (trackerEntry.message.isEmpty())
{
if (warningHashesIt != m_warnings.end())
{
QSet<QString> &warned = *warningHashesIt;
warned.remove(trackerEntry.url);
}
}
else
{
if (warningHashesIt == m_warnings.end())
warningHashesIt = m_warnings.insert(id, {});
warningHashesIt.value().insert(trackerEntry.url);
}
}
else if (trackerEntry.status == BitTorrent::TrackerEntry::NotWorking)
{
if (errorHashesIt == m_errors.end())
errorHashesIt = m_errors.insert(id, {});
errorHashesIt.value().insert(trackerEntry.url);
}
}
if ((errorHashesIt != m_errors.end()) && errorHashesIt.value().isEmpty())
m_errors.erase(errorHashesIt);
if ((warningHashesIt != m_warnings.end()) && warningHashesIt.value().isEmpty())
m_warnings.erase(warningHashesIt);
item(ERROR_ROW)->setText(tr("Error (%1)").arg(m_errors.size()));
item(WARNING_ROW)->setText(tr("Warning (%1)").arg(m_warnings.size()));
if (currentRow() == ERROR_ROW)
applyFilter(ERROR_ROW);
else if (currentRow() == WARNING_ROW)
applyFilter(WARNING_ROW);
}
void TrackersFilterWidget::downloadFavicon(const QString &url)
{
if (!m_downloadTrackerFavicon) return;
Net::DownloadManager::instance()->download(
Net::DownloadRequest(url).saveToFile(true), Preferences::instance()->useProxyForGeneralPurposes()
, this, &TrackersFilterWidget::handleFavicoDownloadFinished);
}
void TrackersFilterWidget::handleFavicoDownloadFinished(const Net::DownloadResult &result)
{
if (result.status != Net::DownloadStatus::Success)
{
if (result.url.endsWith(u".ico", Qt::CaseInsensitive))
downloadFavicon(result.url.left(result.url.size() - 4) + u".png");
return;
}
const QString host = getHost(result.url);
if (!m_trackers.contains(host))
{
Utils::Fs::removeFile(result.filePath);
return;
}
QListWidgetItem *trackerItem = item(rowFromTracker(host));
if (!trackerItem) return;
const QIcon icon {result.filePath.data()};
//Detect a non-decodable icon
QList<QSize> sizes = icon.availableSizes();
bool invalid = (sizes.isEmpty() || icon.pixmap(sizes.first()).isNull());
if (invalid)
{
if (result.url.endsWith(u".ico", Qt::CaseInsensitive))
downloadFavicon(result.url.left(result.url.size() - 4) + u".png");
Utils::Fs::removeFile(result.filePath);
}
else
{
trackerItem->setData(Qt::DecorationRole, QIcon(result.filePath.data()));
m_iconPaths.append(result.filePath);
}
}
void TrackersFilterWidget::showMenu()
{
QMenu *menu = new QMenu(this);
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->addAction(UIThemeManager::instance()->getIcon(u"torrent-start"_qs, u"media-playback-start"_qs), tr("Resume torrents")
, transferList(), &TransferListWidget::startVisibleTorrents);
menu->addAction(UIThemeManager::instance()->getIcon(u"torrent-stop"_qs, u"media-playback-pause"_qs), tr("Pause torrents")
, transferList(), &TransferListWidget::pauseVisibleTorrents);
menu->addAction(UIThemeManager::instance()->getIcon(u"list-remove"_qs), tr("Remove torrents")
, transferList(), &TransferListWidget::deleteVisibleTorrents);
menu->popup(QCursor::pos());
}
void TrackersFilterWidget::applyFilter(const int row)
{
if (row == ALL_ROW)
transferList()->applyTrackerFilterAll();
else if (isVisible())
transferList()->applyTrackerFilter(getTorrentIDs(row));
}
void TrackersFilterWidget::handleTorrentsLoaded(const QVector<BitTorrent::Torrent *> &torrents)
{
QHash<QString, QVector<BitTorrent::TorrentID>> torrentsPerTracker;
for (const BitTorrent::Torrent *torrent : torrents)
{
const BitTorrent::TorrentID torrentID = torrent->id();
const QVector<BitTorrent::TrackerEntry> trackers = torrent->trackers();
for (const BitTorrent::TrackerEntry &tracker : trackers)
torrentsPerTracker[tracker.url].append(torrentID);
// Check for trackerless torrent
if (trackers.isEmpty())
torrentsPerTracker[NULL_HOST].append(torrentID);
}
for (auto it = torrentsPerTracker.cbegin(); it != torrentsPerTracker.cend(); ++it)
{
const QString &trackerURL = it.key();
const QVector<BitTorrent::TorrentID> &torrents = it.value();
addItems(trackerURL, torrents);
}
m_totalTorrents += torrents.count();
item(ALL_ROW)->setText(tr("All (%1)", "this is for the tracker filter").arg(m_totalTorrents));
}
void TrackersFilterWidget::torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent)
{
const BitTorrent::TorrentID torrentID = torrent->id();
const QVector<BitTorrent::TrackerEntry> trackers = torrent->trackers();
for (const BitTorrent::TrackerEntry &tracker : trackers)
removeItem(tracker.url, torrentID);
// Check for trackerless torrent
if (trackers.isEmpty())
removeItem(NULL_HOST, torrentID);
item(ALL_ROW)->setText(tr("All (%1)", "this is for the tracker filter").arg(--m_totalTorrents));
}
QString TrackersFilterWidget::trackerFromRow(int row) const
{
Q_ASSERT(row > 1);
const QString tracker = item(row)->text();
QStringList parts = tracker.split(u' ');
Q_ASSERT(parts.size() >= 2);
parts.removeLast(); // Remove trailing number
return parts.join(u' ');
}
int TrackersFilterWidget::rowFromTracker(const QString &tracker) const
{
Q_ASSERT(!tracker.isEmpty());
for (int i = 4; 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 ERROR_ROW:
return {m_errors.keyBegin(), m_errors.keyEnd()};
case WARNING_ROW:
return {m_warnings.keyBegin(), m_warnings.keyEnd()};
default:
return m_trackers.value(trackerFromRow(row)).torrents;
}
}

View File

@ -0,0 +1,93 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#pragma once
#include <QtContainerFwd>
#include <QHash>
#include "base/bittorrent/trackerentry.h"
#include "base/path.h"
#include "basefilterwidget.h"
class TransferListWidget;
namespace Net
{
struct DownloadResult;
}
class TrackersFilterWidget final : public BaseFilterWidget
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(TrackersFilterWidget)
public:
TrackersFilterWidget(QWidget *parent, TransferListWidget *transferList, bool downloadFavicon);
~TrackersFilterWidget() override;
void addTrackers(const BitTorrent::Torrent *torrent, const QVector<BitTorrent::TrackerEntry> &trackers);
void removeTrackers(const BitTorrent::Torrent *torrent, const QStringList &trackers);
void refreshTrackers(const BitTorrent::Torrent *torrent);
void changeTrackerless(const BitTorrent::Torrent *torrent, bool trackerless);
void handleTrackerEntriesUpdated(const BitTorrent::Torrent *torrent
, const QHash<QString, BitTorrent::TrackerEntry> &updatedTrackerEntries);
void setDownloadTrackerFavicon(bool value);
private slots:
void handleFavicoDownloadFinished(const Net::DownloadResult &result);
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 QVector<BitTorrent::Torrent *> &torrents) override;
void torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent) override;
void addItems(const QString &trackerURL, const QVector<BitTorrent::TorrentID> &torrents);
void removeItem(const QString &trackerURL, const BitTorrent::TorrentID &id);
QString trackerFromRow(int row) const;
int rowFromTracker(const QString &tracker) const;
QSet<BitTorrent::TorrentID> getTorrentIDs(int row) const;
void downloadFavicon(const QString &url);
struct TrackerData
{
QSet<BitTorrent::TorrentID> torrents;
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_warnings; // <torrent ID, tracker hosts>
PathList m_iconPaths;
int m_totalTorrents = 0;
bool m_downloadTrackerFavicon = false;
};

View File

@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@ -48,44 +49,16 @@
#include "base/torrentfilter.h"
#include "base/utils/compare.h"
#include "base/utils/fs.h"
#include "categoryfilterwidget.h"
#include "tagfilterwidget.h"
#include "transferlistfilters/categoryfilterwidget.h"
#include "transferlistfilters/statusfilterwidget.h"
#include "transferlistfilters/tagfilterwidget.h"
#include "transferlistfilters/trackersfilterwidget.h"
#include "transferlistwidget.h"
#include "uithememanager.h"
#include "utils.h"
namespace
{
enum TRACKER_FILTER_ROW
{
ALL_ROW,
TRACKERLESS_ROW,
ERROR_ROW,
WARNING_ROW
};
QString getScheme(const QString &tracker)
{
const QString scheme = QUrl(tracker).scheme();
return !scheme.isEmpty() ? scheme : u"http"_qs;
}
QString getHost(const QString &url)
{
// We want the domain + tld. Subdomains should be disregarded
// If failed to parse the domain or IP address, original input should be returned
const QString host = QUrl(url).host();
if (host.isEmpty())
return url;
// host is in IP format
if (!QHostAddress(host).isNull())
return host;
return host.section(u'.', -2, -1);
}
class ArrowCheckBox final : public QCheckBox
{
public:
@ -109,693 +82,6 @@ namespace
style()->drawControl(QStyle::CE_CheckBoxLabel, &labelOption, &painter, this);
}
};
const QString NULL_HOST = u""_qs;
}
BaseFilterWidget::BaseFilterWidget(QWidget *parent, TransferListWidget *transferList)
: QListWidget(parent)
, transferList(transferList)
{
setFrameShape(QFrame::NoFrame);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
setUniformItemSizes(true);
setSpacing(0);
setIconSize(Utils::Gui::smallIconSize());
#if defined(Q_OS_MACOS)
setAttribute(Qt::WA_MacShowFocusRect, false);
#endif
setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, &BaseFilterWidget::customContextMenuRequested, this, &BaseFilterWidget::showMenu);
connect(this, &BaseFilterWidget::currentRowChanged, this, &BaseFilterWidget::applyFilter);
connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentsLoaded
, this, &BaseFilterWidget::handleTorrentsLoaded);
connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentAboutToBeRemoved
, this, &BaseFilterWidget::torrentAboutToBeDeleted);
}
QSize BaseFilterWidget::sizeHint() const
{
return
{
// Width should be exactly the width of the content
sizeHintForColumn(0),
// Height should be exactly the height of the content
static_cast<int>((sizeHintForRow(0) + 2 * spacing()) * (count() + 0.5)),
};
}
QSize BaseFilterWidget::minimumSizeHint() const
{
QSize size = sizeHint();
size.setWidth(6);
return size;
}
void BaseFilterWidget::toggleFilter(bool checked)
{
setVisible(checked);
if (checked)
applyFilter(currentRow());
else
applyFilter(ALL_ROW);
}
StatusFilterWidget::StatusFilterWidget(QWidget *parent, TransferListWidget *transferList)
: BaseFilterWidget(parent, transferList)
{
// Add status filters
auto *all = new QListWidgetItem(this);
all->setData(Qt::DisplayRole, tr("All (0)", "this is for the status filter"));
all->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"filter-all"_qs, u"filterall"_qs));
auto *downloading = new QListWidgetItem(this);
downloading->setData(Qt::DisplayRole, tr("Downloading (0)"));
downloading->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"downloading"_qs));
auto *seeding = new QListWidgetItem(this);
seeding->setData(Qt::DisplayRole, tr("Seeding (0)"));
seeding->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"upload"_qs, u"uploading"_qs));
auto *completed = new QListWidgetItem(this);
completed->setData(Qt::DisplayRole, tr("Completed (0)"));
completed->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"checked-completed"_qs, u"completed"_qs));
auto *resumed = new QListWidgetItem(this);
resumed->setData(Qt::DisplayRole, tr("Resumed (0)"));
resumed->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"torrent-start"_qs, u"media-playback-start"_qs));
auto *paused = new QListWidgetItem(this);
paused->setData(Qt::DisplayRole, tr("Paused (0)"));
paused->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"stopped"_qs, u"media-playback-pause"_qs));
auto *active = new QListWidgetItem(this);
active->setData(Qt::DisplayRole, tr("Active (0)"));
active->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"filter-active"_qs, u"filteractive"_qs));
auto *inactive = new QListWidgetItem(this);
inactive->setData(Qt::DisplayRole, tr("Inactive (0)"));
inactive->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"filter-inactive"_qs, u"filterinactive"_qs));
auto *stalled = new QListWidgetItem(this);
stalled->setData(Qt::DisplayRole, tr("Stalled (0)"));
stalled->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"filter-stalled"_qs, u"filterstalled"_qs));
auto *stalledUploading = new QListWidgetItem(this);
stalledUploading->setData(Qt::DisplayRole, tr("Stalled Uploading (0)"));
stalledUploading->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"stalledUP"_qs));
auto *stalledDownloading = new QListWidgetItem(this);
stalledDownloading->setData(Qt::DisplayRole, tr("Stalled Downloading (0)"));
stalledDownloading->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"stalledDL"_qs));
auto *checking = new QListWidgetItem(this);
checking->setData(Qt::DisplayRole, tr("Checking (0)"));
checking->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"force-recheck"_qs, u"checking"_qs));
auto *moving = new QListWidgetItem(this);
moving->setData(Qt::DisplayRole, tr("Moving (0)"));
moving->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"set-location"_qs));
auto *errored = new QListWidgetItem(this);
errored->setData(Qt::DisplayRole, tr("Errored (0)"));
errored->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"error"_qs));
const Preferences *const pref = Preferences::instance();
setCurrentRow(pref->getTransSelFilter(), QItemSelectionModel::SelectCurrent);
toggleFilter(pref->getStatusFilterState());
populate();
connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentsUpdated
, this, &StatusFilterWidget::handleTorrentsUpdated);
}
StatusFilterWidget::~StatusFilterWidget()
{
Preferences::instance()->setTransSelFilter(currentRow());
}
void StatusFilterWidget::populate()
{
m_torrentsStatus.clear();
const QVector<BitTorrent::Torrent *> torrents = BitTorrent::Session::instance()->torrents();
for (const BitTorrent::Torrent *torrent : torrents)
updateTorrentStatus(torrent);
updateTexts();
}
void StatusFilterWidget::updateTorrentStatus(const BitTorrent::Torrent *torrent)
{
TorrentFilterBitset &torrentStatus = m_torrentsStatus[torrent];
const auto update = [torrent, &torrentStatus](const TorrentFilter::Type status, int &counter)
{
const bool hasStatus = torrentStatus[status];
const bool needStatus = TorrentFilter(status).match(torrent);
if (needStatus && !hasStatus)
{
++counter;
torrentStatus.set(status);
}
else if (!needStatus && hasStatus)
{
--counter;
torrentStatus.reset(status);
}
};
update(TorrentFilter::Downloading, m_nbDownloading);
update(TorrentFilter::Seeding, m_nbSeeding);
update(TorrentFilter::Completed, m_nbCompleted);
update(TorrentFilter::Resumed, m_nbResumed);
update(TorrentFilter::Paused, m_nbPaused);
update(TorrentFilter::Active, m_nbActive);
update(TorrentFilter::Inactive, m_nbInactive);
update(TorrentFilter::StalledUploading, m_nbStalledUploading);
update(TorrentFilter::StalledDownloading, m_nbStalledDownloading);
update(TorrentFilter::Checking, m_nbChecking);
update(TorrentFilter::Moving, m_nbMoving);
update(TorrentFilter::Errored, m_nbErrored);
m_nbStalled = m_nbStalledUploading + m_nbStalledDownloading;
}
void StatusFilterWidget::updateTexts()
{
const qsizetype torrentsCount = BitTorrent::Session::instance()->torrentsCount();
item(TorrentFilter::All)->setData(Qt::DisplayRole, tr("All (%1)").arg(torrentsCount));
item(TorrentFilter::Downloading)->setData(Qt::DisplayRole, tr("Downloading (%1)").arg(m_nbDownloading));
item(TorrentFilter::Seeding)->setData(Qt::DisplayRole, tr("Seeding (%1)").arg(m_nbSeeding));
item(TorrentFilter::Completed)->setData(Qt::DisplayRole, tr("Completed (%1)").arg(m_nbCompleted));
item(TorrentFilter::Resumed)->setData(Qt::DisplayRole, tr("Resumed (%1)").arg(m_nbResumed));
item(TorrentFilter::Paused)->setData(Qt::DisplayRole, tr("Paused (%1)").arg(m_nbPaused));
item(TorrentFilter::Active)->setData(Qt::DisplayRole, tr("Active (%1)").arg(m_nbActive));
item(TorrentFilter::Inactive)->setData(Qt::DisplayRole, tr("Inactive (%1)").arg(m_nbInactive));
item(TorrentFilter::Stalled)->setData(Qt::DisplayRole, tr("Stalled (%1)").arg(m_nbStalled));
item(TorrentFilter::StalledUploading)->setData(Qt::DisplayRole, tr("Stalled Uploading (%1)").arg(m_nbStalledUploading));
item(TorrentFilter::StalledDownloading)->setData(Qt::DisplayRole, tr("Stalled Downloading (%1)").arg(m_nbStalledDownloading));
item(TorrentFilter::Checking)->setData(Qt::DisplayRole, tr("Checking (%1)").arg(m_nbChecking));
item(TorrentFilter::Moving)->setData(Qt::DisplayRole, tr("Moving (%1)").arg(m_nbMoving));
item(TorrentFilter::Errored)->setData(Qt::DisplayRole, tr("Errored (%1)").arg(m_nbErrored));
}
void StatusFilterWidget::handleTorrentsUpdated(const QVector<BitTorrent::Torrent *> torrents)
{
for (const BitTorrent::Torrent *torrent : torrents)
updateTorrentStatus(torrent);
updateTexts();
}
void StatusFilterWidget::showMenu()
{
QMenu *menu = new QMenu(this);
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->addAction(UIThemeManager::instance()->getIcon(u"torrent-start"_qs, u"media-playback-start"_qs), tr("Resume torrents")
, transferList, &TransferListWidget::startVisibleTorrents);
menu->addAction(UIThemeManager::instance()->getIcon(u"torrent-stop"_qs, u"media-playback-pause"_qs), tr("Pause torrents")
, transferList, &TransferListWidget::pauseVisibleTorrents);
menu->addAction(UIThemeManager::instance()->getIcon(u"list-remove"_qs), tr("Remove torrents")
, transferList, &TransferListWidget::deleteVisibleTorrents);
menu->popup(QCursor::pos());
}
void StatusFilterWidget::applyFilter(int row)
{
transferList->applyStatusFilter(row);
}
void StatusFilterWidget::handleTorrentsLoaded(const QVector<BitTorrent::Torrent *> &torrents)
{
for (const BitTorrent::Torrent *torrent : torrents)
updateTorrentStatus(torrent);
updateTexts();
}
void StatusFilterWidget::torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent)
{
const TorrentFilterBitset status = m_torrentsStatus.take(torrent);
if (status[TorrentFilter::Downloading])
--m_nbDownloading;
if (status[TorrentFilter::Seeding])
--m_nbSeeding;
if (status[TorrentFilter::Completed])
--m_nbCompleted;
if (status[TorrentFilter::Resumed])
--m_nbResumed;
if (status[TorrentFilter::Paused])
--m_nbPaused;
if (status[TorrentFilter::Active])
--m_nbActive;
if (status[TorrentFilter::Inactive])
--m_nbInactive;
if (status[TorrentFilter::StalledUploading])
--m_nbStalledUploading;
if (status[TorrentFilter::StalledDownloading])
--m_nbStalledDownloading;
if (status[TorrentFilter::Checking])
--m_nbChecking;
if (status[TorrentFilter::Moving])
--m_nbMoving;
if (status[TorrentFilter::Errored])
--m_nbErrored;
m_nbStalled = m_nbStalledUploading + m_nbStalledDownloading;
updateTexts();
}
TrackerFiltersList::TrackerFiltersList(QWidget *parent, TransferListWidget *transferList, const bool downloadFavicon)
: BaseFilterWidget(parent, transferList)
, m_downloadTrackerFavicon(downloadFavicon)
{
auto *allTrackers = new QListWidgetItem(this);
allTrackers->setData(Qt::DisplayRole, tr("All (0)", "this is for the tracker filter"));
allTrackers->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"trackers"_qs, u"network-server"_qs));
auto *noTracker = new QListWidgetItem(this);
noTracker->setData(Qt::DisplayRole, tr("Trackerless (0)"));
noTracker->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"trackerless"_qs, u"network-server"_qs));
auto *errorTracker = new QListWidgetItem(this);
errorTracker->setData(Qt::DisplayRole, tr("Error (0)"));
errorTracker->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"tracker-error"_qs, u"dialog-error"_qs));
auto *warningTracker = new QListWidgetItem(this);
warningTracker->setData(Qt::DisplayRole, tr("Warning (0)"));
warningTracker->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"tracker-warning"_qs, u"dialog-warning"_qs));
m_trackers[NULL_HOST] = {{}, noTracker};
handleTorrentsLoaded(BitTorrent::Session::instance()->torrents());
setCurrentRow(0, QItemSelectionModel::SelectCurrent);
toggleFilter(Preferences::instance()->getTrackerFilterState());
}
TrackerFiltersList::~TrackerFiltersList()
{
for (const Path &iconPath : asConst(m_iconPaths))
Utils::Fs::removeFile(iconPath);
}
void TrackerFiltersList::addTrackers(const BitTorrent::Torrent *torrent, const QVector<BitTorrent::TrackerEntry> &trackers)
{
const BitTorrent::TorrentID torrentID = torrent->id();
for (const BitTorrent::TrackerEntry &tracker : trackers)
addItems(tracker.url, {torrentID});
}
void TrackerFiltersList::removeTrackers(const BitTorrent::Torrent *torrent, const QStringList &trackers)
{
const BitTorrent::TorrentID torrentID = torrent->id();
for (const QString &tracker : trackers)
removeItem(tracker, torrentID);
}
void TrackerFiltersList::refreshTrackers(const BitTorrent::Torrent *torrent)
{
const BitTorrent::TorrentID torrentID = torrent->id();
m_errors.remove(torrentID);
m_warnings.remove(torrentID);
Algorithm::removeIf(m_trackers, [this, &torrentID](const QString &host, TrackerData &trackerData)
{
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(u"%1 (%2)"_qs.arg((host.isEmpty() ? tr("Trackerless") : host), QString::number(torrentIDs.size())));
return false;
});
const QVector<BitTorrent::TrackerEntry> trackerEntries = torrent->trackers();
const bool isTrackerless = trackerEntries.isEmpty();
if (isTrackerless)
{
addItems(NULL_HOST, {torrentID});
}
else
{
for (const BitTorrent::TrackerEntry &trackerEntry : trackerEntries)
addItems(trackerEntry.url, {torrentID});
}
updateGeometry();
}
void TrackerFiltersList::changeTrackerless(const BitTorrent::Torrent *torrent, const bool trackerless)
{
if (trackerless)
addItems(NULL_HOST, {torrent->id()});
else
removeItem(NULL_HOST, torrent->id());
}
void TrackerFiltersList::addItems(const QString &trackerURL, const QVector<BitTorrent::TorrentID> &torrents)
{
const QString host = getHost(trackerURL);
auto trackersIt = m_trackers.find(host);
const bool exists = (trackersIt != m_trackers.end());
QListWidgetItem *trackerItem = nullptr;
if (exists)
{
trackerItem = trackersIt->item;
}
else
{
trackerItem = new QListWidgetItem();
trackerItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"trackers"_qs, u"network-server"_qs));
const TrackerData trackerData {{}, trackerItem};
trackersIt = m_trackers.insert(host, trackerData);
const QString scheme = getScheme(trackerURL);
downloadFavicon(u"%1://%2/favicon.ico"_qs.arg((scheme.startsWith(u"http") ? scheme : u"http"_qs), host));
}
Q_ASSERT(trackerItem);
QSet<BitTorrent::TorrentID> &torrentIDs = trackersIt->torrents;
for (const BitTorrent::TorrentID &torrentID : torrents)
torrentIDs.insert(torrentID);
trackerItem->setText(u"%1 (%2)"_qs.arg(((host == NULL_HOST) ? tr("Trackerless") : host), QString::number(torrentIDs.size())));
if (exists)
{
if (item(currentRow()) == trackerItem)
applyFilter(currentRow());
return;
}
Q_ASSERT(count() >= 4);
const Utils::Compare::NaturalLessThan<Qt::CaseSensitive> naturalLessThan {};
int insPos = count();
for (int i = 4; i < count(); ++i)
{
if (naturalLessThan(host, item(i)->text()))
{
insPos = i;
break;
}
}
QListWidget::insertItem(insPos, trackerItem);
updateGeometry();
}
void TrackerFiltersList::removeItem(const QString &trackerURL, const BitTorrent::TorrentID &id)
{
const QString host = getHost(trackerURL);
QSet<BitTorrent::TorrentID> torrentIDs = m_trackers.value(host).torrents;
torrentIDs.remove(id);
QListWidgetItem *trackerItem = nullptr;
if (!host.isEmpty())
{
// Remove from 'Error' and 'Warning' view
const auto errorHashesIt = m_errors.find(id);
if (errorHashesIt != m_errors.end())
{
QSet<QString> &errored = errorHashesIt.value();
errored.remove(trackerURL);
if (errored.isEmpty())
{
m_errors.erase(errorHashesIt);
item(ERROR_ROW)->setText(tr("Error (%1)").arg(m_errors.size()));
if (currentRow() == ERROR_ROW)
applyFilter(ERROR_ROW);
}
}
const auto warningHashesIt = m_warnings.find(id);
if (warningHashesIt != m_warnings.end())
{
QSet<QString> &warned = *warningHashesIt;
warned.remove(trackerURL);
if (warned.isEmpty())
{
m_warnings.erase(warningHashesIt);
item(WARNING_ROW)->setText(tr("Warning (%1)").arg(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)"_qs.arg(host, QString::number(torrentIDs.size())));
}
else
{
trackerItem = item(TRACKERLESS_ROW);
trackerItem->setText(tr("Trackerless (%1)").arg(torrentIDs.size()));
}
m_trackers.insert(host, {torrentIDs, trackerItem});
if (currentItem() == trackerItem)
applyFilter(currentRow());
}
void TrackerFiltersList::setDownloadTrackerFavicon(bool value)
{
if (value == m_downloadTrackerFavicon) return;
m_downloadTrackerFavicon = value;
if (m_downloadTrackerFavicon)
{
for (auto i = m_trackers.cbegin(); i != m_trackers.cend(); ++i)
{
const QString &tracker = i.key();
if (!tracker.isEmpty())
{
const QString scheme = getScheme(tracker);
downloadFavicon(u"%1://%2/favicon.ico"_qs
.arg((scheme.startsWith(u"http") ? scheme : u"http"_qs), getHost(tracker)));
}
}
}
}
void TrackerFiltersList::handleTrackerEntriesUpdated(const BitTorrent::Torrent *torrent
, const QHash<QString, BitTorrent::TrackerEntry> &updatedTrackerEntries)
{
const BitTorrent::TorrentID id = torrent->id();
auto errorHashesIt = m_errors.find(id);
auto warningHashesIt = m_warnings.find(id);
for (const BitTorrent::TrackerEntry &trackerEntry : updatedTrackerEntries)
{
if (trackerEntry.status == BitTorrent::TrackerEntry::Working)
{
if (errorHashesIt != m_errors.end())
{
QSet<QString> &errored = errorHashesIt.value();
errored.remove(trackerEntry.url);
}
if (trackerEntry.message.isEmpty())
{
if (warningHashesIt != m_warnings.end())
{
QSet<QString> &warned = *warningHashesIt;
warned.remove(trackerEntry.url);
}
}
else
{
if (warningHashesIt == m_warnings.end())
warningHashesIt = m_warnings.insert(id, {});
warningHashesIt.value().insert(trackerEntry.url);
}
}
else if (trackerEntry.status == BitTorrent::TrackerEntry::NotWorking)
{
if (errorHashesIt == m_errors.end())
errorHashesIt = m_errors.insert(id, {});
errorHashesIt.value().insert(trackerEntry.url);
}
}
if ((errorHashesIt != m_errors.end()) && errorHashesIt.value().isEmpty())
m_errors.erase(errorHashesIt);
if ((warningHashesIt != m_warnings.end()) && warningHashesIt.value().isEmpty())
m_warnings.erase(warningHashesIt);
item(ERROR_ROW)->setText(tr("Error (%1)").arg(m_errors.size()));
item(WARNING_ROW)->setText(tr("Warning (%1)").arg(m_warnings.size()));
if (currentRow() == ERROR_ROW)
applyFilter(ERROR_ROW);
else if (currentRow() == WARNING_ROW)
applyFilter(WARNING_ROW);
}
void TrackerFiltersList::downloadFavicon(const QString &url)
{
if (!m_downloadTrackerFavicon) return;
Net::DownloadManager::instance()->download(
Net::DownloadRequest(url).saveToFile(true), Preferences::instance()->useProxyForGeneralPurposes()
, this, &TrackerFiltersList::handleFavicoDownloadFinished);
}
void TrackerFiltersList::handleFavicoDownloadFinished(const Net::DownloadResult &result)
{
if (result.status != Net::DownloadStatus::Success)
{
if (result.url.endsWith(u".ico", Qt::CaseInsensitive))
downloadFavicon(result.url.left(result.url.size() - 4) + u".png");
return;
}
const QString host = getHost(result.url);
if (!m_trackers.contains(host))
{
Utils::Fs::removeFile(result.filePath);
return;
}
QListWidgetItem *trackerItem = item(rowFromTracker(host));
if (!trackerItem) return;
const QIcon icon {result.filePath.data()};
//Detect a non-decodable icon
QList<QSize> sizes = icon.availableSizes();
bool invalid = (sizes.isEmpty() || icon.pixmap(sizes.first()).isNull());
if (invalid)
{
if (result.url.endsWith(u".ico", Qt::CaseInsensitive))
downloadFavicon(result.url.left(result.url.size() - 4) + u".png");
Utils::Fs::removeFile(result.filePath);
}
else
{
trackerItem->setData(Qt::DecorationRole, QIcon(result.filePath.data()));
m_iconPaths.append(result.filePath);
}
}
void TrackerFiltersList::showMenu()
{
QMenu *menu = new QMenu(this);
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->addAction(UIThemeManager::instance()->getIcon(u"torrent-start"_qs, u"media-playback-start"_qs), tr("Resume torrents")
, transferList, &TransferListWidget::startVisibleTorrents);
menu->addAction(UIThemeManager::instance()->getIcon(u"torrent-stop"_qs, u"media-playback-pause"_qs), tr("Pause torrents")
, transferList, &TransferListWidget::pauseVisibleTorrents);
menu->addAction(UIThemeManager::instance()->getIcon(u"list-remove"_qs), tr("Remove torrents")
, transferList, &TransferListWidget::deleteVisibleTorrents);
menu->popup(QCursor::pos());
}
void TrackerFiltersList::applyFilter(const int row)
{
if (row == ALL_ROW)
transferList->applyTrackerFilterAll();
else if (isVisible())
transferList->applyTrackerFilter(getTorrentIDs(row));
}
void TrackerFiltersList::handleTorrentsLoaded(const QVector<BitTorrent::Torrent *> &torrents)
{
QHash<QString, QVector<BitTorrent::TorrentID>> torrentsPerTracker;
for (const BitTorrent::Torrent *torrent : torrents)
{
const BitTorrent::TorrentID torrentID = torrent->id();
const QVector<BitTorrent::TrackerEntry> trackers = torrent->trackers();
for (const BitTorrent::TrackerEntry &tracker : trackers)
torrentsPerTracker[tracker.url].append(torrentID);
// Check for trackerless torrent
if (trackers.isEmpty())
torrentsPerTracker[NULL_HOST].append(torrentID);
}
for (auto it = torrentsPerTracker.cbegin(); it != torrentsPerTracker.cend(); ++it)
{
const QString &trackerURL = it.key();
const QVector<BitTorrent::TorrentID> &torrents = it.value();
addItems(trackerURL, torrents);
}
m_totalTorrents += torrents.count();
item(ALL_ROW)->setText(tr("All (%1)", "this is for the tracker filter").arg(m_totalTorrents));
}
void TrackerFiltersList::torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent)
{
const BitTorrent::TorrentID torrentID = torrent->id();
const QVector<BitTorrent::TrackerEntry> trackers = torrent->trackers();
for (const BitTorrent::TrackerEntry &tracker : trackers)
removeItem(tracker.url, torrentID);
// Check for trackerless torrent
if (trackers.isEmpty())
removeItem(NULL_HOST, torrentID);
item(ALL_ROW)->setText(tr("All (%1)", "this is for the tracker filter").arg(--m_totalTorrents));
}
QString TrackerFiltersList::trackerFromRow(int row) const
{
Q_ASSERT(row > 1);
const QString tracker = item(row)->text();
QStringList parts = tracker.split(u' ');
Q_ASSERT(parts.size() >= 2);
parts.removeLast(); // Remove trailing number
return parts.join(u' ');
}
int TrackerFiltersList::rowFromTracker(const QString &tracker) const
{
Q_ASSERT(!tracker.isEmpty());
for (int i = 4; i < count(); ++i)
{
if (tracker == trackerFromRow(i))
return i;
}
return -1;
}
QSet<BitTorrent::TorrentID> TrackerFiltersList::getTorrentIDs(const int row) const
{
switch (row)
{
case TRACKERLESS_ROW:
return m_trackers.value(NULL_HOST).torrents;
case ERROR_ROW:
return {m_errors.keyBegin(), m_errors.keyEnd()};
case WARNING_ROW:
return {m_warnings.keyBegin(), m_warnings.keyEnd()};
default:
return m_trackers.value(trackerFromRow(row)).torrents;
}
}
TransferListFiltersWidget::TransferListFiltersWidget(QWidget *parent, TransferListWidget *transferList, const bool downloadFavicon)
@ -878,44 +164,44 @@ TransferListFiltersWidget::TransferListFiltersWidget(QWidget *parent, TransferLi
trackerLabel->setFont(font);
frameLayout->addWidget(trackerLabel);
m_trackerFilters = new TrackerFiltersList(this, transferList, downloadFavicon);
frameLayout->addWidget(m_trackerFilters);
m_trackersFilterWidget = new TrackersFilterWidget(this, transferList, downloadFavicon);
frameLayout->addWidget(m_trackersFilterWidget);
connect(statusLabel, &QCheckBox::toggled, statusFilters, &StatusFilterWidget::toggleFilter);
connect(statusLabel, &QCheckBox::toggled, pref, &Preferences::setStatusFilterState);
connect(trackerLabel, &QCheckBox::toggled, m_trackerFilters, &TrackerFiltersList::toggleFilter);
connect(trackerLabel, &QCheckBox::toggled, m_trackersFilterWidget, &TrackersFilterWidget::toggleFilter);
connect(trackerLabel, &QCheckBox::toggled, pref, &Preferences::setTrackerFilterState);
}
void TransferListFiltersWidget::setDownloadTrackerFavicon(bool value)
{
m_trackerFilters->setDownloadTrackerFavicon(value);
m_trackersFilterWidget->setDownloadTrackerFavicon(value);
}
void TransferListFiltersWidget::addTrackers(const BitTorrent::Torrent *torrent, const QVector<BitTorrent::TrackerEntry> &trackers)
{
m_trackerFilters->addTrackers(torrent, trackers);
m_trackersFilterWidget->addTrackers(torrent, trackers);
}
void TransferListFiltersWidget::removeTrackers(const BitTorrent::Torrent *torrent, const QStringList &trackers)
{
m_trackerFilters->removeTrackers(torrent, trackers);
m_trackersFilterWidget->removeTrackers(torrent, trackers);
}
void TransferListFiltersWidget::refreshTrackers(const BitTorrent::Torrent *torrent)
{
m_trackerFilters->refreshTrackers(torrent);
m_trackersFilterWidget->refreshTrackers(torrent);
}
void TransferListFiltersWidget::changeTrackerless(const BitTorrent::Torrent *torrent, const bool trackerless)
{
m_trackerFilters->changeTrackerless(torrent, trackerless);
m_trackersFilterWidget->changeTrackerless(torrent, trackerless);
}
void TransferListFiltersWidget::trackerEntriesUpdated(const BitTorrent::Torrent *torrent
, const QHash<QString, BitTorrent::TrackerEntry> &updatedTrackerEntries)
{
m_trackerFilters->handleTrackerEntriesUpdated(torrent, updatedTrackerEntries);
m_trackersFilterWidget->handleTrackerEntriesUpdated(torrent, updatedTrackerEntries);
}
void TransferListFiltersWidget::onCategoryFilterStateChanged(bool enabled)

View File

@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@ -28,145 +29,19 @@
#pragma once
#include <bitset>
#include <QtContainerFwd>
#include <QFrame>
#include <QHash>
#include <QListWidget>
#include <QtContainerFwd>
#include "base/bittorrent/infohash.h"
#include "base/bittorrent/session.h"
#include "base/bittorrent/torrent.h"
#include "base/bittorrent/trackerentry.h"
#include "base/torrentfilter.h"
#include "base/path.h"
class QCheckBox;
class QResizeEvent;
class CategoryFilterWidget;
class StatusFilterWidget;
class TagFilterWidget;
class TrackersFilterWidget;
class TransferListWidget;
namespace Net
{
struct DownloadResult;
}
class BaseFilterWidget : public QListWidget
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(BaseFilterWidget)
public:
BaseFilterWidget(QWidget *parent, TransferListWidget *transferList);
QSize sizeHint() const override;
QSize minimumSizeHint() const override;
public slots:
void toggleFilter(bool checked);
protected:
TransferListWidget *transferList = nullptr;
private slots:
virtual void showMenu() = 0;
virtual void applyFilter(int row) = 0;
virtual void handleTorrentsLoaded(const QVector<BitTorrent::Torrent *> &torrents) = 0;
virtual void torrentAboutToBeDeleted(BitTorrent::Torrent *const) = 0;
};
class StatusFilterWidget final : public BaseFilterWidget
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(StatusFilterWidget)
public:
StatusFilterWidget(QWidget *parent, TransferListWidget *transferList);
~StatusFilterWidget() override;
private slots:
void handleTorrentsUpdated(const QVector<BitTorrent::Torrent *> torrents);
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 QVector<BitTorrent::Torrent *> &torrents) override;
void torrentAboutToBeDeleted(BitTorrent::Torrent *const) override;
void populate();
void updateTorrentStatus(const BitTorrent::Torrent *torrent);
void updateTexts();
using TorrentFilterBitset = std::bitset<32>; // approximated size, this should be the number of TorrentFilter::Type elements
QHash<const BitTorrent::Torrent *, TorrentFilterBitset> m_torrentsStatus;
int m_nbDownloading = 0;
int m_nbSeeding = 0;
int m_nbCompleted = 0;
int m_nbResumed = 0;
int m_nbPaused = 0;
int m_nbActive = 0;
int m_nbInactive = 0;
int m_nbStalled = 0;
int m_nbStalledUploading = 0;
int m_nbStalledDownloading = 0;
int m_nbChecking = 0;
int m_nbMoving = 0;
int m_nbErrored = 0;
};
class TrackerFiltersList final : public BaseFilterWidget
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(TrackerFiltersList)
public:
TrackerFiltersList(QWidget *parent, TransferListWidget *transferList, bool downloadFavicon);
~TrackerFiltersList() override;
void addTrackers(const BitTorrent::Torrent *torrent, const QVector<BitTorrent::TrackerEntry> &trackers);
void removeTrackers(const BitTorrent::Torrent *torrent, const QStringList &trackers);
void refreshTrackers(const BitTorrent::Torrent *torrent);
void changeTrackerless(const BitTorrent::Torrent *torrent, bool trackerless);
void handleTrackerEntriesUpdated(const BitTorrent::Torrent *torrent
, const QHash<QString, BitTorrent::TrackerEntry> &updatedTrackerEntries);
void setDownloadTrackerFavicon(bool value);
private slots:
void handleFavicoDownloadFinished(const Net::DownloadResult &result);
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 QVector<BitTorrent::Torrent *> &torrents) override;
void torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent) override;
void addItems(const QString &trackerURL, const QVector<BitTorrent::TorrentID> &torrents);
void removeItem(const QString &trackerURL, const BitTorrent::TorrentID &id);
QString trackerFromRow(int row) const;
int rowFromTracker(const QString &tracker) const;
QSet<BitTorrent::TorrentID> getTorrentIDs(int row) const;
void downloadFavicon(const QString &url);
struct TrackerData
{
QSet<BitTorrent::TorrentID> torrents;
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_warnings; // <torrent ID, tracker hosts>
PathList m_iconPaths;
int m_totalTorrents = 0;
bool m_downloadTrackerFavicon = false;
};
class TransferListFiltersWidget final : public QFrame
{
Q_OBJECT
@ -193,7 +68,7 @@ private:
void toggleTagFilter(bool enabled);
TransferListWidget *m_transferList = nullptr;
TrackerFiltersList *m_trackerFilters = nullptr;
TrackersFilterWidget *m_trackersFilterWidget = nullptr;
CategoryFilterWidget *m_categoryFilterWidget = nullptr;
TagFilterWidget *m_tagFilterWidget = nullptr;
};