Qt: Add update checking infrastructure

This commit is contained in:
Vicki Pfau 2021-07-25 01:31:27 -07:00
parent 03e35cc7c6
commit 4a7feb66f9
15 changed files with 694 additions and 13 deletions

View File

@ -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

View File

@ -2,6 +2,7 @@ include(ExportDirectory)
set(SOURCE_FILES
commandline.c
thread-proxy.c
updater.c
video-logger.c)
set(GUI_FILES

170
src/feature/updater.c Normal file
View File

@ -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*) &sectionName[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(&section[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(&section[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;
}

View File

@ -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) {

View File

@ -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;
};
}
}

View File

@ -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));
}

View File

@ -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;
};
}

View File

@ -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";
}
}

View File

@ -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;
};
}
}

View File

@ -60,6 +60,7 @@ set(SOURCE_FILES
AbstractUpdater.cpp
Action.cpp
ActionMapper.cpp
ApplicationUpdater.cpp
AssetInfo.cpp
AssetTile.cpp
AssetView.cpp

View File

@ -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() {

View File

@ -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;

View File

@ -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);

View File

@ -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]{};

View File

@ -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">