diff --git a/libs/ui/KisMultiFeedRSSModel.cpp b/libs/ui/KisMultiFeedRSSModel.cpp index 2fa1cdfbef..5bebd6d6e8 100644 --- a/libs/ui/KisMultiFeedRSSModel.cpp +++ b/libs/ui/KisMultiFeedRSSModel.cpp @@ -1,224 +1,226 @@ /************************************************************************** ** ** This file is part of Qt Creator ** ** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies). ** ** Contact: Nokia Corporation (qt-info@nokia.com) ** ** ** GNU Lesser General Public License Usage ** ** This file may be used under the terms of the GNU Lesser General Public ** License version 2.1 as published by the Free Software Foundation and ** appearing in the file LICENSE.LGPL included in the packaging of this file. ** Please review the following information to ensure the GNU Lesser General ** Public License version 2.1 requirements will be met: ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Nokia gives you certain additional ** rights. These rights are described in the Nokia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** Other Usage ** ** Alternatively, this file may be used in accordance with the terms and ** conditions contained in a signed written agreement between you and Nokia. ** ** If you have questions regarding the use of this file, please contact ** Nokia at qt-info@nokia.com. ** **************************************************************************/ #include "KisMultiFeedRSSModel.h" #include #include #include #include #include #include #include #include QString shortenHtml(QString html) { html.replace(QLatin1String("")); uint firstParaEndHtml = (uint) html.indexOf(QLatin1String("

"), html.indexOf(QLatin1String("

