diff --git a/src/gui/ALCheckUpdateDialog.py b/src/gui/ALCheckUpdateDialog.py
new file mode 100644
index 0000000..99494e1
--- /dev/null
+++ b/src/gui/ALCheckUpdateDialog.py
@@ -0,0 +1,175 @@
+# -*- coding: utf-8 -*-
+"""
+Copyright (c) 2026 KenanZhu.
+All rights reserved.
+
+This software is provided "as is", without any warranty of any kind.
+You may use, modify, and distribute this file under the terms of the MIT License.
+See the LICENSE file for details.
+"""
+from PySide6.QtCore import Qt
+from PySide6.QtGui import QDesktopServices
+from PySide6.QtCore import QUrl
+from PySide6.QtWidgets import (
+ QDialog,
+ QDialogButtonBox,
+ QLabel,
+ QVBoxLayout
+)
+
+
+class ALCheckUpdateDialog(QDialog):
+ """
+ Dialog for showing update check result.
+
+ Supports two layouts:
+ - has_update=True : version comparison with green text + action buttons
+ - has_update=False: "already up-to-date" message + close button
+ """
+
+ def __init__(
+ self,
+ parent=None,
+ has_update: bool = False,
+ current_version: str = "",
+ latest_version: str = "",
+ tag_name: str = "",
+ html_url: str = ""
+ ):
+
+ super().__init__(parent)
+ self.__has_update = has_update
+ self.__html_url = html_url
+ self.__current_version = current_version
+ self.__latest_version = latest_version
+ self.__tag_name = tag_name
+
+ self.modifyUi()
+ self.connectSignals()
+
+ def modifyUi(
+ self
+ ):
+
+ self.setWindowTitle("检查更新 - AutoLibrary")
+ self.setMinimumWidth(360)
+ Layout = QVBoxLayout(self)
+ Layout.setSpacing(12)
+ Layout.setContentsMargins(20, 20, 20, 20)
+ if self.__has_update:
+ self.buildUpdateAvailableUi(Layout)
+ else:
+ self.buildUpToDateUi(Layout)
+
+ def buildUpdateAvailableUi(
+ self,
+ layout: QVBoxLayout
+ ):
+
+ TitleLabel = QLabel()
+ TitleLabel.setStyleSheet(
+ "font-size: 14px;"
+ "font-weight: bold;"
+ )
+ TitleLabel.setTextFormat(Qt.TextFormat.RichText)
+ TitleLabel.setText(
+ f"检测到最新版本: "
+ f"{self.__current_version}"
+ f" > "
+ f"{self.__latest_version}"
+ )
+ layout.addWidget(TitleLabel)
+ InfoLabel = QLabel()
+ InfoLabel.setTextFormat(Qt.TextFormat.RichText)
+ InfoLabel.setWordWrap(True)
+ InfoLabel.setText(
+ f"发布版本: {self.__tag_name}
"
+ f"发布页面: {self.__html_url}"
+ )
+ InfoLabel.setOpenExternalLinks(True)
+ layout.addWidget(InfoLabel)
+ layout.addSpacing(8)
+ self.__button_box = QDialogButtonBox()
+ self.__btn_github = self.__button_box.addButton(
+ "前往 GitHub",
+ QDialogButtonBox.ButtonRole.AcceptRole
+ )
+ self.__btn_download = self.__button_box.addButton(
+ "官网下载",
+ QDialogButtonBox.ButtonRole.ActionRole
+ )
+ self.__btn_cancel = self.__button_box.addButton(
+ "取消",
+ QDialogButtonBox.ButtonRole.RejectRole
+ )
+ layout.addWidget(self.__button_box)
+
+ def buildUpToDateUi(
+ self,
+ layout: QVBoxLayout
+ ):
+
+ TitleLabel = QLabel()
+ TitleLabel.setStyleSheet(
+ "font-size: 14px;"
+ "font-weight: bold;"
+ )
+ TitleLabel.setText(f"已是最新版本 !")
+ layout.addWidget(TitleLabel)
+ InfoLabel = QLabel()
+ InfoLabel.setText(f"当前版本: {self.__current_version}")
+ layout.addWidget(InfoLabel)
+ layout.addStretch()
+ self.__button_box = QDialogButtonBox()
+ self.__btn_close = self.__button_box.addButton(
+ "确定",
+ QDialogButtonBox.ButtonRole.AcceptRole
+ )
+ layout.addWidget(self.__button_box)
+
+ def connectSignals(
+ self
+ ):
+
+ if self.__has_update:
+ self.__btn_github.clicked.connect(self.onGoToGitHub)
+ self.__btn_download.clicked.connect(self.onGoToDownload)
+ self.__btn_cancel.clicked.connect(self.reject)
+ else:
+ self.__btn_close.clicked.connect(self.accept)
+
+ def onGoToGitHub(
+ self
+ ):
+
+ QDesktopServices.openUrl(QUrl(self.__html_url))
+ self.accept()
+
+ def onGoToDownload(
+ self
+ ):
+
+ QDesktopServices.openUrl(
+ QUrl("https://www.autolibrary.kenanzhu.com/downloads")
+ )
+ self.accept()
+
+ @staticmethod
+ def showResult(
+ parent,
+ has_update: bool,
+ current_version: str,
+ latest_version: str = "",
+ tag_name: str = "",
+ html_url: str = ""
+ ):
+
+ dialog = ALCheckUpdateDialog(
+ parent,
+ has_update=has_update,
+ current_version=current_version,
+ latest_version=latest_version,
+ tag_name=tag_name,
+ html_url=html_url
+ )
+ dialog.exec()
diff --git a/src/gui/ALCheckUpdateWorker.py b/src/gui/ALCheckUpdateWorker.py
new file mode 100644
index 0000000..3ce0e9d
--- /dev/null
+++ b/src/gui/ALCheckUpdateWorker.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+"""
+Copyright (c) 2026 KenanZhu.
+All rights reserved.
+
+This software is provided "as is", without any warranty of any kind.
+You may use, modify, and distribute this file under the terms of the MIT License.
+See the LICENSE file for details.
+"""
+import requests
+
+from PySide6.QtCore import (
+ QThread,
+ Signal
+)
+
+
+class ALCheckUpdateWorker(QThread):
+ """
+ Worker thread for checking latest release from GitHub API.
+ """
+
+ checkUpdateWorkerIsFinished = Signal(dict)
+ checkUpdateWorkerFinishedWithError = Signal(str)
+
+ def __init__(
+ self,
+ parent=None
+ ):
+
+ super().__init__(parent)
+ self.__api_url = "https://api.github.com/repos/KenanZhu/AutoLibrary/releases/latest"
+
+ def run(
+ self
+ ):
+
+ try:
+ response = requests.get(self.__api_url, timeout=10)
+ response.raise_for_status()
+ data = response.json()
+ self.checkUpdateWorkerIsFinished.emit({
+ "tag_name": data.get("tag_name", ""),
+ "name": data.get("name", ""),
+ "html_url": data.get("html_url", ""),
+ "body": data.get("body", ""),
+ })
+ except requests.RequestException as e:
+ self.checkUpdateWorkerFinishedWithError.emit(f"网络请求失败: \n{e}")
+ except Exception as e:
+ self.checkUpdateWorkerFinishedWithError.emit(f"检查更新时发生未知错误: \n{e}")
diff --git a/src/gui/ALMainWindow.py b/src/gui/ALMainWindow.py
index bda5133..46af84a 100644
--- a/src/gui/ALMainWindow.py
+++ b/src/gui/ALMainWindow.py
@@ -9,6 +9,8 @@ See the LICENSE file for details.
"""
import queue
+import packaging.version as ver
+
from PySide6.QtCore import (
QTimer,
QUrl,
@@ -32,12 +34,15 @@ from PySide6.QtWidgets import (
from base.MsgBase import MsgBase
from gui.ALAboutDialog import ALAboutDialog
from gui.ALBulletinDialog import ALBulletinDialog
+from gui.ALCheckUpdateWorker import ALCheckUpdateWorker
+from gui.ALCheckUpdateDialog import ALCheckUpdateDialog
from gui.ALConfigWidget import ALConfigWidget
from gui.ALSettingsWidget import ALSettingsWidget
from gui.ALTimerTaskManageWidget import ALTimerTaskManageWidget
from gui.ALMainWorker import AutoLibWorker
from gui.ALBulletinPoller import ALBulletinPoller
from gui.ALTimerTaskPoller import ALTimerTaskPoller
+from gui.ALVersionInfo import AL_VERSION
from gui.resources import ALResource
from gui.resources.ui.Ui_ALMainWindow import Ui_ALMainWindow
from managers.bulletin.BulletinManager import instance as bulletinInstance
@@ -86,10 +91,10 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.setWindowIcon(self.Icon)
self.MessageIOTextEdit.setFont(QFont("Courier New", 10))
self.ManualAction.triggered.connect(self.onManualActionTriggered)
+ self.CheckUpdateAction.triggered.connect(self.onCheckUpdateActionTriggered)
self.AboutAction.triggered.connect(self.onAboutActionTriggered)
self.SettingsAction.triggered.connect(self.onSettingsActionTriggered)
- if hasattr(self, 'BulletinAction'):
- self.BulletinAction.triggered.connect(self.onBulletinActionTriggered)
+ self.BulletinAction.triggered.connect(self.onBulletinActionTriggered)
# initialize timer task widget, but not show it
try:
self.__ALTimerTaskManageWidget = ALTimerTaskManageWidget(self)
@@ -112,20 +117,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.__ALTimerTaskManageWidget.timerTaskManageWidgetIsClosed.connect(self.onTimerTaskManageWidgetClosed)
self.__ALTimerTaskManageWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint)
- def onAboutActionTriggered(
- self
- ):
-
- AboutDialog = ALAboutDialog(self)
- AboutDialog.exec()
-
- def onManualActionTriggered(
- self
- ):
-
- url = QUrl("https://manuals.autolibrary.kenanzhu.com")
- QDesktopServices.openUrl(url)
-
def setupTray(
self
):
@@ -161,14 +152,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
2000
)
- def onTrayIconActivated(
- self,
- reason: QSystemTrayIcon.ActivationReason
- ):
-
- if reason == QSystemTrayIcon.DoubleClick:
- self.showNormal()
-
def connectSignals(
self
):
@@ -315,6 +298,67 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
f"定时任务 {timer_task['name']} 执行{'失败' if is_error else '完成'}, uuid: {timer_task['uuid']}"
)
+ @Slot(dict)
+ def onCheckUpdateIsFinished(
+ self,
+ data: dict
+ ):
+
+ worker = self.sender()
+ if worker is not self.__check_update_worker:
+ return
+ worker.checkUpdateWorkerIsFinished.disconnect(self.onCheckUpdateIsFinished)
+ worker.checkUpdateWorkerFinishedWithError.disconnect(self.onCheckUpdateFinishedWithError)
+ worker.wait(3000)
+ worker.deleteLater()
+ self.__check_update_worker = None
+ tag_name = data.get("tag_name", "")
+ html_url = data.get("html_url", "")
+ latest_version = tag_name.lstrip("v")
+ try:
+ local_ver = ver.Version(AL_VERSION)
+ remote_ver = ver.Version(latest_version)
+ except ver.InvalidVersion:
+ self._showTrace("版本号解析失败, 无法比较版本", self.TraceLevel.WARNING)
+ return
+ if remote_ver > local_ver:
+ ALCheckUpdateDialog.showResult(
+ self,
+ has_update=True,
+ current_version=AL_VERSION,
+ latest_version=latest_version,
+ tag_name=tag_name,
+ html_url=html_url
+ )
+ else:
+ ALCheckUpdateDialog.showResult(
+ self,
+ has_update=False,
+ current_version=AL_VERSION
+ )
+ self._showLog("检查更新完成")
+
+ @Slot(str)
+ def onCheckUpdateFinishedWithError(
+ self,
+ error_message: str
+ ):
+
+ worker = self.sender()
+ if worker is not self.__check_update_worker:
+ return
+ worker.checkUpdateWorkerIsFinished.disconnect(self.onCheckUpdateIsFinished)
+ worker.checkUpdateWorkerFinishedWithError.disconnect(self.onCheckUpdateFinishedWithError)
+ worker.wait(3000)
+ worker.deleteLater()
+ self.__check_update_worker = None
+ QMessageBox.warning(
+ self,
+ "检查更新 - AutoLibrary",
+ f"检查更新失败: \n{error_message}",
+ )
+ self._showLog("检查更新失败")
+
@Slot()
def onBulletinDialogClosed(
self
@@ -358,15 +402,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.setControlButtons(True, None, None)
self._showLog("配置窗口已关闭,配置文件路径已更新")
- @Slot()
- def onTrayMessageClicked(
- self
- ):
-
- if self.__notification_type == "bulletin":
- self.__notification_type = ""
- self.onBulletinActionTriggered()
-
@Slot()
def onBulletinActionTriggered(
self
@@ -396,6 +431,53 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.SettingsAction.setEnabled(False)
self._showLog("打开全局设置窗口")
+ @Slot()
+ def onAboutActionTriggered(
+ self
+ ):
+
+ AboutDialog = ALAboutDialog(self)
+ AboutDialog.exec()
+
+ @Slot()
+ def onManualActionTriggered(
+ self
+ ):
+
+ url = QUrl("https://manuals.autolibrary.kenanzhu.com")
+ QDesktopServices.openUrl(url)
+
+ @Slot()
+ def onCheckUpdateActionTriggered(
+ self
+ ):
+
+ if hasattr(self, '__check_update_worker') and self.__check_update_worker is not None:
+ return
+ self.__check_update_worker = ALCheckUpdateWorker(self)
+ self.__check_update_worker.checkUpdateWorkerIsFinished.connect(self.onCheckUpdateIsFinished)
+ self.__check_update_worker.checkUpdateWorkerFinishedWithError.connect(self.onCheckUpdateFinishedWithError)
+ self.__check_update_worker.start()
+ self._showLog("正在检查更新...")
+
+ @Slot()
+ def onTrayMessageClicked(
+ self
+ ):
+
+ if self.__notification_type == "bulletin":
+ self.__notification_type = ""
+ self.onBulletinActionTriggered()
+
+ @Slot(QSystemTrayIcon.ActivationReason)
+ def onTrayIconActivated(
+ self,
+ reason: QSystemTrayIcon.ActivationReason
+ ):
+
+ if reason == QSystemTrayIcon.DoubleClick:
+ self.showNormal()
+
@Slot()
def onTimerTaskManageWidgetButtonClicked(
self
diff --git a/src/gui/resources/ui/ALMainWindow.ui b/src/gui/resources/ui/ALMainWindow.ui
index 84aa38c..e075fc0 100644
--- a/src/gui/resources/ui/ALMainWindow.ui
+++ b/src/gui/resources/ui/ALMainWindow.ui
@@ -286,6 +286,7 @@ font: 700 9pt;
工具
+
@@ -310,6 +312,11 @@ font: 700 9pt;
在线手册
+
+
+ 检查更新
+
+
关于
@@ -320,6 +327,11 @@ font: 700 9pt;
全局设置
+
+
+ 公告栏
+
+
diff --git a/src/gui/resources/ui/ALSettingsWidget.ui b/src/gui/resources/ui/ALSettingsWidget.ui
index 3da0cea..4eed503 100644
--- a/src/gui/resources/ui/ALSettingsWidget.ui
+++ b/src/gui/resources/ui/ALSettingsWidget.ui
@@ -113,6 +113,9 @@
-
+
+ 0
+
QFrame::Shape::NoFrame
@@ -458,8 +461,8 @@
0
0
- 397
- 434
+ 409
+ 383
@@ -577,15 +580,15 @@
25
+
+ 分钟
+
1
1440
-
- 分钟
-
-
@@ -624,6 +627,18 @@
-
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+