From d59ecc3090941cbfd981ddecf0fd06a93b2b6f5c Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Thu, 9 Jul 2026 09:06:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(update):=20=E6=96=B0=E5=A2=9E=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E6=9B=B4=E6=96=B0=E5=8A=9F=E8=83=BD=E4=B8=8E=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E6=A0=8F=E5=85=AC=E5=91=8A=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/ALCheckUpdateDialog.py | 175 +++++++++++++++++++++++ src/gui/ALCheckUpdateWorker.py | 51 +++++++ src/gui/ALMainWindow.py | 148 ++++++++++++++----- src/gui/resources/ui/ALMainWindow.ui | 12 ++ src/gui/resources/ui/ALSettingsWidget.ui | 25 +++- 5 files changed, 373 insertions(+), 38 deletions(-) create mode 100644 src/gui/ALCheckUpdateDialog.py create mode 100644 src/gui/ALCheckUpdateWorker.py 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; 工具 + @@ -295,6 +296,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 + +