657 lines
22 KiB
C++
657 lines
22 KiB
C++
// For license of this file, see <project-root-folder>/LICENSE.md.
|
|
|
|
#include "gui/webviewers/qtextbrowser/textbrowserviewer.h"
|
|
|
|
#include "3rd-party/boolinq/boolinq.h"
|
|
#include "definitions/globals.h"
|
|
#include "gui/dialogs/formmain.h"
|
|
#include "gui/webbrowser.h"
|
|
#include "miscellaneous/application.h"
|
|
#include "miscellaneous/externaltool.h"
|
|
#include "miscellaneous/iconfactory.h"
|
|
#include "miscellaneous/settings.h"
|
|
#include "network-web/adblock/adblockrequestinfo.h"
|
|
#include "network-web/downloader.h"
|
|
#include "network-web/networkfactory.h"
|
|
#include "network-web/webfactory.h"
|
|
|
|
#include <QBuffer>
|
|
#include <QContextMenuEvent>
|
|
#include <QFileIconProvider>
|
|
#include <QScrollBar>
|
|
#include <QTextCodec>
|
|
#include <QTimer>
|
|
#include <QtConcurrent>
|
|
|
|
TextBrowserViewer::TextBrowserViewer(QWidget* parent)
|
|
: QTextBrowser(parent), m_resourcesEnabled(false), m_resourceDownloader(new Downloader()),
|
|
m_resourceDownloaderThread(new QThread(this)), m_loadedResources({}),
|
|
m_placeholderImage(qApp->icons()->miscPixmap(QSL("image-placeholder"))),
|
|
m_placeholderImageError(qApp->icons()->miscPixmap(QSL("image-placeholder-error"))),
|
|
m_downloader(new Downloader(this)), m_document(new TextBrowserDocument(this)) {
|
|
setAutoFillBackground(false);
|
|
setFrameShape(QFrame::Shape::NoFrame);
|
|
setFrameShadow(QFrame::Shadow::Plain);
|
|
setTabChangesFocus(true);
|
|
setOpenLinks(false);
|
|
setWordWrapMode(QTextOption::WrapMode::WordWrap);
|
|
|
|
viewport()->setAutoFillBackground(false);
|
|
|
|
setResourcesEnabled(qApp->settings()->value(GROUP(Messages), SETTING(Messages::ShowResourcesInArticles)).toBool());
|
|
setDocument(m_document.data());
|
|
|
|
m_resourceDownloader->moveToThread(m_resourceDownloaderThread);
|
|
m_resourceDownloaderThread->start();
|
|
|
|
connect(this, &TextBrowserViewer::reloadDocument, this, [this]() {
|
|
const auto scr = verticalScrollBarPosition();
|
|
setHtmlPrivate(html(), m_currentUrl);
|
|
setVerticalScrollBarPosition(scr);
|
|
});
|
|
|
|
connect(m_resourceDownloader, &Downloader::completed, this, &TextBrowserViewer::resourceDownloaded);
|
|
connect(this, &QTextBrowser::anchorClicked, this, &TextBrowserViewer::onAnchorClicked);
|
|
connect(this, QOverload<const QUrl&>::of(&QTextBrowser::highlighted), this, &TextBrowserViewer::linkMouseHighlighted);
|
|
}
|
|
|
|
TextBrowserViewer::~TextBrowserViewer() {
|
|
if (m_resourceDownloaderThread->isRunning()) {
|
|
m_resourceDownloaderThread->quit();
|
|
}
|
|
|
|
m_resourceDownloader->deleteLater();
|
|
}
|
|
|
|
QSize TextBrowserViewer::sizeHint() const {
|
|
auto doc_size = document()->size().toSize();
|
|
|
|
doc_size.setHeight(doc_size.height() + contentsMargins().top() + contentsMargins().bottom());
|
|
return doc_size;
|
|
}
|
|
|
|
QVariant TextBrowserViewer::loadOneResource(int type, const QUrl& name) {
|
|
if (type != QTextDocument::ResourceType::ImageResource) {
|
|
return {};
|
|
}
|
|
|
|
auto resolved_name = (m_currentUrl.isValid() && name.isRelative()) ? m_currentUrl.resolved(name) : name;
|
|
|
|
if (!m_resourcesEnabled || !m_loadedResources.contains(resolved_name)) {
|
|
// Resources are not enabled.
|
|
return m_placeholderImage;
|
|
}
|
|
|
|
// Resources are enabled and we already have the resource.
|
|
QByteArray resource_data = m_loadedResources.value(resolved_name);
|
|
QImage img;
|
|
|
|
if (resource_data.isEmpty()) {
|
|
img = m_placeholderImageError.toImage();
|
|
}
|
|
else {
|
|
img = QImage::fromData(m_loadedResources.value(resolved_name));
|
|
}
|
|
|
|
int acceptable_width = int(width() * ACCEPTABLE_IMAGE_PERCENTUAL_WIDTH);
|
|
int img_width = img.width();
|
|
|
|
if (img_width > acceptable_width) {
|
|
QElapsedTimer tmr;
|
|
|
|
tmr.start();
|
|
img = img.scaledToWidth(acceptable_width, Qt::TransformationMode::SmoothTransformation);
|
|
|
|
qWarningNN << LOGSEC_GUI << "Picture" << QUOTE_W_SPACE(name) << "with width" << QUOTE_W_SPACE(img_width)
|
|
<< "is too wide, down-scaling to prevent horizontal scrollbars. Scaling took"
|
|
<< NONQUOTE_W_SPACE(tmr.elapsed()) << "miliseconds.";
|
|
|
|
QByteArray save_arr;
|
|
QBuffer save_buf(&save_arr, this);
|
|
|
|
if (img.save(&save_buf, "PNG", 100)) {
|
|
save_buf.close();
|
|
m_loadedResources.insert(resolved_name, save_arr);
|
|
}
|
|
else {
|
|
qWarningNN << LOGSEC_GUI << "Failed to save modified image" << QUOTE_W_SPACE(name) << "to cache.";
|
|
}
|
|
}
|
|
|
|
return img;
|
|
}
|
|
|
|
void TextBrowserViewer::bindToBrowser(WebBrowser* browser) {
|
|
installEventFilter(browser);
|
|
|
|
browser->m_actionBack = nullptr;
|
|
browser->m_actionForward = nullptr;
|
|
browser->m_actionReload = nullptr;
|
|
browser->m_actionStop = nullptr;
|
|
}
|
|
|
|
void TextBrowserViewer::findText(const QString& text, bool backwards) {
|
|
if (!text.isEmpty()) {
|
|
bool found =
|
|
QTextBrowser::find(text, backwards ? QTextDocument::FindFlag::FindBackward : QTextDocument::FindFlag(0));
|
|
|
|
if (!found) {
|
|
textCursor().clearSelection();
|
|
moveCursor(QTextCursor::MoveOperation::Start);
|
|
|
|
QTextBrowser::find(text, backwards ? QTextDocument::FindFlag::FindBackward : QTextDocument::FindFlag(0));
|
|
}
|
|
}
|
|
else {
|
|
textCursor().clearSelection();
|
|
moveCursor(QTextCursor::MoveOperation::Start);
|
|
}
|
|
}
|
|
|
|
BlockingResult TextBrowserViewer::blockedWithAdblock(const QUrl& url) {
|
|
AdblockRequestInfo block_request(url);
|
|
|
|
if (url.path().endsWith(QSL("css"))) {
|
|
block_request.setResourceType(QSL("stylesheet"));
|
|
}
|
|
else {
|
|
block_request.setResourceType(QSL("image"));
|
|
}
|
|
|
|
auto block_result = qApp->web()->adBlock()->block(block_request);
|
|
|
|
if (block_result.m_blocked) {
|
|
qWarningNN << LOGSEC_ADBLOCK << "Blocked request:" << QUOTE_W_SPACE_DOT(block_request.requestUrl().toString());
|
|
return block_result;
|
|
}
|
|
else {
|
|
return block_result;
|
|
}
|
|
}
|
|
|
|
void TextBrowserViewer::setUrl(const QUrl& url) {
|
|
emit loadingStarted();
|
|
|
|
QString html_str;
|
|
QUrl nonconst_url = url;
|
|
bool is_error = false;
|
|
auto block_result = blockedWithAdblock(url);
|
|
|
|
if (block_result.m_blocked) {
|
|
is_error = true;
|
|
nonconst_url = QUrl::fromUserInput(QSL(INTERNAL_URL_ADBLOCKED));
|
|
|
|
html_str = QSL("Blocked!!!<br/>%1").arg(url.toString());
|
|
}
|
|
else {
|
|
QEventLoop loop;
|
|
|
|
connect(m_downloader.data(),
|
|
&Downloader::completed,
|
|
&loop,
|
|
&QEventLoop::quit,
|
|
Qt::ConnectionType(Qt::ConnectionType::UniqueConnection | Qt::ConnectionType::AutoConnection));
|
|
m_downloader->manipulateData(url.toString(), QNetworkAccessManager::Operation::GetOperation, {}, 5000);
|
|
|
|
loop.exec();
|
|
|
|
const auto net_error = m_downloader->lastOutputError();
|
|
const QString content_type = m_downloader->lastContentType();
|
|
|
|
if (net_error != QNetworkReply::NetworkError::NoError) {
|
|
is_error = true;
|
|
html_str = QSL("Error!<br/>%1").arg(NetworkFactory::networkErrorText(net_error));
|
|
}
|
|
else {
|
|
if (content_type.startsWith(QSL("image/"))) {
|
|
html_str = QSL("<img src=\"%1\">").arg(nonconst_url.toString());
|
|
}
|
|
else {
|
|
html_str = decodeHtmlData(m_downloader->lastOutputData(), content_type);
|
|
}
|
|
}
|
|
}
|
|
|
|
setHtml(html_str, nonconst_url);
|
|
|
|
emit loadingFinished(!is_error);
|
|
}
|
|
|
|
QString TextBrowserViewer::decodeHtmlData(const QByteArray& data, const QString& content_type) const {
|
|
QString found_charset = QRegularExpression("charset=([0-9a-zA-Z-_]+)").match(content_type).captured(1);
|
|
QTextCodec* codec = QTextCodec::codecForName(found_charset.toLocal8Bit());
|
|
|
|
if (codec == nullptr) {
|
|
// No suitable codec for this encoding was found.
|
|
// Use UTF-8.
|
|
qWarningNN << LOGSEC_GUI << "Did not find charset for content-type" << QUOTE_W_SPACE_DOT(content_type);
|
|
return QString::fromUtf8(data);
|
|
}
|
|
else {
|
|
qDebugNN << LOGSEC_GUI << "Found charset for content-type" << QUOTE_W_SPACE_DOT(content_type);
|
|
return codec->toUnicode(data);
|
|
}
|
|
}
|
|
|
|
QString TextBrowserViewer::html() const {
|
|
return m_currentHtml;
|
|
}
|
|
|
|
QUrl TextBrowserViewer::url() const {
|
|
return m_currentUrl;
|
|
}
|
|
|
|
void TextBrowserViewer::clear() {
|
|
setHtml({});
|
|
}
|
|
|
|
void TextBrowserViewer::loadMessages(const QList<Message>& messages, RootItem* root) {
|
|
emit loadingStarted();
|
|
m_root = root;
|
|
|
|
auto html_messages =
|
|
qApp->settings()->value(GROUP(Messages), SETTING(Messages::UseLegacyArticleFormat)).toBool()
|
|
? prepareLegacyHtmlForMessage(messages, root)
|
|
: qApp->skins()->generateHtmlOfArticles(messages, root, width() * ACCEPTABLE_IMAGE_PERCENTUAL_WIDTH);
|
|
|
|
// Remove other characters which cannot be displayed properly.
|
|
static QRegularExpression exp_symbols("[0-9A-F]{3};");
|
|
|
|
html_messages.m_html = html_messages.m_html.replace(exp_symbols, QString());
|
|
|
|
#if !defined(NDEBUG)
|
|
// IOFactory::writeFile("aaa.html", html_messages.m_html.toUtf8());
|
|
#endif
|
|
|
|
setHtml(html_messages.m_html, html_messages.m_baseUrl);
|
|
|
|
QTextOption op;
|
|
op.setTextDirection(messages.at(0).m_isRtl ? Qt::LayoutDirection::RightToLeft : Qt::LayoutDirection::LeftToRight);
|
|
document()->setDefaultTextOption(op);
|
|
|
|
emit loadingFinished(true);
|
|
}
|
|
|
|
double TextBrowserViewer::verticalScrollBarPosition() const {
|
|
return verticalScrollBar()->value();
|
|
}
|
|
|
|
void TextBrowserViewer::setVerticalScrollBarPosition(double pos) {
|
|
verticalScrollBar()->setValue(int(pos));
|
|
}
|
|
|
|
void TextBrowserViewer::applyFont(const QFont& fon) {
|
|
m_baseFont = fon;
|
|
setFont(fon);
|
|
setZoomFactor(zoomFactor());
|
|
}
|
|
|
|
qreal TextBrowserViewer::zoomFactor() const {
|
|
return m_zoomFactor;
|
|
}
|
|
|
|
void TextBrowserViewer::setZoomFactor(qreal zoom_factor) {
|
|
m_zoomFactor = zoom_factor;
|
|
|
|
auto fon = font();
|
|
|
|
fon.setPointSizeF(m_baseFont.pointSizeF() * zoom_factor);
|
|
|
|
setFont(fon);
|
|
}
|
|
|
|
void TextBrowserViewer::contextMenuEvent(QContextMenuEvent* event) {
|
|
event->accept();
|
|
|
|
auto* menu = createStandardContextMenu(event->pos());
|
|
|
|
if (menu == nullptr) {
|
|
return;
|
|
}
|
|
|
|
/*
|
|
connect(menu, &QMenu::aboutToHide, this, [menu] {
|
|
menu->deleteLater();
|
|
});*/
|
|
|
|
if (m_actionEnableResources.isNull()) {
|
|
m_actionEnableResources.reset(new QAction(qApp->icons()->fromTheme(QSL("viewimage"), QSL("image-x-generic")),
|
|
tr("Enable external resources"),
|
|
this));
|
|
m_actionDownloadLink.reset(new QAction(qApp->icons()->fromTheme(QSL("download")), tr("Download"), this));
|
|
|
|
m_actionEnableResources.data()->setCheckable(true);
|
|
m_actionEnableResources.data()->setChecked(resourcesEnabled());
|
|
|
|
connect(m_actionDownloadLink.data(), &QAction::triggered, this, &TextBrowserViewer::downloadLink);
|
|
connect(m_actionEnableResources.data(), &QAction::toggled, this, &TextBrowserViewer::enableResources);
|
|
}
|
|
|
|
menu->addAction(m_actionEnableResources.data());
|
|
menu->addAction(m_actionDownloadLink.data());
|
|
|
|
auto anchor = anchorAt(event->pos());
|
|
|
|
m_lastContextMenuPos = event->pos();
|
|
m_actionDownloadLink.data()->setEnabled(!anchor.isEmpty());
|
|
|
|
processContextMenu(menu, event);
|
|
|
|
menu->popup(event->globalPos());
|
|
}
|
|
|
|
void TextBrowserViewer::resizeEvent(QResizeEvent* event) {
|
|
// Notify parents about changed geometry.
|
|
updateGeometry();
|
|
QTextBrowser::resizeEvent(event);
|
|
}
|
|
|
|
void TextBrowserViewer::wheelEvent(QWheelEvent* event) {
|
|
// NOTE: Skip base class implemetation.
|
|
QAbstractScrollArea::wheelEvent(event);
|
|
updateMicroFocus();
|
|
}
|
|
|
|
void TextBrowserViewer::enableResources(bool enable) {
|
|
qApp->settings()->setValue(GROUP(Messages), Messages::ShowResourcesInArticles, enable);
|
|
setResourcesEnabled(enable);
|
|
}
|
|
|
|
void TextBrowserViewer::downloadLink() {
|
|
auto url = QUrl(anchorAt(m_lastContextMenuPos));
|
|
|
|
if (url.isValid()) {
|
|
const QUrl resolved_url = (m_currentUrl.isValid() && url.isRelative()) ? m_currentUrl.resolved(url) : url;
|
|
|
|
qApp->downloadManager()->download(resolved_url);
|
|
}
|
|
}
|
|
|
|
void TextBrowserViewer::onAnchorClicked(const QUrl& url) {
|
|
if (!url.isEmpty()) {
|
|
const QUrl resolved_url = (m_currentUrl.isValid() && url.isRelative()) ? m_currentUrl.resolved(url) : url;
|
|
const bool ctrl_pressed =
|
|
Globals::hasFlag(QGuiApplication::keyboardModifiers(), Qt::KeyboardModifier::ControlModifier);
|
|
|
|
if (ctrl_pressed) {
|
|
// Open in new tab.
|
|
qApp->mainForm()->tabWidget()->addLinkedBrowser(resolved_url);
|
|
}
|
|
else {
|
|
bool open_externally_now =
|
|
qApp->settings()->value(GROUP(Browser), SETTING(Browser::OpenLinksInExternalBrowserRightAway)).toBool();
|
|
|
|
if (open_externally_now) {
|
|
qApp->web()->openUrlInExternalBrowser(resolved_url.toString());
|
|
|
|
if (qApp->settings()
|
|
->value(GROUP(Messages), SETTING(Messages::BringAppToFrontAfterMessageOpenedExternally))
|
|
.toBool()) {
|
|
QTimer::singleShot(1000, qApp, []() {
|
|
qApp->mainForm()->display();
|
|
});
|
|
}
|
|
}
|
|
else {
|
|
setUrl(resolved_url);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void TextBrowserViewer::setHtml(const QString& html, const QUrl& base_url) {
|
|
if (m_resourcesEnabled) {
|
|
static QRegularExpression img_tag_rgx("\\<img[^\\>]*src\\s*=\\s*[\"\']([^\"\']*)[\"\'][^\\>]*\\>",
|
|
QRegularExpression::PatternOption::CaseInsensitiveOption |
|
|
QRegularExpression::PatternOption::InvertedGreedinessOption);
|
|
QRegularExpressionMatchIterator i = img_tag_rgx.globalMatch(html);
|
|
QList<QUrl> found_resources;
|
|
|
|
while (i.hasNext()) {
|
|
QRegularExpressionMatch match = i.next();
|
|
auto captured_url = QUrl(match.captured(1));
|
|
auto resolved_captured_url =
|
|
(base_url.isValid() && captured_url.isRelative()) ? base_url.resolved(captured_url) : captured_url;
|
|
|
|
if (!found_resources.contains(resolved_captured_url)) {
|
|
found_resources.append(resolved_captured_url);
|
|
}
|
|
}
|
|
|
|
auto really_needed_resources = boolinq::from(found_resources)
|
|
.where([this](const QUrl& res) {
|
|
return !m_loadedResources.contains(res);
|
|
})
|
|
.toStdList();
|
|
|
|
m_neededResources = FROM_STD_LIST(QList<QUrl>, really_needed_resources);
|
|
}
|
|
else {
|
|
m_neededResources = {};
|
|
}
|
|
|
|
setHtmlPrivate(html, base_url);
|
|
|
|
/*
|
|
QTextCursor cr(m_document.data());
|
|
|
|
cr.movePosition(QTextCursor::MoveOperation::Start);
|
|
|
|
// this can be used instead of regexps, just browse document and collect resource addresses directly
|
|
while (!cr.atEnd()) {
|
|
if (!cr.movePosition(QTextCursor::MoveOperation::NextBlock)) {
|
|
break;
|
|
}
|
|
|
|
QTextBlock::iterator it;
|
|
for (it = cr.block().begin(); !(it.atEnd()); ++it) {
|
|
QTextFragment currentFragment = it.fragment();
|
|
if (currentFragment.isValid()) {
|
|
auto aa = currentFragment.charFormat().anchorHref();
|
|
|
|
if (!aa.isEmpty()) {
|
|
auto xx = 5;
|
|
}
|
|
else if (currentFragment.charFormat().isImageFormat()) {
|
|
aa = currentFragment.charFormat().toImageFormat().name();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
|
|
if (!m_neededResources.isEmpty()) {
|
|
QTimer::singleShot(20, this, &TextBrowserViewer::reloadHtmlDelayed);
|
|
}
|
|
|
|
setVerticalScrollBarPosition(0.0);
|
|
}
|
|
|
|
void TextBrowserViewer::setReadabledHtml(const QString& html, const QUrl& base_url) {
|
|
setHtml(html, base_url);
|
|
}
|
|
|
|
void TextBrowserViewer::setHtmlPrivate(const QString& html, const QUrl& base_url) {
|
|
m_currentUrl = base_url;
|
|
m_currentHtml = html;
|
|
|
|
QTextBrowser::setHtml(html);
|
|
setZoomFactor(m_zoomFactor);
|
|
|
|
emit pageTitleChanged(documentTitle());
|
|
emit pageUrlChanged(base_url);
|
|
}
|
|
|
|
TextBrowserDocument::TextBrowserDocument(TextBrowserViewer* parent) : QTextDocument(parent) {
|
|
m_viewer = parent;
|
|
}
|
|
|
|
QVariant TextBrowserDocument::loadResource(int type, const QUrl& name) {
|
|
return m_viewer->loadOneResource(type, name);
|
|
}
|
|
|
|
void TextBrowserViewer::reloadHtmlDelayed() {
|
|
if (!m_neededResources.isEmpty()) {
|
|
downloadNextNeededResource();
|
|
}
|
|
}
|
|
|
|
void TextBrowserViewer::downloadNextNeededResource() {
|
|
if (m_neededResources.isEmpty()) {
|
|
// Everything is downloaded.
|
|
emit reloadDocument();
|
|
}
|
|
else {
|
|
QUrl res = m_neededResources.takeFirst();
|
|
|
|
QMetaObject::invokeMethod(m_resourceDownloader,
|
|
"manipulateData",
|
|
Qt::ConnectionType::QueuedConnection,
|
|
Q_ARG(QString, qApp->web()->unescapeHtml(res.toString())),
|
|
Q_ARG(QNetworkAccessManager::Operation, QNetworkAccessManager::Operation::GetOperation),
|
|
Q_ARG(QByteArray, {}),
|
|
Q_ARG(int, 5000));
|
|
|
|
/*
|
|
m_resourceDownloader.data()->manipulateData(qApp->web()->unescapeHtml(res.toString()),
|
|
QNetworkAccessManager::Operation::GetOperation,
|
|
{},
|
|
5000);
|
|
*/
|
|
}
|
|
}
|
|
|
|
void TextBrowserViewer::resourceDownloaded(const QUrl& url,
|
|
QNetworkReply::NetworkError status,
|
|
int http_code,
|
|
const QByteArray& contents) {
|
|
Q_UNUSED(http_code)
|
|
|
|
if (status == QNetworkReply::NetworkError::NoError) {
|
|
m_loadedResources.insert(url, contents);
|
|
}
|
|
else {
|
|
m_loadedResources.insert(url, {});
|
|
}
|
|
|
|
downloadNextNeededResource();
|
|
}
|
|
|
|
PreparedHtml TextBrowserViewer::prepareLegacyHtmlForMessage(const QList<Message>& messages, RootItem* selected_item) const {
|
|
PreparedHtml html;
|
|
bool acc_displays_enclosures =
|
|
selected_item == nullptr || selected_item->getParentServiceRoot()->displaysEnclosures();
|
|
|
|
for (const Message& message : messages) {
|
|
bool is_plain = !TextFactory::couldBeHtml(message.m_contents);
|
|
|
|
// Add title.
|
|
if (!message.m_url.isEmpty()) {
|
|
html.m_html += QSL("<h2 align=\"center\"><a href=\"%2\">%1</a></h2>").arg(message.m_title, message.m_url);
|
|
}
|
|
else {
|
|
html.m_html += QSL("<h2 align=\"center\">%1</h2>").arg(message.m_title);
|
|
}
|
|
|
|
// Start contents.
|
|
html.m_html += QSL("<div>");
|
|
|
|
// Add links to enclosures.
|
|
if (acc_displays_enclosures) {
|
|
for (const Enclosure& enc : message.m_enclosures) {
|
|
html.m_html += QSL("[<a href=\"%1\">%2</a>]").arg(enc.m_url, enc.m_mimeType);
|
|
}
|
|
}
|
|
|
|
// Display enclosures which are pictures if user has it enabled.
|
|
auto first_enc_break_added = false;
|
|
|
|
if (acc_displays_enclosures &&
|
|
qApp->settings()->value(GROUP(Messages), SETTING(Messages::DisplayEnclosuresInMessage)).toBool()) {
|
|
for (const Enclosure& enc : message.m_enclosures) {
|
|
if (enc.m_mimeType.startsWith(QSL("image/"))) {
|
|
if (!first_enc_break_added) {
|
|
html.m_html += QSL("<br/>");
|
|
first_enc_break_added = true;
|
|
}
|
|
|
|
html.m_html += QSL("<img src=\"%1\" /><br/>").arg(enc.m_url);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Append actual contents of article and convert to HTML if needed.
|
|
html.m_html += is_plain ? Qt::convertFromPlainText(message.m_contents, Qt::WhiteSpaceMode::WhiteSpaceNormal)
|
|
: message.m_contents;
|
|
|
|
static QRegularExpression img_tag_rgx(QSL("\\<img[^\\>]*src\\s*=\\s*[\"\']([^\"\']*)[\"\'][^\\>]*\\>"),
|
|
QRegularExpression::PatternOption::CaseInsensitiveOption |
|
|
QRegularExpression::PatternOption::InvertedGreedinessOption);
|
|
|
|
// Extract all images links from article to be appended to end of article.
|
|
QRegularExpressionMatchIterator i = img_tag_rgx.globalMatch(html.m_html);
|
|
QString pictures_html;
|
|
|
|
while (i.hasNext()) {
|
|
QRegularExpressionMatch match = i.next();
|
|
auto captured_url = match.captured(1);
|
|
|
|
pictures_html += QSL("<br/>[%1] <a href=\"%2\">%2</a>").arg(tr("image"), captured_url);
|
|
}
|
|
|
|
// Make alla images clickable as links and also resize them if user has it setup.
|
|
auto forced_img_size =
|
|
qApp->settings()->value(GROUP(Messages), SETTING(Messages::LimitArticleImagesHeight)).toInt();
|
|
|
|
// Fixup all "img" tags.
|
|
html.m_html = html.m_html.replace(img_tag_rgx,
|
|
QSL("<a href=\"\\1\"><img height=\"%1\" src=\"\\1\" /></a>")
|
|
.arg(forced_img_size <= 0 ? QString() : QString::number(forced_img_size)));
|
|
|
|
// Append generated list of images.
|
|
html.m_html += pictures_html;
|
|
}
|
|
|
|
// Close contents.
|
|
html.m_html += QSL("</div>");
|
|
|
|
QString base_url;
|
|
auto* feed = selected_item->getParentServiceRoot()
|
|
->getItemFromSubTree([messages](const RootItem* it) {
|
|
return it->kind() == RootItem::Kind::Feed && it->customId() == messages.at(0).m_feedId;
|
|
})
|
|
->toFeed();
|
|
|
|
if (feed != nullptr) {
|
|
QUrl url(NetworkFactory::sanitizeUrl(feed->source()));
|
|
|
|
if (url.isValid()) {
|
|
base_url = url.scheme() + QSL("://") + url.host();
|
|
}
|
|
}
|
|
|
|
html.m_baseUrl = base_url;
|
|
|
|
return html;
|
|
}
|
|
|
|
bool TextBrowserViewer::resourcesEnabled() const {
|
|
return m_resourcesEnabled;
|
|
}
|
|
|
|
void TextBrowserViewer::setResourcesEnabled(bool enabled) {
|
|
m_resourcesEnabled = enabled;
|
|
}
|
|
|
|
ContextMenuData TextBrowserViewer::provideContextMenuData(QContextMenuEvent* event) const {
|
|
ContextMenuData c;
|
|
|
|
QString anchor = anchorAt(event->pos());
|
|
|
|
if (!anchor.isEmpty()) {
|
|
c.m_linkUrl = anchor;
|
|
}
|
|
|
|
return c;
|
|
}
|