mirror of https://github.com/mgba-emu/mgba.git
Qt: Add update checking infrastructure
This commit is contained in:
parent
03e35cc7c6
commit
4a7feb66f9
|
@ -0,0 +1,43 @@
|
|||
/* Copyright (c) 2013-2021 Jeffrey Pfau
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
#ifndef M_UPDATER_H
|
||||
#define M_UPDATER_H
|
||||
|
||||
#include <mgba-util/common.h>
|
||||
|
||||
CXX_GUARD_START
|
||||
|
||||
#include <mgba/core/config.h>
|
||||
#include <mgba-util/configuration.h>
|
||||
|
||||
struct StringList;
|
||||
struct Table;
|
||||
|
||||
struct mUpdaterContext {
|
||||
struct Configuration manifest;
|
||||
};
|
||||
|
||||
struct mUpdate {
|
||||
const char* path;
|
||||
size_t size;
|
||||
int rev;
|
||||
const char* version;
|
||||
const char* commit;
|
||||
const char* sha256;
|
||||
};
|
||||
|
||||
bool mUpdaterInit(struct mUpdaterContext*, const char* manifest);
|
||||
void mUpdaterDeinit(struct mUpdaterContext*);
|
||||
void mUpdaterGetPlatforms(const struct mUpdaterContext*, struct StringList* out);
|
||||
void mUpdaterGetUpdates(const struct mUpdaterContext*, const char* platform, struct Table* out);
|
||||
void mUpdaterGetUpdateForChannel(const struct mUpdaterContext*, const char* platform, const char* channel, struct mUpdate* out);
|
||||
const char* mUpdaterGetBucket(const struct mUpdaterContext*);
|
||||
void mUpdateRecord(struct mCoreConfig*, const char* prefix, const struct mUpdate*);
|
||||
bool mUpdateLoad(const struct mCoreConfig*, const char* prefix, struct mUpdate*);
|
||||
|
||||
CXX_GUARD_END
|
||||
|
||||
#endif
|
|
@ -2,6 +2,7 @@ include(ExportDirectory)
|
|||
set(SOURCE_FILES
|
||||
commandline.c
|
||||
thread-proxy.c
|
||||
updater.c
|
||||
video-logger.c)
|
||||
|
||||
set(GUI_FILES
|
||||
|
|
|
@ -0,0 +1,170 @@
|
|||
/* Copyright (c) 2013-2021 Jeffrey Pfau
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
#include <mgba/feature/updater.h>
|
||||
|
||||
#include <mgba-util/table.h>
|
||||
#include <mgba-util/vector.h>
|
||||
#include <mgba-util/vfs.h>
|
||||
|
||||
struct mUpdateMatch {
|
||||
const char* channel;
|
||||
struct mUpdate* out;
|
||||
};
|
||||
|
||||
static void _updateListSections(const char* sectionName, void* user) {
|
||||
struct StringList* out = user;
|
||||
if (strncmp("platform.", sectionName, 9) == 0) {
|
||||
*StringListAppend(out) = (char*) §ionName[9];
|
||||
}
|
||||
}
|
||||
|
||||
static void _updateUpdate(struct mUpdate* update, const char* item, const char* value) {
|
||||
if (strcmp("name", item) == 0) {
|
||||
update->path = value;
|
||||
} else if (strcmp("version", item) == 0) {
|
||||
update->version = value;
|
||||
} else if (strcmp("size", item) == 0) {
|
||||
update->size = strtoull(value, NULL, 10);
|
||||
} else if (strcmp("rev", item) == 0) {
|
||||
update->rev = strtol(value, NULL, 10);
|
||||
} else if (strcmp("commit", item) == 0) {
|
||||
update->commit = value;
|
||||
} else if (strcmp("sha256", item) == 0) {
|
||||
update->sha256 = value;
|
||||
}
|
||||
}
|
||||
|
||||
static void _updateList(const char* key, const char* value, void* user) {
|
||||
char channel[64] = {};
|
||||
const char* dotLoc;
|
||||
if (strncmp("medusa.", key, 7) == 0) {
|
||||
dotLoc = strchr(&key[7], '.');
|
||||
} else {
|
||||
dotLoc = strchr(key, '.');
|
||||
}
|
||||
if (!dotLoc) {
|
||||
return;
|
||||
}
|
||||
size_t size = dotLoc - key;
|
||||
if (size >= sizeof(channel)) {
|
||||
return;
|
||||
}
|
||||
strncpy(channel, key, size);
|
||||
const char* item = &key[size + 1];
|
||||
|
||||
struct Table* out = user;
|
||||
struct mUpdate* update = HashTableLookup(out, channel);
|
||||
if (!update) {
|
||||
update = calloc(1, sizeof(*update));
|
||||
HashTableInsert(out, channel, update);
|
||||
}
|
||||
_updateUpdate(update, item, value);
|
||||
}
|
||||
|
||||
static void _updateMatch(const char* key, const char* value, void* user) {
|
||||
struct mUpdateMatch* match = user;
|
||||
|
||||
size_t dotLoc = strlen(match->channel);
|
||||
if (dotLoc >= strlen(key) || key[dotLoc] != '.') {
|
||||
return;
|
||||
}
|
||||
if (strncmp(match->channel, key, dotLoc) != 0) {
|
||||
return;
|
||||
}
|
||||
const char* item = &key[dotLoc + 1];
|
||||
|
||||
struct Table* out = user;
|
||||
struct mUpdate* update = HashTableLookup(out, match->channel);
|
||||
if (!update) {
|
||||
update = calloc(1, sizeof(*update));
|
||||
HashTableInsert(out, match->channel, update);
|
||||
}
|
||||
_updateUpdate(update, item, value);
|
||||
}
|
||||
|
||||
bool mUpdaterInit(struct mUpdaterContext* context, const char* manifest) {
|
||||
ConfigurationInit(&context->manifest);
|
||||
|
||||
struct VFile* vf = VFileFromConstMemory(manifest, strlen(manifest) + 1);
|
||||
bool success = vf && ConfigurationReadVFile(&context->manifest, vf);
|
||||
vf->close(vf);
|
||||
if (!success) {
|
||||
ConfigurationDeinit(&context->manifest);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
void mUpdaterDeinit(struct mUpdaterContext* context) {
|
||||
ConfigurationDeinit(&context->manifest);
|
||||
}
|
||||
|
||||
void mUpdaterGetPlatforms(const struct mUpdaterContext* context, struct StringList* out) {
|
||||
StringListClear(out);
|
||||
ConfigurationEnumerateSections(&context->manifest, _updateListSections, out);
|
||||
}
|
||||
|
||||
void mUpdaterGetUpdates(const struct mUpdaterContext* context, const char* platform, struct Table* out) {
|
||||
char section[64] = {'p', 'l', 'a', 't', 'f', 'o', 'r', 'm', '.'};
|
||||
strncpy(§ion[9], platform, sizeof(section) - 10);
|
||||
ConfigurationEnumerate(&context->manifest, section, _updateList, out);
|
||||
}
|
||||
|
||||
void mUpdaterGetUpdateForChannel(const struct mUpdaterContext* context, const char* platform, const char* channel, struct mUpdate* out) {
|
||||
char section[64] = {'p', 'l', 'a', 't', 'f', 'o', 'r', 'm', '.'};
|
||||
strncpy(§ion[9], platform, sizeof(section) - 10);
|
||||
struct mUpdateMatch match = {
|
||||
.channel = channel,
|
||||
.out = out
|
||||
};
|
||||
ConfigurationEnumerate(&context->manifest, section, _updateMatch, &match);
|
||||
}
|
||||
|
||||
const char* mUpdaterGetBucket(const struct mUpdaterContext* context) {
|
||||
return ConfigurationGetValue(&context->manifest, "meta", "bucket");
|
||||
}
|
||||
|
||||
void mUpdateRecord(struct mCoreConfig* config, const char* prefix, const struct mUpdate* update) {
|
||||
char key[128];
|
||||
snprintf(key, sizeof(key), "%s.path", prefix);
|
||||
mCoreConfigSetValue(config, key, update->path);
|
||||
snprintf(key, sizeof(key), "%s.size", prefix);
|
||||
mCoreConfigSetUIntValue(config, key, update->size);
|
||||
snprintf(key, sizeof(key), "%s.rev", prefix);
|
||||
if (update->rev > 0) {
|
||||
mCoreConfigSetIntValue(config, key, update->rev);
|
||||
} else {
|
||||
mCoreConfigSetValue(config, key, NULL);
|
||||
}
|
||||
snprintf(key, sizeof(key), "%s.version", prefix);
|
||||
mCoreConfigSetValue(config, key, update->version);
|
||||
snprintf(key, sizeof(key), "%s.commit", prefix);
|
||||
mCoreConfigSetValue(config, key, update->commit);
|
||||
snprintf(key, sizeof(key), "%s.sha256", prefix);
|
||||
mCoreConfigSetValue(config, key, update->sha256);
|
||||
}
|
||||
|
||||
bool mUpdateLoad(const struct mCoreConfig* config, const char* prefix, struct mUpdate* update) {
|
||||
char key[128];
|
||||
memset(update, 0, sizeof(*update));
|
||||
snprintf(key, sizeof(key), "%s.path", prefix);
|
||||
update->path = mCoreConfigGetValue(config, key);
|
||||
snprintf(key, sizeof(key), "%s.size", prefix);
|
||||
uint32_t size = 0;
|
||||
mCoreConfigGetUIntValue(config, key, &size);
|
||||
if (!update->path && !size) {
|
||||
return false;
|
||||
}
|
||||
update->size = size;
|
||||
snprintf(key, sizeof(key), "%s.rev", prefix);
|
||||
mCoreConfigGetIntValue(config, key, &update->rev);
|
||||
snprintf(key, sizeof(key), "%s.version", prefix);
|
||||
update->version = mCoreConfigGetValue(config, key);
|
||||
snprintf(key, sizeof(key), "%s.commit", prefix);
|
||||
update->commit = mCoreConfigGetValue(config, key);
|
||||
snprintf(key, sizeof(key), "%s.sha256", prefix);
|
||||
update->sha256 = mCoreConfigGetValue(config, key);
|
||||
return true;
|
||||
}
|
|
@ -40,7 +40,17 @@ void AbstractUpdater::downloadUpdate() {
|
|||
chaseRedirects(reply, &AbstractUpdater::updateDownloaded);
|
||||
}
|
||||
|
||||
void AbstractUpdater::progress(qint64 progress, qint64 max) {
|
||||
if (!max) {
|
||||
return;
|
||||
}
|
||||
emit updateProgress(static_cast<float>(progress) / static_cast<float>(max));
|
||||
}
|
||||
|
||||
void AbstractUpdater::chaseRedirects(QNetworkReply* reply, void (AbstractUpdater::*cb)(QNetworkReply*)) {
|
||||
if (m_isUpdating) {
|
||||
connect(reply, &QNetworkReply::downloadProgress, this, &AbstractUpdater::progress);
|
||||
}
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, cb]() {
|
||||
// TODO: check domains, etc
|
||||
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() / 100 == 3) {
|
||||
|
|
|
@ -28,12 +28,16 @@ public slots:
|
|||
signals:
|
||||
void updateAvailable(bool);
|
||||
void updateDone(bool);
|
||||
void updateProgress(float done);
|
||||
|
||||
protected:
|
||||
virtual QUrl manifestLocation() const = 0;
|
||||
virtual QUrl parseManifest(const QByteArray&) const = 0;
|
||||
virtual QUrl parseManifest(const QByteArray&) = 0;
|
||||
virtual QString destination() const = 0;
|
||||
|
||||
private slots:
|
||||
void progress(qint64 progress, qint64 max);
|
||||
|
||||
private:
|
||||
void chaseRedirects(QNetworkReply*, void (AbstractUpdater::*cb)(QNetworkReply*));
|
||||
void manifestDownloaded(QNetworkReply*);
|
||||
|
@ -44,4 +48,4 @@ private:
|
|||
QByteArray m_manifest;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,198 @@
|
|||
/* Copyright (c) 2013-2021 Jeffrey Pfau
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
#include "ApplicationUpdater.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
|
||||
#include "ConfigController.h"
|
||||
#include "GBAApp.h"
|
||||
|
||||
#include <mgba/core/version.h>
|
||||
#include <mgba/feature/updater.h>
|
||||
#include <mgba-util/table.h>
|
||||
|
||||
using namespace QGBA;
|
||||
|
||||
ApplicationUpdater::ApplicationUpdater(ConfigController* config, QObject* parent)
|
||||
: AbstractUpdater(parent)
|
||||
, m_config(config)
|
||||
, m_channel(currentChannel())
|
||||
{
|
||||
QVariant lastCheck = config->getQtOption("lastUpdateCheck");
|
||||
if (lastCheck.isValid()) {
|
||||
m_lastCheck = lastCheck.toDateTime();
|
||||
}
|
||||
|
||||
QByteArray bucket(m_config->getOption("update.bucket").toLatin1());
|
||||
if (!bucket.isNull()) {
|
||||
mUpdate lastUpdate;
|
||||
if (mUpdateLoad(m_config->config(), "update.stable", &lastUpdate)) {
|
||||
m_updates[QLatin1String("stable")] = UpdateInfo(bucket.constData(), &lastUpdate);
|
||||
}
|
||||
if (mUpdateLoad(m_config->config(), "update.dev", &lastUpdate)) {
|
||||
m_updates[QLatin1String("dev")] = UpdateInfo(bucket.constData(), &lastUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
connect(this, &AbstractUpdater::updateAvailable, this, [this, config](bool) {
|
||||
m_lastCheck = QDateTime::currentDateTimeUtc();
|
||||
config->setQtOption("lastUpdateCheck", m_lastCheck);
|
||||
});
|
||||
}
|
||||
|
||||
QUrl ApplicationUpdater::manifestLocation() const {
|
||||
return {"https://mgba.io/latest.ini"};
|
||||
}
|
||||
|
||||
QStringList ApplicationUpdater::listChannels() {
|
||||
QStringList channels;
|
||||
channels << QLatin1String("stable");
|
||||
channels << QLatin1String("dev");
|
||||
return channels;
|
||||
}
|
||||
|
||||
QString ApplicationUpdater::currentChannel() {
|
||||
QLatin1String version(projectVersion);
|
||||
QLatin1String branch(gitBranch);
|
||||
if (branch == QLatin1String("heads/") + version) {
|
||||
return QLatin1String("stable");
|
||||
} else {
|
||||
return QLatin1String("dev");
|
||||
}
|
||||
}
|
||||
|
||||
QString ApplicationUpdater::readableChannel(const QString& channel) {
|
||||
if (channel.isEmpty()) {
|
||||
return readableChannel(currentChannel());
|
||||
}
|
||||
if (channel == QLatin1String("stable")) {
|
||||
return tr("Stable");
|
||||
}
|
||||
if (channel == QLatin1String("dev")) {
|
||||
return tr("Development");
|
||||
}
|
||||
return tr("Unknown");
|
||||
}
|
||||
|
||||
ApplicationUpdater::UpdateInfo ApplicationUpdater::currentVersion() {
|
||||
UpdateInfo info;
|
||||
info.rev = gitRevision;
|
||||
info.commit = QLatin1String(gitCommit);
|
||||
return info;
|
||||
}
|
||||
|
||||
QUrl ApplicationUpdater::parseManifest(const QByteArray& manifest) {
|
||||
const char* bytes = manifest.constData();
|
||||
|
||||
mUpdaterContext context;
|
||||
if (!mUpdaterInit(&context, bytes)) {
|
||||
return {};
|
||||
}
|
||||
m_bucket = QLatin1String(mUpdaterGetBucket(&context));
|
||||
m_config->setOption("update.bucket", m_bucket);
|
||||
|
||||
Table updates;
|
||||
HashTableInit(&updates, 4, free);
|
||||
mUpdaterGetUpdates(&context, platform(), &updates);
|
||||
|
||||
m_updates.clear();
|
||||
HashTableEnumerate(&updates, [](const char* key, void* value, void* user) {
|
||||
const mUpdate* update = static_cast<mUpdate*>(value);
|
||||
ApplicationUpdater* self = static_cast<ApplicationUpdater*>(user);
|
||||
self->m_updates[QString::fromUtf8(key)] = UpdateInfo(self->m_bucket, update);
|
||||
QByteArray prefix(QString("update.%1").arg(key).toUtf8());
|
||||
mUpdateRecord(self->m_config->config(), prefix.constData(), update);
|
||||
}, static_cast<void*>(this));
|
||||
|
||||
HashTableDeinit(&updates);
|
||||
mUpdaterDeinit(&context);
|
||||
|
||||
if (!m_updates.contains(m_channel)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return m_updates[m_channel].url;
|
||||
}
|
||||
|
||||
QString ApplicationUpdater::destination() const {
|
||||
return {};
|
||||
}
|
||||
|
||||
const char* ApplicationUpdater::platform() {
|
||||
#ifdef Q_OS_WIN64
|
||||
return "win64";
|
||||
#elif defined(Q_OS_WIN32)
|
||||
return "win32";
|
||||
#elif defined(Q_OS_MACOS)
|
||||
return "osx";
|
||||
#else
|
||||
// Return one that will be up to date, but we can't download
|
||||
return "win64";
|
||||
#endif
|
||||
}
|
||||
|
||||
ApplicationUpdater::UpdateInfo::UpdateInfo(const QString& prefix, const mUpdate* update)
|
||||
: size(update->size)
|
||||
, url(prefix + update->path)
|
||||
{
|
||||
if (update->rev > 0) {
|
||||
rev = update->rev;
|
||||
}
|
||||
if (update->commit) {
|
||||
commit = update->commit;
|
||||
}
|
||||
if (update->version) {
|
||||
version = QLatin1String(update->version);
|
||||
}
|
||||
if (update->sha256) {
|
||||
sha256 = QByteArray::fromHex(update->sha256);
|
||||
}
|
||||
}
|
||||
|
||||
bool ApplicationUpdater::UpdateInfo::operator<(const ApplicationUpdater::UpdateInfo& other) const {
|
||||
if (rev > 0 && other.rev > 0) {
|
||||
return rev < other.rev;
|
||||
}
|
||||
if (!version.isNull() && !other.version.isNull()) {
|
||||
QStringList components = version.split(QChar('.'));
|
||||
QStringList otherComponents = other.version.split(QChar('.'));
|
||||
for (int i = 0; i < std::max<int>(components.count(), otherComponents.count()); ++i) {
|
||||
int component = 0;
|
||||
int otherComponent = 0;
|
||||
if (i < components.count()) {
|
||||
bool ok = true;
|
||||
component = components[i].toInt(&ok);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (i < otherComponents.count()) {
|
||||
bool ok = true;
|
||||
otherComponent = otherComponents[i].toInt(&ok);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (component < otherComponent) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ApplicationUpdater::UpdateInfo::operator QString() const {
|
||||
if (!version.isNull()) {
|
||||
return version;
|
||||
}
|
||||
int len = strlen(gitCommitShort);
|
||||
const char* pos = strchr(gitCommitShort, '-');
|
||||
if (pos) {
|
||||
len = pos - gitCommitShort;
|
||||
}
|
||||
return QString("r%1-%2").arg(rev).arg(commit.left(len));
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/* Copyright (c) 2013-2021 Jeffrey Pfau
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
#pragma once
|
||||
|
||||
#include "AbstractUpdater.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QHash>
|
||||
#include <QUrl>
|
||||
|
||||
struct mUpdate;
|
||||
|
||||
namespace QGBA {
|
||||
|
||||
class ConfigController;
|
||||
|
||||
class ApplicationUpdater : public AbstractUpdater {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
struct UpdateInfo {
|
||||
UpdateInfo() = default;
|
||||
UpdateInfo(const QString& prefix, const mUpdate*);
|
||||
|
||||
QString version;
|
||||
int rev;
|
||||
QString commit;
|
||||
size_t size;
|
||||
QUrl url;
|
||||
QByteArray sha256;
|
||||
|
||||
bool operator<(const UpdateInfo&) const;
|
||||
operator QString() const;
|
||||
};
|
||||
|
||||
ApplicationUpdater(ConfigController* config, QObject* parent = nullptr);
|
||||
|
||||
static QStringList listChannels();
|
||||
void setChannel(const QString& channel) { m_channel = channel; }
|
||||
static QString currentChannel();
|
||||
static QString readableChannel(const QString& channel = {});
|
||||
|
||||
QHash<QString, UpdateInfo> listUpdates() const { return m_updates; }
|
||||
UpdateInfo updateInfo() const { return m_updates[m_channel]; }
|
||||
static UpdateInfo currentVersion();
|
||||
|
||||
QDateTime lastCheck() const { return m_lastCheck; }
|
||||
|
||||
protected:
|
||||
virtual QUrl manifestLocation() const override;
|
||||
virtual QUrl parseManifest(const QByteArray&) override;
|
||||
virtual QString destination() const override;
|
||||
|
||||
private:
|
||||
static const char* platform();
|
||||
|
||||
ConfigController* m_config;
|
||||
QHash<QString, UpdateInfo> m_updates;
|
||||
QString m_channel;
|
||||
QString m_bucket;
|
||||
QDateTime m_lastCheck;
|
||||
};
|
||||
|
||||
}
|
|
@ -24,7 +24,7 @@ QUrl BattleChipUpdater::manifestLocation() const {
|
|||
return {"https://api.github.com/repos/mgba-emu/chip-assets/releases/latest"};
|
||||
}
|
||||
|
||||
QUrl BattleChipUpdater::parseManifest(const QByteArray& manifest) const {
|
||||
QUrl BattleChipUpdater::parseManifest(const QByteArray& manifest) {
|
||||
QJsonDocument manifestDoc(QJsonDocument::fromJson(manifest));
|
||||
if (manifestDoc.isNull()) {
|
||||
return QUrl();
|
||||
|
@ -44,4 +44,4 @@ QString BattleChipUpdater::destination() const {
|
|||
return info.filePath();
|
||||
}
|
||||
return ConfigController::configDir() + "/chips.rcc";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,8 +15,8 @@ public:
|
|||
|
||||
protected:
|
||||
virtual QUrl manifestLocation() const override;
|
||||
virtual QUrl parseManifest(const QByteArray&) const override;
|
||||
virtual QUrl parseManifest(const QByteArray&) override;
|
||||
virtual QString destination() const override;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@ set(SOURCE_FILES
|
|||
AbstractUpdater.cpp
|
||||
Action.cpp
|
||||
ActionMapper.cpp
|
||||
ApplicationUpdater.cpp
|
||||
AssetInfo.cpp
|
||||
AssetTile.cpp
|
||||
AssetView.cpp
|
||||
|
|
|
@ -39,6 +39,7 @@ mLOG_DEFINE_CATEGORY(QT, "Qt", "platform.qt");
|
|||
GBAApp::GBAApp(int& argc, char* argv[], ConfigController* config)
|
||||
: QApplication(argc, argv)
|
||||
, m_configController(config)
|
||||
, m_updater(config)
|
||||
, m_monospace(QFontDatabase::systemFont(QFontDatabase::FixedFont))
|
||||
{
|
||||
g_app = this;
|
||||
|
@ -80,6 +81,9 @@ GBAApp::GBAApp(int& argc, char* argv[], ConfigController* config)
|
|||
#endif
|
||||
|
||||
connect(this, &GBAApp::aboutToQuit, this, &GBAApp::cleanup);
|
||||
if (m_configController->getOption("updateAutoCheck", 0).toInt()) {
|
||||
QMetaObject::invokeMethod(&m_updater, "checkUpdate", Qt::QueuedConnection);
|
||||
}
|
||||
}
|
||||
|
||||
void GBAApp::cleanup() {
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
#include <functional>
|
||||
|
||||
#include "ApplicationUpdater.h"
|
||||
#include "CoreManager.h"
|
||||
#include "MultiplayerController.h"
|
||||
|
||||
|
@ -75,6 +76,8 @@ public:
|
|||
bool removeWorkerJob(qint64 jobId);
|
||||
bool waitOnJob(qint64 jobId, QObject* context, std::function<void ()> callback);
|
||||
|
||||
ApplicationUpdater* updater() { return &m_updater; }
|
||||
|
||||
signals:
|
||||
void jobFinished(qint64 jobId);
|
||||
|
||||
|
@ -108,6 +111,7 @@ private:
|
|||
QList<Window*> m_windows;
|
||||
MultiplayerController m_multiplayer;
|
||||
CoreManager m_manager;
|
||||
ApplicationUpdater m_updater;
|
||||
|
||||
QMap<qint64, WorkerJob*> m_workerJobs;
|
||||
QMultiMap<qint64, QMetaObject::Connection> m_workerJobCallbacks;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright (c) 2013-2014 Jeffrey Pfau
|
||||
/* Copyright (c) 2013-2021 Jeffrey Pfau
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
@ -35,14 +35,15 @@ SettingsView::SettingsView(ConfigController* controller, InputController* inputC
|
|||
|
||||
m_pageIndex[Page::AV] = 0;
|
||||
m_pageIndex[Page::INTERFACE] = 1;
|
||||
m_pageIndex[Page::EMULATION] = 2;
|
||||
m_pageIndex[Page::ENHANCEMENTS] = 3;
|
||||
m_pageIndex[Page::BIOS] = 4;
|
||||
m_pageIndex[Page::PATHS] = 5;
|
||||
m_pageIndex[Page::LOGGING] = 6;
|
||||
m_pageIndex[Page::UPDATE] = 2;
|
||||
m_pageIndex[Page::EMULATION] = 3;
|
||||
m_pageIndex[Page::ENHANCEMENTS] = 4;
|
||||
m_pageIndex[Page::BIOS] = 5;
|
||||
m_pageIndex[Page::PATHS] = 6;
|
||||
m_pageIndex[Page::LOGGING] = 7;
|
||||
|
||||
#ifdef M_CORE_GB
|
||||
m_pageIndex[Page::GB] = 7;
|
||||
m_pageIndex[Page::GB] = 8;
|
||||
|
||||
for (auto model : GameBoy::modelList()) {
|
||||
m_ui.gbModel->addItem(GameBoy::modelName(model), model);
|
||||
|
@ -175,6 +176,38 @@ SettingsView::SettingsView(ConfigController* controller, InputController* inputC
|
|||
}
|
||||
#endif
|
||||
|
||||
ApplicationUpdater* updater = GBAApp::app()->updater();
|
||||
m_ui.currentChannel->setText(ApplicationUpdater::readableChannel());
|
||||
m_ui.currentVersion->setText(ApplicationUpdater::currentVersion());
|
||||
QDateTime lastCheck = updater->lastCheck();
|
||||
if (!lastCheck.isNull()) {
|
||||
m_ui.lastChecked->setText(lastCheck.toLocalTime().toString());
|
||||
}
|
||||
connect(m_ui.checkUpdate, &QAbstractButton::pressed, updater, &ApplicationUpdater::checkUpdate);
|
||||
connect(updater, &ApplicationUpdater::updateAvailable, this, [this, updater](bool hasUpdate) {
|
||||
updateChecked();
|
||||
if (hasUpdate) {
|
||||
m_ui.availVersion->setText(updater->updateInfo());
|
||||
}
|
||||
});
|
||||
for (const QString& channel : ApplicationUpdater::listChannels()) {
|
||||
m_ui.updateChannel->addItem(ApplicationUpdater::readableChannel(channel), channel);
|
||||
if (channel == ApplicationUpdater::currentChannel()) {
|
||||
m_ui.updateChannel->setCurrentIndex(m_ui.updateChannel->count() - 1);
|
||||
}
|
||||
}
|
||||
connect(m_ui.updateChannel, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, [this, updater](int) {
|
||||
QString channel = m_ui.updateChannel->currentData().toString();
|
||||
updater->setChannel(channel);
|
||||
auto updates = updater->listUpdates();
|
||||
if (updates.contains(channel)) {
|
||||
m_ui.availVersion->setText(updates[channel]);
|
||||
} else {
|
||||
m_ui.availVersion->setText(tr("None"));
|
||||
}
|
||||
});
|
||||
m_ui.availVersion->setText(updater->updateInfo());
|
||||
|
||||
// TODO: Move to reloadConfig()
|
||||
QVariant cameraDriver = m_controller->getQtOption("cameraDriver");
|
||||
m_ui.cameraDriver->addItem(tr("None (Still Image)"), static_cast<int>(InputController::CameraDriver::NONE));
|
||||
|
@ -332,6 +365,12 @@ SettingsView::SettingsView(ConfigController* controller, InputController* inputC
|
|||
}
|
||||
});
|
||||
|
||||
m_checkTimer.setInterval(60);
|
||||
m_checkTimer.setSingleShot(false);
|
||||
connect(&m_checkTimer, &QTimer::timeout, this, &SettingsView::updateChecked);
|
||||
m_checkTimer.start();
|
||||
updateChecked();
|
||||
|
||||
ShortcutView* shortcutView = new ShortcutView();
|
||||
shortcutView->setController(shortcutController);
|
||||
shortcutView->setInputController(inputController);
|
||||
|
@ -446,6 +485,7 @@ void SettingsView::updateConfig() {
|
|||
saveSetting("videoScale", m_ui.videoScale);
|
||||
saveSetting("gba.forceGbp", m_ui.forceGbp);
|
||||
saveSetting("vbaBugCompat", m_ui.vbaBugCompat);
|
||||
saveSetting("updateAutoCheck", m_ui.updateAutoCheck);
|
||||
|
||||
if (m_ui.audioBufferSize->currentText().toInt() > 8192) {
|
||||
m_ui.audioBufferSize->setCurrentText("8192");
|
||||
|
@ -659,6 +699,7 @@ void SettingsView::reloadConfig() {
|
|||
loadSetting("dynamicTitle", m_ui.dynamicTitle, true);
|
||||
loadSetting("gba.forceGbp", m_ui.forceGbp);
|
||||
loadSetting("vbaBugCompat", m_ui.vbaBugCompat, true);
|
||||
loadSetting("updateAutoCheck", m_ui.updateAutoCheck);
|
||||
|
||||
m_ui.libraryStyle->setCurrentIndex(loadSetting("libraryStyle").toInt());
|
||||
|
||||
|
@ -781,6 +822,31 @@ void SettingsView::reloadConfig() {
|
|||
}
|
||||
}
|
||||
|
||||
void SettingsView::updateChecked() {
|
||||
QDateTime now(QDateTime::currentDateTimeUtc());
|
||||
QDateTime lastCheck(GBAApp::app()->updater()->lastCheck());
|
||||
if (!lastCheck.isValid()) {
|
||||
m_ui.lastChecked->setText(tr("Never"));
|
||||
return;
|
||||
}
|
||||
qint64 ago = GBAApp::app()->updater()->lastCheck().secsTo(now);
|
||||
if (ago < 60) {
|
||||
m_ui.lastChecked->setText(tr("Just now"));
|
||||
return;
|
||||
}
|
||||
if (ago < 3600) {
|
||||
m_ui.lastChecked->setText(tr("Less than an hour ago"));
|
||||
return;
|
||||
}
|
||||
ago /= 3600;
|
||||
if (ago < 24) {
|
||||
m_ui.lastChecked->setText(tr("%n hour(s) ago", nullptr, ago));
|
||||
return;
|
||||
}
|
||||
ago /= 24;
|
||||
m_ui.lastChecked->setText(tr("%n day(s) ago", nullptr, ago));
|
||||
}
|
||||
|
||||
void SettingsView::addPage(const QString& name, QWidget* view, Page index) {
|
||||
m_pageIndex[index] = m_ui.tabs->count();
|
||||
m_ui.tabs->addItem(name);
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
#include <QDialog>
|
||||
#include <QMap>
|
||||
#include <QTimer>
|
||||
|
||||
#include "ColorPicker.h"
|
||||
#include "LogConfigModel.h"
|
||||
|
@ -33,6 +34,7 @@ public:
|
|||
enum class Page {
|
||||
AV,
|
||||
INTERFACE,
|
||||
UPDATE,
|
||||
EMULATION,
|
||||
ENHANCEMENTS,
|
||||
BIOS,
|
||||
|
@ -69,6 +71,7 @@ private slots:
|
|||
void selectPath(QLineEdit*, QCheckBox*);
|
||||
void updateConfig();
|
||||
void reloadConfig();
|
||||
void updateChecked();
|
||||
|
||||
private:
|
||||
Ui::SettingsView m_ui;
|
||||
|
@ -77,6 +80,7 @@ private:
|
|||
InputController* m_input;
|
||||
ShaderSelector* m_shader = nullptr;
|
||||
LogConfigModel m_logModel;
|
||||
QTimer m_checkTimer;
|
||||
|
||||
#ifdef M_CORE_GB
|
||||
uint32_t m_gbColors[12]{};
|
||||
|
|
|
@ -50,6 +50,11 @@
|
|||
<string>Interface</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Update</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Emulation</string>
|
||||
|
@ -762,6 +767,110 @@
|
|||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="update">
|
||||
<layout class="QFormLayout" name="formLayout_11">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_46">
|
||||
<property name="text">
|
||||
<string>Current channel:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="currentChannel">
|
||||
<property name="text">
|
||||
<string notr="true">None</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_50">
|
||||
<property name="text">
|
||||
<string>Current version:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLabel" name="currentVersion">
|
||||
<property name="text">
|
||||
<string notr="true">0</string>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="Line" name="line_20">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_45">
|
||||
<property name="text">
|
||||
<string>Update channel:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QComboBox" name="updateChannel"/>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_44">
|
||||
<property name="text">
|
||||
<string>Available version:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QLabel" name="availVersion">
|
||||
<property name="text">
|
||||
<string>(Unknown)</string>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="label_49">
|
||||
<property name="text">
|
||||
<string>Last checked:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QLabel" name="lastChecked">
|
||||
<property name="text">
|
||||
<string notr="true">Never</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0" colspan="2">
|
||||
<widget class="Line" name="line_11">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="QCheckBox" name="updateAutoCheck">
|
||||
<property name="text">
|
||||
<string>Automatically check on start</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="QPushButton" name="checkUpdate">
|
||||
<property name="text">
|
||||
<string>Check now</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="emulation">
|
||||
<layout class="QFormLayout" name="formLayout_2">
|
||||
<property name="fieldGrowthPolicy">
|
||||
|
|
Loading…
Reference in New Issue