/* * Bittorrent Client using Qt and libtorrent. * Copyright (C) 2017 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * Copyright (C) 2006 Arnaud Demaiziere * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * In addition, as a special exception, the copyright holders give permission to * link this program with the OpenSSL project's "OpenSSL" library (or with * modified versions of it that use the same license as the "OpenSSL" library), * and distribute the linked executables. You must obey the GNU General Public * License in all respects for all of the code used other than "OpenSSL". If you * modify file(s), you may extend this exception to your version of the file(s), * but you are not obligated to do so. If you do not wish to do so, delete this * exception statement from your version. */ #include "rsswidget.h" #include #include #include #include #include #include #include #include #include "base/bittorrent/session.h" #include "base/net/downloadmanager.h" #include "base/preferences.h" #include "base/rss/rss_article.h" #include "base/rss/rss_feed.h" #include "base/rss/rss_folder.h" #include "base/rss/rss_session.h" #include "base/utils/misc.h" #include "addnewtorrentdialog.h" #include "articlelistwidget.h" #include "autoexpandabledialog.h" #include "automatedrssdownloader.h" #include "feedlistwidget.h" #include "guiiconprovider.h" #include "ui_rsswidget.h" RSSWidget::RSSWidget(QWidget *parent) : QWidget(parent) , m_ui(new Ui::RSSWidget) { m_ui->setupUi(this); // Icons m_ui->actionCopyFeedURL->setIcon(GuiIconProvider::instance()->getIcon("edit-copy")); m_ui->actionDelete->setIcon(GuiIconProvider::instance()->getIcon("edit-delete")); m_ui->actionDownloadTorrent->setIcon(GuiIconProvider::instance()->getIcon("download")); m_ui->actionMarkItemsRead->setIcon(GuiIconProvider::instance()->getIcon("mail-mark-read")); m_ui->actionNewFolder->setIcon(GuiIconProvider::instance()->getIcon("folder-new")); m_ui->actionNewSubscription->setIcon(GuiIconProvider::instance()->getIcon("list-add")); m_ui->actionOpenNewsURL->setIcon(GuiIconProvider::instance()->getIcon("application-x-mswinurl")); m_ui->actionRename->setIcon(GuiIconProvider::instance()->getIcon("edit-rename")); m_ui->actionUpdate->setIcon(GuiIconProvider::instance()->getIcon("view-refresh")); m_ui->actionUpdateAllFeeds->setIcon(GuiIconProvider::instance()->getIcon("view-refresh")); m_ui->newFeedButton->setIcon(GuiIconProvider::instance()->getIcon("list-add")); m_ui->markReadButton->setIcon(GuiIconProvider::instance()->getIcon("mail-mark-read")); m_ui->updateAllButton->setIcon(GuiIconProvider::instance()->getIcon("view-refresh")); m_ui->rssDownloaderBtn->setIcon(GuiIconProvider::instance()->getIcon("download")); m_articleListWidget = new ArticleListWidget(m_ui->splitterMain); m_ui->splitterMain->insertWidget(0, m_articleListWidget); connect(m_articleListWidget, &ArticleListWidget::customContextMenuRequested, this, &RSSWidget::displayItemsListMenu); connect(m_articleListWidget, &ArticleListWidget::currentItemChanged, this, &RSSWidget::handleCurrentArticleItemChanged); connect(m_articleListWidget, &ArticleListWidget::itemDoubleClicked, this, &RSSWidget::downloadSelectedTorrents); m_feedListWidget = new FeedListWidget(m_ui->splitterSide); m_ui->splitterSide->insertWidget(0, m_feedListWidget); connect(m_feedListWidget, &QAbstractItemView::doubleClicked, this, &RSSWidget::renameSelectedRSSItem); connect(m_feedListWidget, &QTreeWidget::currentItemChanged, this, &RSSWidget::handleCurrentFeedItemChanged); connect(m_feedListWidget, &QWidget::customContextMenuRequested, this, &RSSWidget::displayRSSListMenu); loadFoldersOpenState(); m_feedListWidget->setCurrentItem(m_feedListWidget->stickyUnreadItem()); m_editHotkey = new QShortcut(Qt::Key_F2, m_feedListWidget, 0, 0, Qt::WidgetShortcut); connect(m_editHotkey, &QShortcut::activated, this, &RSSWidget::renameSelectedRSSItem); m_deleteHotkey = new QShortcut(QKeySequence::Delete, m_feedListWidget, 0, 0, Qt::WidgetShortcut); connect(m_deleteHotkey, &QShortcut::activated, this, &RSSWidget::deleteSelectedItems); // Feeds list actions connect(m_ui->actionDelete, &QAction::triggered, this, &RSSWidget::deleteSelectedItems); connect(m_ui->actionRename, &QAction::triggered, this, &RSSWidget::renameSelectedRSSItem); connect(m_ui->actionUpdate, &QAction::triggered, this, &RSSWidget::refreshSelectedItems); connect(m_ui->actionNewFolder, &QAction::triggered, this, &RSSWidget::askNewFolder); connect(m_ui->actionNewSubscription, &QAction::triggered, this, &RSSWidget::on_newFeedButton_clicked); connect(m_ui->actionUpdateAllFeeds, &QAction::triggered, this, &RSSWidget::refreshAllFeeds); connect(m_ui->updateAllButton, &QAbstractButton::clicked, this, &RSSWidget::refreshAllFeeds); connect(m_ui->actionCopyFeedURL, &QAction::triggered, this, &RSSWidget::copySelectedFeedsURL); connect(m_ui->actionMarkItemsRead, &QAction::triggered, this, &RSSWidget::on_markReadButton_clicked); // News list actions connect(m_ui->actionOpenNewsURL, &QAction::triggered, this, &RSSWidget::openSelectedArticlesUrls); connect(m_ui->actionDownloadTorrent, &QAction::triggered, this, &RSSWidget::downloadSelectedTorrents); // Restore sliders position restoreSlidersPosition(); // Bind saveSliders slots connect(m_ui->splitterMain, &QSplitter::splitterMoved, this, &RSSWidget::saveSlidersPosition); connect(m_ui->splitterSide, &QSplitter::splitterMoved, this, &RSSWidget::saveSlidersPosition); if (RSS::Session::instance()->isProcessingEnabled()) m_ui->labelWarn->hide(); connect(RSS::Session::instance(), &RSS::Session::processingStateChanged , this, &RSSWidget::handleSessionProcessingStateChanged); connect(RSS::Session::instance()->rootFolder(), &RSS::Folder::unreadCountChanged , this, &RSSWidget::handleUnreadCountChanged); } RSSWidget::~RSSWidget() { // we need it here to properly mark latest article // as read without having additional code m_articleListWidget->clear(); saveFoldersOpenState(); delete m_editHotkey; delete m_deleteHotkey; delete m_feedListWidget; delete m_ui; } // display a right-click menu void RSSWidget::displayRSSListMenu(const QPoint &pos) { if (!m_feedListWidget->indexAt(pos).isValid()) // No item under the mouse, clear selection m_feedListWidget->clearSelection(); QMenu myRSSListMenu(this); QList selectedItems = m_feedListWidget->selectedItems(); if (selectedItems.size() > 0) { myRSSListMenu.addAction(m_ui->actionUpdate); myRSSListMenu.addAction(m_ui->actionMarkItemsRead); myRSSListMenu.addSeparator(); if (selectedItems.size() == 1) { if (selectedItems.first() != m_feedListWidget->stickyUnreadItem()) { myRSSListMenu.addAction(m_ui->actionRename); myRSSListMenu.addAction(m_ui->actionDelete); myRSSListMenu.addSeparator(); if (m_feedListWidget->isFolder(selectedItems.first())) myRSSListMenu.addAction(m_ui->actionNewFolder); } } else { myRSSListMenu.addAction(m_ui->actionDelete); myRSSListMenu.addSeparator(); } myRSSListMenu.addAction(m_ui->actionNewSubscription); if (m_feedListWidget->isFeed(selectedItems.first())) { myRSSListMenu.addSeparator(); myRSSListMenu.addAction(m_ui->actionCopyFeedURL); } } else { myRSSListMenu.addAction(m_ui->actionNewSubscription); myRSSListMenu.addAction(m_ui->actionNewFolder); myRSSListMenu.addSeparator(); myRSSListMenu.addAction(m_ui->actionUpdateAllFeeds); } myRSSListMenu.exec(QCursor::pos()); } void RSSWidget::displayItemsListMenu(const QPoint &) { bool hasTorrent = false; bool hasLink = false; foreach (const QListWidgetItem *item, m_articleListWidget->selectedItems()) { auto article = reinterpret_cast(item->data(Qt::UserRole).value()); Q_ASSERT(article); if (!article->torrentUrl().isEmpty()) hasTorrent = true; if (!article->link().isEmpty()) hasLink = true; if (hasTorrent && hasLink) break; } QMenu myItemListMenu(this); if (hasTorrent) myItemListMenu.addAction(m_ui->actionDownloadTorrent); if (hasLink) myItemListMenu.addAction(m_ui->actionOpenNewsURL); if (hasTorrent || hasLink) myItemListMenu.exec(QCursor::pos()); } void RSSWidget::askNewFolder() { bool ok; QString newName = AutoExpandableDialog::getText( this, tr("Please choose a folder name"), tr("Folder name:"), QLineEdit::Normal , tr("New folder"), &ok); if (!ok) return; newName = newName.trimmed(); if (newName.isEmpty()) return; // Determine destination folder for new item QTreeWidgetItem *destItem = nullptr; QList selectedItems = m_feedListWidget->selectedItems(); if (!selectedItems.empty()) { destItem = selectedItems.first(); if (!m_feedListWidget->isFolder(destItem)) destItem = destItem->parent(); } // Consider the case where the user clicked on Unread item RSS::Folder *rssDestFolder = ((destItem == m_feedListWidget->stickyUnreadItem()) ? RSS::Session::instance()->rootFolder() : qobject_cast(m_feedListWidget->getRSSItem(destItem))); QString error; const QString newFolderPath = RSS::Item::joinPath(rssDestFolder->path(), newName); if (!RSS::Session::instance()->addFolder(newFolderPath, &error)) QMessageBox::warning(this, "qBittorrent", error, QMessageBox::Ok); // Expand destination folder to display new feed if (destItem != m_feedListWidget->stickyUnreadItem()) destItem->setExpanded(true); // As new RSS items are added synchronously, we can do the following here. m_feedListWidget->setCurrentItem(m_feedListWidget->mapRSSItem(RSS::Session::instance()->itemByPath(newFolderPath))); } // add a stream by a button void RSSWidget::on_newFeedButton_clicked() { // Ask for feed URL const QString clipText = qApp->clipboard()->text(); const QString defaultURL = (Utils::Misc::isUrl(clipText) ? clipText : "http://"); bool ok; QString newURL = AutoExpandableDialog::getText( this, tr("Please type a RSS feed URL"), tr("Feed URL:"), QLineEdit::Normal, defaultURL, &ok); if (!ok) return; newURL = newURL.trimmed(); if (newURL.isEmpty()) return; // Determine destination folder for new item QTreeWidgetItem *destItem = nullptr; QList selectedItems = m_feedListWidget->selectedItems(); if (!selectedItems.empty()) { destItem = selectedItems.first(); if (!m_feedListWidget->isFolder(destItem)) destItem = destItem->parent(); } // Consider the case where the user clicked on Unread item RSS::Folder *rssDestFolder = ((!destItem || (destItem == m_feedListWidget->stickyUnreadItem())) ? RSS::Session::instance()->rootFolder() : qobject_cast(m_feedListWidget->getRSSItem(destItem))); QString error; // NOTE: We still add feed using legacy way (with URL as feed name) const QString newFeedPath = RSS::Item::joinPath(rssDestFolder->path(), newURL); if (!RSS::Session::instance()->addFeed(newURL, newFeedPath, &error)) QMessageBox::warning(this, "qBittorrent", error, QMessageBox::Ok); // Expand destination folder to display new feed if (destItem && (destItem != m_feedListWidget->stickyUnreadItem())) destItem->setExpanded(true); // As new RSS items are added synchronously, we can do the following here. m_feedListWidget->setCurrentItem(m_feedListWidget->mapRSSItem(RSS::Session::instance()->itemByPath(newFeedPath))); } void RSSWidget::deleteSelectedItems() { QList selectedItems = m_feedListWidget->selectedItems(); if (selectedItems.isEmpty()) return; if ((selectedItems.size() == 1) && (selectedItems.first() == m_feedListWidget->stickyUnreadItem())) return; QMessageBox::StandardButton answer = QMessageBox::question( this, tr("Deletion confirmation"), tr("Are you sure you want to delete the selected RSS feeds?") , QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (answer == QMessageBox::No) return; foreach (QTreeWidgetItem *item, selectedItems) if (item != m_feedListWidget->stickyUnreadItem()) RSS::Session::instance()->removeItem(m_feedListWidget->itemPath(item)); } void RSSWidget::loadFoldersOpenState() { const QStringList openedFolders = Preferences::instance()->getRssOpenFolders(); foreach (const QString &varPath, openedFolders) { QTreeWidgetItem *parent = nullptr; foreach (const QString &name, varPath.split("\\")) { int nbChildren = (parent ? parent->childCount() : m_feedListWidget->topLevelItemCount()); for (int i = 0; i < nbChildren; ++i) { QTreeWidgetItem *child = (parent ? parent->child(i) : m_feedListWidget->topLevelItem(i)); if (m_feedListWidget->getRSSItem(child)->name() == name) { parent = child; parent->setExpanded(true); break; } } } } } void RSSWidget::saveFoldersOpenState() { QStringList openedFolders; foreach (QTreeWidgetItem *item, m_feedListWidget->getAllOpenedFolders()) openedFolders << m_feedListWidget->itemPath(item); Preferences::instance()->setRssOpenFolders(openedFolders); } void RSSWidget::refreshAllFeeds() { RSS::Session::instance()->refresh(); } void RSSWidget::downloadSelectedTorrents() { foreach (QListWidgetItem *item, m_articleListWidget->selectedItems()) { auto article = reinterpret_cast(item->data(Qt::UserRole).value()); Q_ASSERT(article); // Mark as read article->markAsRead(); if (!article->torrentUrl().isEmpty()) { if (AddNewTorrentDialog::isEnabled()) AddNewTorrentDialog::show(article->torrentUrl()); else BitTorrent::Session::instance()->addTorrent(article->torrentUrl()); } } } // open the url of the selected RSS articles in the Web browser void RSSWidget::openSelectedArticlesUrls() { foreach (QListWidgetItem *item, m_articleListWidget->selectedItems()) { auto article = reinterpret_cast(item->data(Qt::UserRole).value()); Q_ASSERT(article); // Mark as read article->markAsRead(); if (!article->link().isEmpty()) QDesktopServices::openUrl(QUrl(article->link())); } } void RSSWidget::renameSelectedRSSItem() { QList selectedItems = m_feedListWidget->selectedItems(); if (selectedItems.size() != 1) return; QTreeWidgetItem *item = selectedItems.first(); if (item == m_feedListWidget->stickyUnreadItem()) return; RSS::Item *rssItem = m_feedListWidget->getRSSItem(item); const QString parentPath = RSS::Item::parentPath(rssItem->path()); bool ok; do { QString newName = AutoExpandableDialog::getText( this, tr("Please choose a new name for this RSS feed"), tr("New feed name:") , QLineEdit::Normal, rssItem->name(), &ok); // Check if name is already taken if (!ok) return; QString error; if (!RSS::Session::instance()->moveItem(rssItem, RSS::Item::joinPath(parentPath, newName), &error)) { QMessageBox::warning(0, tr("Rename failed"), error); ok = false; } } while (!ok); } void RSSWidget::refreshSelectedItems() { foreach (QTreeWidgetItem *item, m_feedListWidget->selectedItems()) { if (item == m_feedListWidget->stickyUnreadItem()) { refreshAllFeeds(); return; } m_feedListWidget->getRSSItem(item)->refresh(); } } void RSSWidget::copySelectedFeedsURL() { QStringList URLs; foreach (QTreeWidgetItem *item, m_feedListWidget->selectedItems()) { if (auto feed = qobject_cast(m_feedListWidget->getRSSItem(item))) URLs << feed->url(); } qApp->clipboard()->setText(URLs.join("\n")); } void RSSWidget::handleCurrentFeedItemChanged(QTreeWidgetItem *currentItem) { if (!currentItem) { m_articleListWidget->clear(); return; } m_articleListWidget->setRSSItem(m_feedListWidget->getRSSItem(currentItem) , (currentItem == m_feedListWidget->stickyUnreadItem())); } void RSSWidget::on_markReadButton_clicked() { foreach (QTreeWidgetItem *item, m_feedListWidget->selectedItems()) { m_feedListWidget->getRSSItem(item)->markAsRead(); if (item == m_feedListWidget->stickyUnreadItem()) break; // all items was read } } // display a news void RSSWidget::handleCurrentArticleItemChanged(QListWidgetItem *currentItem, QListWidgetItem *previousItem) { m_ui->textBrowser->clear(); if (previousItem) { auto article = m_articleListWidget->getRSSArticle(previousItem); Q_ASSERT(article); article->markAsRead(); } if (!currentItem) return; auto article = m_articleListWidget->getRSSArticle(currentItem); Q_ASSERT(article); QString html; html += "
"; html += "
" + article->title() + "
"; if (article->date().isValid()) html += "
" + tr("Date: ") + "" + article->date().toLocalTime().toString(Qt::SystemLocaleLongDate) + "
"; if (!article->author().isEmpty()) html += "
" + tr("Author: ") + "" + article->author() + "
"; html += "
"; html += "
"; if (Qt::mightBeRichText(article->description())) { html += article->description(); } else { QString description = article->description(); QRegExp rx; // If description is plain text, replace BBCode tags with HTML and wrap everything in
 so it looks nice
        rx.setMinimal(true);
        rx.setCaseSensitivity(Qt::CaseInsensitive);

        rx.setPattern("\\[img\\](.+)\\[/img\\]");
        description = description.replace(rx, "");

        rx.setPattern("\\[url=(\")?(.+)\\1\\]");
        description = description.replace(rx, "");
        description = description.replace("[/url]", "", Qt::CaseInsensitive);

        rx.setPattern("\\[(/)?([bius])\\]");
        description = description.replace(rx, "<\\1\\2>");

        rx.setPattern("\\[color=(\")?(.+)\\1\\]");
        description = description.replace(rx, "");
        description = description.replace("[/color]", "", Qt::CaseInsensitive);

        rx.setPattern("\\[size=(\")?(.+)\\d\\1\\]");
        description = description.replace(rx, "");
        description = description.replace("[/size]", "", Qt::CaseInsensitive);

        html += "
" + description + "
"; } html += "
"; m_ui->textBrowser->setHtml(html); } void RSSWidget::saveSlidersPosition() { // Remember sliders positions Preferences *const pref = Preferences::instance(); pref->setRssSideSplitterState(m_ui->splitterSide->saveState()); pref->setRssMainSplitterState(m_ui->splitterMain->saveState()); } void RSSWidget::restoreSlidersPosition() { const Preferences *const pref = Preferences::instance(); const QByteArray stateSide = pref->getRssSideSplitterState(); if (!stateSide.isEmpty()) m_ui->splitterSide->restoreState(stateSide); const QByteArray stateMain = pref->getRssMainSplitterState(); if (!stateMain.isEmpty()) m_ui->splitterMain->restoreState(stateMain); } void RSSWidget::updateRefreshInterval(uint val) { RSS::Session::instance()->setRefreshInterval(val); } void RSSWidget::on_rssDownloaderBtn_clicked() { AutomatedRssDownloader(this).exec(); } void RSSWidget::handleSessionProcessingStateChanged(bool enabled) { m_ui->labelWarn->setVisible(!enabled); } void RSSWidget::handleUnreadCountChanged() { emit unreadCountUpdated(RSS::Session::instance()->rootFolder()->unreadCount()); }