From ed56e7581b2b34a1f27dbcd63024eaccc65bed13 Mon Sep 17 00:00:00 2001 From: Martin Rotter Date: Sun, 18 Oct 2020 14:27:22 +0200 Subject: [PATCH] Work on labels, huge changes in plugin-base code, also working sync-in common logic, working label obtaining for TT-RSS. --- src/librssguard/gui/messagepreviewer.cpp | 4 +- src/librssguard/gui/messagesview.cpp | 2 +- .../miscellaneous/databasecleaner.cpp | 2 + .../miscellaneous/databasequeries.cpp | 79 ++++++++++++++++++- .../miscellaneous/databasequeries.h | 5 ++ .../services/abstract/rootitem.cpp | 7 +- src/librssguard/services/abstract/rootitem.h | 2 + .../services/abstract/serviceroot.cpp | 51 +++++++----- .../services/abstract/serviceroot.h | 5 ++ .../tt-rss/network/ttrssnetworkfactory.cpp | 59 ++++++++++++++ .../tt-rss/network/ttrssnetworkfactory.h | 12 ++- .../services/tt-rss/ttrssserviceroot.cpp | 14 +++- 12 files changed, 209 insertions(+), 33 deletions(-) diff --git a/src/librssguard/gui/messagepreviewer.cpp b/src/librssguard/gui/messagepreviewer.cpp index 02516b97f..28fee2941 100755 --- a/src/librssguard/gui/messagepreviewer.cpp +++ b/src/librssguard/gui/messagepreviewer.cpp @@ -205,7 +205,7 @@ void MessagePreviewer::updateLabels(bool only_clear) { return; } - if (m_root.data() != nullptr) { + if (m_root.data() != nullptr && m_root.data()->getParentServiceRoot()->labelsNode()->labels().size() > 0) { m_separator = m_toolBar->addSeparator(); QSqlDatabase database = qApp->database()->connection(metaObject()->className()); @@ -216,7 +216,7 @@ void MessagePreviewer::updateLabels(bool only_clear) { btn_label->setCheckable(true); btn_label->setIcon(Label::generateIcon(label->color())); btn_label->setAutoRaise(false); - btn_label->setText(label->title()); + btn_label->setText(QSL(" ") + label->title()); btn_label->setToolButtonStyle(Qt::ToolButtonStyle::ToolButtonTextBesideIcon); btn_label->setChecked(DatabaseQueries::isLabelAssignedToMessage(database, label, m_message)); diff --git a/src/librssguard/gui/messagesview.cpp b/src/librssguard/gui/messagesview.cpp index 3c6910014..8288ebeda 100644 --- a/src/librssguard/gui/messagesview.cpp +++ b/src/librssguard/gui/messagesview.cpp @@ -152,7 +152,7 @@ void MessagesView::setupAppearance() { header()->setDefaultSectionSize(MESSAGES_VIEW_DEFAULT_COL); header()->setMinimumSectionSize(MESSAGES_VIEW_MINIMUM_COL); header()->setCascadingSectionResizes(false); - header()->setStretchLastSection(true); + header()->setStretchLastSection(false); } void MessagesView::focusInEvent(QFocusEvent* event) { diff --git a/src/librssguard/miscellaneous/databasecleaner.cpp b/src/librssguard/miscellaneous/databasecleaner.cpp index f2071c77d..868aa3333 100644 --- a/src/librssguard/miscellaneous/databasecleaner.cpp +++ b/src/librssguard/miscellaneous/databasecleaner.cpp @@ -60,6 +60,8 @@ void DatabaseCleaner::purgeDatabaseData(const CleanerOrders& which_data) { emit purgeProgress(progress, tr("Starred messages purged...")); } + result &= DatabaseQueries::purgeLeftoverLabelAssignments(database); + if (which_data.m_shrinkDatabase) { progress += difference; emit purgeProgress(progress, tr("Shrinking database file...")); diff --git a/src/librssguard/miscellaneous/databasequeries.cpp b/src/librssguard/miscellaneous/databasequeries.cpp index 87c756d47..4059d2ed4 100755 --- a/src/librssguard/miscellaneous/databasequeries.cpp +++ b/src/librssguard/miscellaneous/databasequeries.cpp @@ -156,7 +156,9 @@ bool DatabaseQueries::createLabel(const QSqlDatabase& db, Label* label, int acco // NOTE: This custom ID in this object will be probably // overwritten in online-synchronized labels. - label->setCustomId(QString::number(label->id())); + if (label->customId().isEmpty()) { + label->setCustomId(QString::number(label->id())); + } } // Fixup missing custom IDs. @@ -988,7 +990,9 @@ bool DatabaseQueries::deleteAccount(const QSqlDatabase& db, int account_id) { << QSL("DELETE FROM Feeds WHERE account_id = :account_id;") << QSL("DELETE FROM Categories WHERE account_id = :account_id;") << QSL("DELETE FROM MessageFiltersInFeeds WHERE account_id = :account_id;") - << QSL("DELETE FROM Accounts WHERE id = :account_id;"); + << QSL("DELETE FROM Accounts WHERE id = :account_id;") + << QSL("DELETE FROM LabelsInMessages WHERE account_id = :account_id;") + << QSL("DELETE FROM Labels WHERE account_id = :account_id;"); for (const QString& q : queries) { query.prepare(q); @@ -1029,6 +1033,17 @@ bool DatabaseQueries::deleteAccountData(const QSqlDatabase& db, int account_id, q.bindValue(QSL(":account_id"), account_id); result &= q.exec(); + if (delete_messages_too) { + // If we delete message, make sure to delete message/label assignments too. + q.prepare(QSL("DELETE FROM LabelsInMessages WHERE account_id = :account_id;")); + q.bindValue(QSL(":account_id"), account_id); + result &= q.exec(); + } + + q.prepare(QSL("DELETE FROM Labels WHERE account_id = :account_id;")); + q.bindValue(QSL(":account_id"), account_id); + result &= q.exec(); + return result; } @@ -1118,8 +1133,8 @@ bool DatabaseQueries::purgeLeftoverMessages(const QSqlDatabase& db, int account_ QSqlQuery q(db); q.setForwardOnly(true); - q.prepare( - QSL("DELETE FROM Messages WHERE account_id = :account_id AND feed NOT IN (SELECT custom_id FROM Feeds WHERE account_id = :account_id);")); + q.prepare(QSL("DELETE FROM Messages " + "WHERE account_id = :account_id AND feed NOT IN (SELECT custom_id FROM Feeds WHERE account_id = :account_id);")); q.bindValue(QSL(":account_id"), account_id); if (!q.exec()) { @@ -1134,6 +1149,52 @@ bool DatabaseQueries::purgeLeftoverMessages(const QSqlDatabase& db, int account_ } } +bool DatabaseQueries::purgeLeftoverLabelAssignments(const QSqlDatabase& db, int account_id) { + QSqlQuery q(db); + bool succ = false; + + if (account_id <= 0) { + succ = q.exec(QSL("DELETE FROM LabelsInMessages " + "WHERE NOT EXISTS (SELECT * FROM Messages WHERE Messages.account_id = LabelsInMessages.account_id AND Messages.custom_id = LabelsInMessages.message);")) + && + q.exec(QSL("DELETE FROM LabelsInMessages " + "WHERE NOT EXISTS (SELECT * FROM Labels WHERE Labels.account_id = LabelsInMessages.account_id AND Labels.custom_id = LabelsInMessages.label);")); + } + else { + q.prepare(QSL("DELETE FROM LabelsInMessages " + "WHERE account_id = :account_id AND " + " (message NOT IN (SELECT custom_id FROM Messages WHERE account_id = :account_id) OR " + " label NOT IN (SELECT custom_id FROM Labels WHERE account_id = :account_id));")); + q.bindValue(QSL(":account_id"), account_id); + succ = q.exec(); + } + + if (!succ) { + auto xx = q.lastError().text(); + + qWarningNN << LOGSEC_DB + << "Removing of leftover label assignments failed: '" + << q.lastError().text() + << "'."; + } + + return succ; +} + +bool DatabaseQueries::purgeLabelsAndLabelAssignments(const QSqlDatabase& db, int account_id) { + QSqlQuery q(db); + + q.prepare(QSL("DELETE FROM LabelsInMessages WHERE account_id = :account_id;")); + q.bindValue(QSL(":account_id"), account_id); + auto succ = q.exec(); + + q.prepare(QSL("DELETE FROM Labels WHERE account_id = :account_id;")); + q.bindValue(QSL(":account_id"), account_id); + succ &= q.exec(); + + return succ; +} + bool DatabaseQueries::storeAccountTree(const QSqlDatabase& db, RootItem* tree_root, int account_id) { QSqlQuery query_category(db); QSqlQuery query_feed(db); @@ -1179,6 +1240,16 @@ bool DatabaseQueries::storeAccountTree(const QSqlDatabase& db, RootItem* tree_ro return false; } } + else if (child->kind() == RootItem::Kind::Labels) { + // Add all labels. + for (RootItem* lbl : child->childItems()) { + Label* label = lbl->toLabel(); + + if (!createLabel(db, label, account_id)) { + return false; + } + } + } } return true; diff --git a/src/librssguard/miscellaneous/databasequeries.h b/src/librssguard/miscellaneous/databasequeries.h index 48fe15796..6522a6988 100644 --- a/src/librssguard/miscellaneous/databasequeries.h +++ b/src/librssguard/miscellaneous/databasequeries.h @@ -47,6 +47,11 @@ class DatabaseQueries { static bool purgeMessagesFromBin(const QSqlDatabase& db, bool clear_only_read, int account_id); static bool purgeLeftoverMessages(const QSqlDatabase& db, int account_id); + // Purges message/label assignments where source message or label does not exist. + // If account ID smaller than 0 is passed, then do this for all accounts. + static bool purgeLeftoverLabelAssignments(const QSqlDatabase& db, int account_id = -1); + static bool purgeLabelsAndLabelAssignments(const QSqlDatabase& db, int account_id); + // Counts of unread/all messages. static QMap> getMessageCountsForCategory(const QSqlDatabase& db, const QString& custom_id, int account_id, bool only_total_counts, diff --git a/src/librssguard/services/abstract/rootitem.cpp b/src/librssguard/services/abstract/rootitem.cpp index 27a7487cb..6b8ea2c20 100644 --- a/src/librssguard/services/abstract/rootitem.cpp +++ b/src/librssguard/services/abstract/rootitem.cpp @@ -7,6 +7,7 @@ #include "miscellaneous/iconfactory.h" #include "services/abstract/category.h" #include "services/abstract/feed.h" +#include "services/abstract/label.h" #include "services/abstract/recyclebin.h" #include "services/abstract/serviceroot.h" @@ -14,7 +15,7 @@ RootItem::RootItem(RootItem* parent_item) : QObject(nullptr), m_kind(RootItem::Kind::Root), m_id(NO_PARENT_CATEGORY), m_customId(QL1S("")), - m_title(QString()), m_description(QString()), m_keepOnTop(false), m_parentItem(parent_item) {} + m_title(QString()), m_description(QString()), m_keepOnTop(false), m_childItems(QList()), m_parentItem(parent_item) {} RootItem::RootItem(const RootItem& other) : RootItem(nullptr) { setTitle(other.title()); @@ -479,6 +480,10 @@ Feed* RootItem::toFeed() const { return dynamic_cast(const_cast(this)); } +Label* RootItem::toLabel() const { + return dynamic_cast(const_cast(this)); +} + ServiceRoot* RootItem::toServiceRoot() const { return dynamic_cast(const_cast(this)); } diff --git a/src/librssguard/services/abstract/rootitem.h b/src/librssguard/services/abstract/rootitem.h index fd1a406ed..2f792ce97 100644 --- a/src/librssguard/services/abstract/rootitem.h +++ b/src/librssguard/services/abstract/rootitem.h @@ -11,6 +11,7 @@ class Category; class Feed; +class Label; class ServiceRoot; class QAction; @@ -173,6 +174,7 @@ class RSSGUARD_DLLSPEC RootItem : public QObject { // Converters Category* toCategory() const; Feed* toFeed() const; + Label* toLabel() const; ServiceRoot* toServiceRoot() const; bool keepOnTop() const; diff --git a/src/librssguard/services/abstract/serviceroot.cpp b/src/librssguard/services/abstract/serviceroot.cpp index 0ee22efe7..2e3d1604a 100644 --- a/src/librssguard/services/abstract/serviceroot.cpp +++ b/src/librssguard/services/abstract/serviceroot.cpp @@ -141,8 +141,8 @@ void ServiceRoot::updateCounts(bool including_total_count) { void ServiceRoot::completelyRemoveAllData() { // Purge old data from SQL and clean all model items. - removeOldAccountFromDatabase(true); cleanAllItemsFromModel(); + removeOldAccountFromDatabase(true); updateCounts(true); itemChanged(QList() << this); requestReloadMessageList(true); @@ -156,10 +156,18 @@ void ServiceRoot::removeOldAccountFromDatabase(bool including_messages) { void ServiceRoot::cleanAllItemsFromModel() { for (RootItem* top_level_item : childItems()) { - if (top_level_item->kind() != RootItem::Kind::Bin && top_level_item->kind() != RootItem::Kind::Important) { + if (top_level_item->kind() != RootItem::Kind::Bin && + top_level_item->kind() != RootItem::Kind::Important && + top_level_item->kind() != RootItem::Kind::Labels) { requestItemRemoval(top_level_item); } } + + if (labelsNode() != nullptr) { + for (RootItem* lbl : labelsNode()->childItems()) { + requestItemRemoval(lbl); + } + } } bool ServiceRoot::cleanFeeds(QList items, bool clean_read_only) { @@ -199,23 +207,6 @@ bool ServiceRoot::cleanFeeds(QList items, bool clean_read_only) { void ServiceRoot::storeNewFeedTree(RootItem* root) { DatabaseQueries::storeAccountTree(qApp->database()->connection(metaObject()->className()), root, accountId()); - - /*if (DatabaseQueries::storeAccountTree(database, root, accountId())) { - RecycleBin* bin = recycleBin(); - - if (bin != nullptr && !childItems().contains(bin)) { - // As the last item, add recycle bin, which is needed. - appendChild(bin); - bin->updateCounts(true); - } - - ImportantNode* imp = importantNode(); - - if (imp != nullptr && !childItems().contains(imp)) { - appendChild(imp); - imp->updateCounts(true); - } - }*/ } void ServiceRoot::removeLeftOverMessages() { @@ -230,6 +221,12 @@ void ServiceRoot::removeLeftOverMessageFilterAssignments() { DatabaseQueries::purgeLeftoverMessageFilterAssignments(database, accountId()); } +void ServiceRoot::removeLeftOverMessageLabelAssignments() { + QSqlDatabase database = qApp->database()->connection(metaObject()->className()); + + DatabaseQueries::purgeLeftoverLabelAssignments(database, accountId()); +} + QList ServiceRoot::undeletedMessages() const { QSqlDatabase database = qApp->database()->connection(metaObject()->className()); @@ -345,10 +342,22 @@ void ServiceRoot::syncIn() { // so remove left over messages and filter assignments. removeLeftOverMessages(); removeLeftOverMessageFilterAssignments(); + removeLeftOverMessageLabelAssignments(); for (RootItem* top_level_item : new_tree->childItems()) { - top_level_item->setParent(nullptr); - requestItemReassignment(top_level_item, this); + if (top_level_item->kind() != Kind::Labels) { + top_level_item->setParent(nullptr); + requestItemReassignment(top_level_item, this); + } + else { + // It seems that some labels got synced-in. + if (labelsNode() != nullptr) { + for (RootItem* new_lbl : top_level_item->childItems()) { + new_lbl->setParent(nullptr); + requestItemReassignment(new_lbl, labelsNode()); + } + } + } } new_tree->clearChildren(); diff --git a/src/librssguard/services/abstract/serviceroot.h b/src/librssguard/services/abstract/serviceroot.h index ff43fafaf..6ec4bed8e 100644 --- a/src/librssguard/services/abstract/serviceroot.h +++ b/src/librssguard/services/abstract/serviceroot.h @@ -191,6 +191,11 @@ class ServiceRoot : public RootItem { // from another machine and then performs sync-in on this machine. void removeLeftOverMessageFilterAssignments(); + // Removes all labels/message assignments which are + // assigned to non-existing messages or which are + // assigned from non-existing labels. + void removeLeftOverMessageLabelAssignments(); + QStringList textualFeedUrls(const QList& feeds) const; QStringList textualFeedIds(const QList& feeds) const; QStringList customIDsOfMessages(const QList& changes); diff --git a/src/librssguard/services/tt-rss/network/ttrssnetworkfactory.cpp b/src/librssguard/services/tt-rss/network/ttrssnetworkfactory.cpp index f64dfe89a..5947c5e83 100644 --- a/src/librssguard/services/tt-rss/network/ttrssnetworkfactory.cpp +++ b/src/librssguard/services/tt-rss/network/ttrssnetworkfactory.cpp @@ -8,6 +8,7 @@ #include "miscellaneous/textfactory.h" #include "network-web/networkfactory.h" #include "services/abstract/category.h" +#include "services/abstract/label.h" #include "services/abstract/rootitem.h" #include "services/tt-rss/definitions.h" #include "services/tt-rss/ttrssfeed.h" @@ -150,6 +151,47 @@ TtRssResponse TtRssNetworkFactory::logout() { } } +TtRssGetLabelsResponse TtRssNetworkFactory::getLabels() { + QJsonObject json; + + json["op"] = QSL("getLabels"); + json["sid"] = m_sessionId; + + const int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + QByteArray result_raw; + QList> headers; + + headers << QPair(HTTP_HEADERS_CONTENT_TYPE, TTRSS_CONTENT_TYPE_JSON); + headers << NetworkFactory::generateBasicAuthHeader(m_authUsername, m_authPassword); + + NetworkResult network_reply = NetworkFactory::performNetworkOperation(m_fullUrl, timeout, + QJsonDocument(json).toJson(QJsonDocument::Compact), + result_raw, + QNetworkAccessManager::PostOperation, + headers); + TtRssGetLabelsResponse result(QString::fromUtf8(result_raw)); + + if (result.isNotLoggedIn()) { + // We are not logged in. + login(); + json["sid"] = m_sessionId; + network_reply = NetworkFactory::performNetworkOperation(m_fullUrl, timeout, QJsonDocument(json).toJson(QJsonDocument::Compact), + result_raw, + QNetworkAccessManager::PostOperation, + headers); + result = TtRssGetLabelsResponse(QString::fromUtf8(result_raw)); + } + + if (network_reply.first != QNetworkReply::NoError) { + qWarningNN << LOGSEC_TTRSS + << "getLabels failed with error:" + << QUOTE_W_SPACE_DOT(network_reply.first); + } + + m_lastError = network_reply.first; + return result; +} + TtRssGetFeedsCategoriesResponse TtRssNetworkFactory::getFeedsCategories() { QJsonObject json; @@ -497,6 +539,7 @@ bool TtRssResponse::hasError() const { TtRssGetFeedsCategoriesResponse::TtRssGetFeedsCategoriesResponse(const QString& raw_content) : TtRssResponse(raw_content) {} TtRssGetFeedsCategoriesResponse::~TtRssGetFeedsCategoriesResponse() = default; + RootItem* TtRssGetFeedsCategoriesResponse::feedsCategories(bool obtain_icons, QString base_address) const { auto* parent = new RootItem(); @@ -675,3 +718,19 @@ QString TtRssUnsubscribeFeedResponse::code() const { return QString(); } + +TtRssGetLabelsResponse::TtRssGetLabelsResponse(const QString& raw_content) : TtRssResponse(raw_content) {} + +QList TtRssGetLabelsResponse::labels() const { + QList labels; + + for (const QJsonValue& lbl_val : m_rawContent["content"].toArray()) { + QJsonObject lbl_obj = lbl_val.toObject(); + Label* lbl = new Label(lbl_obj["caption"].toString(), QColor(lbl_obj["fg_color"].toString())); + + lbl->setCustomId(QString::number(lbl_obj["id"].toInt())); + labels.append(lbl); + } + + return labels; +} diff --git a/src/librssguard/services/tt-rss/network/ttrssnetworkfactory.h b/src/librssguard/services/tt-rss/network/ttrssnetworkfactory.h index 90419b88e..b7902d294 100644 --- a/src/librssguard/services/tt-rss/network/ttrssnetworkfactory.h +++ b/src/librssguard/services/tt-rss/network/ttrssnetworkfactory.h @@ -11,8 +11,8 @@ #include class RootItem; - class TtRssFeed; +class Label; class TtRssResponse { public: @@ -41,6 +41,13 @@ class TtRssLoginResponse : public TtRssResponse { QString sessionId() const; }; +class TtRssGetLabelsResponse : public TtRssResponse { + public: + explicit TtRssGetLabelsResponse(const QString& raw_content = QString()); + + QList labels() const; +}; + class TtRssGetFeedsCategoriesResponse : public TtRssResponse { public: explicit TtRssGetFeedsCategoriesResponse(const QString& raw_content = QString()); @@ -139,6 +146,9 @@ class TtRssNetworkFactory { // Logs user out. TtRssResponse logout(); + // Gets list of labels from the server. + TtRssGetLabelsResponse getLabels(); + // Gets feeds from the server. TtRssGetFeedsCategoriesResponse getFeedsCategories(); diff --git a/src/librssguard/services/tt-rss/ttrssserviceroot.cpp b/src/librssguard/services/tt-rss/ttrssserviceroot.cpp index 9cd180964..b92bdec20 100644 --- a/src/librssguard/services/tt-rss/ttrssserviceroot.cpp +++ b/src/librssguard/services/tt-rss/ttrssserviceroot.cpp @@ -10,6 +10,7 @@ #include "miscellaneous/textfactory.h" #include "network-web/networkfactory.h" #include "services/abstract/importantnode.h" +#include "services/abstract/labelsnode.h" #include "services/abstract/recyclebin.h" #include "services/tt-rss/definitions.h" #include "services/tt-rss/gui/formeditttrssaccount.h" @@ -36,7 +37,7 @@ void TtRssServiceRoot::start(bool freshly_activated) { loadFromDatabase(); loadCacheFromFile(accountId()); - if (childCount() <= 2) { + if (childCount() <= 3) { syncIn(); } } @@ -222,10 +223,17 @@ void TtRssServiceRoot::updateTitle() { } RootItem* TtRssServiceRoot::obtainNewTreeForSyncIn() const { - TtRssGetFeedsCategoriesResponse feed_cats_response = m_network->getFeedsCategories(); + TtRssGetFeedsCategoriesResponse feed_cats = m_network->getFeedsCategories(); + TtRssGetLabelsResponse labels = m_network->getLabels(); if (m_network->lastError() == QNetworkReply::NoError) { - return feed_cats_response.feedsCategories(true, m_network->url()); + auto* tree = feed_cats.feedsCategories(true, m_network->url()); + auto* lblroot = new LabelsNode(tree); + + lblroot->setChildItems(labels.labels()); + tree->appendChild(lblroot); + + return tree; } else { return nullptr;