diff --git a/src/base/preferences.cpp b/src/base/preferences.cpp index 9e2636997..a46d96161 100644 --- a/src/base/preferences.cpp +++ b/src/base/preferences.cpp @@ -1551,6 +1551,16 @@ void Preferences::setTransSelFilter(const int index) setValue(u"TransferListFilters/selectedFilterIndex"_qs, index); } +bool Preferences::getHideZeroStatusFilters() const +{ + return value(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)) diff --git a/src/base/preferences.h b/src/base/preferences.h index 2cc338133..57b6e0499 100644 --- a/src/base/preferences.h +++ b/src/base/preferences.h @@ -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; diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index f0ebcdc91..52657d84a 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -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 diff --git a/src/gui/gui.pri b/src/gui/gui.pri index c25d5800e..c07cdfa06 100644 --- a/src/gui/gui.pri +++ b/src/gui/gui.pri @@ -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 \ diff --git a/src/gui/optionsdialog.cpp b/src/gui/optionsdialog.cpp index 92455e7b1..78a7490c5 100644 --- a/src/gui/optionsdialog.cpp +++ b/src/gui/optionsdialog.cpp @@ -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()); diff --git a/src/gui/optionsdialog.ui b/src/gui/optionsdialog.ui index a1d1c0520..06c7ebb61 100644 --- a/src/gui/optionsdialog.ui +++ b/src/gui/optionsdialog.ui @@ -384,6 +384,13 @@ + + + + Auto hide zero status filters + + + @@ -3761,6 +3768,7 @@ Use ';' to split multiple entries. Can use wildcard '*'. checkAltRowColors actionTorrentDlOnDblClBox actionTorrentFnOnDblClBox + checkBoxHideZeroStatusFilters checkStartup checkShowSplash windowStateComboBox diff --git a/src/gui/transferlistfilters/basefilterwidget.cpp b/src/gui/transferlistfilters/basefilterwidget.cpp new file mode 100644 index 000000000..baf41d800 --- /dev/null +++ b/src/gui/transferlistfilters/basefilterwidget.cpp @@ -0,0 +1,92 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev + * Copyright (C) 2006 Christophe Dumez + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "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((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); +} diff --git a/src/gui/transferlistfilters/basefilterwidget.h b/src/gui/transferlistfilters/basefilterwidget.h new file mode 100644 index 000000000..bcb2753c8 --- /dev/null +++ b/src/gui/transferlistfilters/basefilterwidget.h @@ -0,0 +1,63 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev + * Copyright (C) 2006 Christophe Dumez + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include +#include + +#include "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 &torrents) = 0; + virtual void torrentAboutToBeDeleted(BitTorrent::Torrent *const) = 0; + +private: + TransferListWidget *m_transferList = nullptr; +}; diff --git a/src/gui/categoryfiltermodel.cpp b/src/gui/transferlistfilters/categoryfiltermodel.cpp similarity index 99% rename from src/gui/categoryfiltermodel.cpp rename to src/gui/transferlistfilters/categoryfiltermodel.cpp index 7b3844770..8229fcc40 100644 --- a/src/gui/categoryfiltermodel.cpp +++ b/src/gui/transferlistfilters/categoryfiltermodel.cpp @@ -33,7 +33,7 @@ #include "base/bittorrent/session.h" #include "base/global.h" -#include "uithememanager.h" +#include "gui/uithememanager.h" class CategoryModelItem { diff --git a/src/gui/categoryfiltermodel.h b/src/gui/transferlistfilters/categoryfiltermodel.h similarity index 100% rename from src/gui/categoryfiltermodel.h rename to src/gui/transferlistfilters/categoryfiltermodel.h diff --git a/src/gui/categoryfilterproxymodel.cpp b/src/gui/transferlistfilters/categoryfilterproxymodel.cpp similarity index 100% rename from src/gui/categoryfilterproxymodel.cpp rename to src/gui/transferlistfilters/categoryfilterproxymodel.cpp diff --git a/src/gui/categoryfilterproxymodel.h b/src/gui/transferlistfilters/categoryfilterproxymodel.h similarity index 100% rename from src/gui/categoryfilterproxymodel.h rename to src/gui/transferlistfilters/categoryfilterproxymodel.h diff --git a/src/gui/categoryfilterwidget.cpp b/src/gui/transferlistfilters/categoryfilterwidget.cpp similarity index 98% rename from src/gui/categoryfilterwidget.cpp rename to src/gui/transferlistfilters/categoryfilterwidget.cpp index 5c22bfbd4..a41316ef6 100644 --- a/src/gui/categoryfilterwidget.cpp +++ b/src/gui/transferlistfilters/categoryfilterwidget.cpp @@ -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 { diff --git a/src/gui/categoryfilterwidget.h b/src/gui/transferlistfilters/categoryfilterwidget.h similarity index 100% rename from src/gui/categoryfilterwidget.h rename to src/gui/transferlistfilters/categoryfilterwidget.h diff --git a/src/gui/transferlistfilters/statusfilterwidget.cpp b/src/gui/transferlistfilters/statusfilterwidget.cpp new file mode 100644 index 000000000..e0caf03f7 --- /dev/null +++ b/src/gui/transferlistfilters/statusfilterwidget.cpp @@ -0,0 +1,291 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev + * Copyright (C) 2006 Christophe Dumez + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "statusfilterwidget.h" + +#include +#include + +#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 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((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 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 &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(); +} diff --git a/src/gui/transferlistfilters/statusfilterwidget.h b/src/gui/transferlistfilters/statusfilterwidget.h new file mode 100644 index 000000000..1e13bc0d7 --- /dev/null +++ b/src/gui/transferlistfilters/statusfilterwidget.h @@ -0,0 +1,81 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev + * Copyright (C) 2006 Christophe Dumez + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include + +#include +#include + +#include "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 &torrents) override; + void torrentAboutToBeDeleted(BitTorrent::Torrent *const) override; + + void configure(); + + void update(const QVector 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 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; +}; diff --git a/src/gui/tagfiltermodel.cpp b/src/gui/transferlistfilters/tagfiltermodel.cpp similarity index 99% rename from src/gui/tagfiltermodel.cpp rename to src/gui/transferlistfilters/tagfiltermodel.cpp index c12029efc..c43bbff39 100644 --- a/src/gui/tagfiltermodel.cpp +++ b/src/gui/transferlistfilters/tagfiltermodel.cpp @@ -34,7 +34,7 @@ #include "base/bittorrent/session.h" #include "base/global.h" -#include "uithememanager.h" +#include "gui/uithememanager.h" namespace { diff --git a/src/gui/tagfiltermodel.h b/src/gui/transferlistfilters/tagfiltermodel.h similarity index 100% rename from src/gui/tagfiltermodel.h rename to src/gui/transferlistfilters/tagfiltermodel.h diff --git a/src/gui/tagfilterproxymodel.cpp b/src/gui/transferlistfilters/tagfilterproxymodel.cpp similarity index 100% rename from src/gui/tagfilterproxymodel.cpp rename to src/gui/transferlistfilters/tagfilterproxymodel.cpp diff --git a/src/gui/tagfilterproxymodel.h b/src/gui/transferlistfilters/tagfilterproxymodel.h similarity index 100% rename from src/gui/tagfilterproxymodel.h rename to src/gui/transferlistfilters/tagfilterproxymodel.h diff --git a/src/gui/tagfilterwidget.cpp b/src/gui/transferlistfilters/tagfilterwidget.cpp similarity index 98% rename from src/gui/tagfilterwidget.cpp rename to src/gui/transferlistfilters/tagfilterwidget.cpp index 125769fc3..1a97b82c0 100644 --- a/src/gui/tagfilterwidget.cpp +++ b/src/gui/transferlistfilters/tagfilterwidget.cpp @@ -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 { diff --git a/src/gui/tagfilterwidget.h b/src/gui/transferlistfilters/tagfilterwidget.h similarity index 100% rename from src/gui/tagfilterwidget.h rename to src/gui/transferlistfilters/tagfilterwidget.h diff --git a/src/gui/transferlistfilters/trackersfilterwidget.cpp b/src/gui/transferlistfilters/trackersfilterwidget.cpp new file mode 100644 index 000000000..6e15baa57 --- /dev/null +++ b/src/gui/transferlistfilters/trackersfilterwidget.cpp @@ -0,0 +1,513 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev + * Copyright (C) 2006 Christophe Dumez + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "trackersfilterwidget.h" + +#include +#include +#include +#include + +#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 &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 &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 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 &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 &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 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 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 &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 &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 &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 &errored = errorHashesIt.value(); + errored.remove(trackerEntry.url); + } + + if (trackerEntry.message.isEmpty()) + { + if (warningHashesIt != m_warnings.end()) + { + QSet &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 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 &torrents) +{ + QHash> torrentsPerTracker; + for (const BitTorrent::Torrent *torrent : torrents) + { + const BitTorrent::TorrentID torrentID = torrent->id(); + const QVector 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 &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 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 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; + } +} diff --git a/src/gui/transferlistfilters/trackersfilterwidget.h b/src/gui/transferlistfilters/trackersfilterwidget.h new file mode 100644 index 000000000..153b6b3ec --- /dev/null +++ b/src/gui/transferlistfilters/trackersfilterwidget.h @@ -0,0 +1,93 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev + * Copyright (C) 2006 Christophe Dumez + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include +#include + +#include "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 &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 &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 &torrents) override; + void torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent) override; + + void addItems(const QString &trackerURL, const QVector &torrents); + void removeItem(const QString &trackerURL, const BitTorrent::TorrentID &id); + QString trackerFromRow(int row) const; + int rowFromTracker(const QString &tracker) const; + QSet getTorrentIDs(int row) const; + void downloadFavicon(const QString &url); + + struct TrackerData + { + QSet torrents; + QListWidgetItem *item = nullptr; + }; + + QHash m_trackers; // + QHash> m_errors; // + QHash> m_warnings; // + PathList m_iconPaths; + int m_totalTorrents = 0; + bool m_downloadTrackerFavicon = false; +}; diff --git a/src/gui/transferlistfilterswidget.cpp b/src/gui/transferlistfilterswidget.cpp index e2eee9e0b..6dc82d8cf 100644 --- a/src/gui/transferlistfilterswidget.cpp +++ b/src/gui/transferlistfilterswidget.cpp @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -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((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 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 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 &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 &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 &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 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 &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 &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 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 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 &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 &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 &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 &errored = errorHashesIt.value(); - errored.remove(trackerEntry.url); - } - - if (trackerEntry.message.isEmpty()) - { - if (warningHashesIt != m_warnings.end()) - { - QSet &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 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 &torrents) -{ - QHash> torrentsPerTracker; - for (const BitTorrent::Torrent *torrent : torrents) - { - const BitTorrent::TorrentID torrentID = torrent->id(); - const QVector 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 &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 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 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 &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 &updatedTrackerEntries) { - m_trackerFilters->handleTrackerEntriesUpdated(torrent, updatedTrackerEntries); + m_trackersFilterWidget->handleTrackerEntriesUpdated(torrent, updatedTrackerEntries); } void TransferListFiltersWidget::onCategoryFilterStateChanged(bool enabled) diff --git a/src/gui/transferlistfilterswidget.h b/src/gui/transferlistfilterswidget.h index e02cee7ea..896756332 100644 --- a/src/gui/transferlistfilterswidget.h +++ b/src/gui/transferlistfilterswidget.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -28,145 +29,19 @@ #pragma once -#include - +#include #include #include -#include -#include -#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 &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 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 &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 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 &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 &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 &torrents) override; - void torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent) override; - - void addItems(const QString &trackerURL, const QVector &torrents); - void removeItem(const QString &trackerURL, const BitTorrent::TorrentID &id); - QString trackerFromRow(int row) const; - int rowFromTracker(const QString &tracker) const; - QSet getTorrentIDs(int row) const; - void downloadFavicon(const QString &url); - - struct TrackerData - { - QSet torrents; - QListWidgetItem *item = nullptr; - }; - - QHash m_trackers; // - QHash> m_errors; // - QHash> m_warnings; // - 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; };