diff --git a/app/app.pro b/app/app.pro index 26098ae3..43f2d41a 100644 --- a/app/app.pro +++ b/app/app.pro @@ -82,7 +82,8 @@ SOURCES += \ streaming/audio.cpp \ gui/computermodel.cpp \ gui/appmodel.cpp \ - streaming/streamutils.cpp + streaming/streamutils.cpp \ + backend/autoupdatechecker.cpp HEADERS += \ utils.h \ @@ -97,7 +98,8 @@ HEADERS += \ gui/computermodel.h \ gui/appmodel.h \ streaming/video/decoder.h \ - streaming/streamutils.h + streaming/streamutils.h \ + backend/autoupdatechecker.h # Platform-specific renderers and decoders ffmpeg { @@ -203,3 +205,4 @@ macx { } VERSION = 0.1.0 +DEFINES += VERSION_STR=\\\"0.1.0\\\" diff --git a/app/backend/autoupdatechecker.cpp b/app/backend/autoupdatechecker.cpp new file mode 100644 index 00000000..7eb80710 --- /dev/null +++ b/app/backend/autoupdatechecker.cpp @@ -0,0 +1,142 @@ +#include "autoupdatechecker.h" + +#include +#include +#include +#include + +AutoUpdateChecker::AutoUpdateChecker(QObject *parent) : + QObject(parent), + m_Nam(this) +{ + // Never communicate over HTTP + m_Nam.setStrictTransportSecurityEnabled(true); + + connect(&m_Nam, SIGNAL(finished(QNetworkReply*)), + this, SLOT(handleUpdateCheckRequestFinished(QNetworkReply*))); + + QString currentVersion(VERSION_STR); + qDebug() << "Current Moonlight version:" << currentVersion; + parseStringToVersionQuad(currentVersion, m_CurrentVersionQuad); + + // Should at least have a 1.0-style version number + Q_ASSERT(m_CurrentVersionQuad.count() > 1); +} + +void AutoUpdateChecker::start() +{ +#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) // Only run update checker on platforms without auto-update + // We'll get a callback when this is finished + QUrl url("https://moonlight-stream.com/updates/qt.json"); + m_Nam.get(QNetworkRequest(url)); +#endif +} + +void AutoUpdateChecker::parseStringToVersionQuad(QString& string, QVector& version) +{ + QStringList list = string.split('.'); + for (const QString& component : list) { + version.append(component.toInt()); + } +} + +void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply) +{ + Q_ASSERT(reply->isFinished()); + + if (reply->error() == QNetworkReply::NoError) { + QTextStream stream(reply); + stream.setCodec("UTF-8"); + + // Read all data and queue the reply for deletion + QString jsonString = stream.readAll(); + reply->deleteLater(); + + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonString.toUtf8(), &error); + if (jsonDoc.isNull()) { + qWarning() << "Update manifest malformed:" << error.errorString(); + return; + } + + QJsonArray array = jsonDoc.array(); + if (array.isEmpty()) { + qWarning() << "Update manifest doesn't contain an array"; + return; + } + + for (QJsonValueRef updateEntry : array) { + if (updateEntry.isObject()) { + QJsonObject updateObj = updateEntry.toObject(); + if (!updateObj.contains("platform") || + !updateObj.contains("arch") || + !updateObj.contains("version") || + !updateObj.contains("browser_url")) { + qWarning() << "Update manifest entry missing vital field"; + continue; + } + + if (!updateObj["platform"].isString() || + !updateObj["arch"].isString() || + !updateObj["version"].isString() || + !updateObj["browser_url"].isString()) { + qWarning() << "Update manifest entry has unexpected vital field type"; + continue; + } + + if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() && + updateObj["platform"] == QSysInfo::productType()) { + qDebug() << "Found update manifest match for current platform"; + + QString latestVersion = updateObj["version"].toString(); + qDebug() << "Latest version of Moonlight for this platform is:" << latestVersion; + + QVector latestVersionQuad; + + parseStringToVersionQuad(latestVersion, latestVersionQuad); + + for (int i = 0;; i++) { + int latestVer = 0; + int currentVer = 0; + + // Treat missing decimal places as 0 + if (i < latestVersionQuad.count()) { + latestVer = latestVersionQuad[i]; + } + if (i < m_CurrentVersionQuad.count()) { + currentVer = m_CurrentVersionQuad[i]; + } + if (i >= latestVersionQuad.count() && i >= m_CurrentVersionQuad.count()) { + break; + } + + if (currentVer < latestVer) { + qDebug() << "Update available"; + + emit onUpdateAvailable(updateObj["browser_url"].toString()); + return; + } + else if (currentVer > latestVer) { + qDebug() << "Update manifest version lower than current version"; + return; + } + } + + qDebug() << "Update manifest version equal to current version"; + + return; + } + } + else { + qWarning() << "Update manifest contained unrecognized entry:" << updateEntry.toString(); + } + } + + qWarning() << "No entry in update manifest found for current platform: " + << QSysInfo::buildCpuArchitecture() << QSysInfo::productType(); + } + else { + qWarning() << "Update checking failed with error: " << reply->error(); + reply->deleteLater(); + } +} diff --git a/app/backend/autoupdatechecker.h b/app/backend/autoupdatechecker.h new file mode 100644 index 00000000..26cdd8dd --- /dev/null +++ b/app/backend/autoupdatechecker.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +class AutoUpdateChecker : public QObject +{ + Q_OBJECT +public: + explicit AutoUpdateChecker(QObject *parent = nullptr); + + Q_INVOKABLE void start(); + +signals: + void onUpdateAvailable(QString url); + +private slots: + void handleUpdateCheckRequestFinished(QNetworkReply* reply); + +private: + void parseStringToVersionQuad(QString& string, QVector& version); + + QVector m_CurrentVersionQuad; + QNetworkAccessManager m_Nam; +}; diff --git a/app/gui/main.qml b/app/gui/main.qml index 04320976..3a081045 100644 --- a/app/gui/main.qml +++ b/app/gui/main.qml @@ -4,6 +4,8 @@ import QtQuick.Layouts 1.3 import QtQuick.Controls.Material 2.1 +import AutoUpdateChecker 1.0 + ApplicationWindow { id: window visible: true @@ -66,6 +68,36 @@ ApplicationWindow { Layout.fillWidth: true } + ToolButton { + property string browserUrl: "" + + id: updateButton + icon.source: "qrc:/res/update.svg" + + // Invisible until we get a callback notifying us that + // an update is available + visible: false + + onClicked: Qt.openUrlExternally(browserUrl); + + Menu { + x: parent.width + transformOrigin: Menu.TopRight + } + + + function updateAvailable(url) + { + updateButton.browserUrl = url + updateButton.visible = true + } + + Component.onCompleted: { + AutoUpdateChecker.onUpdateAvailable.connect(updateAvailable) + AutoUpdateChecker.start() + } + } + ToolButton { icon.source: "qrc:/res/question_mark.svg" @@ -73,7 +105,6 @@ ApplicationWindow { onClicked: Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide"); Menu { - id: helpButton x: parent.width transformOrigin: Menu.TopRight } @@ -87,7 +118,6 @@ ApplicationWindow { onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping") Menu { - id: gamepadMappingMenu x: parent.width transformOrigin: Menu.TopRight } @@ -98,7 +128,6 @@ ApplicationWindow { onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings") Menu { - id: optionsMenu x: parent.width transformOrigin: Menu.TopRight } diff --git a/app/main.cpp b/app/main.cpp index 992ada15..4d9244ae 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -13,6 +13,7 @@ #include "gui/computermodel.h" #include "gui/appmodel.h" +#include "backend/autoupdatechecker.h" #include "streaming/session.hpp" #include "settings/streamingpreferences.h" @@ -172,6 +173,11 @@ int main(int argc, char *argv[]) [](QQmlEngine*, QJSEngine*) -> QObject* { return new ComputerManager(); }); + qmlRegisterSingletonType("AutoUpdateChecker", 1, 0, + "AutoUpdateChecker", + [](QQmlEngine*, QJSEngine*) -> QObject* { + return new AutoUpdateChecker(); + }); QQuickStyle::setStyle("Material"); diff --git a/app/res/update.svg b/app/res/update.svg new file mode 100644 index 00000000..0b97aee4 --- /dev/null +++ b/app/res/update.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/resources.qrc b/app/resources.qrc index be6ef0c2..1f1d0128 100644 --- a/app/resources.qrc +++ b/app/resources.qrc @@ -13,5 +13,6 @@ res/arrow_left.svg res/question_mark.svg res/moonlight.svg + res/update.svg