"))+1); uint firstParaEndBr = (uint) html.indexOf(QLatin1String("request().url(); requestUrl = source.toString(); streamReader.setDevice(reply); RssItemList list; while (!streamReader.atEnd()) { switch (streamReader.readNext()) { case QXmlStreamReader::StartElement: if (streamReader.name() == QLatin1String("item")) list.append(parseItem()); else if (streamReader.name() == QLatin1String("title")) blogName = streamReader.readElementText(); else if (streamReader.name() == QLatin1String("link")) { if (!streamReader.namespaceUri().isEmpty()) break; QString favIconString(streamReader.readElementText()); QUrl favIconUrl(favIconString); favIconUrl.setPath(QLatin1String("favicon.ico")); blogIcon = favIconUrl.toString(); blogIcon = QString(); // XXX: fix the favicon on krita.org! } break; default: break; } } return list; } private: QXmlStreamReader streamReader; QString requestUrl; QString blogIcon; QString blogName; }; MultiFeedRssModel::MultiFeedRssModel(QObject *parent) : QAbstractListModel(parent), m_networkAccessManager(new KisNetworkAccessManager), m_articleCount(0) { connect(m_networkAccessManager, SIGNAL(finished(QNetworkReply*)), SLOT(appendFeedData(QNetworkReply*)), Qt::QueuedConnection); } MultiFeedRssModel::~MultiFeedRssModel() { } QHash MultiFeedRssModel::roleNames() const { QHash roleNames; roleNames[TitleRole] = "title"; roleNames[DescriptionRole] = "description"; roleNames[PubDateRole] = "pubDate"; roleNames[LinkRole] = "link"; roleNames[CategoryRole] = "category"; roleNames[BlogNameRole] = "blogName"; roleNames[BlogIconRole] = "blogIcon"; return roleNames; } void MultiFeedRssModel::addFeed(const QString& feed) { const QUrl feedUrl(feed); QMetaObject::invokeMethod(m_networkAccessManager, "getUrl", Qt::QueuedConnection, Q_ARG(QUrl, feedUrl)); } bool sortForPubDate(const RssItem& item1, const RssItem& item2) { return item1.pubDate > item2.pubDate; } void MultiFeedRssModel::appendFeedData(QNetworkReply *reply) { RssReader reader; m_aggregatedFeed.append(reader.parse(reply)); std::sort(m_aggregatedFeed.begin(), m_aggregatedFeed.end(), sortForPubDate); setArticleCount(m_aggregatedFeed.size()); beginResetModel(); endResetModel(); + + emit feedDataChanged(); } void MultiFeedRssModel::removeFeed(const QString &feed) { QMutableListIterator it(m_aggregatedFeed); while (it.hasNext()) { RssItem item = it.next(); if (item.source == feed) it.remove(); } setArticleCount(m_aggregatedFeed.size()); } int MultiFeedRssModel::rowCount(const QModelIndex &) const { return m_aggregatedFeed.size(); } QVariant MultiFeedRssModel::data(const QModelIndex &index, int role) const { RssItem item = m_aggregatedFeed.at(index.row()); switch (role) { case Qt::DisplayRole: { return QString("" + item.title + "" "
(" + item.pubDate.toLocalTime().toString(Qt::DefaultLocaleShortDate) + ") " + item.description.left(90).append("...") + "


"); } case TitleRole: return item.title; case DescriptionRole: return item.description; case PubDateRole: return item.pubDate.toString("dd-MM-yyyy hh:mm"); case LinkRole: return item.link; case CategoryRole: return item.category; case BlogNameRole: return item.blogName; case BlogIconRole: return item.blogIcon; } return QVariant(); } diff --git a/libs/ui/KisMultiFeedRSSModel.h b/libs/ui/KisMultiFeedRSSModel.h index f9cd51d109..7575013244 100644 --- a/libs/ui/KisMultiFeedRSSModel.h +++ b/libs/ui/KisMultiFeedRSSModel.h @@ -1,107 +1,108 @@ /************************************************************************** ** ** This file is part of Qt Creator ** ** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies). ** ** Contact: Nokia Corporation (qt-info@nokia.com) ** ** ** GNU Lesser General Public License Usage ** ** This file may be used under the terms of the GNU Lesser General Public ** License version 2.1 as published by the Free Software Foundation and ** appearing in the file LICENSE.LGPL included in the packaging of this file. ** Please review the following information to ensure the GNU Lesser General ** Public License version 2.1 requirements will be met: ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Nokia gives you certain additional ** rights. These rights are described in the Nokia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** Other Usage ** ** Alternatively, this file may be used in accordance with the terms and ** conditions contained in a signed written agreement between you and Nokia. ** ** If you have questions regarding the use of this file, please contact ** Nokia at qt-info@nokia.com. ** **************************************************************************/ #ifndef MULTIFEEDRSSMODEL_H #define MULTIFEEDRSSMODEL_H #include #include #include #include class QThread; class QNetworkReply; class QNetworkAccessManager; struct RssItem { QString source; QString title; QString link; QString description; QString category; QString blogName; QString blogIcon; QDateTime pubDate; }; typedef QList RssItemList; class KisNetworkAccessManager; enum RssRoles { TitleRole = Qt::UserRole + 1, DescriptionRole, LinkRole, PubDateRole, CategoryRole, BlogNameRole, BlogIconRole }; class KRITAUI_EXPORT MultiFeedRssModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(int articleCount READ articleCount WRITE setArticleCount NOTIFY articleCountChanged) public: explicit MultiFeedRssModel(QObject *parent = 0); ~MultiFeedRssModel() override; QHash roleNames() const override; void addFeed(const QString& feed); void removeFeed(const QString& feed); int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; int articleCount() const { return m_articleCount; } public Q_SLOTS: void setArticleCount(int arg) { if (m_articleCount != arg) { m_articleCount = arg; emit articleCountChanged(arg); } } Q_SIGNALS: void articleCountChanged(int arg); + void feedDataChanged(); private Q_SLOTS: void appendFeedData(QNetworkReply *reply); private: QStringList m_sites; RssItemList m_aggregatedFeed; QNetworkAccessManager *m_networkAccessManager; QThread *m_namThread; int m_articleCount; }; #endif // MULTIFEEDRSSMODEL_H diff --git a/libs/ui/widgets/KisNewsWidget.cpp b/libs/ui/widgets/KisNewsWidget.cpp index 85aeecb896..61926b3f0f 100644 --- a/libs/ui/widgets/KisNewsWidget.cpp +++ b/libs/ui/widgets/KisNewsWidget.cpp @@ -1,132 +1,218 @@ /* * Copyright (c) 2018 boud * * 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. */ #include "KisNewsWidget.h" #include #include #include #include #include #include #include #include "kis_config.h" #include "KisMultiFeedRSSModel.h" +#include "QRegularExpression" KisNewsDelegate::KisNewsDelegate(QObject *parent) : QStyledItemDelegate(parent) { } void KisNewsDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { painter->save(); QStyleOptionViewItem optionCopy = option; initStyleOption(&optionCopy, index); QStyle *style = optionCopy.widget? optionCopy.widget->style() : QApplication::style(); QTextDocument doc; doc.setHtml(optionCopy.text); doc.setDocumentMargin(10); /// Painting item without text optionCopy.text = QString(); style->drawControl(QStyle::CE_ItemViewItem, &optionCopy, painter); QAbstractTextDocumentLayout::PaintContext ctx; // Highlighting text if item is selected if (optionCopy.state & QStyle::State_Selected) { ctx.palette.setColor(QPalette::Text, optionCopy.palette.color(QPalette::Active, QPalette::HighlightedText)); } painter->translate(optionCopy.rect.left(), optionCopy.rect.top()); QRect clip(0, 0, optionCopy.rect.width(), optionCopy.rect.height()); doc.setPageSize(clip.size()); doc.drawContents(painter, clip); painter->restore(); } QSize KisNewsDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { QStyleOptionViewItem optionCopy = option; initStyleOption(&optionCopy, index); QTextDocument doc; doc.setHtml(optionCopy.text); doc.setTextWidth(optionCopy.rect.width()); return QSize(doc.idealWidth(), doc.size().height()); } KisNewsWidget::KisNewsWidget(QWidget *parent) : QWidget(parent) { setupUi(this); m_rssModel = new MultiFeedRssModel(this); + connect(m_rssModel, SIGNAL(feedDataChanged()), this, SLOT(rssDataChanged())); + setCursor(Qt::PointingHandCursor); listNews->setModel(m_rssModel); listNews->setItemDelegate(new KisNewsDelegate(listNews)); connect(listNews, SIGNAL(clicked(QModelIndex)), this, SLOT(itemSelected(QModelIndex))); } void KisNewsWidget::setAnalyticsTracking(QString text) { analyticsTrackingParameters = text; } void KisNewsWidget::toggleNews(bool toggle) { KisConfig cfg(false); cfg.writeEntry("FetchNews", toggle); if (toggle) { m_rssModel->addFeed(QLatin1String("https://krita.org/en/feed/")); } else { m_rssModel->removeFeed(QLatin1String("https://krita.org/en/feed/")); } } void KisNewsWidget::itemSelected(const QModelIndex &idx) { if (idx.isValid()) { QString link = idx.data(RssRoles::LinkRole).toString(); // append query string for analytics tracking if we set it if (analyticsTrackingParameters != "") { // use title in analytics query string QString linkTitle = idx.data(RssRoles::TitleRole).toString(); linkTitle = linkTitle.simplified(); // trims and makes 1 white space linkTitle = linkTitle.replace(" ", ""); analyticsTrackingParameters = analyticsTrackingParameters.append(linkTitle); QDesktopServices::openUrl(QUrl(link.append(analyticsTrackingParameters))); } else { QDesktopServices::openUrl(QUrl(link)); } } } + +void KisNewsWidget::rssDataChanged() +{ + + // grab the latest release post and URL for reference later + // if we need to update + for (int i = 0; i < m_rssModel->rowCount(); i++) + { + const QModelIndex &idx = m_rssModel->index(i); + + if (idx.isValid()) { + + // only use official release announcements to get version number + if ( idx.data(RssRoles::CategoryRole).toString() != "Official Release") { + continue; + } + + QString linkTitle = idx.data(RssRoles::TitleRole).toString(); + + // come up with a regex pattern to find version number + QRegularExpression versionRegex("\\d\\.\\d\\.?\\d?\\.?\\d"); + + QRegularExpressionMatch matched = versionRegex.match(linkTitle); + + // only take the top match for release version since that is the newest + if (matched.hasMatch()) { + newVersionNumber = matched.captured(0); + newVersionLink = idx.data(RssRoles::LinkRole).toString(); + break; + } + + } + } + + // see if we need to update our version, or we are on a dev version + calculateVersionUpdateStatus(); +} + +void KisNewsWidget::calculateVersionUpdateStatus() +{ + // do version compare to see if there is a new version available + // also check to see if we are on a dev version (newer than newest release) + QStringList currentVersionParts = qApp->applicationVersion().split("."); + QStringList onlineReleaseAnnouncement = newVersionNumber.split("."); + + // is the major version different? + if (onlineReleaseAnnouncement[0] > currentVersionParts[0] ) { + needsVersionUpdate = true; // we are a major version behind + return; + } + else if (onlineReleaseAnnouncement[0] < currentVersionParts[0] ) { + isDevelopmentVersion = true; + return; + } + + // major versions are the same, so check minor versions + if (onlineReleaseAnnouncement[1] > currentVersionParts[1] ) { + needsVersionUpdate = true; // we are a minor version behind + return; + } + else if (onlineReleaseAnnouncement[1] < currentVersionParts[1] ) { + isDevelopmentVersion = true; + return; + } + + // minor versions are the same, so maybe bugfix version is different + // sometimes we don't communicate this, implictly make 0 if it doesn't exist + if (onlineReleaseAnnouncement[2].isNull()) { + onlineReleaseAnnouncement[2] = "0"; + } + if (currentVersionParts[2].isNull()) { + currentVersionParts[2] = "0"; + } + + if (onlineReleaseAnnouncement[2] > currentVersionParts[2] ) { + needsVersionUpdate = true; // we are a bugfix version behind + return; + } + else if (onlineReleaseAnnouncement[2] < currentVersionParts[2] ) { + isDevelopmentVersion = true; + return; + } +} diff --git a/libs/ui/widgets/KisNewsWidget.h b/libs/ui/widgets/KisNewsWidget.h index 7a74470b19..5a1e9a01eb 100644 --- a/libs/ui/widgets/KisNewsWidget.h +++ b/libs/ui/widgets/KisNewsWidget.h @@ -1,58 +1,69 @@ /* * Copyright (c) 2018 boud * * 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. */ #ifndef KISNEWSWIDGET_H #define KISNEWSWIDGET_H #include #include #include #include class MultiFeedRssModel; class KisNewsDelegate : public QStyledItemDelegate { Q_OBJECT public: KisNewsDelegate(QObject *parent = 0); void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; }; /** * @brief The KisNewsWidget class shows the latest news from Krita.org */ class KisNewsWidget : public QWidget, public Ui::KisNewsPage { Q_OBJECT public: explicit KisNewsWidget(QWidget *parent = nullptr); void setAnalyticsTracking(QString text); private Q_SLOTS: void toggleNews(bool toggle); void itemSelected(const QModelIndex &idx); + void rssDataChanged(); private: bool m_getNews {false}; MultiFeedRssModel *m_rssModel {0}; QString analyticsTrackingParameters; + + /// for new Krita version notification + QString newVersionNumber; + QString newVersionLink; + + // version checking logic tells us we need to update our Krita + void calculateVersionUpdateStatus(); + bool isDevelopmentVersion = false; + bool needsVersionUpdate = false; + }; #endif // KISNEWSWIDGET_H