Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ set(COMMON_SOURCES
${CMAKE_SOURCE_DIR}/common/src/logger.cpp
${CMAKE_SOURCE_DIR}/common/src/process_parameter.cpp
${CMAKE_SOURCE_DIR}/common/src/stream_context.cpp
${CMAKE_SOURCE_DIR}/common/src/updater.cpp
${CMAKE_SOURCE_DIR}/engine/src/converter.cpp
)

Expand All @@ -39,6 +40,7 @@ set(COMMON_HEADERS
${CMAKE_SOURCE_DIR}/common/include/process_parameter.h
${CMAKE_SOURCE_DIR}/common/include/process_observer.h
${CMAKE_SOURCE_DIR}/common/include/stream_context.h
${CMAKE_SOURCE_DIR}/common/include/updater.h
${CMAKE_SOURCE_DIR}/engine/include/converter.h
${CMAKE_SOURCE_DIR}/transcoder/include/transcoder.h
)
Expand Down
7 changes: 7 additions & 0 deletions src/builder/include/open_converter.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
#include "../../common/include/logger.h"
#include "../../common/include/process_observer.h"
#include "../../common/include/process_parameter.h"
#include "../../common/include/updater.h"

QT_BEGIN_NAMESPACE
namespace Ui {
Expand Down Expand Up @@ -94,6 +95,11 @@ private slots:
void OnQueueButtonClicked();
void SlotLogToggled(bool checked);
void SlotAbout();
void SlotCheckForUpdates();
void OnUpdateAvailable(const QString &currentVer, const QString &latestVer,
const QString &downloadUrl, const QString &releaseNotes);
void OnNoUpdateAvailable();
void OnUpdateCheckFailed(const QString &errorMsg);

private:
Ui::OpenConverter *ui;
Expand Down Expand Up @@ -122,6 +128,7 @@ private slots:
QPushButton *queueButton;
SharedData *sharedData;
BatchQueueDialog *batchQueueDialog;
Updater *updater;

void LoadLanguage(const QString &rLanguage);
void HandleConverterResult(bool flag);
Expand Down
64 changes: 64 additions & 0 deletions src/builder/src/open_converter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
#include <QUrl>
#include <QVBoxLayout>
#include <QStandardPaths>
#include <QTimer>
#include <QDesktopServices>

#include "../../common/include/encode_parameter.h"
#include "../../common/include/info.h"
Expand Down Expand Up @@ -125,6 +127,16 @@
ui->action_enableLog->setChecked(checked);
});

QSettings settings(m_settingsPath, QSettings::IniFormat);
QCheckBox *autoUpdateCheckBox = new QCheckBox(tr("Check for updates on startup"), settingsDialog);

Check warning on line 131 in src/builder/src/open_converter.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace the redundant type with "auto".

See more on https://sonarcloud.io/project/issues?id=OpenConverterLab_OpenConverter&issues=AZ54oDo1yyFq8UYE4Zg5&open=AZ54oDo1yyFq8UYE4Zg5&pullRequest=149
autoUpdateCheckBox->setChecked(settings.value("update/autoUpdate", true).toBool());
layout->addWidget(autoUpdateCheckBox);

connect(autoUpdateCheckBox, &QCheckBox::toggled, this, [this](bool checked) {
QSettings s(m_settingsPath, QSettings::IniFormat);
s.setValue("update/autoUpdate", checked);
});

layout->addStretch();

QPushButton *clearButton = new QPushButton(tr("Reset All Settings"), settingsDialog);
Expand All @@ -151,6 +163,12 @@
// On Windows/Linux this menu stays visible as "Help" (placed before Language).
QMenu *helpMenu = new QMenu(tr("Help"), this);
helpMenu->addAction(prefAction);

QAction *checkUpdateAction = new QAction(tr("Check for Updates..."), this);

Check warning on line 167 in src/builder/src/open_converter.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace the redundant type with "auto".

See more on https://sonarcloud.io/project/issues?id=OpenConverterLab_OpenConverter&issues=AZ54oDo1yyFq8UYE4Zg4&open=AZ54oDo1yyFq8UYE4Zg4&pullRequest=149
checkUpdateAction->setMenuRole(QAction::NoRole);
connect(checkUpdateAction, &QAction::triggered, this, &OpenConverter::SlotCheckForUpdates);
helpMenu->addAction(checkUpdateAction);

