greader plugin now has ability to export/import feeds via API usage

This commit is contained in:
Martin Rotter 2023-11-09 12:34:00 +01:00
parent 95df21e385
commit edc30fbe48
18 changed files with 298 additions and 47 deletions

View File

@ -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);

View File

@ -3,6 +3,7 @@
#include "gui/notifications/basetoastnotification.h"
#include "miscellaneous/iconfactory.h"
#include "miscellaneous/settings.h"
#include <QCloseEvent>
#include <QTimer>
@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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 <a href="%2">translations</a>.)")
.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<QPair<QByteArray, QByteArray>> hdrs = {
{"Authorization",
"Bearer "
"0fbcad4c39d21a55f63f8a1b6d07cc56bb1e2eb2047bfaf1ee22425e3edf1c2b217f4d13b3cebba9"}};
QByteArray stats_out, people_out;
QMap<QString, int> 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<QString> 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);

View File

@ -36,11 +36,22 @@
</attribute>
</widget>
</item>
<item>
<widget class="LabelWithStatus" name="m_lblAuthors" native="true"/>
</item>
<item>
<widget class="QLabel" name="m_lblHelp"/>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>LabelWithStatus</class>
<extends>QWidget</extends>
<header>labelwithstatus.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -30,6 +30,9 @@ SettingsNotifications::SettingsNotifications(Settings* settings, QWidget* parent
connect(m_ui.m_sbScreen, QOverload<int>::of(&QSpinBox::valueChanged), this, &SettingsNotifications::dirtifySettings);
connect(m_ui.m_sbScreen, QOverload<int>::of(&QSpinBox::valueChanged), this, &SettingsNotifications::requireRestart);
connect(m_ui.m_sbMargin, QOverload<int>::of(&QSpinBox::valueChanged), this, &SettingsNotifications::dirtifySettings);
connect(m_ui.m_sbWidth, QOverload<int>::of(&QSpinBox::valueChanged), this, &SettingsNotifications::dirtifySettings);
connect(m_ui.m_sbScreen, QOverload<int>::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<ToastNotificationsManager::NotificationPosition>());
// qApp->m_toastNotifications
onEndSaveSettings();
}

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>367</width>
<height>300</height>
<width>407</width>
<height>357</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout">
@ -76,27 +76,64 @@
<item row="0" column="1">
<widget class="QComboBox" name="m_cbCustomNotificationsPosition"/>
</item>
<item row="1" column="1">
<item row="3" column="1">
<widget class="QSpinBox" name="m_sbScreen">
<property name="value">
<number>99</number>
</property>
</widget>
</item>
<item row="1" column="0">
<item row="3" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Screen</string>
</property>
</widget>
</item>
<item row="2" column="1">
<item row="4" column="1">
<widget class="QLabel" name="m_lblScreenInfo">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="m_sbWidth">
<property name="suffix">
<string> px</string>
</property>
<property name="minimum">
<number>50</number>
</property>
<property name="maximum">
<number>1000</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Width</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="m_sbMargin">
<property name="suffix">
<string> px</string>
</property>
<property name="maximum">
<number>100</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Margins</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@ -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();

View File

@ -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<Language> 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;
}

View File

@ -11,7 +11,6 @@
struct Language {
QString m_name;
QString m_code;
QString m_author;
};
class RSSGUARD_DLLSPEC Localization : public QObject {

View File

@ -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;

View File

@ -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;

View File

@ -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/"

View File

@ -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);

View File

@ -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,

View File

@ -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 <QFileDialog>
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<Message> GreaderServiceRoot::obtainNewMessages(Feed* feed,
const QHash<ServiceRoot::BagOfMessages, QStringList>&
stated_messages,
@ -178,6 +234,23 @@ QString GreaderServiceRoot::code() const {
return GreaderEntryPoint().code();
}
QList<QAction*> 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<RootItem::ReadStatus, QStringList> i(msg_cache.m_cachedStatesRead);

View File

@ -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<QAction*> 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;