From edc30fbe4876ca5a62e5cbb56eef3f2c8419ff79 Mon Sep 17 00:00:00 2001 From: Martin Rotter Date: Thu, 9 Nov 2023 12:34:00 +0100 Subject: [PATCH] greader plugin now has ability to export/import feeds via API usage --- src/librssguard/gui/feedsview.cpp | 2 + .../notifications/basetoastnotification.cpp | 3 +- .../toastnotificationsmanager.cpp | 2 + .../notifications/toastnotificationsmanager.h | 2 + .../gui/settings/settingslocalization.cpp | 54 +++++++++++--- .../gui/settings/settingslocalization.ui | 11 +++ .../gui/settings/settingsnotifications.cpp | 8 ++ .../gui/settings/settingsnotifications.ui | 47 ++++++++++-- src/librssguard/miscellaneous/application.cpp | 3 - .../miscellaneous/localization.cpp | 5 +- src/librssguard/miscellaneous/localization.h | 1 - src/librssguard/miscellaneous/settings.cpp | 6 ++ src/librssguard/miscellaneous/settings.h | 6 ++ .../services/greader/definitions.h | 48 ++++++------ .../services/greader/greadernetwork.cpp | 63 +++++++++++++++- .../services/greader/greadernetwork.h | 6 +- .../services/greader/greaderserviceroot.cpp | 73 +++++++++++++++++++ .../services/greader/greaderserviceroot.h | 5 ++ 18 files changed, 298 insertions(+), 47 deletions(-) diff --git a/src/librssguard/gui/feedsview.cpp b/src/librssguard/gui/feedsview.cpp index 75963f7a8..55a7f22ff 100644 --- a/src/librssguard/gui/feedsview.cpp +++ b/src/librssguard/gui/feedsview.cpp @@ -241,6 +241,7 @@ void FeedsView::clearSelectedItems() { {}, QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No, QMessageBox::StandardButton::No) != QMessageBox::StandardButton::Yes) { + return; } for (auto* it : selectedItems()) { @@ -257,6 +258,7 @@ void FeedsView::clearAllItems() { {}, QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No, QMessageBox::StandardButton::No) != QMessageBox::StandardButton::Yes) { + return; } m_sourceModel->markItemCleared(m_sourceModel->rootItem(), false); diff --git a/src/librssguard/gui/notifications/basetoastnotification.cpp b/src/librssguard/gui/notifications/basetoastnotification.cpp index b54b6271b..2538a11ea 100644 --- a/src/librssguard/gui/notifications/basetoastnotification.cpp +++ b/src/librssguard/gui/notifications/basetoastnotification.cpp @@ -3,6 +3,7 @@ #include "gui/notifications/basetoastnotification.h" #include "miscellaneous/iconfactory.h" +#include "miscellaneous/settings.h" #include #include @@ -14,7 +15,7 @@ using namespace std::chrono_literals; BaseToastNotification::BaseToastNotification(QWidget* parent) : QDialog(parent), m_timerId(-1) { setAttribute(Qt::WidgetAttribute::WA_ShowWithoutActivating); - setFixedWidth(NOTIFICATIONS_WIDTH); + setFixedWidth(qApp->settings()->value(GROUP(GUI), SETTING(GUI::ToastNotificationsWidth)).toInt()); setFocusPolicy(Qt::FocusPolicy::NoFocus); setAttribute(Qt::WidgetAttribute::WA_DeleteOnClose, false); diff --git a/src/librssguard/gui/notifications/toastnotificationsmanager.cpp b/src/librssguard/gui/notifications/toastnotificationsmanager.cpp index 61b553e61..16f98627d 100644 --- a/src/librssguard/gui/notifications/toastnotificationsmanager.cpp +++ b/src/librssguard/gui/notifications/toastnotificationsmanager.cpp @@ -56,6 +56,8 @@ void ToastNotificationsManager::setPosition(NotificationPosition position) { m_position = position; } +void ToastNotificationsManager::resetNotifications() {} + void ToastNotificationsManager::clear() { for (BaseToastNotification* notif : m_activeNotifications) { closeNotification(notif, true); diff --git a/src/librssguard/gui/notifications/toastnotificationsmanager.h b/src/librssguard/gui/notifications/toastnotificationsmanager.h index 6617fad00..e155a490e 100644 --- a/src/librssguard/gui/notifications/toastnotificationsmanager.h +++ b/src/librssguard/gui/notifications/toastnotificationsmanager.h @@ -42,6 +42,8 @@ class ToastNotificationsManager : public QObject { NotificationPosition position() const; void setPosition(NotificationPosition position); + void resetNotifications(); + public slots: void clear(); void showNotification(Notification::Event event, const GuiMessage& msg, const GuiAction& action); diff --git a/src/librssguard/gui/settings/settingslocalization.cpp b/src/librssguard/gui/settings/settingslocalization.cpp index 41c8c9794..6d9bc1b6a 100644 --- a/src/librssguard/gui/settings/settingslocalization.cpp +++ b/src/librssguard/gui/settings/settingslocalization.cpp @@ -17,11 +17,12 @@ SettingsLocalization::SettingsLocalization(Settings* settings, QWidget* parent) : SettingsPanel(settings, parent), m_ui(new Ui::SettingsLocalization) { m_ui->setupUi(this); - m_ui->m_treeLanguages->setColumnCount(4); + m_ui->m_lblAuthors->label()->setWordWrap(true); + m_ui->m_treeLanguages->setColumnCount(3); m_ui->m_treeLanguages->setHeaderHidden(false); m_ui->m_treeLanguages->setHeaderLabels(QStringList() << /*: Language column of language list. */ tr("Language") << /*: Lang. code column of language list. */ tr("Code") - << tr("Translation progress") << tr("Author")); + << tr("Translation progress")); m_ui->m_lblHelp->setText(tr(R"(Help us to improve %1 translations.)") .arg(QSL(APP_NAME), QSL("https://crowdin.com/project/rssguard"))); @@ -32,7 +33,6 @@ SettingsLocalization::SettingsLocalization(Settings* settings, QWidget* parent) m_ui->m_treeLanguages->header()->setSectionResizeMode(0, QHeaderView::ResizeMode::ResizeToContents); m_ui->m_treeLanguages->header()->setSectionResizeMode(1, QHeaderView::ResizeMode::ResizeToContents); m_ui->m_treeLanguages->header()->setSectionResizeMode(2, QHeaderView::ResizeMode::ResizeToContents); - m_ui->m_treeLanguages->header()->setSectionResizeMode(3, QHeaderView::ResizeMode::ResizeToContents); connect(m_ui->m_treeLanguages, &QTreeWidget::currentItemChanged, this, &SettingsLocalization::requireRestart); connect(m_ui->m_treeLanguages, &QTreeWidget::currentItemChanged, this, &SettingsLocalization::dirtifySettings); @@ -48,20 +48,46 @@ void SettingsLocalization::loadSettings() { auto langs = qApp->localization()->installedLanguages(); // Also, load statistics with restricted access token. - QByteArray stats_out; + QList> hdrs = { + {"Authorization", + "Bearer " + "0fbcad4c39d21a55f63f8a1b6d07cc56bb1e2eb2047bfaf1ee22425e3edf1c2b217f4d13b3cebba9"}}; + QByteArray stats_out, people_out; QMap percentages_langs; + QString all_translators; + NetworkResult stats_res = NetworkFactory:: - performNetworkOperation(QSL("https://api.crowdin.com/api/v2/projects/608575/languages/progress"), + performNetworkOperation(QSL("https://api.crowdin.com/api/v2/projects/608575/languages/progress?limit=100"), qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(), {}, stats_out, QNetworkAccessManager::Operation::GetOperation, - {{"Authorization", - "Bearer " - "0fbcad4c39d21a55f63f8a1b6d07cc56bb1e2eb2047bfaf1ee22425e3edf1c2b217f4d13b3cebba9"}}); + hdrs); + + NetworkResult people_res = + NetworkFactory::performNetworkOperation(QSL("https://api.crowdin.com/api/v2/projects/608575/members?limit=500"), + qApp->settings() + ->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)) + .toInt(), + {}, + people_out, + QNetworkAccessManager::Operation::GetOperation, + hdrs); if (stats_res.m_networkError == QNetworkReply::NetworkError::NoError) { QJsonDocument stats_doc = QJsonDocument::fromJson(stats_out); + QJsonDocument people_doc = QJsonDocument::fromJson(people_out); + QJsonArray people_arr = people_doc.object()["data"].toArray(); + std::vector people_desc; + + std::transform(people_arr.begin(), people_arr.end(), std::back_inserter(people_desc), [](const QJsonValue& b) { + return b.toObject()["data"].toObject()["username"].toString(); + }); + + all_translators = + std::accumulate(std::next(people_desc.begin()), people_desc.end(), people_desc.at(0), [](auto lhs, auto rhs) { + return std::move(lhs) + ", " + rhs; + }); for (const QJsonValue& val_lang : stats_doc.object()["data"].toArray()) { QString lang_id = val_lang.toObject()["data"].toObject()["languageId"].toString().replace(QSL("-"), QSL("_")); @@ -75,6 +101,17 @@ void SettingsLocalization::loadSettings() { } } + if (all_translators.isEmpty()) { + m_ui->m_lblAuthors->setStatus(WidgetWithStatus::StatusType::Information, + tr("Big thanks to all translators!"), + tr("Big thanks to all translators!")); + } + else { + m_ui->m_lblAuthors->setStatus(WidgetWithStatus::StatusType::Information, + tr("Translations provided by: %1").arg(all_translators), + tr("Big thanks to all translators!")); + } + for (const Language& language : qAsConst(langs)) { auto* item = new QTreeWidgetItem(m_ui->m_treeLanguages); int perc_translated = percentages_langs.value(language.m_code); @@ -87,7 +124,6 @@ void SettingsLocalization::loadSettings() { } item->setText(2, QSL("%1 %").arg(perc_translated >= 0 ? QString::number(perc_translated) : QSL("-"))); - item->setText(3, language.m_author); item->setIcon(0, qApp->icons()->miscIcon(QSL(FLAG_ICON_SUBFOLDER) + QDir::separator() + language.m_code)); QColor col_translated = QColor::fromHsv(perc_translated, 200, 230); diff --git a/src/librssguard/gui/settings/settingslocalization.ui b/src/librssguard/gui/settings/settingslocalization.ui index 271de9068..5a8b09e9b 100644 --- a/src/librssguard/gui/settings/settingslocalization.ui +++ b/src/librssguard/gui/settings/settingslocalization.ui @@ -36,11 +36,22 @@ + + + + + + LabelWithStatus + QWidget +
labelwithstatus.h
+ 1 +
+
diff --git a/src/librssguard/gui/settings/settingsnotifications.cpp b/src/librssguard/gui/settings/settingsnotifications.cpp index a8d7c4390..6adcceb62 100644 --- a/src/librssguard/gui/settings/settingsnotifications.cpp +++ b/src/librssguard/gui/settings/settingsnotifications.cpp @@ -30,6 +30,9 @@ SettingsNotifications::SettingsNotifications(Settings* settings, QWidget* parent connect(m_ui.m_sbScreen, QOverload::of(&QSpinBox::valueChanged), this, &SettingsNotifications::dirtifySettings); connect(m_ui.m_sbScreen, QOverload::of(&QSpinBox::valueChanged), this, &SettingsNotifications::requireRestart); + connect(m_ui.m_sbMargin, QOverload::of(&QSpinBox::valueChanged), this, &SettingsNotifications::dirtifySettings); + connect(m_ui.m_sbWidth, QOverload::of(&QSpinBox::valueChanged), this, &SettingsNotifications::dirtifySettings); + connect(m_ui.m_sbScreen, QOverload::of(&QSpinBox::valueChanged), this, &SettingsNotifications::showScreenInfo); connect(m_ui.m_cbCustomNotificationsPosition, @@ -65,6 +68,8 @@ void SettingsNotifications::loadSettings() { m_ui.m_rbNativeNotifications ->setChecked(!settings()->value(GROUP(GUI), SETTING(GUI::UseToastNotifications)).toBool()); m_ui.m_sbScreen->setValue(settings()->value(GROUP(GUI), SETTING(GUI::ToastNotificationsScreen)).toInt()); + m_ui.m_sbWidth->setValue(settings()->value(GROUP(GUI), SETTING(GUI::ToastNotificationsWidth)).toInt()); + m_ui.m_sbMargin->setValue(settings()->value(GROUP(GUI), SETTING(GUI::ToastNotificationsMargin)).toInt()); m_ui.m_cbCustomNotificationsPosition ->setCurrentIndex(m_ui.m_cbCustomNotificationsPosition @@ -84,12 +89,15 @@ void SettingsNotifications::saveSettings() { settings()->setValue(GROUP(GUI), GUI::UseToastNotifications, m_ui.m_rbCustomNotifications->isChecked()); settings()->setValue(GROUP(GUI), GUI::ToastNotificationsScreen, m_ui.m_sbScreen->value()); + settings()->setValue(GROUP(GUI), GUI::ToastNotificationsWidth, m_ui.m_sbWidth->value()); + settings()->setValue(GROUP(GUI), GUI::ToastNotificationsMargin, m_ui.m_sbMargin->value()); settings()->setValue(GROUP(GUI), GUI::ToastNotificationsPosition, m_ui.m_cbCustomNotificationsPosition->currentData() .value()); + // qApp->m_toastNotifications onEndSaveSettings(); } diff --git a/src/librssguard/gui/settings/settingsnotifications.ui b/src/librssguard/gui/settings/settingsnotifications.ui index 6f3aa6a4c..49f58bc46 100644 --- a/src/librssguard/gui/settings/settingsnotifications.ui +++ b/src/librssguard/gui/settings/settingsnotifications.ui @@ -6,8 +6,8 @@ 0 0 - 367 - 300 + 407 + 357 @@ -76,27 +76,64 @@ - + 99 - + Screen - + + + + + px + + + 50 + + + 1000 + + + + + + + Width + + + + + + + px + + + 100 + + + + + + + Margins + + + diff --git a/src/librssguard/miscellaneous/application.cpp b/src/librssguard/miscellaneous/application.cpp index a81eab364..7e9ac3031 100644 --- a/src/librssguard/miscellaneous/application.cpp +++ b/src/librssguard/miscellaneous/application.cpp @@ -149,9 +149,6 @@ Application::Application(const QString& id, int& argc, char** argv, const QStrin determineFirstRuns(); - //: Name of translator - optional. - QObject::tr("LANG_AUTHOR"); - // Add an extra path for non-system icon themes and set current icon theme // and skin. m_icons->setupSearchPaths(); diff --git a/src/librssguard/miscellaneous/localization.cpp b/src/librssguard/miscellaneous/localization.cpp index c4e6ab9df..5f0d6d5e2 100644 --- a/src/librssguard/miscellaneous/localization.cpp +++ b/src/librssguard/miscellaneous/localization.cpp @@ -27,7 +27,7 @@ void Localization::loadActiveLanguage() { << QUOTE_W_SPACE_DOT(desired_localization); if (app_translator->load(QLocale(desired_localization), QSL("rssguard"), QSL("_"), APP_LANG_PATH)) { - const QString real_loaded_locale = app_translator->translate("QObject", "LANG_ABBREV"); + const QString real_loaded_locale = app_translator->language(); QCoreApplication::installTranslator(app_translator); @@ -76,8 +76,7 @@ QList Localization::installedLanguages() const { if (translator.load(file.absoluteFilePath())) { Language new_language; - new_language.m_code = translator.language().replace(QSL("-"), QSL("_")); - new_language.m_author = translator.translate("QObject", "LANG_AUTHOR"); + new_language.m_code = translator.language(); new_language.m_name = QLocale(new_language.m_code).nativeLanguageName(); languages << new_language; } diff --git a/src/librssguard/miscellaneous/localization.h b/src/librssguard/miscellaneous/localization.h index 9e21d1d49..785bd1e4e 100644 --- a/src/librssguard/miscellaneous/localization.h +++ b/src/librssguard/miscellaneous/localization.h @@ -11,7 +11,6 @@ struct Language { QString m_name; QString m_code; - QString m_author; }; class RSSGUARD_DLLSPEC Localization : public QObject { diff --git a/src/librssguard/miscellaneous/settings.cpp b/src/librssguard/miscellaneous/settings.cpp index a2d4fb76f..5514ec581 100644 --- a/src/librssguard/miscellaneous/settings.cpp +++ b/src/librssguard/miscellaneous/settings.cpp @@ -295,6 +295,12 @@ GUI::ToastNotificationsPositionDef = ToastNotificationsManager::NotificationPosi DKEY GUI::ToastNotificationsScreen = "toast_notifications_screen"; DVALUE(int) GUI::ToastNotificationsScreenDef = -1; +DKEY GUI::ToastNotificationsMargin = "toast_notifications_margin"; +DVALUE(int) GUI::ToastNotificationsMarginDef = NOTIFICATIONS_MARGIN; + +DKEY GUI::ToastNotificationsWidth = "toast_notifications_width"; +DVALUE(int) GUI::ToastNotificationsWidthDef = NOTIFICATIONS_WIDTH; + DKEY GUI::HideMainWindowWhenMinimized = "hide_when_minimized"; DVALUE(bool) GUI::HideMainWindowWhenMinimizedDef = false; diff --git a/src/librssguard/miscellaneous/settings.h b/src/librssguard/miscellaneous/settings.h index e048d367f..fb6076906 100644 --- a/src/librssguard/miscellaneous/settings.h +++ b/src/librssguard/miscellaneous/settings.h @@ -228,6 +228,12 @@ namespace GUI { KEY ToastNotificationsScreen; VALUE(int) ToastNotificationsScreenDef; + KEY ToastNotificationsMargin; + VALUE(int) ToastNotificationsMarginDef; + + KEY ToastNotificationsWidth; + VALUE(int) ToastNotificationsWidthDef; + KEY MessageViewState; VALUE(QString) MessageViewStateDef; diff --git a/src/librssguard/services/greader/definitions.h b/src/librssguard/services/greader/definitions.h index 4d74d7894..b2acb8c9b 100644 --- a/src/librssguard/services/greader/definitions.h +++ b/src/librssguard/services/greader/definitions.h @@ -4,9 +4,9 @@ #define GREADER_DEFAULT_BATCH_SIZE 100 // URLs. -#define GREADER_URL_REEDAH "https://www.reedah.com" -#define GREADER_URL_TOR "https://theoldreader.com" -#define GREADER_URL_BAZQUX "https://bazqux.com" +#define GREADER_URL_REEDAH "https://www.reedah.com" +#define GREADER_URL_TOR "https://theoldreader.com" +#define GREADER_URL_BAZQUX "https://bazqux.com" #define GREADER_URL_INOREADER "https://www.inoreader.com" // States. @@ -14,29 +14,31 @@ // Means "read" message. If both "reading-list" and "read" are specified, message is READ. If this state // is not present, message is UNREAD. -#define GREADER_API_STATE_READ "state/com.google/read" +#define GREADER_API_STATE_READ "state/com.google/read" #define GREADER_API_STATE_IMPORTANT "state/com.google/starred" #define GREADER_API_FULL_STATE_READING_LIST "user/-/state/com.google/reading-list" -#define GREADER_API_FULL_STATE_READ "user/-/state/com.google/read" -#define GREADER_API_FULL_STATE_IMPORTANT "user/-/state/com.google/starred" +#define GREADER_API_FULL_STATE_READ "user/-/state/com.google/read" +#define GREADER_API_FULL_STATE_IMPORTANT "user/-/state/com.google/starred" // API. -#define GREADER_API_CLIENT_LOGIN "accounts/ClientLogin" -#define GREADER_API_TAG_LIST "reader/api/0/tag/list?output=json" -#define GREADER_API_SUBSCRIPTION_LIST "reader/api/0/subscription/list?output=json" -#define GREADER_API_STREAM_CONTENTS "reader/api/0/stream/contents/%1?output=json&n=%2" -#define GREADER_API_EDIT_TAG "reader/api/0/edit-tag" -#define GREADER_API_ITEM_IDS "reader/api/0/stream/items/ids?output=json&n=%2&s=%1" -#define GREADER_API_ITEM_CONTENTS "reader/api/0/stream/items/contents?output=json&n=200000" -#define GREADER_API_TOKEN "reader/api/0/token" -#define GREADER_API_USER_INFO "reader/api/0/user-info?output=json" +#define GREADER_API_CLIENT_LOGIN "accounts/ClientLogin" +#define GREADER_API_TAG_LIST "reader/api/0/tag/list?output=json" +#define GREADER_API_SUBSCRIPTION_LIST "reader/api/0/subscription/list?output=json" +#define GREADER_API_STREAM_CONTENTS "reader/api/0/stream/contents/%1?output=json&n=%2" +#define GREADER_API_EDIT_TAG "reader/api/0/edit-tag" +#define GREADER_API_SUBSCRIPTION_EXPORT "reader/api/0/subscription/export" +#define GREADER_API_SUBSCRIPTION_IMPORT "reader/api/0/subscription/import" +#define GREADER_API_ITEM_IDS "reader/api/0/stream/items/ids?output=json&n=%2&s=%1" +#define GREADER_API_ITEM_CONTENTS "reader/api/0/stream/items/contents?output=json&n=200000" +#define GREADER_API_TOKEN "reader/api/0/token" +#define GREADER_API_USER_INFO "reader/api/0/user-info?output=json" // Misc. -#define GREADET_API_ITEM_IDS_MAX 200000 -#define GREADER_API_EDIT_TAG_BATCH 200 +#define GREADER_API_ITEM_IDS_MAX 200000 +#define GREADER_API_EDIT_TAG_BATCH 200 #define GREADER_API_ITEM_CONTENTS_BATCH 999 -#define GREADER_GLOBAL_UPDATE_THRES 0.3 +#define GREADER_GLOBAL_UPDATE_THRES 0.3 // The Old Reader. #define TOR_SPONSORED_STREAM_ID "tor/sponsored" @@ -45,14 +47,14 @@ // Inoreader. #define INO_ITEM_CONTENTS_BATCH 250 -#define INO_HEADER_APPID "AppId" +#define INO_HEADER_APPID "AppId" #define INO_HEADER_APPKEY "AppKey" #define INO_OAUTH_REDIRECT_URI_PORT 14488 -#define INO_OAUTH_SCOPE "read write" -#define INO_OAUTH_TOKEN_URL "https://www.inoreader.com/oauth2/token" -#define INO_OAUTH_AUTH_URL "https://www.inoreader.com/oauth2/auth" -#define INO_REG_API_URL "https://www.inoreader.com/developers/register-app" +#define INO_OAUTH_SCOPE "read write" +#define INO_OAUTH_TOKEN_URL "https://www.inoreader.com/oauth2/token" +#define INO_OAUTH_AUTH_URL "https://www.inoreader.com/oauth2/auth" +#define INO_REG_API_URL "https://www.inoreader.com/developers/register-app" // FreshRSS. #define FRESHRSS_BASE_URL_PATH "api/greader.php/" diff --git a/src/librssguard/services/greader/greadernetwork.cpp b/src/librssguard/services/greader/greadernetwork.cpp index 398f44285..bf12ee913 100644 --- a/src/librssguard/services/greader/greadernetwork.cpp +++ b/src/librssguard/services/greader/greadernetwork.cpp @@ -341,6 +341,61 @@ QNetworkReply::NetworkError GreaderNetwork::markMessagesStarred(RootItem::Import proxy); } +void GreaderNetwork::subscriptionImport(const QByteArray& opml_data, const QNetworkProxy& proxy) { + if (!ensureLogin(proxy)) { + throw ApplicationException(tr("login failed")); + } + + QString full_url = generateFullUrl(Operations::SubscriptionImport); + auto timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + + QByteArray output; + auto result = NetworkFactory::performNetworkOperation(full_url, + timeout, + opml_data, + output, + QNetworkAccessManager::Operation::PostOperation, + {authHeader()}, + false, + {}, + {}, + proxy); + + if (result.m_networkError != QNetworkReply::NetworkError::NoError) { + qCriticalNN << LOGSEC_GREADER << "Cannot get OPML data, network error:" << QUOTE_W_SPACE_DOT(result.m_networkError); + throw NetworkException(result.m_networkError, output); + } +} + +QByteArray GreaderNetwork::subscriptionExport(const QNetworkProxy& proxy) { + if (!ensureLogin(proxy)) { + throw ApplicationException(tr("login failed")); + } + + QString full_url = generateFullUrl(Operations::SubscriptionExport); + auto timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + + QByteArray output; + auto result = NetworkFactory::performNetworkOperation(full_url, + timeout, + {}, + output, + QNetworkAccessManager::Operation::GetOperation, + {authHeader()}, + false, + {}, + {}, + proxy); + + if (result.m_networkError != QNetworkReply::NetworkError::NoError) { + qCriticalNN << LOGSEC_GREADER << "Cannot get OPML data, network error:" << QUOTE_W_SPACE_DOT(result.m_networkError); + throw NetworkException(result.m_networkError, output); + } + else { + return output; + } +} + QStringList GreaderNetwork::itemIds(const QString& stream_id, bool unread_only, const QNetworkProxy& proxy, @@ -357,7 +412,7 @@ QStringList GreaderNetwork::itemIds(const QString& stream_id, QString full_url = generateFullUrl(Operations::ItemIds) .arg(m_service == GreaderServiceRoot::Service::TheOldReader ? stream_id : QUrl::toPercentEncoding(stream_id), - QString::number(max_count <= 0 ? GREADET_API_ITEM_IDS_MAX : max_count)); + QString::number(max_count <= 0 ? GREADER_API_ITEM_IDS_MAX : max_count)); auto timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); if (unread_only) { @@ -1066,6 +1121,12 @@ QString GreaderNetwork::generateFullUrl(GreaderNetwork::Operations operation) co case Operations::ClientLogin: return sanitizedBaseUrl() + QSL(GREADER_API_CLIENT_LOGIN); + case Operations::SubscriptionExport: + return sanitizedBaseUrl() + QSL(GREADER_API_SUBSCRIPTION_EXPORT); + + case Operations::SubscriptionImport: + return sanitizedBaseUrl() + QSL(GREADER_API_SUBSCRIPTION_IMPORT); + case Operations::Token: return sanitizedBaseUrl() + QSL(GREADER_API_TOKEN); diff --git a/src/librssguard/services/greader/greadernetwork.h b/src/librssguard/services/greader/greadernetwork.h index 3793a56fd..2bf379723 100644 --- a/src/librssguard/services/greader/greadernetwork.h +++ b/src/librssguard/services/greader/greadernetwork.h @@ -24,7 +24,9 @@ class GreaderNetwork : public QObject { Token, UserInfo, ItemIds, - ItemContents + ItemContents, + SubscriptionExport, + SubscriptionImport }; explicit GreaderNetwork(QObject* parent = nullptr); @@ -82,6 +84,8 @@ class GreaderNetwork : public QObject { void setOauth(OAuth2Service* oauth); // API methods. + void subscriptionImport(const QByteArray& opml_data, const QNetworkProxy& proxy); + QByteArray subscriptionExport(const QNetworkProxy& proxy); QNetworkReply::NetworkError editLabels(const QString& state, bool assign, const QStringList& msg_custom_ids, diff --git a/src/librssguard/services/greader/greaderserviceroot.cpp b/src/librssguard/services/greader/greaderserviceroot.cpp index 744c103fa..4618fbe18 100644 --- a/src/librssguard/services/greader/greaderserviceroot.cpp +++ b/src/librssguard/services/greader/greaderserviceroot.cpp @@ -4,6 +4,7 @@ #include "database/databasequeries.h" #include "definitions/definitions.h" +#include "gui/messagebox.h" #include "miscellaneous/application.h" #include "miscellaneous/iconfactory.h" #include "miscellaneous/textfactory.h" @@ -13,6 +14,8 @@ #include "services/greader/greadernetwork.h" #include "services/greader/gui/formeditgreaderaccount.h" +#include + GreaderServiceRoot::GreaderServiceRoot(RootItem* parent) : ServiceRoot(parent), m_network(new GreaderNetwork(this)) { setIcon(GreaderEntryPoint().icon()); m_network->setRoot(this); @@ -130,6 +133,59 @@ QString GreaderServiceRoot::serviceToString(Service service) { } } +void GreaderServiceRoot::importFeeds() { + const QString filter_opml20 = tr("OPML 2.0 files (*.opml *.xml)"); + const QString selected_file = QFileDialog::getOpenFileName(qApp->mainFormWidget(), + tr("Select file for feeds import"), + qApp->homeFolder(), + filter_opml20); + + if (!QFile::exists(selected_file)) { + return; + } + + try { + m_network->subscriptionImport(IOFactory::readFile(selected_file), networkProxy()); + MsgBox::show(qApp->mainFormWidget(), + QMessageBox::Icon::Information, + tr("Done"), + tr("Data imported successfully. Reloading feed tree.")); + + syncIn(); + } + catch (const ApplicationException& ex) { + MsgBox::show(qApp->mainFormWidget(), + QMessageBox::Icon::Critical, + tr("Cannot import feeds"), + tr("Error: %1").arg(ex.message())); + } +} + +void GreaderServiceRoot::exportFeeds() { + const QString the_file = qApp->homeFolder() + QDir::separator() + + QSL("rssguard_feeds_%1.opml").arg(QDate::currentDate().toString(Qt::DateFormat::ISODate)); + const QString filter_opml20 = tr("OPML 2.0 files (*.opml *.xml)"); + const QString selected_file = + QFileDialog::getSaveFileName(qApp->mainFormWidget(), tr("Select file for feeds export"), the_file, filter_opml20); + + if (selected_file.isEmpty()) { + return; + } + + try { + QByteArray data = m_network->subscriptionExport(networkProxy()); + IOFactory::writeFile(selected_file, data); + + MsgBox::show(qApp->mainFormWidget(), QMessageBox::Icon::Information, tr("Done"), tr("Data exported successfully.")); + } + catch (const ApplicationException& ex) { + MsgBox::show(qApp->mainFormWidget(), + QMessageBox::Icon::Critical, + tr("Cannot export feeds"), + tr("Error: %1").arg(ex.message())); + } +} + QList GreaderServiceRoot::obtainNewMessages(Feed* feed, const QHash& stated_messages, @@ -178,6 +234,23 @@ QString GreaderServiceRoot::code() const { return GreaderEntryPoint().code(); } +QList GreaderServiceRoot::serviceMenu() { + if (m_serviceMenu.isEmpty()) { + ServiceRoot::serviceMenu(); + + auto* action_export_feeds = new QAction(qApp->icons()->fromTheme(QSL("document-export")), tr("Export feeds"), this); + auto* action_import_feeds = new QAction(qApp->icons()->fromTheme(QSL("document-import")), tr("Import feeds"), this); + + connect(action_export_feeds, &QAction::triggered, this, &GreaderServiceRoot::exportFeeds); + connect(action_import_feeds, &QAction::triggered, this, &GreaderServiceRoot::importFeeds); + + m_serviceMenu.append(action_export_feeds); + m_serviceMenu.append(action_import_feeds); + } + + return m_serviceMenu; +} + void GreaderServiceRoot::saveAllCachedData(bool ignore_errors) { auto msg_cache = takeMessageCache(); QMapIterator i(msg_cache.m_cachedStatesRead); diff --git a/src/librssguard/services/greader/greaderserviceroot.h b/src/librssguard/services/greader/greaderserviceroot.h index 352eff535..d0f1d6fe2 100644 --- a/src/librssguard/services/greader/greaderserviceroot.h +++ b/src/librssguard/services/greader/greaderserviceroot.h @@ -32,6 +32,7 @@ class GreaderServiceRoot : public ServiceRoot, public CacheForServiceRoot { virtual FormAccountDetails* accountSetupDialog() const; virtual void start(bool freshly_activated); virtual QString code() const; + virtual QList serviceMenu(); virtual void saveAllCachedData(bool ignore_errors); virtual LabelOperation supportedLabelOperations() const; virtual QVariantHash customDatabaseData() const; @@ -49,6 +50,10 @@ class GreaderServiceRoot : public ServiceRoot, public CacheForServiceRoot { static QString serviceToString(Service service); + private slots: + void importFeeds(); + void exportFeeds(); + protected: virtual RootItem* obtainNewTreeForSyncIn() const;