From 7a4119259703179909dd37153031f11a3ef617e8 Mon Sep 17 00:00:00 2001 From: Vladimir Golovnev Date: Tue, 5 Dec 2023 17:01:09 +0300 Subject: [PATCH] Add a class to represent a tag PR #20028. Closes #19744. --- src/app/application.cpp | 5 +- src/base/CMakeLists.txt | 2 + src/base/bittorrent/addtorrentparams.cpp | 6 +- .../bittorrent/bencoderesumedatastorage.cpp | 6 +- src/base/bittorrent/dbresumedatastorage.cpp | 2 +- src/base/bittorrent/session.h | 18 ++-- src/base/bittorrent/sessionimpl.cpp | 40 ++++---- src/base/bittorrent/sessionimpl.h | 14 +-- src/base/bittorrent/torrent.h | 6 +- src/base/bittorrent/torrentimpl.cpp | 10 +- src/base/bittorrent/torrentimpl.h | 6 +- src/base/tag.cpp | 92 +++++++++++++++++++ src/base/tag.h | 67 ++++++++++++++ src/base/tagset.cpp | 7 +- src/base/tagset.h | 11 ++- src/base/torrentfilter.cpp | 22 ++--- src/base/torrentfilter.h | 11 ++- src/gui/addnewtorrentdialog.cpp | 4 +- src/gui/addtorrentparamswidget.cpp | 4 +- src/gui/mainwindow.cpp | 2 +- src/gui/torrenttagsdialog.cpp | 18 ++-- .../transferlistfilters/tagfiltermodel.cpp | 92 ++++++++----------- src/gui/transferlistfilters/tagfiltermodel.h | 20 ++-- .../tagfilterproxymodel.cpp | 4 +- .../transferlistfilters/tagfilterproxymodel.h | 6 +- .../transferlistfilters/tagfilterwidget.cpp | 42 ++++----- src/gui/transferlistfilters/tagfilterwidget.h | 9 +- src/gui/transferlistfilterswidget.cpp | 2 +- src/gui/transferlistmodel.cpp | 5 +- src/gui/transferlistsortmodel.cpp | 8 +- src/gui/transferlistsortmodel.h | 2 +- src/gui/transferlistwidget.cpp | 44 +++++---- src/gui/transferlistwidget.h | 9 +- src/webui/api/serialize/serialize_torrent.cpp | 8 +- src/webui/api/serialize/serialize_torrent.h | 2 +- src/webui/api/synccontroller.cpp | 20 ++-- src/webui/api/synccontroller.h | 9 +- src/webui/api/torrentscontroller.cpp | 37 +++++--- 38 files changed, 421 insertions(+), 251 deletions(-) create mode 100644 src/base/tag.cpp create mode 100644 src/base/tag.h diff --git a/src/app/application.cpp b/src/app/application.cpp index c7cb2a02c..b700483b5 100644 --- a/src/app/application.cpp +++ b/src/app/application.cpp @@ -522,7 +522,10 @@ void Application::runExternalProgram(const QString &programTemplate, const BitTo str.replace(i, 2, torrent->contentPath().toString()); break; case u'G': - str.replace(i, 2, torrent->tags().join(u","_s)); + { + const TagSet &tags = torrent->tags(); + str.replace(i, 2, QStringList(tags.cbegin(), tags.cend()).join(u","_s)); + } break; case u'I': str.replace(i, 2, (torrent->infoHash().v1().isValid() ? torrent->infoHash().v1().toString() : u"-"_s)); diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index ab24be9aa..bcfc5f763 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -85,6 +85,7 @@ add_library(qbt_base STATIC search/searchhandler.h search/searchpluginmanager.h settingsstorage.h + tag.h tagset.h torrentfileguard.h torrentfileswatcher.h @@ -174,6 +175,7 @@ add_library(qbt_base STATIC search/searchhandler.cpp search/searchpluginmanager.cpp settingsstorage.cpp + tag.cpp tagset.cpp torrentfileguard.cpp torrentfileswatcher.cpp diff --git a/src/base/bittorrent/addtorrentparams.cpp b/src/base/bittorrent/addtorrentparams.cpp index c657038dc..ec21cfe5e 100644 --- a/src/base/bittorrent/addtorrentparams.cpp +++ b/src/base/bittorrent/addtorrentparams.cpp @@ -60,7 +60,7 @@ namespace { TagSet tags; for (const QJsonValue &jsonVal : jsonArr) - tags.insert(jsonVal.toString()); + tags.insert(Tag(jsonVal.toString())); return tags; } @@ -68,8 +68,8 @@ namespace QJsonArray serializeTagSet(const TagSet &tags) { QJsonArray arr; - for (const QString &tag : tags) - arr.append(tag); + for (const Tag &tag : tags) + arr.append(tag.toString()); return arr; } diff --git a/src/base/bittorrent/bencoderesumedatastorage.cpp b/src/base/bittorrent/bencoderesumedatastorage.cpp index 4e6d0e46e..6ed39dab0 100644 --- a/src/base/bittorrent/bencoderesumedatastorage.cpp +++ b/src/base/bittorrent/bencoderesumedatastorage.cpp @@ -85,8 +85,8 @@ namespace { ListType entryList; entryList.reserve(input.size()); - for (const QString &setValue : input) - entryList.emplace_back(setValue.toStdString()); + for (const Tag &setValue : input) + entryList.emplace_back(setValue.toString().toStdString()); return entryList; } } @@ -263,7 +263,7 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::loadTorre { for (int i = 0; i < tagsNode.list_size(); ++i) { - const QString tag = fromLTString(tagsNode.list_string_value_at(i)); + const Tag tag {fromLTString(tagsNode.list_string_value_at(i))}; torrentParams.tags.insert(tag); } } diff --git a/src/base/bittorrent/dbresumedatastorage.cpp b/src/base/bittorrent/dbresumedatastorage.cpp index 3d92d3628..e68001724 100644 --- a/src/base/bittorrent/dbresumedatastorage.cpp +++ b/src/base/bittorrent/dbresumedatastorage.cpp @@ -854,7 +854,7 @@ namespace query.bindValue(DB_COLUMN_NAME.placeholder, m_resumeData.name); query.bindValue(DB_COLUMN_CATEGORY.placeholder, m_resumeData.category); query.bindValue(DB_COLUMN_TAGS.placeholder, (m_resumeData.tags.isEmpty() - ? QString() : m_resumeData.tags.join(u","_s))); + ? QString() : QStringList(m_resumeData.tags.cbegin(), m_resumeData.tags.cend()).join(u","_s))); query.bindValue(DB_COLUMN_CONTENT_LAYOUT.placeholder, Utils::String::fromEnum(m_resumeData.contentLayout)); query.bindValue(DB_COLUMN_RATIO_LIMIT.placeholder, static_cast(m_resumeData.ratioLimit * 1000)); query.bindValue(DB_COLUMN_SEEDING_TIME_LIMIT.placeholder, m_resumeData.seedingTimeLimit); diff --git a/src/base/bittorrent/session.h b/src/base/bittorrent/session.h index b84085b48..39f78bce5 100644 --- a/src/base/bittorrent/session.h +++ b/src/base/bittorrent/session.h @@ -33,6 +33,7 @@ #include #include "base/pathfwd.h" +#include "base/tagset.h" #include "addtorrentparams.h" #include "categoryoptions.h" #include "trackerentry.h" @@ -177,11 +178,10 @@ namespace BitTorrent virtual Path suggestedSavePath(const QString &categoryName, std::optional useAutoTMM) const = 0; virtual Path suggestedDownloadPath(const QString &categoryName, std::optional useAutoTMM) const = 0; - static bool isValidTag(const QString &tag); - virtual QSet tags() const = 0; - virtual bool hasTag(const QString &tag) const = 0; - virtual bool addTag(const QString &tag) = 0; - virtual bool removeTag(const QString &tag) = 0; + virtual TagSet tags() const = 0; + virtual bool hasTag(const Tag &tag) const = 0; + virtual bool addTag(const Tag &tag) = 0; + virtual bool removeTag(const Tag &tag) = 0; // Torrent Management Mode subsystem (TMM) // @@ -473,8 +473,8 @@ namespace BitTorrent void speedLimitModeChanged(bool alternative); void statsUpdated(); void subcategoriesSupportChanged(); - void tagAdded(const QString &tag); - void tagRemoved(const QString &tag); + void tagAdded(const Tag &tag); + void tagRemoved(const Tag &tag); void torrentAboutToBeRemoved(Torrent *torrent); void torrentAdded(Torrent *torrent); void torrentCategoryChanged(Torrent *torrent, const QString &oldCategory); @@ -487,8 +487,8 @@ namespace BitTorrent void torrentSavingModeChanged(Torrent *torrent); void torrentsLoaded(const QVector &torrents); void torrentsUpdated(const QVector &torrents); - void torrentTagAdded(Torrent *torrent, const QString &tag); - void torrentTagRemoved(Torrent *torrent, const QString &tag); + void torrentTagAdded(Torrent *torrent, const Tag &tag); + void torrentTagRemoved(Torrent *torrent, const Tag &tag); void trackerError(Torrent *torrent, const QString &tracker); void trackersAdded(Torrent *torrent, const QVector &trackers); void trackersChanged(Torrent *torrent); diff --git a/src/base/bittorrent/sessionimpl.cpp b/src/base/bittorrent/sessionimpl.cpp index 8a5c5fc99..8de83fdaf 100644 --- a/src/base/bittorrent/sessionimpl.cpp +++ b/src/base/bittorrent/sessionimpl.cpp @@ -80,12 +80,9 @@ #include "base/net/proxyconfigurationmanager.h" #include "base/preferences.h" #include "base/profile.h" -#include "base/torrentfilter.h" #include "base/unicodestrings.h" -#include "base/utils/bytearray.h" #include "base/utils/fs.h" #include "base/utils/io.h" -#include "base/utils/misc.h" #include "base/utils/net.h" #include "base/utils/random.h" #include "base/version.h" @@ -373,11 +370,6 @@ QString Session::parentCategoryName(const QString &category) return {}; } -bool Session::isValidTag(const QString &tag) -{ - return (!tag.trimmed().isEmpty() && !tag.contains(u',')); -} - QStringList Session::expandCategory(const QString &category) { QStringList result; @@ -560,7 +552,8 @@ SessionImpl::SessionImpl(QObject *parent) } const QStringList storedTags = m_storedTags.get(); - m_tags = {storedTags.cbegin(), storedTags.cend()}; + m_tags.insert(storedTags.cbegin(), storedTags.cend()); + std::erase_if(m_tags, [](const Tag &tag) { return !tag.isValid(); }); updateSeedingLimitTimer(); populateAdditionalTrackers(); @@ -1022,34 +1015,37 @@ Path SessionImpl::suggestedDownloadPath(const QString &categoryName, std::option return path; } -QSet SessionImpl::tags() const +TagSet SessionImpl::tags() const { return m_tags; } -bool SessionImpl::hasTag(const QString &tag) const +bool SessionImpl::hasTag(const Tag &tag) const { return m_tags.contains(tag); } -bool SessionImpl::addTag(const QString &tag) +bool SessionImpl::addTag(const Tag &tag) { - if (!isValidTag(tag) || hasTag(tag)) + if (!tag.isValid() || hasTag(tag)) return false; m_tags.insert(tag); - m_storedTags = m_tags.values(); + m_storedTags = QStringList(m_tags.cbegin(), m_tags.cend()); + emit tagAdded(tag); return true; } -bool SessionImpl::removeTag(const QString &tag) +bool SessionImpl::removeTag(const Tag &tag) { if (m_tags.remove(tag)) { for (TorrentImpl *const torrent : asConst(m_torrents)) torrent->removeTag(tag); - m_storedTags = m_tags.values(); + + m_storedTags = QStringList(m_tags.cbegin(), m_tags.cend()); + emit tagRemoved(tag); return true; } @@ -1472,7 +1468,7 @@ void SessionImpl::processNextResumeData(ResumeSessionContext *context) } } - std::erase_if(resumeData.tags, [this, &torrentID](const QString &tag) + std::erase_if(resumeData.tags, [this, &torrentID](const Tag &tag) { if (hasTag(tag)) return false; @@ -1481,12 +1477,12 @@ void SessionImpl::processNextResumeData(ResumeSessionContext *context) { LogMsg(tr("Detected inconsistent data: tag is missing from the configuration file." " Tag will be recovered." - " Torrent: \"%1\". Tag: \"%2\"").arg(torrentID.toString(), tag), Log::WARNING); + " Torrent: \"%1\". Tag: \"%2\"").arg(torrentID.toString(), tag.toString()), Log::WARNING); return false; } LogMsg(tr("Detected inconsistent data: invalid tag. Torrent: \"%1\". Tag: \"%2\"") - .arg(torrentID.toString(), tag), Log::WARNING); + .arg(torrentID.toString(), tag.toString()), Log::WARNING); return true; }); @@ -2701,7 +2697,7 @@ LoadTorrentParams SessionImpl::initLoadTorrentParams(const AddTorrentParams &add } } - for (const QString &tag : addTorrentParams.tags) + for (const Tag &tag : addTorrentParams.tags) { if (hasTag(tag) || addTag(tag)) loadTorrentParams.tags.insert(tag); @@ -4863,12 +4859,12 @@ void SessionImpl::handleTorrentCategoryChanged(TorrentImpl *const torrent, const emit torrentCategoryChanged(torrent, oldCategory); } -void SessionImpl::handleTorrentTagAdded(TorrentImpl *const torrent, const QString &tag) +void SessionImpl::handleTorrentTagAdded(TorrentImpl *const torrent, const Tag &tag) { emit torrentTagAdded(torrent, tag); } -void SessionImpl::handleTorrentTagRemoved(TorrentImpl *const torrent, const QString &tag) +void SessionImpl::handleTorrentTagRemoved(TorrentImpl *const torrent, const Tag &tag) { emit torrentTagRemoved(torrent, tag); } diff --git a/src/base/bittorrent/sessionimpl.h b/src/base/bittorrent/sessionimpl.h index a7807ff27..983427fc2 100644 --- a/src/base/bittorrent/sessionimpl.h +++ b/src/base/bittorrent/sessionimpl.h @@ -156,10 +156,10 @@ namespace BitTorrent Path suggestedSavePath(const QString &categoryName, std::optional useAutoTMM) const override; Path suggestedDownloadPath(const QString &categoryName, std::optional useAutoTMM) const override; - QSet tags() const override; - bool hasTag(const QString &tag) const override; - bool addTag(const QString &tag) override; - bool removeTag(const QString &tag) override; + TagSet tags() const override; + bool hasTag(const Tag &tag) const override; + bool addTag(const Tag &tag) override; + bool removeTag(const Tag &tag) override; bool isAutoTMMDisabledByDefault() const override; void setAutoTMMDisabledByDefault(bool value) override; @@ -433,8 +433,8 @@ namespace BitTorrent void handleTorrentNameChanged(TorrentImpl *torrent); void handleTorrentSavePathChanged(TorrentImpl *torrent); void handleTorrentCategoryChanged(TorrentImpl *torrent, const QString &oldCategory); - void handleTorrentTagAdded(TorrentImpl *torrent, const QString &tag); - void handleTorrentTagRemoved(TorrentImpl *torrent, const QString &tag); + void handleTorrentTagAdded(TorrentImpl *torrent, const Tag &tag); + void handleTorrentTagRemoved(TorrentImpl *torrent, const Tag &tag); void handleTorrentSavingModeChanged(TorrentImpl *torrent); void handleTorrentMetadataReceived(TorrentImpl *torrent); void handleTorrentPaused(TorrentImpl *torrent); @@ -755,7 +755,7 @@ namespace BitTorrent QSet m_needSaveResumeDataTorrents; QHash m_changedTorrentIDs; QMap m_categories; - QSet m_tags; + TagSet m_tags; // This field holds amounts of peers reported by trackers in their responses to announces // (torrent.tracker_name.tracker_local_endpoint.protocol_version.num_peers) diff --git a/src/base/bittorrent/torrent.h b/src/base/bittorrent/torrent.h index e7cf4cde5..31347138a 100644 --- a/src/base/bittorrent/torrent.h +++ b/src/base/bittorrent/torrent.h @@ -202,9 +202,9 @@ namespace BitTorrent virtual bool setCategory(const QString &category) = 0; virtual TagSet tags() const = 0; - virtual bool hasTag(const QString &tag) const = 0; - virtual bool addTag(const QString &tag) = 0; - virtual bool removeTag(const QString &tag) = 0; + virtual bool hasTag(const Tag &tag) const = 0; + virtual bool addTag(const Tag &tag) = 0; + virtual bool removeTag(const Tag &tag) = 0; virtual void removeAllTags() = 0; virtual int piecesCount() const = 0; diff --git a/src/base/bittorrent/torrentimpl.cpp b/src/base/bittorrent/torrentimpl.cpp index 323a934a5..1c7bc88f2 100644 --- a/src/base/bittorrent/torrentimpl.cpp +++ b/src/base/bittorrent/torrentimpl.cpp @@ -863,14 +863,14 @@ TagSet TorrentImpl::tags() const return m_tags; } -bool TorrentImpl::hasTag(const QString &tag) const +bool TorrentImpl::hasTag(const Tag &tag) const { return m_tags.contains(tag); } -bool TorrentImpl::addTag(const QString &tag) +bool TorrentImpl::addTag(const Tag &tag) { - if (!m_session->isValidTag(tag)) + if (!tag.isValid()) return false; if (hasTag(tag)) return false; @@ -886,7 +886,7 @@ bool TorrentImpl::addTag(const QString &tag) return true; } -bool TorrentImpl::removeTag(const QString &tag) +bool TorrentImpl::removeTag(const Tag &tag) { if (m_tags.remove(tag)) { @@ -899,7 +899,7 @@ bool TorrentImpl::removeTag(const QString &tag) void TorrentImpl::removeAllTags() { - for (const QString &tag : asConst(tags())) + for (const Tag &tag : asConst(tags())) removeTag(tag); } diff --git a/src/base/bittorrent/torrentimpl.h b/src/base/bittorrent/torrentimpl.h index dada6664a..65761d99b 100644 --- a/src/base/bittorrent/torrentimpl.h +++ b/src/base/bittorrent/torrentimpl.h @@ -128,9 +128,9 @@ namespace BitTorrent bool setCategory(const QString &category) override; TagSet tags() const override; - bool hasTag(const QString &tag) const override; - bool addTag(const QString &tag) override; - bool removeTag(const QString &tag) override; + bool hasTag(const Tag &tag) const override; + bool addTag(const Tag &tag) override; + bool removeTag(const Tag &tag) override; void removeAllTags() override; int filesCount() const override; diff --git a/src/base/tag.cpp b/src/base/tag.cpp new file mode 100644 index 000000000..2559a512b --- /dev/null +++ b/src/base/tag.cpp @@ -0,0 +1,92 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "tag.h" + +#include + +#include "base/concepts/stringable.h" +#include "base/global.h" + +namespace +{ + QString cleanTag(const QString &tag) + { + return tag.trimmed(); + } +} + +// `Tag` should satisfy `Stringable` concept in order to be stored in settings as string +static_assert(Stringable); + +Tag::Tag(const QString &tagStr) + : m_tagStr {cleanTag(tagStr)} +{ +} + +Tag::Tag(const std::string &tagStr) + : Tag(QString::fromStdString(tagStr)) +{ +} + +bool Tag::isValid() const +{ + if (isEmpty()) + return false; + + return !m_tagStr.contains(u','); +} + +bool Tag::isEmpty() const +{ + return m_tagStr.isEmpty(); +} + +QString Tag::toString() const noexcept +{ + return m_tagStr; +} + +Tag::operator QString() const noexcept +{ + return toString(); +} + +QDataStream &operator<<(QDataStream &out, const Tag &tag) +{ + out << tag.toString(); + return out; +} + +QDataStream &operator>>(QDataStream &in, Tag &tag) +{ + QString tagStr; + in >> tagStr; + tag = Tag(tagStr); + return in; +} diff --git a/src/base/tag.h b/src/base/tag.h new file mode 100644 index 000000000..35fbca1f8 --- /dev/null +++ b/src/base/tag.h @@ -0,0 +1,67 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include +#include + +class Tag final +{ +public: + Tag() = default; + + explicit Tag(const QString &tagStr); + explicit Tag(const std::string &tagStr); + + bool isValid() const; + bool isEmpty() const; + + QString toString() const noexcept; + + explicit operator QString() const noexcept; + + friend bool operator==(const Tag &, const Tag &) = default; + +private: + QString m_tagStr; +}; + +Q_DECLARE_METATYPE(Tag) + +QDataStream &operator<<(QDataStream &out, const Tag &tag); +QDataStream &operator>>(QDataStream &in, Tag &tag); + +template <> +struct std::hash +{ + std::size_t operator()(const Tag &tag, const std::size_t seed = 0) const noexcept + { + return qHash(tag.toString(), seed); + } +}; diff --git a/src/base/tagset.cpp b/src/base/tagset.cpp index a63156d0f..2691ba6db 100644 --- a/src/base/tagset.cpp +++ b/src/base/tagset.cpp @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * Copyright (C) 2021 Mike Tzou (Chocobo1) * * This program is free software; you can redistribute it and/or @@ -28,10 +29,10 @@ #include "tagset.h" -bool TagLessThan::operator()(const QString &left, const QString &right) const +bool TagLessThan::operator()(const Tag &left, const Tag &right) const { - const int result = m_compare(left, right); + const int result = m_compare(left.toString(), right.toString()); if (result != 0) return (result < 0); - return (m_subCompare(left, right) < 0); + return (m_subCompare(left.toString(), right.toString()) < 0); } diff --git a/src/base/tagset.h b/src/base/tagset.h index 329bc59de..1f69d4011 100644 --- a/src/base/tagset.h +++ b/src/base/tagset.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * Copyright (C) 2021 Mike Tzou (Chocobo1) * * This program is free software; you can redistribute it and/or @@ -29,21 +30,21 @@ #pragma once #include -#include -#include "base/orderedset.h" -#include "base/utils/compare.h" +#include "orderedset.h" +#include "tag.h" +#include "utils/compare.h" class TagLessThan { public: - bool operator()(const QString &left, const QString &right) const; + bool operator()(const Tag &left, const Tag &right) const; private: Utils::Compare::NaturalCompare m_compare; Utils::Compare::NaturalCompare m_subCompare; }; -using TagSet = OrderedSet; +using TagSet = OrderedSet; Q_DECLARE_METATYPE(TagSet) diff --git a/src/base/torrentfilter.cpp b/src/base/torrentfilter.cpp index 1c81da452..bcfd03c24 100644 --- a/src/base/torrentfilter.cpp +++ b/src/base/torrentfilter.cpp @@ -33,7 +33,7 @@ const std::optional TorrentFilter::AnyCategory; const std::optional TorrentFilter::AnyID; -const std::optional TorrentFilter::AnyTag; +const std::optional TorrentFilter::AnyTag; const TorrentFilter TorrentFilter::DownloadingTorrent(TorrentFilter::Downloading); const TorrentFilter TorrentFilter::SeedingTorrent(TorrentFilter::Seeding); @@ -52,19 +52,19 @@ const TorrentFilter TorrentFilter::ErroredTorrent(TorrentFilter::Errored); using BitTorrent::Torrent; TorrentFilter::TorrentFilter(const Type type, const std::optional &idSet - , const std::optional &category, const std::optional &tag) - : m_type(type) - , m_category(category) - , m_tag(tag) - , m_idSet(idSet) + , const std::optional &category, const std::optional &tag) + : m_type {type} + , m_category {category} + , m_tag {tag} + , m_idSet {idSet} { } TorrentFilter::TorrentFilter(const QString &filter, const std::optional &idSet - , const std::optional &category, const std::optional &tag) - : m_category(category) - , m_tag(tag) - , m_idSet(idSet) + , const std::optional &category, const std::optional &tag) + : m_category {category} + , m_tag {tag} + , m_idSet {idSet} { setTypeByName(filter); } @@ -136,7 +136,7 @@ bool TorrentFilter::setCategory(const std::optional &category) return false; } -bool TorrentFilter::setTag(const std::optional &tag) +bool TorrentFilter::setTag(const std::optional &tag) { if (m_tag != tag) { diff --git a/src/base/torrentfilter.h b/src/base/torrentfilter.h index 3358ef380..004db3649 100644 --- a/src/base/torrentfilter.h +++ b/src/base/torrentfilter.h @@ -34,6 +34,7 @@ #include #include "base/bittorrent/infohash.h" +#include "base/tag.h" namespace BitTorrent { @@ -66,7 +67,7 @@ public: // These mean any permutation, including no category / tag. static const std::optional AnyCategory; static const std::optional AnyID; - static const std::optional AnyTag; + static const std::optional AnyTag; static const TorrentFilter DownloadingTorrent; static const TorrentFilter SeedingTorrent; @@ -85,15 +86,15 @@ public: TorrentFilter() = default; // category & tags: pass empty string for uncategorized / untagged torrents. TorrentFilter(Type type, const std::optional &idSet = AnyID - , const std::optional &category = AnyCategory, const std::optional &tag = AnyTag); + , const std::optional &category = AnyCategory, const std::optional &tag = AnyTag); TorrentFilter(const QString &filter, const std::optional &idSet = AnyID - , const std::optional &category = AnyCategory, const std::optional &tags = AnyTag); + , const std::optional &category = AnyCategory, const std::optional &tags = AnyTag); bool setType(Type type); bool setTypeByName(const QString &filter); bool setTorrentIDSet(const std::optional &idSet); bool setCategory(const std::optional &category); - bool setTag(const std::optional &tag); + bool setTag(const std::optional &tag); bool match(const BitTorrent::Torrent *torrent) const; @@ -105,6 +106,6 @@ private: Type m_type {All}; std::optional m_category; - std::optional m_tag; + std::optional m_tag; std::optional m_idSet; }; diff --git a/src/gui/addnewtorrentdialog.cpp b/src/gui/addnewtorrentdialog.cpp index 098b8a820..1790f4238 100644 --- a/src/gui/addnewtorrentdialog.cpp +++ b/src/gui/addnewtorrentdialog.cpp @@ -363,7 +363,7 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to connect(m_ui->categoryComboBox, &QComboBox::currentIndexChanged, this, &AddNewTorrentDialog::categoryChanged); - m_ui->tagsLineEdit->setText(m_torrentParams.tags.join(u", "_s)); + m_ui->tagsLineEdit->setText(QStringList(m_torrentParams.tags.cbegin(), m_torrentParams.tags.cend()).join(u", "_s)); connect(m_ui->tagsEditButton, &QAbstractButton::clicked, this, [this] { auto *dlg = new TorrentTagsDialog(m_torrentParams.tags, this); @@ -371,7 +371,7 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to connect(dlg, &TorrentTagsDialog::accepted, this, [this, dlg] { m_torrentParams.tags = dlg->tags(); - m_ui->tagsLineEdit->setText(m_torrentParams.tags.join(u", "_s)); + m_ui->tagsLineEdit->setText(QStringList(m_torrentParams.tags.cbegin(), m_torrentParams.tags.cend()).join(u", "_s)); }); dlg->open(); }); diff --git a/src/gui/addtorrentparamswidget.cpp b/src/gui/addtorrentparamswidget.cpp index d82fe600b..5c3113f08 100644 --- a/src/gui/addtorrentparamswidget.cpp +++ b/src/gui/addtorrentparamswidget.cpp @@ -112,7 +112,7 @@ AddTorrentParamsWidget::AddTorrentParamsWidget(BitTorrent::AddTorrentParams addT connect(dlg, &TorrentTagsDialog::accepted, this, [this, dlg] { m_addTorrentParams.tags = dlg->tags(); - m_ui->tagsLineEdit->setText(m_addTorrentParams.tags.join(u", "_s)); + m_ui->tagsLineEdit->setText(QStringList(m_addTorrentParams.tags.cbegin(), m_addTorrentParams.tags.cend()).join(u", "_s)); }); dlg->open(); }); @@ -230,7 +230,7 @@ void AddTorrentParamsWidget::populate() m_addTorrentParams.stopCondition = data.value(); }); - m_ui->tagsLineEdit->setText(m_addTorrentParams.tags.join(u", "_s)); + m_ui->tagsLineEdit->setText(QStringList(m_addTorrentParams.tags.cbegin(), m_addTorrentParams.tags.cend()).join(u", "_s)); m_ui->startTorrentComboBox->disconnect(this); m_ui->startTorrentComboBox->setCurrentIndex(m_addTorrentParams.addPaused diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp index 15bd6e360..e69f6fca4 100644 --- a/src/gui/mainwindow.cpp +++ b/src/gui/mainwindow.cpp @@ -464,7 +464,7 @@ MainWindow::MainWindow(IGUIApplication *app, WindowState initialState) { m_transferListWidget->applyStatusFilter(pref->getTransSelFilter()); m_transferListWidget->applyCategoryFilter(QString()); - m_transferListWidget->applyTagFilter(QString()); + m_transferListWidget->applyTagFilter(std::nullopt); m_transferListWidget->applyTrackerFilterAll(); } diff --git a/src/gui/torrenttagsdialog.cpp b/src/gui/torrenttagsdialog.cpp index eb4d86168..9e48443b0 100644 --- a/src/gui/torrenttagsdialog.cpp +++ b/src/gui/torrenttagsdialog.cpp @@ -53,9 +53,9 @@ TorrentTagsDialog::TorrentTagsDialog(const TagSet &initialTags, QWidget *parent) connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); auto *tagsLayout = new FlowLayout(m_ui->scrollArea); - for (const QString &tag : asConst(initialTags.united(BitTorrent::Session::instance()->tags()))) + for (const Tag &tag : asConst(initialTags.united(BitTorrent::Session::instance()->tags()))) { - auto *tagWidget = new QCheckBox(tag); + auto *tagWidget = new QCheckBox(tag.toString()); if (initialTags.contains(tag)) tagWidget->setChecked(true); tagsLayout->addWidget(tagWidget); @@ -83,7 +83,7 @@ TagSet TorrentTagsDialog::tags() const { const auto *tagWidget = static_cast(layout->itemAt(i)->widget()); if (tagWidget->isChecked()) - tags.insert(tagWidget->text()); + tags.insert(Tag(tagWidget->text())); } return tags; @@ -92,18 +92,18 @@ TagSet TorrentTagsDialog::tags() const void TorrentTagsDialog::addNewTag() { bool done = false; - QString tag; + Tag tag; while (!done) { bool ok = false; - tag = AutoExpandableDialog::getText(this - , tr("New Tag"), tr("Tag:"), QLineEdit::Normal, tag, &ok).trimmed(); + tag = Tag(AutoExpandableDialog::getText(this, tr("New Tag") + , tr("Tag:"), QLineEdit::Normal, tag.toString(), &ok)); if (!ok || tag.isEmpty()) break; - if (!BitTorrent::Session::isValidTag(tag)) + if (!tag.isValid()) { - QMessageBox::warning(this, tr("Invalid tag name"), tr("Tag name '%1' is invalid.").arg(tag)); + QMessageBox::warning(this, tr("Invalid tag name"), tr("Tag name '%1' is invalid.").arg(tag.toString())); } else if (BitTorrent::Session::instance()->tags().contains(tag)) { @@ -113,7 +113,7 @@ void TorrentTagsDialog::addNewTag() { auto *layout = m_ui->scrollArea->layout(); auto *btn = layout->takeAt(layout->count() - 1); - auto *tagWidget = new QCheckBox(tag); + auto *tagWidget = new QCheckBox(tag.toString()); tagWidget->setChecked(true); layout->addWidget(tagWidget); layout->addItem(btn); diff --git a/src/gui/transferlistfilters/tagfiltermodel.cpp b/src/gui/transferlistfilters/tagfiltermodel.cpp index 91fdaf37b..f9099da46 100644 --- a/src/gui/transferlistfilters/tagfiltermodel.cpp +++ b/src/gui/transferlistfilters/tagfiltermodel.cpp @@ -29,7 +29,6 @@ #include "tagfiltermodel.h" -#include #include #include @@ -40,33 +39,16 @@ const int ROW_ALL = 0; const int ROW_UNTAGGED = 1; -namespace -{ - QString getSpecialAllTag() - { - const QString ALL_TAG = u" "_s; - Q_ASSERT(!BitTorrent::Session::isValidTag(ALL_TAG)); - return ALL_TAG; - } - - QString getSpecialUntaggedTag() - { - const QString UNTAGGED_TAG = u" "_s; - Q_ASSERT(!BitTorrent::Session::isValidTag(UNTAGGED_TAG)); - return UNTAGGED_TAG; - } -} - class TagModelItem { public: - TagModelItem(const QString &tag, int torrentsCount = 0) - : m_tag(tag) - , m_torrentsCount(torrentsCount) + TagModelItem(const Tag &tag, int torrentsCount = 0) + : m_tag {tag} + , m_torrentsCount {torrentsCount} { } - QString tag() const + Tag tag() const { return m_tag; } @@ -88,7 +70,7 @@ public: } private: - QString m_tag; + Tag m_tag; int m_torrentsCount; }; @@ -124,12 +106,25 @@ QVariant TagFilterModel::data(const QModelIndex &index, int role) const Q_ASSERT(isValidRow(row)); const TagModelItem &item = m_tagItems[row]; + const auto displayName = [&row, tag = item.tag()] + { + switch (row) + { + case 0: + return tr("All"); + case 1: + return tr("Untagged"); + default: + return tag.toString(); + } + }; + switch (role) { case Qt::DecorationRole: return UIThemeManager::instance()->getIcon(u"tags"_s, u"inode-directory"_s); case Qt::DisplayRole: - return u"%1 (%2)"_s.arg(tagDisplayName(item.tag())).arg(item.torrentsCount()); + return u"%1 (%2)"_s.arg(displayName(), QString::number(item.torrentsCount())); case Qt::UserRole: return item.torrentsCount(); default: @@ -171,7 +166,7 @@ bool TagFilterModel::isValidRow(int row) const return (row >= 0) && (row < m_tagItems.size()); } -QModelIndex TagFilterModel::index(const QString &tag) const +QModelIndex TagFilterModel::index(const Tag &tag) const { const int row = findRow(tag); if (!isValidRow(row)) @@ -179,7 +174,7 @@ QModelIndex TagFilterModel::index(const QString &tag) const return index(row, 0, QModelIndex()); } -QString TagFilterModel::tag(const QModelIndex &index) const +Tag TagFilterModel::tag(const QModelIndex &index) const { if (!index.isValid()) return {}; @@ -188,7 +183,7 @@ QString TagFilterModel::tag(const QModelIndex &index) const return m_tagItems[row].tag(); } -void TagFilterModel::tagAdded(const QString &tag) +void TagFilterModel::tagAdded(const Tag &tag) { const int row = m_tagItems.count(); beginInsertRows(QModelIndex(), row, row); @@ -196,7 +191,7 @@ void TagFilterModel::tagAdded(const QString &tag) endInsertRows(); } -void TagFilterModel::tagRemoved(const QString &tag) +void TagFilterModel::tagRemoved(const Tag &tag) { QModelIndex i = index(tag); beginRemoveRows(i.parent(), i.row(), i.row()); @@ -204,7 +199,7 @@ void TagFilterModel::tagRemoved(const QString &tag) endRemoveRows(); } -void TagFilterModel::torrentTagAdded(BitTorrent::Torrent *const torrent, const QString &tag) +void TagFilterModel::torrentTagAdded(BitTorrent::Torrent *const torrent, const Tag &tag) { if (torrent->tags().count() == 1) { @@ -222,7 +217,7 @@ void TagFilterModel::torrentTagAdded(BitTorrent::Torrent *const torrent, const Q emit dataChanged(i, i); } -void TagFilterModel::torrentTagRemoved(BitTorrent::Torrent *const torrent, const QString &tag) +void TagFilterModel::torrentTagRemoved(BitTorrent::Torrent *const torrent, const Tag &tag) { if (torrent->tags().empty()) { @@ -275,7 +270,7 @@ void TagFilterModel::torrentAboutToBeRemoved(BitTorrent::Torrent *const torrent) } else { - for (const QString &tag : asConst(torrent->tags())) + for (const Tag &tag : asConst(torrent->tags())) { const int row = findRow(tag); Q_ASSERT(isValidRow(row)); @@ -289,15 +284,6 @@ void TagFilterModel::torrentAboutToBeRemoved(BitTorrent::Torrent *const torrent) } } -QString TagFilterModel::tagDisplayName(const QString &tag) -{ - if (tag == getSpecialAllTag()) - return tr("All"); - if (tag == getSpecialUntaggedTag()) - return tr("Untagged"); - return tag; -} - void TagFilterModel::populate() { using Torrent = BitTorrent::Torrent; @@ -306,21 +292,21 @@ void TagFilterModel::populate() const auto torrents = session->torrents(); // All torrents - addToModel(getSpecialAllTag(), torrents.count()); + addToModel(Tag(), torrents.count()); - const int untaggedCount = std::count_if(torrents.cbegin(), torrents.cend(), - [](Torrent *torrent) { return torrent->tags().isEmpty(); }); - addToModel(getSpecialUntaggedTag(), untaggedCount); + const int untaggedCount = std::count_if(torrents.cbegin(), torrents.cend() + , [](Torrent *torrent) { return torrent->tags().isEmpty(); }); + addToModel(Tag(), untaggedCount); - for (const QString &tag : asConst(session->tags())) + for (const Tag &tag : asConst(session->tags())) { - const int count = std::count_if(torrents.cbegin(), torrents.cend(), - [tag](Torrent *torrent) { return torrent->hasTag(tag); }); + const int count = std::count_if(torrents.cbegin(), torrents.cend() + , [tag](Torrent *torrent) { return torrent->hasTag(tag); }); addToModel(tag, count); } } -void TagFilterModel::addToModel(const QString &tag, int count) +void TagFilterModel::addToModel(const Tag &tag, const int count) { m_tagItems.append(TagModelItem(tag, count)); } @@ -331,9 +317,9 @@ void TagFilterModel::removeFromModel(int row) m_tagItems.removeAt(row); } -int TagFilterModel::findRow(const QString &tag) const +int TagFilterModel::findRow(const Tag &tag) const { - if (!BitTorrent::Session::isValidTag(tag)) + if (!tag.isValid()) return -1; for (int i = 0; i < m_tagItems.size(); ++i) @@ -345,7 +331,7 @@ int TagFilterModel::findRow(const QString &tag) const return -1; } -TagModelItem *TagFilterModel::findItem(const QString &tag) +TagModelItem *TagFilterModel::findItem(const Tag &tag) { const int row = findRow(tag); if (!isValidRow(row)) @@ -357,13 +343,13 @@ QVector TagFilterModel::findItems(const TagSet &tags) { QVector items; items.reserve(tags.count()); - for (const QString &tag : tags) + for (const Tag &tag : tags) { TagModelItem *item = findItem(tag); if (item) items.push_back(item); else - qWarning() << u"Requested tag '%1' missing from the model."_s.arg(tag); + qWarning() << u"Requested tag '%1' missing from the model."_s.arg(tag.toString()); } return items; } diff --git a/src/gui/transferlistfilters/tagfiltermodel.h b/src/gui/transferlistfilters/tagfiltermodel.h index d7033941d..9718307a2 100644 --- a/src/gui/transferlistfilters/tagfiltermodel.h +++ b/src/gui/transferlistfilters/tagfiltermodel.h @@ -56,26 +56,24 @@ public: QModelIndex index(int row, int column, const QModelIndex &parent = {}) const override; int rowCount(const QModelIndex &parent = {}) const override; - QModelIndex index(const QString &tag) const; - QString tag(const QModelIndex &index) const; + QModelIndex index(const Tag &tag) const; + Tag tag(const QModelIndex &index) const; private slots: - void tagAdded(const QString &tag); - void tagRemoved(const QString &tag); - void torrentTagAdded(BitTorrent::Torrent *torrent, const QString &tag); - void torrentTagRemoved(BitTorrent::Torrent *, const QString &tag); + void tagAdded(const Tag &tag); + void tagRemoved(const Tag &tag); + void torrentTagAdded(BitTorrent::Torrent *torrent, const Tag &tag); + void torrentTagRemoved(BitTorrent::Torrent *, const Tag &tag); void torrentsLoaded(const QVector &torrents); void torrentAboutToBeRemoved(BitTorrent::Torrent *torrent); private: - static QString tagDisplayName(const QString &tag); - void populate(); - void addToModel(const QString &tag, int count); + void addToModel(const Tag &tag, int count); void removeFromModel(int row); bool isValidRow(int row) const; - int findRow(const QString &tag) const; - TagModelItem *findItem(const QString &tag); + int findRow(const Tag &tag) const; + TagModelItem *findItem(const Tag &tag); QVector findItems(const TagSet &tags); TagModelItem *allTagsItem(); TagModelItem *untaggedItem(); diff --git a/src/gui/transferlistfilters/tagfilterproxymodel.cpp b/src/gui/transferlistfilters/tagfilterproxymodel.cpp index 732888c48..866a400d3 100644 --- a/src/gui/transferlistfilters/tagfilterproxymodel.cpp +++ b/src/gui/transferlistfilters/tagfilterproxymodel.cpp @@ -35,12 +35,12 @@ TagFilterProxyModel::TagFilterProxyModel(QObject *parent) { } -QModelIndex TagFilterProxyModel::index(const QString &tag) const +QModelIndex TagFilterProxyModel::index(const Tag &tag) const { return mapFromSource(static_cast(sourceModel())->index(tag)); } -QString TagFilterProxyModel::tag(const QModelIndex &index) const +Tag TagFilterProxyModel::tag(const QModelIndex &index) const { return static_cast(sourceModel())->tag(mapToSource(index)); } diff --git a/src/gui/transferlistfilters/tagfilterproxymodel.h b/src/gui/transferlistfilters/tagfilterproxymodel.h index 40390812f..314fe84cb 100644 --- a/src/gui/transferlistfilters/tagfilterproxymodel.h +++ b/src/gui/transferlistfilters/tagfilterproxymodel.h @@ -34,14 +34,16 @@ class QString; +class Tag; + class TagFilterProxyModel final : public QSortFilterProxyModel { public: explicit TagFilterProxyModel(QObject *parent = nullptr); // TagFilterModel methods which we need to relay - QModelIndex index(const QString &tag) const; - QString tag(const QModelIndex &index) const; + QModelIndex index(const Tag &tag) const; + Tag tag(const QModelIndex &index) const; protected: bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; diff --git a/src/gui/transferlistfilters/tagfilterwidget.cpp b/src/gui/transferlistfilters/tagfilterwidget.cpp index 79b8bce2e..d5882e49c 100644 --- a/src/gui/transferlistfilters/tagfilterwidget.cpp +++ b/src/gui/transferlistfilters/tagfilterwidget.cpp @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * Copyright (C) 2017 Tony Gregerson * * This program is free software; you can redistribute it and/or @@ -41,17 +42,15 @@ namespace { - QString getTagFilter(const TagFilterProxyModel *const model, const QModelIndex &index) + std::optional getTagFilter(const TagFilterProxyModel *const model, const QModelIndex &index) { - QString tagFilter; // Defaults to All - if (index.isValid()) - { - if (index.row() == 1) - tagFilter = u""_s; // Untagged - else if (index.row() > 1) - tagFilter = model->tag(index); - } - return tagFilter; + if (!index.isValid() || (index.row() == 0)) + return std::nullopt; // All tags + + if (index.row() == 1) + return Tag(); // Untagged + + return model->tag(index); } } @@ -85,7 +84,7 @@ TagFilterWidget::TagFilterWidget(QWidget *parent) connect(model(), &QAbstractItemModel::modelReset, this, &TagFilterWidget::callUpdateGeometry); } -QString TagFilterWidget::currentTag() const +std::optional TagFilterWidget::currentTag() const { QModelIndex current; const auto selectedRows = selectionModel()->selectedRows(); @@ -157,35 +156,36 @@ void TagFilterWidget::rowsInserted(const QModelIndex &parent, int start, int end updateGeometry(); } -QString TagFilterWidget::askTagName() +Tag TagFilterWidget::askTagName() { bool ok = false; - QString tag = u""_s; + Tag tag; bool invalid = true; while (invalid) { invalid = false; - tag = AutoExpandableDialog::getText( - this, tr("New Tag"), tr("Tag:"), QLineEdit::Normal, tag, &ok).trimmed(); + tag = Tag(AutoExpandableDialog::getText(this, tr("New Tag"), tr("Tag:") + , QLineEdit::Normal, tag.toString(), &ok)); if (ok && !tag.isEmpty()) { - if (!BitTorrent::Session::isValidTag(tag)) + if (!tag.isValid()) { QMessageBox::warning( this, tr("Invalid tag name") - , tr("Tag name '%1' is invalid").arg(tag)); + , tr("Tag name '%1' is invalid").arg(tag.toString())); invalid = true; } } } - return ok ? tag : QString(); + return ok ? tag : Tag(); } void TagFilterWidget::addTag() { - const QString tag = askTagName(); - if (tag.isEmpty()) return; + const Tag tag = askTagName(); + if (tag.isEmpty()) + return; if (BitTorrent::Session::instance()->tags().contains(tag)) QMessageBox::warning(this, tr("Tag exists"), tr("Tag name already exists.")); @@ -207,7 +207,7 @@ void TagFilterWidget::removeTag() void TagFilterWidget::removeUnusedTags() { auto *session = BitTorrent::Session::instance(); - for (const QString &tag : asConst(session->tags())) + for (const Tag &tag : asConst(session->tags())) if (model()->data(static_cast(model())->index(tag), Qt::UserRole) == 0) session->removeTag(tag); updateGeometry(); diff --git a/src/gui/transferlistfilters/tagfilterwidget.h b/src/gui/transferlistfilters/tagfilterwidget.h index d5be96cf0..7a8165d36 100644 --- a/src/gui/transferlistfilters/tagfilterwidget.h +++ b/src/gui/transferlistfilters/tagfilterwidget.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * Copyright (C) 2017 Tony Gregerson * * This program is free software; you can redistribute it and/or @@ -30,6 +31,8 @@ #include +class Tag; + class TagFilterWidget final : public QTreeView { Q_OBJECT @@ -38,10 +41,10 @@ class TagFilterWidget final : public QTreeView public: explicit TagFilterWidget(QWidget *parent = nullptr); - QString currentTag() const; + std::optional currentTag() const; signals: - void tagChanged(const QString &tag); + void tagChanged(const std::optional &tag); void actionResumeTorrentsTriggered(); void actionPauseTorrentsTriggered(); void actionDeleteTorrentsTriggered(); @@ -58,5 +61,5 @@ private: QSize sizeHint() const override; QSize minimumSizeHint() const override; void rowsInserted(const QModelIndex &parent, int start, int end) override; - QString askTagName(); + Tag askTagName(); }; diff --git a/src/gui/transferlistfilterswidget.cpp b/src/gui/transferlistfilterswidget.cpp index f99b502a6..53d4d60af 100644 --- a/src/gui/transferlistfilterswidget.cpp +++ b/src/gui/transferlistfilterswidget.cpp @@ -218,5 +218,5 @@ void TransferListFiltersWidget::onTagFilterStateChanged(bool enabled) void TransferListFiltersWidget::toggleTagFilter(bool enabled) { m_tagFilterWidget->setVisible(enabled); - m_transferList->applyTagFilter(enabled ? m_tagFilterWidget->currentTag() : QString()); + m_transferList->applyTagFilter(enabled ? m_tagFilterWidget->currentTag() : std::nullopt); } diff --git a/src/gui/transferlistmodel.cpp b/src/gui/transferlistmodel.cpp index 272ea253c..efa4db33b 100644 --- a/src/gui/transferlistmodel.cpp +++ b/src/gui/transferlistmodel.cpp @@ -380,7 +380,10 @@ QString TransferListModel::displayValue(const BitTorrent::Torrent *torrent, cons case TR_CATEGORY: return torrent->category(); case TR_TAGS: - return torrent->tags().join(u", "_s); + { + const TagSet &tags = torrent->tags(); + return QStringList(tags.cbegin(), tags.cend()).join(u", "_s); + } case TR_ADD_DATE: return QLocale().toString(torrent->addedTime().toLocalTime(), QLocale::ShortFormat); case TR_SEED_DATE: diff --git a/src/gui/transferlistsortmodel.cpp b/src/gui/transferlistsortmodel.cpp index 131d729a2..98ab11c63 100644 --- a/src/gui/transferlistsortmodel.cpp +++ b/src/gui/transferlistsortmodel.cpp @@ -59,10 +59,10 @@ namespace int customCompare(const TagSet &left, const TagSet &right, const Utils::Compare::NaturalCompare &compare) { for (auto leftIter = left.cbegin(), rightIter = right.cbegin(); - (leftIter != left.cend()) && (rightIter != right.cend()); - ++leftIter, ++rightIter) + (leftIter != left.cend()) && (rightIter != right.cend()); + ++leftIter, ++rightIter) { - const int result = compare(*leftIter, *rightIter); + const int result = compare(leftIter->toString(), rightIter->toString()); if (result != 0) return result; } @@ -130,7 +130,7 @@ void TransferListSortModel::disableCategoryFilter() invalidateFilter(); } -void TransferListSortModel::setTagFilter(const QString &tag) +void TransferListSortModel::setTagFilter(const Tag &tag) { if (m_filter.setTag(tag)) invalidateFilter(); diff --git a/src/gui/transferlistsortmodel.h b/src/gui/transferlistsortmodel.h index 46d6cc52a..3b3a59694 100644 --- a/src/gui/transferlistsortmodel.h +++ b/src/gui/transferlistsortmodel.h @@ -52,7 +52,7 @@ public: void setStatusFilter(TorrentFilter::Type filter); void setCategoryFilter(const QString &category); void disableCategoryFilter(); - void setTagFilter(const QString &tag); + void setTagFilter(const Tag &tag); void disableTagFilter(); void setTrackerFilter(const QSet &torrentIDs); void disableTrackerFilter(); diff --git a/src/gui/transferlistwidget.cpp b/src/gui/transferlistwidget.cpp index c731775db..5c06a3c15 100644 --- a/src/gui/transferlistwidget.cpp +++ b/src/gui/transferlistwidget.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 @@ -770,8 +771,8 @@ void TransferListWidget::askNewCategoryForSelection() void TransferListWidget::askAddTagsForSelection() { - const QStringList tags = askTagsForSelection(tr("Add Tags")); - for (const QString &tag : tags) + const TagSet tags = askTagsForSelection(tr("Add Tags")); + for (const Tag &tag : tags) addSelectionTag(tag); } @@ -871,9 +872,9 @@ void TransferListWidget::confirmRemoveAllTagsForSelection() clearSelectionTags(); } -QStringList TransferListWidget::askTagsForSelection(const QString &dialogTitle) +TagSet TransferListWidget::askTagsForSelection(const QString &dialogTitle) { - QStringList tags; + TagSet tags; bool invalid = true; while (invalid) { @@ -883,18 +884,23 @@ QStringList TransferListWidget::askTagsForSelection(const QString &dialogTitle) this, dialogTitle, tr("Comma-separated tags:"), QLineEdit::Normal, {}, &ok).trimmed(); if (!ok || tagsInput.isEmpty()) return {}; - tags = tagsInput.split(u',', Qt::SkipEmptyParts); - for (QString &tag : tags) + + const QStringList tagStrings = tagsInput.split(u',', Qt::SkipEmptyParts); + tags.clear(); + for (const QString &tagStr : tagStrings) { - tag = tag.trimmed(); - if (!BitTorrent::Session::isValidTag(tag)) + const Tag tag {tagStr}; + if (!tag.isValid()) { - QMessageBox::warning(this, tr("Invalid tag") - , tr("Tag name: '%1' is invalid").arg(tag)); + QMessageBox::warning(this, tr("Invalid tag"), tr("Tag name: '%1' is invalid").arg(tag.toString())); invalid = true; } + + if (!invalid) + tags.insert(tag); } } + return tags; } @@ -939,12 +945,12 @@ void TransferListWidget::setSelectionCategory(const QString &category) applyToSelectedTorrents([&category](BitTorrent::Torrent *torrent) { torrent->setCategory(category); }); } -void TransferListWidget::addSelectionTag(const QString &tag) +void TransferListWidget::addSelectionTag(const Tag &tag) { applyToSelectedTorrents([&tag](BitTorrent::Torrent *const torrent) { torrent->addTag(tag); }); } -void TransferListWidget::removeSelectionTag(const QString &tag) +void TransferListWidget::removeSelectionTag(const Tag &tag) { applyToSelectedTorrents([&tag](BitTorrent::Torrent *const torrent) { torrent->removeTag(tag); }); } @@ -1187,9 +1193,6 @@ void TransferListWidget::displayListMenu() } // Tag Menu - QStringList tags(BitTorrent::Session::instance()->tags().values()); - std::sort(tags.begin(), tags.end(), Utils::Compare::NaturalLessThan()); - QMenu *tagsMenu = listMenu->addMenu(UIThemeManager::instance()->getIcon(u"tags"_s, u"view-categories"_s), tr("Ta&gs")); tagsMenu->addAction(UIThemeManager::instance()->getIcon(u"list-add"_s), tr("&Add...", "Add / assign multiple tags...") @@ -1204,9 +1207,10 @@ void TransferListWidget::displayListMenu() }); tagsMenu->addSeparator(); - for (const QString &tag : asConst(tags)) + const TagSet tags = BitTorrent::Session::instance()->tags(); + for (const Tag &tag : asConst(tags)) { - auto *action = new TriStateAction(tag, tagsMenu); + auto *action = new TriStateAction(tag.toString(), tagsMenu); action->setCloseOnInteraction(false); const Qt::CheckState initialState = tagsInAll.contains(tag) ? Qt::Checked @@ -1320,12 +1324,12 @@ void TransferListWidget::applyCategoryFilter(const QString &category) m_sortFilterModel->setCategoryFilter(category); } -void TransferListWidget::applyTagFilter(const QString &tag) +void TransferListWidget::applyTagFilter(const std::optional &tag) { - if (tag.isNull()) + if (!tag) m_sortFilterModel->disableTagFilter(); else - m_sortFilterModel->setTagFilter(tag); + m_sortFilterModel->setTagFilter(*tag); } void TransferListWidget::applyTrackerFilterAll() diff --git a/src/gui/transferlistwidget.h b/src/gui/transferlistwidget.h index 88a00662b..6b1505a6d 100644 --- a/src/gui/transferlistwidget.h +++ b/src/gui/transferlistwidget.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 @@ -63,8 +64,8 @@ public: public slots: void setSelectionCategory(const QString &category); - void addSelectionTag(const QString &tag); - void removeSelectionTag(const QString &tag); + void addSelectionTag(const Tag &tag); + void removeSelectionTag(const Tag &tag); void clearSelectionTags(); void setSelectedTorrentsLocation(); void pauseAllTorrents(); @@ -96,7 +97,7 @@ public slots: void applyFilter(const QString &name, const TransferListModel::Column &type); void applyStatusFilter(int f); void applyCategoryFilter(const QString &category); - void applyTagFilter(const QString &tag); + void applyTagFilter(const std::optional &tag); void applyTrackerFilterAll(); void applyTrackerFilter(const QSet &torrentIDs); void previewFile(const Path &filePath); @@ -128,7 +129,7 @@ private: void editTorrentTrackers(); void exportTorrent(); void confirmRemoveAllTagsForSelection(); - QStringList askTagsForSelection(const QString &dialogTitle); + TagSet askTagsForSelection(const QString &dialogTitle); void applyToSelectedTorrents(const std::function &fn); QVector getVisibleTorrents() const; int visibleColumnsCount() const; diff --git a/src/webui/api/serialize/serialize_torrent.cpp b/src/webui/api/serialize/serialize_torrent.cpp index 15c4007af..d9dadbd1d 100644 --- a/src/webui/api/serialize/serialize_torrent.cpp +++ b/src/webui/api/serialize/serialize_torrent.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2018 Vladimir Golovnev + * Copyright (C) 2018-2023 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -29,6 +29,7 @@ #include "serialize_torrent.h" #include +#include #include #include "base/bittorrent/infohash.h" @@ -36,7 +37,6 @@ #include "base/bittorrent/trackerentry.h" #include "base/path.h" #include "base/tagset.h" -#include "base/utils/fs.h" namespace { @@ -106,6 +106,8 @@ QVariantMap serialize(const BitTorrent::Torrent &torrent) : (QDateTime::currentDateTime().toSecsSinceEpoch() - timeSinceActivity); }; + const TagSet &tags = torrent.tags(); + return { {KEY_TORRENT_ID, torrent.id().toString()}, {KEY_TORRENT_INFOHASHV1, torrent.infoHash().v1().toString()}, @@ -128,7 +130,7 @@ QVariantMap serialize(const BitTorrent::Torrent &torrent) {KEY_TORRENT_FIRST_LAST_PIECE_PRIO, torrent.hasFirstLastPiecePriority()}, {KEY_TORRENT_CATEGORY, torrent.category()}, - {KEY_TORRENT_TAGS, torrent.tags().join(u", "_s)}, + {KEY_TORRENT_TAGS, QStringList(tags.cbegin(), tags.cend()).join(u", "_s)}, {KEY_TORRENT_SUPER_SEEDING, torrent.superSeeding()}, {KEY_TORRENT_FORCE_START, torrent.isForced()}, {KEY_TORRENT_SAVE_PATH, torrent.savePath().toString()}, diff --git a/src/webui/api/serialize/serialize_torrent.h b/src/webui/api/serialize/serialize_torrent.h index 3a67551a9..1a8e8e3fb 100644 --- a/src/webui/api/serialize/serialize_torrent.h +++ b/src/webui/api/serialize/serialize_torrent.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2018 Vladimir Golovnev + * Copyright (C) 2018-2023 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License diff --git a/src/webui/api/synccontroller.cpp b/src/webui/api/synccontroller.cpp index aea6693f4..cb26a38fb 100644 --- a/src/webui/api/synccontroller.cpp +++ b/src/webui/api/synccontroller.cpp @@ -540,8 +540,8 @@ void SyncController::makeMaindataSnapshot() m_maindataSnapshot.categories[categoryName] = category.toVariantMap(); } - for (const QString &tag : asConst(session->tags())) - m_maindataSnapshot.tags.append(tag); + for (const Tag &tag : asConst(session->tags())) + m_maindataSnapshot.tags.append(tag.toString()); for (auto trackersIter = m_knownTrackers.cbegin(); trackersIter != m_knownTrackers.cend(); ++trackersIter) { @@ -815,16 +815,16 @@ void SyncController::onSubcategoriesSupportChanged() } } -void SyncController::onTagAdded(const QString &tag) +void SyncController::onTagAdded(const Tag &tag) { - m_removedTags.remove(tag); - m_addedTags.insert(tag); + m_removedTags.remove(tag.toString()); + m_addedTags.insert(tag.toString()); } -void SyncController::onTagRemoved(const QString &tag) +void SyncController::onTagRemoved(const Tag &tag) { - m_addedTags.remove(tag); - m_removedTags.insert(tag); + m_addedTags.remove(tag.toString()); + m_removedTags.insert(tag.toString()); } void SyncController::onTorrentAdded(BitTorrent::Torrent *torrent) @@ -902,12 +902,12 @@ void SyncController::onTorrentSavingModeChanged(BitTorrent::Torrent *torrent) m_updatedTorrents.insert(torrent->id()); } -void SyncController::onTorrentTagAdded(BitTorrent::Torrent *torrent, [[maybe_unused]] const QString &tag) +void SyncController::onTorrentTagAdded(BitTorrent::Torrent *torrent, [[maybe_unused]] const Tag &tag) { m_updatedTorrents.insert(torrent->id()); } -void SyncController::onTorrentTagRemoved(BitTorrent::Torrent *torrent, [[maybe_unused]] const QString &tag) +void SyncController::onTorrentTagRemoved(BitTorrent::Torrent *torrent, [[maybe_unused]] const Tag &tag) { m_updatedTorrents.insert(torrent->id()); } diff --git a/src/webui/api/synccontroller.h b/src/webui/api/synccontroller.h index 2123f625d..da0b085f3 100644 --- a/src/webui/api/synccontroller.h +++ b/src/webui/api/synccontroller.h @@ -32,6 +32,7 @@ #include #include "base/bittorrent/infohash.h" +#include "base/tag.h" #include "apicontroller.h" namespace BitTorrent @@ -64,8 +65,8 @@ private: void onCategoryRemoved(const QString &categoryName); void onCategoryOptionsChanged(const QString &categoryName); void onSubcategoriesSupportChanged(); - void onTagAdded(const QString &tag); - void onTagRemoved(const QString &tag); + void onTagAdded(const Tag &tag); + void onTagRemoved(const Tag &tag); void onTorrentAdded(BitTorrent::Torrent *torrent); void onTorrentAboutToBeRemoved(BitTorrent::Torrent *torrent); void onTorrentCategoryChanged(BitTorrent::Torrent *torrent, const QString &oldCategory); @@ -74,8 +75,8 @@ private: void onTorrentResumed(BitTorrent::Torrent *torrent); void onTorrentSavePathChanged(BitTorrent::Torrent *torrent); void onTorrentSavingModeChanged(BitTorrent::Torrent *torrent); - void onTorrentTagAdded(BitTorrent::Torrent *torrent, const QString &tag); - void onTorrentTagRemoved(BitTorrent::Torrent *torrent, const QString &tag); + void onTorrentTagAdded(BitTorrent::Torrent *torrent, const Tag &tag); + void onTorrentTagRemoved(BitTorrent::Torrent *torrent, const Tag &tag); void onTorrentsUpdated(const QVector &torrents); void onTorrentTrackersChanged(BitTorrent::Torrent *torrent); diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index 6a3b73876..0216eb48a 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -152,6 +152,15 @@ namespace return it.value(); } + std::optional getOptionalTag(const StringMap ¶ms, const QString &name) + { + const auto it = params.constFind(name); + if (it == params.cend()) + return std::nullopt; + + return Tag(it.value()); + } + QJsonArray getStickyTrackers(const BitTorrent::Torrent *const torrent) { int seedsDHT = 0, seedsPeX = 0, seedsLSD = 0, leechesDHT = 0, leechesPeX = 0, leechesLSD = 0; @@ -273,7 +282,7 @@ void TorrentsController::infoAction() { const QString filter {params()[u"filter"_s]}; const std::optional category = getOptionalString(params(), u"category"_s); - const std::optional tag = getOptionalString(params(), u"tag"_s); + const std::optional tag = getOptionalTag(params(), u"tag"_s); const QString sortedColumn {params()[u"sort"_s]}; const bool reverse {parseBool(params()[u"reverse"_s]).value_or(false)}; int limit {params()[u"limit"_s].toInt()}; @@ -1317,12 +1326,11 @@ void TorrentsController::addTagsAction() const QStringList hashes {params()[u"hashes"_s].split(u'|')}; const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)}; - for (const QString &tag : tags) + for (const QString &tagStr : tags) { - const QString tagTrimmed {tag.trimmed()}; - applyToTorrents(hashes, [&tagTrimmed](BitTorrent::Torrent *const torrent) + applyToTorrents(hashes, [&tagStr](BitTorrent::Torrent *const torrent) { - torrent->addTag(tagTrimmed); + torrent->addTag(Tag(tagStr)); }); } } @@ -1334,12 +1342,11 @@ void TorrentsController::removeTagsAction() const QStringList hashes {params()[u"hashes"_s].split(u'|')}; const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)}; - for (const QString &tag : tags) + for (const QString &tagStr : tags) { - const QString tagTrimmed {tag.trimmed()}; - applyToTorrents(hashes, [&tagTrimmed](BitTorrent::Torrent *const torrent) + applyToTorrents(hashes, [&tagStr](BitTorrent::Torrent *const torrent) { - torrent->removeTag(tagTrimmed); + torrent->removeTag(Tag(tagStr)); }); } @@ -1358,8 +1365,8 @@ void TorrentsController::createTagsAction() const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)}; - for (const QString &tag : tags) - BitTorrent::Session::instance()->addTag(tag.trimmed()); + for (const QString &tagStr : tags) + BitTorrent::Session::instance()->addTag(Tag(tagStr)); } void TorrentsController::deleteTagsAction() @@ -1367,15 +1374,15 @@ void TorrentsController::deleteTagsAction() requireParams({u"tags"_s}); const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)}; - for (const QString &tag : tags) - BitTorrent::Session::instance()->removeTag(tag.trimmed()); + for (const QString &tagStr : tags) + BitTorrent::Session::instance()->removeTag(Tag(tagStr)); } void TorrentsController::tagsAction() { QJsonArray result; - for (const QString &tag : asConst(BitTorrent::Session::instance()->tags())) - result << tag; + for (const Tag &tag : asConst(BitTorrent::Session::instance()->tags())) + result << tag.toString(); setResult(result); }