Some work about adaptive color scheme for Web UI (PR #19901) http://[316:c51a:62a3:8b9::4]/d4708/qBittorrent/src/branch/adaptive-webui
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

531 lines
22 KiB

/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2006 Arnaud Demaiziere <arnaud@qbittorrent.org>
*
* 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 <QClipboard>
#include <QDebug>
#include <QDesktopServices>
#include <QDragMoveEvent>
#include <QMenu>
#include <QMessageBox>
#include <QRegularExpression>
#include <QStandardItemModel>
#include <QString>
#include "base/bittorrent/session.h"
#include "base/global.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 "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"));
#ifndef Q_OS_MAC
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"));
#endif
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, nullptr, nullptr, Qt::WidgetShortcut);
connect(m_editHotkey, &QShortcut::activated, this, &RSSWidget::renameSelectedRSSItem);
m_deleteHotkey = new QShortcut(QKeySequence::Delete, m_feedListWidget, nullptr, nullptr, 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<QTreeWidgetItem *> 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;
for (const QListWidgetItem *item : asConst(m_articleListWidget->selectedItems())) {
auto article = reinterpret_cast<RSS::Article *>(item->data(Qt::UserRole).value<quintptr>());
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<QTreeWidgetItem *> 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<RSS::Folder *>(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 && (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 = Net::DownloadManager::hasSupportedScheme(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<QTreeWidgetItem *> 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<RSS::Folder *>(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()
{
const QList<QTreeWidgetItem *> 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;
for (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();
for (const QString &varPath : openedFolders) {
QTreeWidgetItem *parent = nullptr;
for (const QString &name : asConst(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;
for (QTreeWidgetItem *item : asConst(m_feedListWidget->getAllOpenedFolders()))
openedFolders << m_feedListWidget->itemPath(item);
Preferences::instance()->setRssOpenFolders(openedFolders);
}
void RSSWidget::refreshAllFeeds()
{
RSS::Session::instance()->refresh();
}
void RSSWidget::downloadSelectedTorrents()
{
for (QListWidgetItem *item : asConst(m_articleListWidget->selectedItems())) {
auto article = reinterpret_cast<RSS::Article *>(item->data(Qt::UserRole).value<quintptr>());
Q_ASSERT(article);
// Mark as read
article->markAsRead();
if (!article->torrentUrl().isEmpty()) {
if (AddNewTorrentDialog::isEnabled())
AddNewTorrentDialog::show(article->torrentUrl(), window());
else
BitTorrent::Session::instance()->addTorrent(article->torrentUrl());
}
}
}
// open the url of the selected RSS articles in the Web browser
void RSSWidget::openSelectedArticlesUrls()
{
for (QListWidgetItem *item : asConst(m_articleListWidget->selectedItems())) {
auto article = reinterpret_cast<RSS::Article *>(item->data(Qt::UserRole).value<quintptr>());
Q_ASSERT(article);
// Mark as read
article->markAsRead();
if (!article->link().isEmpty())
QDesktopServices::openUrl(QUrl(article->link()));
}
}
void RSSWidget::renameSelectedRSSItem()
{
QList<QTreeWidgetItem *> 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(nullptr, tr("Rename failed"), error);
ok = false;
}
} while (!ok);
}
void RSSWidget::refreshSelectedItems()
{
for (QTreeWidgetItem *item : asConst(m_feedListWidget->selectedItems())) {
if (item == m_feedListWidget->stickyUnreadItem()) {
refreshAllFeeds();
return;
}
m_feedListWidget->getRSSItem(item)->refresh();
}
}
void RSSWidget::copySelectedFeedsURL()
{
QStringList URLs;
for (QTreeWidgetItem *item : asConst(m_feedListWidget->selectedItems())) {
if (auto feed = qobject_cast<RSS::Feed *>(m_feedListWidget->getRSSItem(item)))
URLs << feed->url();
}
qApp->clipboard()->setText(URLs.join('\n'));
}
void RSSWidget::handleCurrentFeedItemChanged(QTreeWidgetItem *currentItem)
{
m_articleListWidget->setRSSItem(m_feedListWidget->getRSSItem(currentItem)
, (currentItem == m_feedListWidget->stickyUnreadItem()));
}
void RSSWidget::on_markReadButton_clicked()
{
for (QTreeWidgetItem *item : asConst(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 += "<div style='border: 2px solid red; margin-left: 5px; margin-right: 5px; margin-bottom: 5px;'>";
html += "<div style='background-color: #678db2; font-weight: bold; color: #fff;'>" + article->title() + "</div>";
if (article->date().isValid())
html += "<div style='background-color: #efefef;'><b>" + tr("Date: ") + "</b>" + article->date().toLocalTime().toString(Qt::SystemLocaleLongDate) + "</div>";
if (!article->author().isEmpty())
html += "<div style='background-color: #efefef;'><b>" + tr("Author: ") + "</b>" + article->author() + "</div>";
html += "</div>";
html += "<div style='margin-left: 5px; margin-right: 5px;'>";
if (Qt::mightBeRichText(article->description())) {
html += article->description();
}
else {
QString description = article->description();
QRegularExpression rx;
// If description is plain text, replace BBCode tags with HTML and wrap everything in <pre></pre> so it looks nice
rx.setPatternOptions(QRegularExpression::InvertedGreedinessOption
| QRegularExpression::CaseInsensitiveOption);
rx.setPattern("\\[img\\](.+)\\[/img\\]");
description = description.replace(rx, "<img src=\"\\1\">");
rx.setPattern("\\[url=(\")?(.+)\\1\\]");
description = description.replace(rx, "<a href=\"\\2\">");
description = description.replace("[/url]", "</a>", Qt::CaseInsensitive);
rx.setPattern("\\[(/)?([bius])\\]");
description = description.replace(rx, "<\\1\\2>");
rx.setPattern("\\[color=(\")?(.+)\\1\\]");
description = description.replace(rx, "<span style=\"color:\\2\">");
description = description.replace("[/color]", "</span>", Qt::CaseInsensitive);
rx.setPattern("\\[size=(\")?(.+)\\d\\1\\]");
description = description.replace(rx, "<span style=\"font-size:\\2px\">");
description = description.replace("[/size]", "</span>", Qt::CaseInsensitive);
html += "<pre>" + description + "</pre>";
}
html += "</div>";
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());
}