helpMenu->addAction(ui->action_about);
menuBar()->insertMenu(ui->menuLanguage->menuAction(), helpMenu);

Expand All @@ -171,6 +189,15 @@
// Initialize batch queue dialog
batchQueueDialog = nullptr;

// Initialize updater
updater = new Updater(this);
connect(updater, &Updater::UpdateAvailable,
this, &OpenConverter::OnUpdateAvailable);
connect(updater, &Updater::NoUpdateAvailable,
this, &OpenConverter::OnNoUpdateAvailable);
connect(updater, &Updater::CheckFailed,
this, &OpenConverter::OnUpdateCheckFailed);

#ifdef ENABLE_FFMPEG
QAction *act_ffmpeg = new QAction(tr("FFMPEG"), this);
act_ffmpeg->setObjectName("FFMPEG");
Expand Down Expand Up @@ -266,6 +293,14 @@

connect(ui->menuTranscoder, SIGNAL(triggered(QAction *)), this,
SLOT(SlotTranscoderChanged(QAction *)));

// Auto-check for updates on startup
bool autoUpdate = settings.value("update/autoUpdate", true).toBool();
if (autoUpdate) {
QTimer::singleShot(2000, this, [this]() {
updater->CheckForUpdates(true);
});
}
}

void OpenConverter::dragEnterEvent(QDragEnterEvent *event) {
Expand Down Expand Up @@ -655,4 +690,33 @@
aboutDialog->exec();
}

void OpenConverter::SlotCheckForUpdates() {
updater->CheckForUpdates(false);
}

void OpenConverter::OnUpdateAvailable(const QString &currentVer, const QString &latestVer,
const QString &downloadUrl, const QString &releaseNotes) {
QMessageBox msgBox(this);
msgBox.setWindowTitle(tr("Update Available"));
msgBox.setIcon(QMessageBox::Information);
msgBox.setText(tr("A new version of OpenConverter is available!"));
msgBox.setInformativeText(tr("Current: %1\nLatest: %2\n\n%3")
.arg(currentVer, latestVer, releaseNotes.left(200)));
msgBox.setStandardButtons(QMessageBox::Open | QMessageBox::Cancel);
msgBox.button(QMessageBox::Open)->setText(tr("Download"));
msgBox.button(QMessageBox::Cancel)->setText(tr("Later"));

if (msgBox.exec() == QMessageBox::Open) {
QDesktopServices::openUrl(QUrl(downloadUrl));
}
}

void OpenConverter::OnNoUpdateAvailable() {
ui->statusBar->showMessage(tr("You are running the latest version"), 3000);
}

void OpenConverter::OnUpdateCheckFailed(const QString &errorMsg) {
ui->statusBar->showMessage(tr("Update check failed: %1").arg(errorMsg), 5000);
}

#include "open_converter.moc"
55 changes: 55 additions & 0 deletions src/common/include/updater.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2026 Finch
* Email: 1418875140@qq.com
*
* 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.
*/

#ifndef UPDATER_H
#define UPDATER_H

#include <QObject>
#include <QString>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QVersionNumber>

class Updater : public QObject {
Q_OBJECT

public:
explicit Updater(QObject *parent = nullptr);
~Updater();

// silent=true: 启动时静默检查,有更新才弹窗
// silent=false: 手动检查,无更新也提示
void CheckForUpdates(bool silent);

signals:
void UpdateAvailable(const QString &currentVer, const QString &latestVer,
const QString &downloadUrl, const QString &releaseNotes);
void NoUpdateAvailable();
void CheckFailed(const QString &errorMsg);

private slots:
void OnReleaseInfoReceived(QNetworkReply *reply);

private:
QNetworkAccessManager *m_manager;
bool m_silent;

static bool IsNewerVersion(const QString &remote, const QString &local);
static QString ExtractPlatformDownloadUrl(const QJsonObject &release);
};

#endif // UPDATER_H
125 changes: 125 additions & 0 deletions src/common/src/updater.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright 2026 Finch
* Email: 1418875140@qq.com
*
* 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 "updater.h"

#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkRequest>
#include <QUrl>

