Add tags feature
* show the tags in the entry preview * allow searching by tag * add a sidebar listing the tags in the database * filter entries by tag on click * Introduce a new TagsEdit widget that provides pill aesthetics, fast removal functionality and autocompletion * add tests for the tags feature * introduce the "is" tag for searching. Support for weak passwords and expired added.
This commit is contained in:
committed by
Jonathan White
parent
56a1b465a1
commit
4a21cee98c
@@ -117,6 +117,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
|
||||
{Config::GUI_ListViewState, {QS("GUI/ListViewState"), Local, {}}},
|
||||
{Config::GUI_SearchViewState, {QS("GUI/SearchViewState"), Local, {}}},
|
||||
{Config::GUI_SplitterState, {QS("GUI/SplitterState"), Local, {}}},
|
||||
{Config::GUI_GroupSplitterState, {QS("GUI/GroupSplitterState"), Local, {}}},
|
||||
{Config::GUI_PreviewSplitterState, {QS("GUI/PreviewSplitterState"), Local, {}}},
|
||||
{Config::GUI_AutoTypeSelectDialogSize, {QS("GUI/AutoTypeSelectDialogSize"), Local, QSize(600, 250)}},
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ public:
|
||||
GUI_SearchViewState,
|
||||
GUI_PreviewSplitterState,
|
||||
GUI_SplitterState,
|
||||
GUI_GroupSplitterState,
|
||||
GUI_AutoTypeSelectDialogSize,
|
||||
GUI_CheckForUpdatesNextCheck,
|
||||
|
||||
|
||||
@@ -52,7 +52,11 @@ Database::Database()
|
||||
|
||||
// other signals
|
||||
connect(m_metadata, &Metadata::modified, this, &Database::markAsModified);
|
||||
connect(this, &Database::databaseOpened, this, [this]() { updateCommonUsernames(); });
|
||||
connect(this, &Database::databaseOpened, this, [this]() {
|
||||
updateCommonUsernames();
|
||||
updateTagList();
|
||||
});
|
||||
connect(this, &Database::modified, this, [this] { updateTagList(); });
|
||||
connect(this, &Database::databaseSaved, this, [this]() { updateCommonUsernames(); });
|
||||
connect(m_fileWatcher, &FileWatcher::fileChanged, this, &Database::databaseFileChanged);
|
||||
|
||||
@@ -504,6 +508,7 @@ void Database::releaseData()
|
||||
|
||||
m_deletedObjects.clear();
|
||||
m_commonUsernames.clear();
|
||||
m_tagList.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -700,17 +705,46 @@ void Database::addDeletedObject(const QUuid& uuid)
|
||||
addDeletedObject(delObj);
|
||||
}
|
||||
|
||||
QList<QString> Database::commonUsernames()
|
||||
const QStringList& Database::commonUsernames() const
|
||||
{
|
||||
return m_commonUsernames;
|
||||
}
|
||||
|
||||
const QStringList& Database::tagList() const
|
||||
{
|
||||
return m_tagList;
|
||||
}
|
||||
|
||||
void Database::updateCommonUsernames(int topN)
|
||||
{
|
||||
m_commonUsernames.clear();
|
||||
m_commonUsernames.append(rootGroup()->usernamesRecursive(topN));
|
||||
}
|
||||
|
||||
void Database::updateTagList()
|
||||
{
|
||||
m_tagList.clear();
|
||||
if (!m_rootGroup) {
|
||||
emit tagListUpdated();
|
||||
return;
|
||||
}
|
||||
|
||||
// Search groups recursively looking for tags
|
||||
// Use a set to prevent adding duplicates
|
||||
QSet<QString> tagSet;
|
||||
for (const auto group : m_rootGroup->groupsRecursive(true)) {
|
||||
for (const auto entry : group->entries()) {
|
||||
for (auto tag : entry->tagList()) {
|
||||
tagSet.insert(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_tagList = tagSet.toList();
|
||||
m_tagList.sort();
|
||||
emit tagListUpdated();
|
||||
}
|
||||
|
||||
const QUuid& Database::cipher() const
|
||||
{
|
||||
return m_data.cipher;
|
||||
|
||||
@@ -125,7 +125,8 @@ public:
|
||||
bool containsDeletedObject(const DeletedObject& uuid) const;
|
||||
void setDeletedObjects(const QList<DeletedObject>& delObjs);
|
||||
|
||||
QList<QString> commonUsernames();
|
||||
const QStringList& commonUsernames() const;
|
||||
const QStringList& tagList() const;
|
||||
|
||||
QSharedPointer<const CompositeKey> key() const;
|
||||
bool setKey(const QSharedPointer<const CompositeKey>& key,
|
||||
@@ -151,6 +152,7 @@ public slots:
|
||||
void markAsModified();
|
||||
void markAsClean();
|
||||
void updateCommonUsernames(int topN = 10);
|
||||
void updateTagList();
|
||||
void markNonDataChange();
|
||||
|
||||
signals:
|
||||
@@ -166,6 +168,7 @@ signals:
|
||||
void databaseSaved();
|
||||
void databaseDiscarded();
|
||||
void databaseFileChanged();
|
||||
void tagListUpdated();
|
||||
|
||||
private:
|
||||
struct DatabaseData
|
||||
@@ -228,7 +231,8 @@ private:
|
||||
bool m_hasNonDataChange = false;
|
||||
QString m_keyError;
|
||||
|
||||
QList<QString> m_commonUsernames;
|
||||
QStringList m_commonUsernames;
|
||||
QStringList m_tagList;
|
||||
|
||||
QUuid m_uuid;
|
||||
static QHash<QUuid, QPointer<Database>> s_uuidMap;
|
||||
|
||||
@@ -190,6 +190,12 @@ QString Entry::tags() const
|
||||
return m_data.tags;
|
||||
}
|
||||
|
||||
QStringList Entry::tagList() const
|
||||
{
|
||||
static QRegExp rx("(\\ |\\,|\\.|\\:|\\t|\\;)");
|
||||
return tags().split(rx, QString::SkipEmptyParts);
|
||||
}
|
||||
|
||||
const TimeInfo& Entry::timeInfo() const
|
||||
{
|
||||
return m_data.timeInfo;
|
||||
@@ -210,7 +216,7 @@ QString Entry::defaultAutoTypeSequence() const
|
||||
return m_data.defaultAutoTypeSequence;
|
||||
}
|
||||
|
||||
const QSharedPointer<PasswordHealth>& Entry::passwordHealth()
|
||||
const QSharedPointer<PasswordHealth> Entry::passwordHealth()
|
||||
{
|
||||
if (!m_data.passwordHealth) {
|
||||
m_data.passwordHealth.reset(new PasswordHealth(resolvePlaceholder(password())));
|
||||
@@ -218,6 +224,14 @@ const QSharedPointer<PasswordHealth>& Entry::passwordHealth()
|
||||
return m_data.passwordHealth;
|
||||
}
|
||||
|
||||
const QSharedPointer<PasswordHealth> Entry::passwordHealth() const
|
||||
{
|
||||
if (!m_data.passwordHealth) {
|
||||
return QSharedPointer<PasswordHealth>::create(resolvePlaceholder(password()));
|
||||
}
|
||||
return m_data.passwordHealth;
|
||||
}
|
||||
|
||||
bool Entry::excludeFromReports() const
|
||||
{
|
||||
return m_data.excludeFromReports
|
||||
|
||||
@@ -88,6 +88,7 @@ public:
|
||||
QString backgroundColor() const;
|
||||
QString overrideUrl() const;
|
||||
QString tags() const;
|
||||
QStringList tagList() const;
|
||||
const TimeInfo& timeInfo() const;
|
||||
bool autoTypeEnabled() const;
|
||||
int autoTypeObfuscation() const;
|
||||
@@ -113,7 +114,8 @@ public:
|
||||
QUuid previousParentGroupUuid() const;
|
||||
int size() const;
|
||||
QString path() const;
|
||||
const QSharedPointer<PasswordHealth>& passwordHealth();
|
||||
const QSharedPointer<PasswordHealth> passwordHealth();
|
||||
const QSharedPointer<PasswordHealth> passwordHealth() const;
|
||||
bool excludeFromReports() const;
|
||||
void setExcludeFromReports(bool state);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
#include "EntrySearcher.h"
|
||||
|
||||
#include "PasswordHealth.h"
|
||||
#include "core/Group.h"
|
||||
#include "core/Tools.h"
|
||||
|
||||
@@ -152,7 +153,7 @@ bool EntrySearcher::searchEntryImpl(const Entry* entry)
|
||||
auto hierarchy = entry->group()->hierarchy().join('/').prepend("/");
|
||||
|
||||
// By default, empty term matches every entry.
|
||||
// However when skipping protected fields, we will recject everything instead
|
||||
// However when skipping protected fields, we will reject everything instead
|
||||
bool found = !m_skipProtected;
|
||||
for (const auto& term : m_searchTerms) {
|
||||
switch (term.field) {
|
||||
@@ -195,11 +196,31 @@ bool EntrySearcher::searchEntryImpl(const Entry* entry)
|
||||
found = term.regex.match(entry->group()->name()).hasMatch();
|
||||
}
|
||||
break;
|
||||
case Field::Tag:
|
||||
found = term.regex.match(entry->tags()).hasMatch();
|
||||
break;
|
||||
case Field::Is:
|
||||
if (term.word.compare("expired", Qt::CaseInsensitive) == 0) {
|
||||
found = entry->isExpired();
|
||||
break;
|
||||
} else if (term.word.compare("weak", Qt::CaseInsensitive) == 0) {
|
||||
if (!entry->excludeFromReports() && !entry->password().isEmpty() && !entry->isExpired()) {
|
||||
const auto quality = entry->passwordHealth()->quality();
|
||||
if (quality == PasswordHealth::Quality::Bad || quality == PasswordHealth::Quality::Poor
|
||||
|| quality == PasswordHealth::Quality::Weak) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
found = false;
|
||||
break;
|
||||
default:
|
||||
// Terms without a specific field try to match title, username, url, and notes
|
||||
found = term.regex.match(entry->resolvePlaceholder(entry->title())).hasMatch()
|
||||
|| term.regex.match(entry->resolvePlaceholder(entry->username())).hasMatch()
|
||||
|| term.regex.match(entry->resolvePlaceholder(entry->url())).hasMatch()
|
||||
|| term.regex.match(entry->resolvePlaceholder(entry->tags())).hasMatch()
|
||||
|| term.regex.match(entry->notes()).hasMatch();
|
||||
}
|
||||
|
||||
@@ -226,10 +247,13 @@ void EntrySearcher::parseSearchTerms(const QString& searchString)
|
||||
{QStringLiteral("pw"), Field::Password},
|
||||
{QStringLiteral("password"), Field::Password},
|
||||
{QStringLiteral("title"), Field::Title},
|
||||
{QStringLiteral("t"), Field::Title},
|
||||
{QStringLiteral("u"), Field::Username}, // u: stands for username rather than url
|
||||
{QStringLiteral("url"), Field::Url},
|
||||
{QStringLiteral("username"), Field::Username},
|
||||
{QStringLiteral("group"), Field::Group}};
|
||||
{QStringLiteral("group"), Field::Group},
|
||||
{QStringLiteral("tag"), Field::Tag},
|
||||
{QStringLiteral("is"), Field::Is}};
|
||||
|
||||
m_searchTerms.clear();
|
||||
auto results = m_termParser.globalMatch(searchString);
|
||||
|
||||
@@ -38,7 +38,9 @@ public:
|
||||
AttributeKV,
|
||||
Attachment,
|
||||
AttributeValue,
|
||||
Group
|
||||
Group,
|
||||
Tag,
|
||||
Is
|
||||
};
|
||||
|
||||
struct SearchTerm
|
||||
|
||||
Reference in New Issue
Block a user