diff options
-rw-r--r-- | CMakeLists.txt | 15 | ||||
-rw-r--r-- | MultiMC.cpp | 5 | ||||
-rw-r--r-- | MultiMC.h | 7 | ||||
-rw-r--r-- | config.h.in | 4 | ||||
-rw-r--r-- | gui/MainWindow.cpp | 42 | ||||
-rw-r--r-- | gui/MainWindow.h | 6 | ||||
-rw-r--r-- | gui/MainWindow.ui | 44 | ||||
-rw-r--r-- | logic/news/NewsChecker.cpp | 135 | ||||
-rw-r--r-- | logic/news/NewsChecker.h | 105 | ||||
-rw-r--r-- | logic/news/NewsEntry.cpp | 77 | ||||
-rw-r--r-- | logic/news/NewsEntry.h | 65 |
11 files changed, 495 insertions, 10 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index ab04c280..9ccd513d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -120,6 +120,11 @@ IF(${BIGENDIAN}) ENDIF(${BIGENDIAN}) +######## Set URLs ######## + +SET(MultiMC_NEWS_RSS_URL "http://multimc.org/rss.xml" CACHE STRING "URL to fetch MultiMC's news RSS feed from.") + + ######## Set version numbers ######## SET(MultiMC_VERSION_MAJOR 1) SET(MultiMC_VERSION_MINOR 0) @@ -333,6 +338,12 @@ logic/updater/UpdateChecker.cpp logic/updater/DownloadUpdateTask.h logic/updater/DownloadUpdateTask.cpp +# News System +logic/news/NewsChecker.h +logic/news/NewsChecker.cpp +logic/news/NewsEntry.h +logic/news/NewsEntry.cpp + # legacy instances logic/LegacyInstance.h logic/LegacyInstance.cpp @@ -516,8 +527,8 @@ ADD_EXECUTABLE(MultiMC MACOSX_BUNDLE WIN32 main.cpp ${MULTIMC_RCS}) # Link TARGET_LINK_LIBRARIES(MultiMC MultiMC_common) TARGET_LINK_LIBRARIES(MultiMC_common xz-embedded unpack200 quazip libUtil libSettings libGroupView ${MultiMC_LINK_ADDITIONAL_LIBS}) -QT5_USE_MODULES(MultiMC Core Widgets Network Xml Concurrent ${MultiMC_QT_ADDITIONAL_MODULES}) -QT5_USE_MODULES(MultiMC_common Core Widgets Network Xml Concurrent ${MultiMC_QT_ADDITIONAL_MODULES}) +QT5_USE_MODULES(MultiMC Core Widgets Network Xml WebKit Concurrent ${MultiMC_QT_ADDITIONAL_MODULES}) +QT5_USE_MODULES(MultiMC_common Core Widgets Network Xml WebKit Concurrent ${MultiMC_QT_ADDITIONAL_MODULES}) ADD_DEPENDENCIES(MultiMC_common MultiMCLauncher JavaCheck) ################################ INSTALLATION AND PACKAGING ################################ diff --git a/MultiMC.cpp b/MultiMC.cpp index 93524af7..4e06f558 100644 --- a/MultiMC.cpp +++ b/MultiMC.cpp @@ -18,6 +18,8 @@ #include "logic/lists/MinecraftVersionList.h" #include "logic/lists/ForgeVersionList.h" +#include "logic/news/NewsChecker.h" + #include "logic/InstanceLauncher.h" #include "logic/net/HttpMetaCache.h" @@ -162,6 +164,9 @@ MultiMC::MultiMC(int &argc, char **argv, const QString &root) // initialize the updater m_updateChecker.reset(new UpdateChecker()); + // initialize the news checker + m_newsChecker.reset(new NewsChecker(NEWS_RSS_URL)); + // and instances auto InstDirSetting = m_settings->getSetting("InstanceDir"); m_instances.reset(new InstanceList(InstDirSetting->get().toString(), this)); @@ -17,6 +17,7 @@ class QNetworkAccessManager; class ForgeVersionList; class JavaVersionList; class UpdateChecker; +class NewsChecker; #if defined(MMC) #undef MMC @@ -89,6 +90,11 @@ public: return m_updateChecker; } + std::shared_ptr<NewsChecker> newsChecker() + { + return m_newsChecker; + } + std::shared_ptr<LWJGLVersionList> lwjgllist(); std::shared_ptr<ForgeVersionList> forgelist(); @@ -137,6 +143,7 @@ private: std::shared_ptr<SettingsObject> m_settings; std::shared_ptr<InstanceList> m_instances; std::shared_ptr<UpdateChecker> m_updateChecker; + std::shared_ptr<NewsChecker> m_newsChecker; std::shared_ptr<MojangAccountList> m_accounts; std::shared_ptr<IconList> m_icons; std::shared_ptr<QNetworkAccessManager> m_qnam; diff --git a/config.h.in b/config.h.in index b58dc322..aa604056 100644 --- a/config.h.in +++ b/config.h.in @@ -16,3 +16,7 @@ // This is printed on start to standard output #define VERSION_STR "@MultiMC_VERSION_STRING@" +// This is used to fetch the news RSS feed. +// It defaults in CMakeLists.txt to "http://multimc.org/rss.xml" +#define NEWS_RSS_URL "@MultiMC_NEWS_RSS_URL@" + diff --git a/gui/MainWindow.cpp b/gui/MainWindow.cpp index b55be903..42823fa5 100644 --- a/gui/MainWindow.cpp +++ b/gui/MainWindow.cpp @@ -75,6 +75,8 @@ #include "logic/updater/DownloadUpdateTask.h" +#include "logic/news/NewsChecker.h" + #include "logic/net/URLConstants.h" #include "logic/BaseInstance.h" @@ -116,6 +118,17 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi renameButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); } + // Add the news label to the news toolbar. + { + newsLabel = new QToolButton(); + newsLabel->setIcon(QIcon(":/icons/toolbar/news")); + newsLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + newsLabel->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + ui->newsToolBar->insertWidget(ui->actionMoreNews, newsLabel); + QObject::connect(MMC->newsChecker().get(), &NewsChecker::newsLoaded, this, &MainWindow::updateNewsLabel); + updateNewsLabel(); + } + // Create the instance list widget { view = new KCategorizedView(ui->centralWidget); @@ -252,6 +265,9 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi MMC->lwjgllist()->loadList(); } + MMC->newsChecker()->reloadNews(); + updateNewsLabel(); + // set up the updater object. auto updater = MMC->updateChecker(); connect(updater.get(), &UpdateChecker::updateAvailable, this, @@ -439,6 +455,30 @@ bool MainWindow::eventFilter(QObject *obj, QEvent *ev) return QMainWindow::eventFilter(obj, ev); } +void MainWindow::updateNewsLabel() +{ + auto newsChecker = MMC->newsChecker(); + if (newsChecker->isLoadingNews()) + { + newsLabel->setText(tr("Loading news...")); + newsLabel->setEnabled(false); + } + else + { + QList<NewsEntryPtr> entries = newsChecker->getNewsEntries(); + if (entries.length() > 0) + { + newsLabel->setText(entries[0]->title); + newsLabel->setEnabled(true); + } + else + { + newsLabel->setText(tr("No news available.")); + newsLabel->setEnabled(false); + } + } +} + void MainWindow::updateAvailable(QString repo, QString versionName, int versionId) { UpdateDialog dlg; @@ -729,7 +769,7 @@ void MainWindow::on_actionReportBug_triggered() openWebPage(QUrl("http://multimc.myjetbrains.com/youtrack/dashboard#newissue=yes")); } -void MainWindow::on_actionNews_triggered() +void MainWindow::on_actionMoreNews_triggered() { openWebPage(QUrl("http://multimc.org/posts.html")); } diff --git a/gui/MainWindow.h b/gui/MainWindow.h index d6b7b845..f5b25006 100644 --- a/gui/MainWindow.h +++ b/gui/MainWindow.h @@ -85,7 +85,7 @@ slots: void on_actionReportBug_triggered(); - void on_actionNews_triggered(); + void on_actionMoreNews_triggered(); void on_mainToolBar_visibilityChanged(bool); @@ -166,6 +166,8 @@ slots: void changeActiveAccount(); void repopulateAccountsMenu(); + + void updateNewsLabel(); /*! * Runs the DownloadUpdateTask and installs updates. @@ -185,6 +187,8 @@ private: MinecraftProcess *proc; ConsoleWindow *console; LabeledToolButton *renameButton; + QToolButton *changeIconButton; + QToolButton* newsLabel; BaseInstance *m_selectedInstance; QString m_currentInstIcon; diff --git a/gui/MainWindow.ui b/gui/MainWindow.ui index 82e3b05f..1a3f53cd 100644 --- a/gui/MainWindow.ui +++ b/gui/MainWindow.ui @@ -7,7 +7,7 @@ <x>0</x> <y>0</y> <width>688</width> - <height>650</height> + <height>460</height> </rect> </property> <property name="windowTitle"> @@ -72,7 +72,6 @@ <addaction name="actionSettings"/> <addaction name="separator"/> <addaction name="actionReportBug"/> - <addaction name="actionNews"/> <addaction name="actionAbout"/> <addaction name="separator"/> <addaction name="actionCAT"/> @@ -121,6 +120,36 @@ <addaction name="separator"/> <addaction name="actionDeleteInstance"/> </widget> + <widget class="QToolBar" name="newsToolBar"> + <property name="windowTitle"> + <string>News Toolbar</string> + </property> + <property name="movable"> + <bool>false</bool> + </property> + <property name="allowedAreas"> + <set>Qt::BottomToolBarArea</set> + </property> + <property name="iconSize"> + <size> + <width>16</width> + <height>16</height> + </size> + </property> + <property name="toolButtonStyle"> + <enum>Qt::ToolButtonTextBesideIcon</enum> + </property> + <property name="floatable"> + <bool>false</bool> + </property> + <attribute name="toolBarArea"> + <enum>BottomToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak"> + <bool>false</bool> + </attribute> + <addaction name="actionMoreNews"/> + </widget> <action name="actionAddInstance"> <property name="icon"> <iconset resource="../graphics.qrc"> @@ -229,19 +258,22 @@ <string>Open the bug tracker to report a bug with MultiMC.</string> </property> </action> - <action name="actionNews"> + <action name="actionMoreNews"> <property name="icon"> <iconset resource="../graphics.qrc"> <normaloff>:/icons/toolbar/news</normaloff>:/icons/toolbar/news</iconset> </property> <property name="text"> - <string>News</string> + <string>More News</string> + </property> + <property name="iconText"> + <string>More...</string> </property> <property name="toolTip"> - <string>Open the MultiMC dev blog to read news about MultiMC.</string> + <string>Open the MultiMC development blog to read more news about MultiMC.</string> </property> <property name="statusTip"> - <string>Open the MultiMC dev blog to read news about MultiMC.</string> + <string>Open the MultiMC development blog to read more news about MultiMC.</string> </property> </action> <action name="actionAbout"> diff --git a/logic/news/NewsChecker.cpp b/logic/news/NewsChecker.cpp new file mode 100644 index 00000000..8fc44fa9 --- /dev/null +++ b/logic/news/NewsChecker.cpp @@ -0,0 +1,135 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "NewsChecker.h" + +#include <QByteArray> +#include <QDomDocument> + +#include <logger/QsLog.h> + +NewsChecker::NewsChecker(const QString& feedUrl) +{ + m_feedUrl = feedUrl; +} + +void NewsChecker::reloadNews() +{ + // Start a netjob to download the RSS feed and call rssDownloadFinished() when it's done. + if (isLoadingNews()) + { + QLOG_INFO() << "Ignored request to reload news. Currently reloading already."; + return; + } + + QLOG_INFO() << "Reloading news."; + + NetJob* job = new NetJob("News RSS Feed"); + job->addNetAction(ByteArrayDownload::make(m_feedUrl)); + QObject::connect(job, &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished); + QObject::connect(job, &NetJob::failed, this, &NewsChecker::rssDownloadFailed); + m_newsNetJob.reset(job); + job->start(); +} + +void NewsChecker::rssDownloadFinished() +{ + // Parse the XML file and process the RSS feed entries. + QLOG_DEBUG() << "Finished loading RSS feed."; + + QByteArray data; + { + ByteArrayDownloadPtr dl = std::dynamic_pointer_cast<ByteArrayDownload>(m_newsNetJob->first()); + data = dl->m_data; + m_newsNetJob.reset(); + } + + QDomDocument doc; + { + // Stuff to store error info in. + QString errorMsg = "Unknown error."; + int errorLine = -1; + int errorCol = -1; + + // Parse the XML. + if (!doc.setContent(data, false, &errorMsg, &errorLine, &errorCol)) + { + QString fullErrorMsg = QString("Error parsing RSS feed XML. %s at %d:%d.").arg(errorMsg, errorLine, errorCol); + fail(fullErrorMsg); + return; + } + } + + // If the parsing succeeded, read it. + QDomNodeList items = doc.elementsByTagName("item"); + m_newsEntries.clear(); + for (int i = 0; i < items.length(); i++) + { + QDomElement element = items.at(i).toElement(); + NewsEntryPtr entry; + entry.reset(new NewsEntry()); + QString errorMsg = "An unknown error occurred."; + if (NewsEntry::fromXmlElement(element, entry.get(), &errorMsg)) + { + QLOG_DEBUG() << "Loaded news entry" << entry->title; + m_newsEntries.append(entry); + } + else + { + QLOG_WARN() << "Failed to load news entry at index" << i << ":" << errorMsg; + } + } + + succeed(); +} + +void NewsChecker::rssDownloadFailed() +{ + // Set an error message and fail. + fail("Failed to load news RSS feed."); +} + + +QList<NewsEntryPtr> NewsChecker::getNewsEntries() const +{ + return m_newsEntries; +} + +bool NewsChecker::isLoadingNews() const +{ + return m_newsNetJob.get() != nullptr; +} + +QString NewsChecker::getLastLoadErrorMsg() const +{ + return m_lastLoadError; +} + +void NewsChecker::succeed() +{ + m_lastLoadError = ""; + QLOG_DEBUG() << "News loading succeeded."; + m_newsNetJob.reset(); + emit newsLoaded(); +} + +void NewsChecker::fail(const QString& errorMsg) +{ + m_lastLoadError = errorMsg; + QLOG_DEBUG() << "Failed to load news:" << errorMsg; + m_newsNetJob.reset(); + emit newsLoadingFailed(errorMsg); +} + diff --git a/logic/news/NewsChecker.h b/logic/news/NewsChecker.h new file mode 100644 index 00000000..820fe626 --- /dev/null +++ b/logic/news/NewsChecker.h @@ -0,0 +1,105 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QObject> +#include <QString> +#include <QList> + +#include <logic/net/NetJob.h> + +#include "NewsEntry.h" + +class NewsChecker : public QObject +{ + Q_OBJECT +public: + /*! + * Constructs a news reader to read from the given RSS feed URL. + */ + NewsChecker(const QString& feedUrl); + + /*! + * Returns the error message for the last time the news was loaded. + * Empty string if the last load was successful. + */ + QString getLastLoadErrorMsg() const; + + /*! + * Returns true if the news has been loaded successfully. + */ + bool isNewsLoaded() const; + + //! True if the news is currently loading. If true, reloadNews() will do nothing. + bool isLoadingNews() const; + + /*! + * Returns a list of news entries. + */ + QList<NewsEntryPtr> getNewsEntries() const; + + /*! + * Reloads the news from the website's RSS feed. + * If the news is already loading, this does nothing. + */ + void Q_SLOT reloadNews(); + +signals: + /*! + * Signal fired after the news has finished loading. + */ + void newsLoaded(); + + /*! + * Signal fired after the news fails to load. + */ + void newsLoadingFailed(QString errorMsg); + +protected slots: + void rssDownloadFinished(); + void rssDownloadFailed(); + +protected: + //! The URL for the RSS feed to fetch. + QString m_feedUrl; + + //! List of news entries. + QList<NewsEntryPtr> m_newsEntries; + + //! The network job to use to load the news. + NetJobPtr m_newsNetJob; + + //! True if news has been loaded. + bool m_loadedNews; + + /*! + * Gets the error message that was given last time the news was loaded. + * If the last news load succeeded, this will be an empty string. + */ + QString m_lastLoadError; + + + /*! + * Emits newsLoaded() and sets m_lastLoadError to empty string. + */ + void Q_SLOT succeed(); + + /*! + * Emits newsLoadingFailed() and sets m_lastLoadError to the given message. + */ + void Q_SLOT fail(const QString& errorMsg); +}; + diff --git a/logic/news/NewsEntry.cpp b/logic/news/NewsEntry.cpp new file mode 100644 index 00000000..4c940f2e --- /dev/null +++ b/logic/news/NewsEntry.cpp @@ -0,0 +1,77 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "NewsEntry.h" + +#include <QDomNodeList> +#include <QVariant> + +NewsEntry::NewsEntry(QObject* parent) : + QObject(parent) +{ + this->title = tr("Untitled"); + this->content = tr("No content."); + this->link = ""; + this->author = tr("Unknown Author"); + this->pubDate = QDateTime::currentDateTime(); +} + +NewsEntry::NewsEntry(const QString& title, const QString& content, const QString& link, const QString& author, const QDateTime& pubDate, QObject* parent) : + QObject(parent) +{ + this->title = title; + this->content = content; + this->link = link; + this->author = author; + this->pubDate = pubDate; +} + +/*! + * Gets the text content of the given child element as a QVariant. + */ +inline QString childValue(const QDomElement& element, const QString& childName, QString defaultVal="") +{ + QDomNodeList nodes = element.elementsByTagName(childName); + if (nodes.count() > 0) + { + QDomElement element = nodes.at(0).toElement(); + return element.text(); + } + else + { + return defaultVal; + } +} + +bool NewsEntry::fromXmlElement(const QDomElement& element, NewsEntry* entry, QString* errorMsg) +{ + QString title = childValue(element, "title", tr("Untitled")); + QString content = childValue(element, "description", tr("No content.")); + QString link = childValue(element, "link"); + QString author = childValue(element, "dc:creator", tr("Unknown Author")); + QString pubDateStr = childValue(element, "pubDate"); + + // FIXME: For now, we're just ignoring timezones. We assume that all time zones in the RSS feed are the same. + QString dateFormat("ddd, dd MMM yyyy hh:mm:ss"); + QDateTime pubDate = QDateTime::fromString(pubDateStr, dateFormat); + + entry->title = title; + entry->content = content; + entry->link = link; + entry->author = author; + entry->pubDate = pubDate; + return true; +} + diff --git a/logic/news/NewsEntry.h b/logic/news/NewsEntry.h new file mode 100644 index 00000000..6bfa1adc --- /dev/null +++ b/logic/news/NewsEntry.h @@ -0,0 +1,65 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QObject> +#include <QString> +#include <QDomElement> +#include <QDateTime> + +#include <memory> + +class NewsEntry : public QObject +{ + Q_OBJECT + +public: + /*! + * Constructs an empty news entry. + */ + explicit NewsEntry(QObject* parent=0); + + /*! + * Constructs a new news entry. + * Note that content may contain HTML. + */ + NewsEntry(const QString& title, const QString& content, const QString& link, const QString& author, const QDateTime& pubDate, QObject* parent=0); + + /*! + * Attempts to load information from the given XML element into the given news entry pointer. + * If this fails, the function will return false and store an error message in the errorMsg pointer. + */ + static bool fromXmlElement(const QDomElement& element, NewsEntry* entry, QString* errorMsg=0); + + + //! The post title. + QString title; + + //! The post's content. May contain HTML. + QString content; + + //! URL to the post. + QString link; + + //! The post's author. + QString author; + + //! The date and time that this post was published. + QDateTime pubDate; +}; + +typedef std::shared_ptr<NewsEntry> NewsEntryPtr; + |