static const char *kGitHubApiUrl =
"https://api.github.com/repos/OpenConverterLab/OpenConverter/releases/latest";

Check failure on line 27 in src/common/src/updater.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Global pointers should be const at every level.

See more on https://sonarcloud.io/project/issues?id=OpenConverterLab_OpenConverter&issues=AZ54oDn0yyFq8UYE4Zg2&open=AZ54oDn0yyFq8UYE4Zg2&pullRequest=149

Updater::Updater(QObject *parent)
: QObject(parent), m_manager(new QNetworkAccessManager(this)), m_silent(true) {

Check warning on line 30 in src/common/src/updater.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not use the constructor's initializer list for data member "m_silent". Use the in-class initializer instead.

See more on https://sonarcloud.io/project/issues?id=OpenConverterLab_OpenConverter&issues=AZ54oDn0yyFq8UYE4Zg1&open=AZ54oDn0yyFq8UYE4Zg1&pullRequest=149
connect(m_manager, &QNetworkAccessManager::finished,
this, &Updater::OnReleaseInfoReceived);
}

Updater::~Updater() = default;

void Updater::CheckForUpdates(bool silent) {
m_silent = silent;
QNetworkRequest request(QUrl(kGitHubApiUrl));

Check warning on line 39 in src/common/src/updater.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move this function declaration outside of this block scope, or if the intent was to declare a variable, use a syntax that avoids the most vexing parse.

See more on https://sonarcloud.io/project/issues?id=OpenConverterLab_OpenConverter&issues=AZ54oDn0yyFq8UYE4Zg3&open=AZ54oDn0yyFq8UYE4Zg3&pullRequest=149
request.setRawHeader("Accept", "application/vnd.github.v3+json");
request.setRawHeader("User-Agent", "OpenConverter");
m_manager->get(request);
}

void Updater::OnReleaseInfoReceived(QNetworkReply *reply) {
reply->deleteLater();

if (reply->error() != QNetworkReply::NoError) {
if (!m_silent) {
emit CheckFailed(reply->errorString());
}
return;
}

QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
if (!doc.isObject()) {
if (!m_silent) {
emit CheckFailed(tr("Invalid response from GitHub"));
}
return;
}

QJsonObject release = doc.object();
QString tagName = release.value("tag_name").toString();
QString releaseNotes = release.value("body").toString();
QString downloadUrl = ExtractPlatformDownloadUrl(release);

if (tagName.isEmpty()) {
if (!m_silent) {
emit CheckFailed(tr("No release found"));
}
return;
}

QString currentVer = OC_VERSION;
QString latestVer = tagName.startsWith('v') ? tagName.mid(1) : tagName;

if (IsNewerVersion(latestVer, currentVer)) {
emit UpdateAvailable(currentVer, latestVer, downloadUrl, releaseNotes);
} else {
emit NoUpdateAvailable();
}
}

bool Updater::IsNewerVersion(const QString &remote, const QString &local) {
QVersionNumber remoteVer = QVersionNumber::fromString(remote);
QVersionNumber localVer = QVersionNumber::fromString(local);
return remoteVer > localVer;
}

QString Updater::ExtractPlatformDownloadUrl(const QJsonObject &release) {
QJsonArray assets = release.value("assets").toArray();

// 根据平台匹配文件名关键字
QString keyword;
#ifdef Q_OS_WIN
keyword = "win";
#elif defined(Q_OS_MACOS)
keyword = "macos";
#elif defined(Q_OS_LINUX)
keyword = "linux";
#endif

// 优先匹配平台关键字,其次匹配通用安装包
for (const QJsonValue &val : assets) {
QJsonObject asset = val.toObject();
QString name = asset.value("name").toString().toLower();
if (!keyword.isEmpty() && name.contains(keyword)) {
return asset.value("browser_download_url").toString();
}
}

// 回退:找 dmg / exe / msi / AppImage
for (const QJsonValue &val : assets) {
QJsonObject asset = val.toObject();
QString name = asset.value("name").toString().toLower();
if (name.endsWith(".dmg") || name.endsWith(".exe") ||
name.endsWith(".msi") || name.endsWith(".appimage")) {
return asset.value("browser_download_url").toString();
}
}

// 最后回退到 release 页面
return release.value("html_url").toString();
}
Loading