1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2017  Vladimir Golovnev <glassez@yandex.ru>
4  * Copyright (C) 2006  Christophe Dumez <chris@qbittorrent.org>
5  * Copyright (C) 2006  Arnaud Demaiziere <arnaud@qbittorrent.org>
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
20  *
21  * In addition, as a special exception, the copyright holders give permission to
22  * link this program with the OpenSSL project's "OpenSSL" library (or with
23  * modified versions of it that use the same license as the "OpenSSL" library),
24  * and distribute the linked executables. You must obey the GNU General Public
25  * License in all respects for all of the code used other than "OpenSSL".  If you
26  * modify file(s), you may extend this exception to your version of the file(s),
27  * but you are not obligated to do so. If you do not wish to do so, delete this
28  * exception statement from your version.
29  */
30 
31 #include "rsswidget.h"
32 
33 #include <QClipboard>
34 #include <QDesktopServices>
35 #include <QDragMoveEvent>
36 #include <QMenu>
37 #include <QMessageBox>
38 #include <QRegularExpression>
39 #include <QShortcut>
40 #include <QString>
41 
42 #include "base/bittorrent/session.h"
43 #include "base/global.h"
44 #include "base/net/downloadmanager.h"
45 #include "base/preferences.h"
46 #include "base/rss/rss_article.h"
47 #include "base/rss/rss_feed.h"
48 #include "base/rss/rss_folder.h"
49 #include "base/rss/rss_session.h"
50 #include "gui/addnewtorrentdialog.h"
51 #include "gui/autoexpandabledialog.h"
52 #include "gui/uithememanager.h"
53 #include "articlelistwidget.h"
54 #include "automatedrssdownloader.h"
55 #include "feedlistwidget.h"
56 #include "ui_rsswidget.h"
57 
RSSWidget(QWidget * parent)58 RSSWidget::RSSWidget(QWidget *parent)
59     : QWidget(parent)
60     , m_ui(new Ui::RSSWidget)
61 {
62     m_ui->setupUi(this);
63 
64     // Icons
65     m_ui->actionCopyFeedURL->setIcon(UIThemeManager::instance()->getIcon("edit-copy"));
66     m_ui->actionDelete->setIcon(UIThemeManager::instance()->getIcon("edit-delete"));
67     m_ui->actionDownloadTorrent->setIcon(UIThemeManager::instance()->getIcon("download"));
68     m_ui->actionMarkItemsRead->setIcon(UIThemeManager::instance()->getIcon("mail-mark-read"));
69     m_ui->actionNewFolder->setIcon(UIThemeManager::instance()->getIcon("folder-new"));
70     m_ui->actionNewSubscription->setIcon(UIThemeManager::instance()->getIcon("list-add"));
71     m_ui->actionOpenNewsURL->setIcon(UIThemeManager::instance()->getIcon("application-x-mswinurl"));
72     m_ui->actionRename->setIcon(UIThemeManager::instance()->getIcon("edit-rename"));
73     m_ui->actionUpdate->setIcon(UIThemeManager::instance()->getIcon("view-refresh"));
74     m_ui->actionUpdateAllFeeds->setIcon(UIThemeManager::instance()->getIcon("view-refresh"));
75 #ifndef Q_OS_MACOS
76     m_ui->newFeedButton->setIcon(UIThemeManager::instance()->getIcon("list-add"));
77     m_ui->markReadButton->setIcon(UIThemeManager::instance()->getIcon("mail-mark-read"));
78     m_ui->updateAllButton->setIcon(UIThemeManager::instance()->getIcon("view-refresh"));
79     m_ui->rssDownloaderBtn->setIcon(UIThemeManager::instance()->getIcon("download"));
80 #endif
81 
82     m_articleListWidget = new ArticleListWidget(m_ui->splitterMain);
83     m_ui->splitterMain->insertWidget(0, m_articleListWidget);
84     connect(m_articleListWidget, &ArticleListWidget::customContextMenuRequested, this, &RSSWidget::displayItemsListMenu);
85     connect(m_articleListWidget, &ArticleListWidget::currentItemChanged, this, &RSSWidget::handleCurrentArticleItemChanged);
86     connect(m_articleListWidget, &ArticleListWidget::itemDoubleClicked, this, &RSSWidget::downloadSelectedTorrents);
87 
88     m_feedListWidget = new FeedListWidget(m_ui->splitterSide);
89     m_ui->splitterSide->insertWidget(0, m_feedListWidget);
90     connect(m_feedListWidget, &QAbstractItemView::doubleClicked, this, &RSSWidget::renameSelectedRSSItem);
91     connect(m_feedListWidget, &QTreeWidget::currentItemChanged, this, &RSSWidget::handleCurrentFeedItemChanged);
92     connect(m_feedListWidget, &QWidget::customContextMenuRequested, this, &RSSWidget::displayRSSListMenu);
93     loadFoldersOpenState();
94     m_feedListWidget->setCurrentItem(m_feedListWidget->stickyUnreadItem());
95 
96     const auto *editHotkey = new QShortcut(Qt::Key_F2, m_feedListWidget, nullptr, nullptr, Qt::WidgetShortcut);
97     connect(editHotkey, &QShortcut::activated, this, &RSSWidget::renameSelectedRSSItem);
98     const auto *deleteHotkey = new QShortcut(QKeySequence::Delete, m_feedListWidget, nullptr, nullptr, Qt::WidgetShortcut);
99     connect(deleteHotkey, &QShortcut::activated, this, &RSSWidget::deleteSelectedItems);
100 
101     // Feeds list actions
102     connect(m_ui->actionDelete, &QAction::triggered, this, &RSSWidget::deleteSelectedItems);
103     connect(m_ui->actionRename, &QAction::triggered, this, &RSSWidget::renameSelectedRSSItem);
104     connect(m_ui->actionUpdate, &QAction::triggered, this, &RSSWidget::refreshSelectedItems);
105     connect(m_ui->actionNewFolder, &QAction::triggered, this, &RSSWidget::askNewFolder);
106     connect(m_ui->actionNewSubscription, &QAction::triggered, this, &RSSWidget::on_newFeedButton_clicked);
107     connect(m_ui->actionUpdateAllFeeds, &QAction::triggered, this, &RSSWidget::refreshAllFeeds);
108     connect(m_ui->updateAllButton, &QAbstractButton::clicked, this, &RSSWidget::refreshAllFeeds);
109     connect(m_ui->actionCopyFeedURL, &QAction::triggered, this, &RSSWidget::copySelectedFeedsURL);
110     connect(m_ui->actionMarkItemsRead, &QAction::triggered, this, &RSSWidget::on_markReadButton_clicked);
111 
112     // News list actions
113     connect(m_ui->actionOpenNewsURL, &QAction::triggered, this, &RSSWidget::openSelectedArticlesUrls);
114     connect(m_ui->actionDownloadTorrent, &QAction::triggered, this, &RSSWidget::downloadSelectedTorrents);
115 
116     // Restore sliders position
117     restoreSlidersPosition();
118     // Bind saveSliders slots
119     connect(m_ui->splitterMain, &QSplitter::splitterMoved, this, &RSSWidget::saveSlidersPosition);
120     connect(m_ui->splitterSide, &QSplitter::splitterMoved, this, &RSSWidget::saveSlidersPosition);
121 
122     if (RSS::Session::instance()->isProcessingEnabled())
123         m_ui->labelWarn->hide();
124     connect(RSS::Session::instance(), &RSS::Session::processingStateChanged
125             , this, &RSSWidget::handleSessionProcessingStateChanged);
126     connect(RSS::Session::instance()->rootFolder(), &RSS::Folder::unreadCountChanged
127             , this, &RSSWidget::handleUnreadCountChanged);
128 }
129 
~RSSWidget()130 RSSWidget::~RSSWidget()
131 {
132     // we need it here to properly mark latest article
133     // as read without having additional code
134     m_articleListWidget->clear();
135 
136     saveFoldersOpenState();
137 
138     delete m_feedListWidget;
139     delete m_ui;
140 }
141 
142 // display a right-click menu
displayRSSListMenu(const QPoint & pos)143 void RSSWidget::displayRSSListMenu(const QPoint &pos)
144 {
145     if (!m_feedListWidget->indexAt(pos).isValid())
146         // No item under the mouse, clear selection
147         m_feedListWidget->clearSelection();
148 
149     QMenu *menu = new QMenu(this);
150     menu->setAttribute(Qt::WA_DeleteOnClose);
151 
152     const QList<QTreeWidgetItem *> selectedItems = m_feedListWidget->selectedItems();
153     if (!selectedItems.isEmpty())
154     {
155         menu->addAction(m_ui->actionUpdate);
156         menu->addAction(m_ui->actionMarkItemsRead);
157         menu->addSeparator();
158 
159         if (selectedItems.size() == 1)
160         {
161             if (selectedItems.first() != m_feedListWidget->stickyUnreadItem())
162             {
163                 menu->addAction(m_ui->actionRename);
164                 menu->addAction(m_ui->actionDelete);
165                 menu->addSeparator();
166                 if (m_feedListWidget->isFolder(selectedItems.first()))
167                     menu->addAction(m_ui->actionNewFolder);
168             }
169         }
170         else
171         {
172             menu->addAction(m_ui->actionDelete);
173             menu->addSeparator();
174         }
175 
176         menu->addAction(m_ui->actionNewSubscription);
177 
178         if (m_feedListWidget->isFeed(selectedItems.first()))
179         {
180             menu->addSeparator();
181             menu->addAction(m_ui->actionCopyFeedURL);
182         }
183     }
184     else
185     {
186         menu->addAction(m_ui->actionNewSubscription);
187         menu->addAction(m_ui->actionNewFolder);
188         menu->addSeparator();
189         menu->addAction(m_ui->actionUpdateAllFeeds);
190     }
191 
192     menu->popup(QCursor::pos());
193 }
194 
displayItemsListMenu(const QPoint &)195 void RSSWidget::displayItemsListMenu(const QPoint &)
196 {
197     bool hasTorrent = false;
198     bool hasLink = false;
199     for (const QListWidgetItem *item : asConst(m_articleListWidget->selectedItems()))
200     {
201         auto article = reinterpret_cast<RSS::Article *>(item->data(Qt::UserRole).value<quintptr>());
202         Q_ASSERT(article);
203 
204         if (!article->torrentUrl().isEmpty())
205             hasTorrent = true;
206         if (!article->link().isEmpty())
207             hasLink = true;
208         if (hasTorrent && hasLink)
209             break;
210     }
211 
212     QMenu *myItemListMenu = new QMenu(this);
213     myItemListMenu->setAttribute(Qt::WA_DeleteOnClose);
214 
215     if (hasTorrent)
216         myItemListMenu->addAction(m_ui->actionDownloadTorrent);
217     if (hasLink)
218         myItemListMenu->addAction(m_ui->actionOpenNewsURL);
219 
220     if (!myItemListMenu->isEmpty())
221         myItemListMenu->popup(QCursor::pos());
222 }
223 
askNewFolder()224 void RSSWidget::askNewFolder()
225 {
226     bool ok = false;
227     QString newName = AutoExpandableDialog::getText(
228                 this, tr("Please choose a folder name"), tr("Folder name:"), QLineEdit::Normal
229                 , tr("New folder"), &ok);
230     if (!ok) return;
231 
232     newName = newName.trimmed();
233     if (newName.isEmpty()) return;
234 
235     // Determine destination folder for new item
236     QTreeWidgetItem *destItem = nullptr;
237     QList<QTreeWidgetItem *> selectedItems = m_feedListWidget->selectedItems();
238     if (!selectedItems.empty())
239     {
240         destItem = selectedItems.first();
241         if (!m_feedListWidget->isFolder(destItem))
242             destItem = destItem->parent();
243     }
244     // Consider the case where the user clicked on Unread item
245     RSS::Folder *rssDestFolder = ((!destItem || (destItem == m_feedListWidget->stickyUnreadItem()))
246                                   ? RSS::Session::instance()->rootFolder()
247                                   : qobject_cast<RSS::Folder *>(m_feedListWidget->getRSSItem(destItem)));
248 
249     QString error;
250     const QString newFolderPath = RSS::Item::joinPath(rssDestFolder->path(), newName);
251     if (!RSS::Session::instance()->addFolder(newFolderPath, &error))
252         QMessageBox::warning(this, "qBittorrent", error, QMessageBox::Ok);
253 
254     // Expand destination folder to display new feed
255     if (destItem && (destItem != m_feedListWidget->stickyUnreadItem()))
256         destItem->setExpanded(true);
257     // As new RSS items are added synchronously, we can do the following here.
258     m_feedListWidget->setCurrentItem(m_feedListWidget->mapRSSItem(RSS::Session::instance()->itemByPath(newFolderPath)));
259 }
260 
261 // add a stream by a button
on_newFeedButton_clicked()262 void RSSWidget::on_newFeedButton_clicked()
263 {
264     // Ask for feed URL
265     const QString clipText = qApp->clipboard()->text();
266     const QString defaultURL = Net::DownloadManager::hasSupportedScheme(clipText) ? clipText : "http://";
267 
268     bool ok = false;
269     QString newURL = AutoExpandableDialog::getText(
270                 this, tr("Please type a RSS feed URL"), tr("Feed URL:"), QLineEdit::Normal, defaultURL, &ok);
271     if (!ok) return;
272 
273     newURL = newURL.trimmed();
274     if (newURL.isEmpty()) return;
275 
276     // Determine destination folder for new item
277     QTreeWidgetItem *destItem = nullptr;
278     QList<QTreeWidgetItem *> selectedItems = m_feedListWidget->selectedItems();
279     if (!selectedItems.empty())
280     {
281         destItem = selectedItems.first();
282         if (!m_feedListWidget->isFolder(destItem))
283             destItem = destItem->parent();
284     }
285     // Consider the case where the user clicked on Unread item
286     RSS::Folder *rssDestFolder = ((!destItem || (destItem == m_feedListWidget->stickyUnreadItem()))
287                                   ? RSS::Session::instance()->rootFolder()
288                                   : qobject_cast<RSS::Folder *>(m_feedListWidget->getRSSItem(destItem)));
289 
290     QString error;
291     // NOTE: We still add feed using legacy way (with URL as feed name)
292     const QString newFeedPath = RSS::Item::joinPath(rssDestFolder->path(), newURL);
293     if (!RSS::Session::instance()->addFeed(newURL, newFeedPath, &error))
294         QMessageBox::warning(this, "qBittorrent", error, QMessageBox::Ok);
295 
296     // Expand destination folder to display new feed
297     if (destItem && (destItem != m_feedListWidget->stickyUnreadItem()))
298         destItem->setExpanded(true);
299     // As new RSS items are added synchronously, we can do the following here.
300     m_feedListWidget->setCurrentItem(m_feedListWidget->mapRSSItem(RSS::Session::instance()->itemByPath(newFeedPath)));
301 }
302 
deleteSelectedItems()303 void RSSWidget::deleteSelectedItems()
304 {
305     const QList<QTreeWidgetItem *> selectedItems = m_feedListWidget->selectedItems();
306     if (selectedItems.isEmpty())
307         return;
308     if ((selectedItems.size() == 1) && (selectedItems.first() == m_feedListWidget->stickyUnreadItem()))
309         return;
310 
311     QMessageBox::StandardButton answer = QMessageBox::question(
312                 this, tr("Deletion confirmation"), tr("Are you sure you want to delete the selected RSS feeds?")
313                 , QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
314     if (answer == QMessageBox::No)
315         return;
316 
317     for (QTreeWidgetItem *item : selectedItems)
318         if (item != m_feedListWidget->stickyUnreadItem())
319             RSS::Session::instance()->removeItem(m_feedListWidget->itemPath(item));
320 }
321 
loadFoldersOpenState()322 void RSSWidget::loadFoldersOpenState()
323 {
324     const QStringList openedFolders = Preferences::instance()->getRssOpenFolders();
325     for (const QString &varPath : openedFolders)
326     {
327         QTreeWidgetItem *parent = nullptr;
328         for (const QString &name : asConst(varPath.split('\\')))
329         {
330             int nbChildren = (parent ? parent->childCount() : m_feedListWidget->topLevelItemCount());
331             for (int i = 0; i < nbChildren; ++i)
332             {
333                 QTreeWidgetItem *child = (parent ? parent->child(i) : m_feedListWidget->topLevelItem(i));
334                 if (m_feedListWidget->getRSSItem(child)->name() == name)
335                 {
336                     parent = child;
337                     parent->setExpanded(true);
338                     break;
339                 }
340             }
341         }
342     }
343 }
344 
saveFoldersOpenState()345 void RSSWidget::saveFoldersOpenState()
346 {
347     QStringList openedFolders;
348     for (QTreeWidgetItem *item : asConst(m_feedListWidget->getAllOpenedFolders()))
349         openedFolders << m_feedListWidget->itemPath(item);
350     Preferences::instance()->setRssOpenFolders(openedFolders);
351 }
352 
refreshAllFeeds()353 void RSSWidget::refreshAllFeeds()
354 {
355     RSS::Session::instance()->refresh();
356 }
357 
downloadSelectedTorrents()358 void RSSWidget::downloadSelectedTorrents()
359 {
360     for (QListWidgetItem *item : asConst(m_articleListWidget->selectedItems()))
361     {
362         auto article = reinterpret_cast<RSS::Article *>(item->data(Qt::UserRole).value<quintptr>());
363         Q_ASSERT(article);
364 
365         // Mark as read
366         article->markAsRead();
367 
368         if (!article->torrentUrl().isEmpty())
369         {
370             if (AddNewTorrentDialog::isEnabled())
371                 AddNewTorrentDialog::show(article->torrentUrl(), window());
372             else
373                 BitTorrent::Session::instance()->addTorrent(article->torrentUrl());
374         }
375     }
376 }
377 
378 // open the url of the selected RSS articles in the Web browser
openSelectedArticlesUrls()379 void RSSWidget::openSelectedArticlesUrls()
380 {
381     for (QListWidgetItem *item : asConst(m_articleListWidget->selectedItems()))
382     {
383         auto article = reinterpret_cast<RSS::Article *>(item->data(Qt::UserRole).value<quintptr>());
384         Q_ASSERT(article);
385 
386         // Mark as read
387         article->markAsRead();
388 
389         if (!article->link().isEmpty())
390             QDesktopServices::openUrl(QUrl(article->link()));
391     }
392 }
393 
renameSelectedRSSItem()394 void RSSWidget::renameSelectedRSSItem()
395 {
396     QList<QTreeWidgetItem *> selectedItems = m_feedListWidget->selectedItems();
397     if (selectedItems.size() != 1) return;
398 
399     QTreeWidgetItem *item = selectedItems.first();
400     if (item == m_feedListWidget->stickyUnreadItem())
401         return;
402 
403     RSS::Item *rssItem = m_feedListWidget->getRSSItem(item);
404     const QString parentPath = RSS::Item::parentPath(rssItem->path());
405     bool ok = false;
406     do
407     {
408         QString newName = AutoExpandableDialog::getText(
409                     this, tr("Please choose a new name for this RSS feed"), tr("New feed name:")
410                     , QLineEdit::Normal, rssItem->name(), &ok);
411         // Check if name is already taken
412         if (!ok) return;
413 
414         QString error;
415         if (!RSS::Session::instance()->moveItem(rssItem, RSS::Item::joinPath(parentPath, newName), &error))
416         {
417             QMessageBox::warning(nullptr, tr("Rename failed"), error);
418             ok = false;
419         }
420     } while (!ok);
421 }
422 
refreshSelectedItems()423 void RSSWidget::refreshSelectedItems()
424 {
425     for (QTreeWidgetItem *item : asConst(m_feedListWidget->selectedItems()))
426     {
427         if (item == m_feedListWidget->stickyUnreadItem())
428         {
429             refreshAllFeeds();
430             return;
431         }
432 
433         m_feedListWidget->getRSSItem(item)->refresh();
434     }
435 }
436 
copySelectedFeedsURL()437 void RSSWidget::copySelectedFeedsURL()
438 {
439     QStringList URLs;
440     for (QTreeWidgetItem *item : asConst(m_feedListWidget->selectedItems()))
441     {
442         if (auto feed = qobject_cast<RSS::Feed *>(m_feedListWidget->getRSSItem(item)))
443             URLs << feed->url();
444     }
445     qApp->clipboard()->setText(URLs.join('\n'));
446 }
447 
handleCurrentFeedItemChanged(QTreeWidgetItem * currentItem)448 void RSSWidget::handleCurrentFeedItemChanged(QTreeWidgetItem *currentItem)
449 {
450     m_articleListWidget->setRSSItem(m_feedListWidget->getRSSItem(currentItem)
451                                     , (currentItem == m_feedListWidget->stickyUnreadItem()));
452 }
453 
on_markReadButton_clicked()454 void RSSWidget::on_markReadButton_clicked()
455 {
456     for (QTreeWidgetItem *item : asConst(m_feedListWidget->selectedItems()))
457     {
458         m_feedListWidget->getRSSItem(item)->markAsRead();
459         if (item == m_feedListWidget->stickyUnreadItem())
460             break; // all items was read
461     }
462 }
463 
464 // display a news
handleCurrentArticleItemChanged(QListWidgetItem * currentItem,QListWidgetItem * previousItem)465 void RSSWidget::handleCurrentArticleItemChanged(QListWidgetItem *currentItem, QListWidgetItem *previousItem)
466 {
467     m_ui->textBrowser->clear();
468 
469     if (previousItem)
470     {
471         auto article = m_articleListWidget->getRSSArticle(previousItem);
472         Q_ASSERT(article);
473         article->markAsRead();
474     }
475 
476     if (!currentItem) return;
477 
478     auto article = m_articleListWidget->getRSSArticle(currentItem);
479     Q_ASSERT(article);
480 
481     const QString highlightedBaseColor = m_ui->textBrowser->palette().color(QPalette::Highlight).name();
482     const QString highlightedBaseTextColor = m_ui->textBrowser->palette().color(QPalette::HighlightedText).name();
483     const QString alternateBaseColor = m_ui->textBrowser->palette().color(QPalette::AlternateBase).name();
484 
485     QString html =
486         QString::fromLatin1("<div style='border: 2px solid red; margin-left: 5px; margin-right: 5px; margin-bottom: 5px;'>") +
487         QString::fromLatin1("<div style='background-color: \"%1\"; font-weight: bold; color: \"%2\";'>%3</div>").arg(highlightedBaseColor, highlightedBaseTextColor, article->title());
488     if (article->date().isValid())
489         html += QString::fromLatin1("<div style='background-color: \"%1\";'><b>%2</b>%3</div>").arg(alternateBaseColor, tr("Date: "), QLocale::system().toString(article->date().toLocalTime()));
490     if (!article->author().isEmpty())
491         html += QString::fromLatin1("<div style='background-color: \"%1\";'><b>%2</b>%3</div>").arg(alternateBaseColor, tr("Author: "), article->author());
492     html += "</div>"
493             "<div style='margin-left: 5px; margin-right: 5px;'>";
494     if (Qt::mightBeRichText(article->description()))
495     {
496         html += article->description();
497     }
498     else
499     {
500         QString description = article->description();
501         QRegularExpression rx;
502         // If description is plain text, replace BBCode tags with HTML and wrap everything in <pre></pre> so it looks nice
503         rx.setPatternOptions(QRegularExpression::InvertedGreedinessOption
504             | QRegularExpression::CaseInsensitiveOption);
505 
506         rx.setPattern("\\[img\\](.+)\\[/img\\]");
507         description = description.replace(rx, "<img src=\"\\1\">");
508 
509         rx.setPattern("\\[url=(\")?(.+)\\1\\]");
510         description = description.replace(rx, "<a href=\"\\2\">");
511         description = description.replace("[/url]", "</a>", Qt::CaseInsensitive);
512 
513         rx.setPattern("\\[(/)?([bius])\\]");
514         description = description.replace(rx, "<\\1\\2>");
515 
516         rx.setPattern("\\[color=(\")?(.+)\\1\\]");
517         description = description.replace(rx, "<span style=\"color:\\2\">");
518         description = description.replace("[/color]", "</span>", Qt::CaseInsensitive);
519 
520         rx.setPattern("\\[size=(\")?(.+)\\d\\1\\]");
521         description = description.replace(rx, "<span style=\"font-size:\\2px\">");
522         description = description.replace("[/size]", "</span>", Qt::CaseInsensitive);
523 
524         html += "<pre>" + description + "</pre>";
525     }
526     html += "</div>";
527     m_ui->textBrowser->setHtml(html);
528 }
529 
saveSlidersPosition()530 void RSSWidget::saveSlidersPosition()
531 {
532     // Remember sliders positions
533     Preferences *const pref = Preferences::instance();
534     pref->setRssSideSplitterState(m_ui->splitterSide->saveState());
535     pref->setRssMainSplitterState(m_ui->splitterMain->saveState());
536 }
537 
restoreSlidersPosition()538 void RSSWidget::restoreSlidersPosition()
539 {
540     const Preferences *const pref = Preferences::instance();
541     const QByteArray stateSide = pref->getRssSideSplitterState();
542     if (!stateSide.isEmpty())
543         m_ui->splitterSide->restoreState(stateSide);
544     const QByteArray stateMain = pref->getRssMainSplitterState();
545     if (!stateMain.isEmpty())
546         m_ui->splitterMain->restoreState(stateMain);
547 }
548 
updateRefreshInterval(int val) const549 void RSSWidget::updateRefreshInterval(int val) const
550 {
551     RSS::Session::instance()->setRefreshInterval(val);
552 }
553 
on_rssDownloaderBtn_clicked()554 void RSSWidget::on_rssDownloaderBtn_clicked()
555 {
556     auto *downloader = new AutomatedRssDownloader(this);
557     downloader->setAttribute(Qt::WA_DeleteOnClose);
558     downloader->open();
559 }
560 
handleSessionProcessingStateChanged(bool enabled)561 void RSSWidget::handleSessionProcessingStateChanged(bool enabled)
562 {
563     m_ui->labelWarn->setVisible(!enabled);
564 }
565 
handleUnreadCountChanged()566 void RSSWidget::handleUnreadCountChanged()
567 {
568     emit unreadCountUpdated(RSS::Session::instance()->rootFolder()->unreadCount());
569 }
570