diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 30a7a9fa..d5cc381f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 ) @@ -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 ) diff --git a/src/builder/include/open_converter.h b/src/builder/include/open_converter.h index 01857a05..c879f640 100644 --- a/src/builder/include/open_converter.h +++ b/src/builder/include/open_converter.h @@ -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 { @@ -94,6 +95,11 @@ private slots: void OnQueueButtonClicked(); void SlotLogToggled(bool checked); void SlotAbout(); + void SlotCheckForUpdates(); + void OnUpdateAvailable(const QString ¤tVer, const QString &latestVer, + const QString &downloadUrl, const QString &releaseNotes); + void OnNoUpdateAvailable(); + void OnUpdateCheckFailed(const QString &errorMsg); private: Ui::OpenConverter *ui; @@ -122,6 +128,7 @@ private slots: QPushButton *queueButton; SharedData *sharedData; BatchQueueDialog *batchQueueDialog; + Updater *updater; void LoadLanguage(const QString &rLanguage); void HandleConverterResult(bool flag); diff --git a/src/builder/src/open_converter.cpp b/src/builder/src/open_converter.cpp index 6ff6b020..d765c4fc 100644 --- a/src/builder/src/open_converter.cpp +++ b/src/builder/src/open_converter.cpp @@ -46,6 +46,8 @@ #include #include #include +#include +#include #include "../../common/include/encode_parameter.h" #include "../../common/include/info.h" @@ -125,6 +127,16 @@ OpenConverter::OpenConverter(QWidget *parent) ui->action_enableLog->setChecked(checked); }); + QSettings settings(m_settingsPath, QSettings::IniFormat); + QCheckBox *autoUpdateCheckBox = new QCheckBox(tr("Check for updates on startup"), settingsDialog); + 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); @@ -151,6 +163,12 @@ OpenConverter::OpenConverter(QWidget *parent) // 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); + 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); @@ -171,6 +189,15 @@ OpenConverter::OpenConverter(QWidget *parent) // 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"); @@ -266,6 +293,14 @@ OpenConverter::OpenConverter(QWidget *parent) 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) { @@ -655,4 +690,33 @@ void OpenConverter::SlotAbout() { aboutDialog->exec(); } +void OpenConverter::SlotCheckForUpdates() { + updater->CheckForUpdates(false); +} + +void OpenConverter::OnUpdateAvailable(const QString ¤tVer, 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" diff --git a/src/common/include/updater.h b/src/common/include/updater.h new file mode 100644 index 00000000..4085b63e --- /dev/null +++ b/src/common/include/updater.h @@ -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 +#include +#include +#include +#include + +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 ¤tVer, 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 diff --git a/src/common/src/updater.cpp b/src/common/src/updater.cpp new file mode 100644 index 00000000..d20e1eb1 --- /dev/null +++ b/src/common/src/updater.cpp @@ -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 +#include +#include +#include +#include + +static const char *kGitHubApiUrl = + "https://api.github.com/repos/OpenConverterLab/OpenConverter/releases/latest"; + +Updater::Updater(QObject *parent) + : QObject(parent), m_manager(new QNetworkAccessManager(this)), m_silent(true) { + connect(m_manager, &QNetworkAccessManager::finished, + this, &Updater::OnReleaseInfoReceived); +} + +Updater::~Updater() = default; + +void Updater::CheckForUpdates(bool silent) { + m_silent = silent; + QNetworkRequest request(QUrl(kGitHubApiUrl)); + 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(); +}