Add a Tags (multi-label) feature to the GUI. Closes #13.

See https://github.com/qbittorrent/qBittorrent/issues/13 for details.
This commit is contained in:
Tony Gregerson
2017-06-04 19:22:17 -05:00
parent ff80208534
commit 467e516801
27 changed files with 1315 additions and 35 deletions

View File

@@ -28,6 +28,7 @@
#pragma once
#include <QSet>
#include <QString>
#include <QVector>
@@ -39,6 +40,7 @@ namespace BitTorrent
{
QString name;
QString category;
QSet<QString> tags;
QString savePath;
bool disableTempPath = false; // e.g. for imported torrents
bool sequential = false;

View File

@@ -127,6 +127,43 @@ namespace
return result;
}
template <typename Entry>
QSet<QString> entryListToSetImpl(const Entry &entry)
{
Q_ASSERT(entry.type() == Entry::list_t);
QSet<QString> output;
for (int i = 0; i < entry.list_size(); ++i) {
const QString tag = QString::fromStdString(entry.list_string_value_at(i));
if (Session::isValidTag(tag))
output.insert(tag);
else
qWarning() << QString("Dropping invalid stored tag: %1").arg(tag);
}
return output;
}
#if LIBTORRENT_VERSION_NUM < 10100
bool isList(const libt::lazy_entry *entry)
{
return entry && (entry->type() == libt::lazy_entry::list_t);
}
QSet<QString> entryListToSet(const libt::lazy_entry *entry)
{
return entry ? entryListToSetImpl(*entry) : QSet<QString>();
}
#else
bool isList(const libt::bdecode_node &entry)
{
return entry.type() == libt::bdecode_node::list_t;
}
QSet<QString> entryListToSet(const libt::bdecode_node &entry)
{
return entryListToSetImpl(entry);
}
#endif
QString normalizePath(const QString &path)
{
QString tmp = Utils::Fs::fromNativePath(path.trimmed());
@@ -260,6 +297,7 @@ Session::Session(QObject *parent)
, m_isForceProxyEnabled(BITTORRENT_SESSION_KEY("ForceProxy"), true)
, m_isProxyPeerConnectionsEnabled(BITTORRENT_SESSION_KEY("ProxyPeerConnections"), false)
, m_storedCategories(BITTORRENT_SESSION_KEY("Categories"))
, m_storedTags(BITTORRENT_SESSION_KEY("Tags"))
, m_maxRatioAction(BITTORRENT_SESSION_KEY("MaxRatioAction"), Pause)
, m_defaultSavePath(BITTORRENT_SESSION_KEY("DefaultSavePath"), specialFolderLocation(SpecialFolder::Downloads), normalizePath)
, m_tempPath(BITTORRENT_SESSION_KEY("TempPath"), defaultSavePath() + "temp/", normalizePath)
@@ -400,6 +438,8 @@ Session::Session(QObject *parent)
m_storedCategories = map_cast(m_categories);
}
m_tags = QSet<QString>::fromList(m_storedTags.value());
m_refreshTimer = new QTimer(this);
m_refreshTimer->setInterval(refreshInterval());
connect(m_refreshTimer, SIGNAL(timeout()), SLOT(refresh()));
@@ -724,6 +764,47 @@ void Session::setSubcategoriesEnabled(bool value)
emit subcategoriesSupportChanged();
}
QSet<QString> Session::tags() const
{
return m_tags;
}
bool Session::isValidTag(const QString &tag)
{
return (!tag.trimmed().isEmpty() && !tag.contains(','));
}
bool Session::hasTag(const QString &tag) const
{
return m_tags.contains(tag);
}
bool Session::addTag(const QString &tag)
{
if (!isValidTag(tag))
return false;
if (!hasTag(tag)) {
m_tags.insert(tag);
m_storedTags = m_tags.toList();
emit tagAdded(tag);
return true;
}
return false;
}
bool Session::removeTag(const QString &tag)
{
if (m_tags.remove(tag)) {
foreach (TorrentHandle *const torrent, torrents())
torrent->removeTag(tag);
m_storedTags = m_tags.toList();
emit tagRemoved(tag);
return true;
}
return false;
}
bool Session::isAutoTMMDisabledByDefault() const
{
return m_isAutoTMMDisabledByDefault;
@@ -2997,6 +3078,16 @@ void Session::handleTorrentCategoryChanged(TorrentHandle *const torrent, const Q
emit torrentCategoryChanged(torrent, oldCategory);
}
void Session::handleTorrentTagAdded(TorrentHandle *const torrent, const QString &tag)
{
emit torrentTagAdded(torrent, tag);
}
void Session::handleTorrentTagRemoved(TorrentHandle *const torrent, const QString &tag)
{
emit torrentTagRemoved(torrent, tag);
}
void Session::handleTorrentSavingModeChanged(TorrentHandle * const torrent)
{
emit torrentSavingModeChanged(torrent);
@@ -3930,6 +4021,10 @@ namespace
if (torrentData.category.isEmpty())
// **************************************************************************************
torrentData.category = QString::fromStdString(fast.dict_find_string_value("qBt-category"));
// auto because the return type depends on the #if above.
const auto tagsEntry = fast.dict_find_list("qBt-tags");
if (isList(tagsEntry))
torrentData.tags = entryListToSet(tagsEntry);
torrentData.name = QString::fromStdString(fast.dict_find_string_value("qBt-name"));
torrentData.hasSeedStatus = fast.dict_find_int_value("qBt-seedStatus");
torrentData.disableTempPath = fast.dict_find_int_value("qBt-tempPathDisabled");

View File

@@ -44,6 +44,7 @@
#endif
#include <QNetworkConfigurationManager>
#include <QPointer>
#include <QSet>
#include <QStringList>
#include <QVector>
#include <QWaitCondition>
@@ -223,6 +224,12 @@ namespace BitTorrent
bool isSubcategoriesEnabled() const;
void setSubcategoriesEnabled(bool value);
static bool isValidTag(const QString &tag);
QSet<QString> tags() const;
bool hasTag(const QString &tag) const;
bool addTag(const QString &tag);
bool removeTag(const QString &tag);
// Torrent Management Mode subsystem (TMM)
//
// Each torrent can be either in Manual mode or in Automatic mode
@@ -400,6 +407,8 @@ namespace BitTorrent
void handleTorrentShareLimitChanged(TorrentHandle *const torrent);
void handleTorrentSavePathChanged(TorrentHandle *const torrent);
void handleTorrentCategoryChanged(TorrentHandle *const torrent, const QString &oldCategory);
void handleTorrentTagAdded(TorrentHandle *const torrent, const QString &tag);
void handleTorrentTagRemoved(TorrentHandle *const torrent, const QString &tag);
void handleTorrentSavingModeChanged(TorrentHandle *const torrent);
void handleTorrentMetadataReceived(TorrentHandle *const torrent);
void handleTorrentPaused(TorrentHandle *const torrent);
@@ -431,6 +440,8 @@ namespace BitTorrent
void torrentFinishedChecking(BitTorrent::TorrentHandle *const torrent);
void torrentSavePathChanged(BitTorrent::TorrentHandle *const torrent);
void torrentCategoryChanged(BitTorrent::TorrentHandle *const torrent, const QString &oldCategory);
void torrentTagAdded(TorrentHandle *const torrent, const QString &tag);
void torrentTagRemoved(TorrentHandle *const torrent, const QString &tag);
void torrentSavingModeChanged(BitTorrent::TorrentHandle *const torrent);
void allTorrentsFinished();
void metadataLoaded(const BitTorrent::TorrentInfo &info);
@@ -452,6 +463,8 @@ namespace BitTorrent
void categoryAdded(const QString &categoryName);
void categoryRemoved(const QString &categoryName);
void subcategoriesSupportChanged();
void tagAdded(const QString &tag);
void tagRemoved(const QString &tag);
private slots:
void configureDeferred();
@@ -606,6 +619,7 @@ namespace BitTorrent
CachedSettingValue<bool> m_isForceProxyEnabled;
CachedSettingValue<bool> m_isProxyPeerConnectionsEnabled;
CachedSettingValue<QVariantMap> m_storedCategories;
CachedSettingValue<QStringList> m_storedTags;
CachedSettingValue<int> m_maxRatioAction;
CachedSettingValue<QString> m_defaultSavePath;
CachedSettingValue<QString> m_tempPath;
@@ -650,6 +664,7 @@ namespace BitTorrent
QHash<QString, AddTorrentParams> m_downloadedTorrents;
TorrentStatusReport m_torrentStatusReport;
QStringMap m_categories;
QSet<QString> m_tags;
#if LIBTORRENT_VERSION_NUM < 10100
QMutex m_alertsMutex;

View File

@@ -68,6 +68,19 @@ const QString QB_EXT {".!qB"};
namespace libt = libtorrent;
using namespace BitTorrent;
namespace
{
using ListType = libt::entry::list_type;
ListType setToEntryList(const QSet<QString> &input)
{
ListType entryList;
foreach (const QString &setValue, input)
entryList.emplace_back(setValue.toStdString());
return entryList;
}
}
// AddTorrentData
AddTorrentData::AddTorrentData()
@@ -89,6 +102,7 @@ AddTorrentData::AddTorrentData(const AddTorrentParams &params)
: resumed(false)
, name(params.name)
, category(params.category)
, tags(params.tags)
, savePath(params.savePath)
, disableTempPath(params.disableTempPath)
, sequential(params.sequential)
@@ -213,6 +227,7 @@ TorrentHandle::TorrentHandle(Session *session, const libtorrent::torrent_handle
, m_name(data.name)
, m_savePath(Utils::Fs::toNativePath(data.savePath))
, m_category(data.category)
, m_tags(data.tags)
, m_hasSeedStatus(data.hasSeedStatus)
, m_ratioLimit(data.ratioLimit)
, m_seedingTimeLimit(data.seedingTimeLimit)
@@ -578,6 +593,50 @@ bool TorrentHandle::belongsToCategory(const QString &category) const
return false;
}
QSet<QString> TorrentHandle::tags() const
{
return m_tags;
}
bool TorrentHandle::hasTag(const QString &tag) const
{
return m_tags.contains(tag);
}
bool TorrentHandle::addTag(const QString &tag)
{
if (!Session::isValidTag(tag))
return false;
if (!hasTag(tag)) {
if (!m_session->hasTag(tag))
if (!m_session->addTag(tag))
return false;
m_tags.insert(tag);
m_session->handleTorrentTagAdded(this, tag);
m_needSaveResumeData = true;
return true;
}
return false;
}
bool TorrentHandle::removeTag(const QString &tag)
{
if (m_tags.remove(tag)) {
m_session->handleTorrentTagRemoved(this, tag);
m_needSaveResumeData = true;
return true;
}
return false;
}
void TorrentHandle::removeAllTags()
{
// QT automatically copies the container in foreach, so it's safe to mutate it.
foreach (const QString &tag, m_tags)
removeTag(tag);
}
QDateTime TorrentHandle::addedTime() const
{
return QDateTime::fromTime_t(m_nativeStatus.added_time);
@@ -1617,6 +1676,7 @@ void TorrentHandle::handleSaveResumeDataAlert(libtorrent::save_resume_data_alert
resumeData["qBt-ratioLimit"] = QString::number(m_ratioLimit).toStdString();
resumeData["qBt-seedingTimeLimit"] = QString::number(m_seedingTimeLimit).toStdString();
resumeData["qBt-category"] = m_category.toStdString();
resumeData["qBt-tags"] = setToEntryList(m_tags);
resumeData["qBt-name"] = m_name.toStdString();
resumeData["qBt-seedStatus"] = m_hasSeedStatus;
resumeData["qBt-tempPathDisabled"] = m_tempPathDisabled;

View File

@@ -30,12 +30,13 @@
#ifndef BITTORRENT_TORRENTHANDLE_H
#define BITTORRENT_TORRENTHANDLE_H
#include <QObject>
#include <QString>
#include <QDateTime>
#include <QQueue>
#include <QVector>
#include <QHash>
#include <QObject>
#include <QQueue>
#include <QSet>
#include <QString>
#include <QVector>
#include <libtorrent/torrent_handle.hpp>
#include <libtorrent/version.hpp>
@@ -93,6 +94,7 @@ namespace BitTorrent
// for both new and resumed torrents
QString name;
QString category;
QSet<QString> tags;
QString savePath;
bool disableTempPath;
bool sequential;
@@ -248,6 +250,12 @@ namespace BitTorrent
bool belongsToCategory(const QString &category) const;
bool setCategory(const QString &category);
QSet<QString> tags() const;
bool hasTag(const QString &tag) const;
bool addTag(const QString &tag);
bool removeTag(const QString &tag);
void removeAllTags();
bool hasRootFolder() const;
int filesCount() const;
@@ -445,6 +453,7 @@ namespace BitTorrent
QString m_name;
QString m_savePath;
QString m_category;
QSet<QString> m_tags;
bool m_hasSeedStatus;
qreal m_ratioLimit;
int m_seedingTimeLimit;

View File

@@ -1064,6 +1064,16 @@ void Preferences::setConfirmTorrentRecheck(bool enabled)
setValue("Preferences/Advanced/confirmTorrentRecheck", enabled);
}
bool Preferences::confirmRemoveAllTags() const
{
return value("Preferences/Advanced/confirmRemoveAllTags", true).toBool();
}
void Preferences::setConfirmRemoveAllTags(bool enabled)
{
setValue("Preferences/Advanced/confirmRemoveAllTags", enabled);
}
TrayIcon::Style Preferences::trayIconStyle() const
{
return TrayIcon::Style(value("Preferences/Advanced/TrayIconStyle", TrayIcon::NORMAL).toInt());
@@ -1327,6 +1337,16 @@ void Preferences::setCategoryFilterState(const bool checked)
setValue("TransferListFilters/CategoryFilterState", checked);
}
bool Preferences::getTagFilterState() const
{
return value("TransferListFilters/TagFilterState", true).toBool();
}
void Preferences::setTagFilterState(const bool checked)
{
setValue("TransferListFilters/TagFilterState", checked);
}
bool Preferences::getTrackerFilterState() const
{
return value("TransferListFilters/trackerFilterState", true).toBool();

View File

@@ -260,6 +260,8 @@ public:
void setConfirmTorrentDeletion(bool enabled);
bool confirmTorrentRecheck() const;
void setConfirmTorrentRecheck(bool enabled);
bool confirmRemoveAllTags() const;
void setConfirmRemoveAllTags(bool enabled);
TrayIcon::Style trayIconStyle() const;
void setTrayIconStyle(TrayIcon::Style style);
@@ -313,6 +315,7 @@ public:
void setTorImportGeometry(const QByteArray &geometry);
bool getStatusFilterState() const;
bool getCategoryFilterState() const;
bool getTagFilterState() const;
bool getTrackerFilterState() const;
int getTransSelFilter() const;
void setTransSelFilter(const int &index);
@@ -340,6 +343,7 @@ public:
public slots:
void setStatusFilterState(bool checked);
void setCategoryFilterState(bool checked);
void setTagFilterState(bool checked);
void setTrackerFilterState(bool checked);
void apply();

View File

@@ -31,6 +31,7 @@
const QString TorrentFilter::AnyCategory;
const QStringSet TorrentFilter::AnyHash = (QStringSet() << QString());
const QString TorrentFilter::AnyTag;
const TorrentFilter TorrentFilter::DownloadingTorrent(TorrentFilter::Downloading);
const TorrentFilter TorrentFilter::SeedingTorrent(TorrentFilter::Seeding);
@@ -49,16 +50,18 @@ TorrentFilter::TorrentFilter()
{
}
TorrentFilter::TorrentFilter(const Type type, const QStringSet &hashSet, const QString &category)
TorrentFilter::TorrentFilter(const Type type, const QStringSet &hashSet, const QString &category, const QString &tag)
: m_type(type)
, m_category(category)
, m_tag(tag)
, m_hashSet(hashSet)
{
}
TorrentFilter::TorrentFilter(const QString &filter, const QStringSet &hashSet, const QString &category)
TorrentFilter::TorrentFilter(const QString &filter, const QStringSet &hashSet, const QString &category, const QString &tag)
: m_type(All)
, m_category(category)
, m_tag(tag)
, m_hashSet(hashSet)
{
setTypeByName(filter);
@@ -121,11 +124,24 @@ bool TorrentFilter::setCategory(const QString &category)
return false;
}
bool TorrentFilter::setTag(const QString &tag)
{
// QString::operator==() doesn't distinguish between empty and null strings.
if ((m_tag != tag)
|| (m_tag.isNull() && !tag.isNull())
|| (!m_tag.isNull() && tag.isNull())) {
m_tag = tag;
return true;
}
return false;
}
bool TorrentFilter::match(TorrentHandle *const torrent) const
{
if (!torrent) return false;
return (matchState(torrent) && matchHash(torrent) && matchCategory(torrent));
return (matchState(torrent) && matchHash(torrent) && matchCategory(torrent) && matchTag(torrent));
}
bool TorrentFilter::matchState(BitTorrent::TorrentHandle *const torrent) const
@@ -165,3 +181,11 @@ bool TorrentFilter::matchCategory(BitTorrent::TorrentHandle *const torrent) cons
if (m_category.isNull()) return true;
else return (torrent->belongsToCategory(m_category));
}
bool TorrentFilter::matchTag(BitTorrent::TorrentHandle *const torrent) const
{
// Empty tag is a special value to indicate we're filtering for untagged torrents.
if (m_tag.isNull()) return true;
else if (m_tag.isEmpty()) return torrent->tags().isEmpty();
else return (torrent->hasTag(m_tag));
}

View File

@@ -58,8 +58,10 @@ public:
Errored
};
// These mean any permutation, including no category / tag.
static const QString AnyCategory;
static const QStringSet AnyHash;
static const QString AnyTag;
static const TorrentFilter DownloadingTorrent;
static const TorrentFilter SeedingTorrent;
@@ -71,14 +73,16 @@ public:
static const TorrentFilter ErroredTorrent;
TorrentFilter();
// category: pass empty string for "no category" or null string (QString()) for "any category"
TorrentFilter(const Type type, const QStringSet &hashSet = AnyHash, const QString &category = AnyCategory);
TorrentFilter(const QString &filter, const QStringSet &hashSet = AnyHash, const QString &category = AnyCategory);
// category & tags: pass empty string for uncategorized / untagged torrents.
// Pass null string (QString()) to disable filtering (i.e. all torrents).
TorrentFilter(const Type type, const QStringSet &hashSet = AnyHash, const QString &category = AnyCategory, const QString &tag = AnyTag);
TorrentFilter(const QString &filter, const QStringSet &hashSet = AnyHash, const QString &category = AnyCategory, const QString &tags = AnyTag);
bool setType(Type type);
bool setTypeByName(const QString &filter);
bool setHashSet(const QStringSet &hashSet);
bool setCategory(const QString &category);
bool setTag(const QString &tag);
bool match(BitTorrent::TorrentHandle *const torrent) const;
@@ -86,9 +90,11 @@ private:
bool matchState(BitTorrent::TorrentHandle *const torrent) const;
bool matchHash(BitTorrent::TorrentHandle *const torrent) const;
bool matchCategory(BitTorrent::TorrentHandle *const torrent) const;
bool matchTag(BitTorrent::TorrentHandle *const torrent) const;
Type m_type;
QString m_category;
QString m_tag;
QStringSet m_hashSet;
};