1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-06-17 23:13:03 +08:00

feat(gui): 新增 ALStatusLabel 状态标签组件和浏览器驱动下载对话框

This commit is contained in:
2026-03-21 00:55:02 +08:00
parent 84cff6acc3
commit afa1d39051
2 changed files with 764 additions and 0 deletions
+246
View File
@@ -0,0 +1,246 @@
from enum import Enum
from PySide6.QtWidgets import (
QLabel
)
from PySide6.QtCore import (
Qt, Property, QPropertyAnimation, QEasingCurve
)
from PySide6.QtGui import (
QPainter, QColor, QConicalGradient, QPalette
)
class ALStatusLabel(QLabel):
class Status(Enum):
"""
Enum class for representing the status of ALStatusLabel.
"""
WAITING = 0
RUNNING = 1
SUCCESS = 2
WARNING = 3
FAILURE = 4
def __init__(
self,
parent = None
):
super().__init__(parent)
self.__status = self.Status.WAITING
self.__icon_angle = 0
self.setupUi()
def setupUi(
self
):
self.setFixedSize(36, 36)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.RunningAnimation = QPropertyAnimation(self, b"iconAngle")
self.RunningAnimation.setDuration(1000)
self.RunningAnimation.setStartValue(0)
self.RunningAnimation.setEndValue(-360)
self.RunningAnimation.setLoopCount(-1)
self.RunningAnimation.setEasingCurve(QEasingCurve.Type.Linear)
def isDarkMode(
self
) -> bool:
return self.palette().color(QPalette.ColorRole.Window).value() < 128
def getMarkColor(
self
) -> QColor:
return QColor("#FFFFFF") if self.isDarkMode() else QColor("#454545")
@Property(Status)
def status(
self
) -> Status:
return self.__status
@Property(int)
def iconAngle(
self
) -> int:
return self.__icon_angle
@status.setter
def status(
self,
status: Status
):
if status not in self.Status:
raise ValueError(f"Invalid (class)Status[enum.Enum] value: {status}")
self.__status = status
if self.__status == self.Status.RUNNING:
self.RunningAnimation.start()
else:
self.RunningAnimation.stop()
self.update()
@iconAngle.setter
def iconAngle(
self,
value: int
):
self.__icon_angle = value
self.update()
def paintEvent(
self,
event
):
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
center_x = self.width()/2
center_y = self.height()/2
radius = min(center_x, center_y) - 3
match self.__status:
case self.Status.WAITING:
pen = painter.pen()
pen.setWidth(2)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setColor(QColor("#969696")) # grey
painter.setPen(pen)
painter.drawEllipse(
int(center_x - radius),
int(center_y - radius),
int(radius*2),
int(radius*2)
)
case self.Status.RUNNING:
gradient = QConicalGradient(center_x, center_y, self.__icon_angle)
gradient.setColorAt(0.0, QColor("#2294FF" if self.isDarkMode() else "#0094FF"))
gradient.setColorAt(1.0, QColor("#2294FF00"))
pen = painter.pen()
pen.setWidth(3)
pen.setBrush(gradient)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
painter.setPen(pen)
painter.drawEllipse(
int(center_x - radius),
int(center_y - radius),
int(radius*2),
int(radius*2)
)
case self.Status.SUCCESS:
# draw the success green circle
pen = painter.pen()
pen.setWidth(2)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setColor(QColor("#4CAF50" if self.isDarkMode() else "#00AF50")) # green
painter.setPen(pen)
painter.drawEllipse(
int(center_x - radius),
int(center_y - radius),
int(radius*2),
int(radius*2)
)
# draw the success check mark '✓'
painter.setPen(Qt.PenStyle.SolidLine)
pen = painter.pen()
pen.setWidth(3)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
# white when dark mode, black when light mode
pen.setColor(self.getMarkColor())
painter.setPen(pen)
mark_size = radius/2
mark_path = [
(center_x - mark_size, center_y),
(center_x - mark_size/3, center_y + mark_size/2),
(center_x + mark_size, center_y - mark_size/2)
]
painter.drawLine(
int(mark_path[0][0]),int(mark_path[0][1]),
int(mark_path[1][0]),int(mark_path[1][1])
)
painter.drawLine(
int(mark_path[1][0]),int(mark_path[1][1]),
int(mark_path[2][0]),int(mark_path[2][1])
)
case self.Status.WARNING:
# draw the warning orange circle
pen = painter.pen()
pen.setWidth(2)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setColor(QColor("#FF9800")) # orange
painter.setPen(pen)
painter.drawEllipse(
int(center_x - radius),
int(center_y - radius),
int(radius*2),
int(radius*2)
)
# draw the warning exclamation mark '!'
painter.setPen(Qt.PenStyle.SolidLine)
pen = painter.pen()
pen.setWidth(3)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
# white when dark mode, black when light mode
pen.setColor(self.getMarkColor())
painter.setPen(pen)
painter.drawLine(
int(center_x), int(center_y - radius/2),
int(center_x), int(center_y + radius/6)
)
painter.drawPoint(
int(center_x), int(center_y + radius/2)
)
case self.Status.FAILURE:
# draw the failure red circle
pen = painter.pen()
pen.setWidth(2)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setColor(QColor("#DC0000")) # red
painter.setPen(pen)
painter.drawEllipse(
int(center_x - radius),
int(center_y - radius),
int(radius*2),
int(radius*2)
)
# draw the failure cross mark '✗'
painter.setPen(Qt.PenStyle.SolidLine)
pen = painter.pen()
pen.setWidth(3)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
# white when dark mode, black when light mode
pen.setColor(self.getMarkColor())
painter.setPen(pen)
mark_size = radius/3
painter.drawLine(
int(center_x - mark_size), int(center_y - mark_size),
int(center_x + mark_size), int(center_y + mark_size)
)
painter.drawLine(
int(center_x + mark_size), int(center_y - mark_size),
int(center_x - mark_size), int(center_y + mark_size)
)
painter.end()
super().paintEvent(event)
+518
View File
@@ -0,0 +1,518 @@
# -*- 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 threading
from typing import Optional
from PySide6.QtCore import (
Qt, Slot, QThread, Signal
)
from PySide6.QtWidgets import (
QDialog, QLabel, QComboBox, QProgressBar,
QPushButton, QVBoxLayout, QHBoxLayout,
QMessageBox, QFrame, QLineEdit
)
from PySide6.QtGui import (
QCloseEvent
)
from managers.driver.WebDriverManager import (
instance as webdriver_manager_instance,
WebDriverManager, WebDriverInfo, WebDriverType
)
from gui.ALStatusLabel import ALStatusLabel
class DownloadWorker(QThread):
"""
Worker thread for downloading web drivers.
"""
progress = Signal(float, int, float, str)
finished = Signal(object, str)
error = Signal(str)
cancelled = Signal()
def __init__(
self,
driver_manager: WebDriverManager,
driver_info: WebDriverInfo
):
super().__init__()
self.__driver_manager = driver_manager
self.__driver_info = driver_info
self.__driver_path = None
self.__cancelled = False
self.__cancel_event = threading.Event()
def cancel(
self
):
"""
Cancel the download operation.
"""
self.__cancelled = True
self.__cancel_event.set()
def run(
self
):
try:
if self.__cancelled:
self.cancelled.emit()
return
self.__driver_path = self.__driver_manager.installDriver(
self.__driver_info,
progress_callback=self.onProgress,
cancel_event=self.__cancel_event
)
if self.__cancelled:
self.cancelled.emit()
return
if self.__driver_path:
self.finished.emit(self.__driver_path, "")
else:
self.error.emit("下载失败: 未返回有效路径")
except Exception as e:
if not self.__cancelled:
self.error.emit(f"下载失败: {str(e)}")
def onProgress(
self,
downloaded: float,
total: int,
speed: float,
message: str
):
if self.__cancel_event.is_set():
self.__cancelled = True
if not self.__cancelled:
self.progress.emit(downloaded, total, speed, message)
class ALWebDriverDownloadDialog(QDialog):
def __init__(
self,
parent: Optional[QDialog] = None,
driver_dir: str = ""
):
"""
Web driver download dialog.
Args:
parent: Parent widget.
driver_dir: Driver directory path.
"""
super().__init__(parent)
self.__driver_dir = driver_dir
self.__driver_manager: Optional[WebDriverManager] = None
self.__confirmed = False
self.__selected_driver_info: Optional[WebDriverInfo] = None
self.__driver_infos: list[WebDriverInfo] = []
self.__download_thread: Optional[DownloadWorker] = None
self.setupUi()
self.connectSignals()
self.initializeDriverManager()
self.refreshDriverList()
def showEvent(
self,
event
):
result = super().showEvent(event)
if self.parent():
screen_rect = self.screen().geometry()
target_pos = self.parent().geometry().center()
target_pos.setX(target_pos.x() - self.width()//2)
target_pos.setY(target_pos.y() - self.height()//2)
if target_pos.x() < 0:
target_pos.setX(0)
if target_pos.x() + self.width() > screen_rect.width():
target_pos.setX(screen_rect.width() - self.width())
if target_pos.y() < 0:
target_pos.setY(0)
if target_pos.y() + self.height() > screen_rect.height():
target_pos.setY(screen_rect.height() - self.height())
self.move(target_pos)
return result
def setupUi(
self
):
self.setModal(True)
self.setMaximumHeight(240)
self.setMinimumHeight(240)
self.setWindowTitle("浏览器驱动下载 - AutoLibrary")
self.MainLayout = QVBoxLayout(self)
self.MainLayout.setContentsMargins(5, 5, 5, 5)
self.MainLayout.setSpacing(5)
self.BrowserCountLabel = QLabel("检测到 0 个可用浏览器:")
self.MainLayout.addWidget(self.BrowserCountLabel)
self.DriverInfoLayout = QHBoxLayout()
self.DriverInfoLayout.setSpacing(5)
self.DriverComboBox = QComboBox()
self.DriverInfoLayout.addWidget(self.DriverComboBox)
self.StatusLabel = ALStatusLabel()
self.StatusLabel.setFixedSize(32, 32)
self.DriverInfoLayout.addWidget(self.StatusLabel)
self.MainLayout.addLayout(self.DriverInfoLayout)
self.DetailLayout = QVBoxLayout()
self.DetailLayout.setSpacing(5)
self.DetailLayout.setContentsMargins(5, 5, 5, 5)
self.BrowserTypeLabel = QLabel("类型:")
self.DetailLayout.addWidget(self.BrowserTypeLabel)
self.VersionLabel = QLabel("版本:")
self.DetailLayout.addWidget(self.VersionLabel)
self.PathLabel = QLineEdit()
self.PathLabel.setReadOnly(True)
self.PathLabel.setText("路径:未安装")
self.DetailLayout.addWidget(self.PathLabel)
self.MainLayout.addLayout(self.DetailLayout)
self.Line = QFrame()
self.Line.setFrameShape(QFrame.Shape.HLine)
self.Line.setFrameShadow(QFrame.Shadow.Sunken)
self.MainLayout.addWidget(self.Line)
self.ProgressBar = QProgressBar()
self.ProgressBar.setValue(0)
self.ProgressBar.setTextVisible(False)
self.MainLayout.addWidget(self.ProgressBar)
self.ProgressText = QLabel("")
self.ProgressText.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.MainLayout.addWidget(self.ProgressText)
self.ControlLayout = QHBoxLayout()
self.ControlLayout.setSpacing(8)
self.ControlLayout.setAlignment(Qt.AlignmentFlag.AlignRight)
self.RefreshButton = QPushButton("刷新")
self.RefreshButton.setFixedSize(80, 25)
self.DownloadButton = QPushButton("下载驱动")
self.DownloadButton.setFixedSize(80, 25)
self.DeleteButton = QPushButton("删除驱动")
self.DeleteButton.setFixedSize(80, 25)
self.CancelButton = QPushButton("取消")
self.CancelButton.setFixedSize(80, 25)
self.ConfirmButton = QPushButton("确认")
self.ConfirmButton.setFixedSize(80, 25)
self.ConfirmButton.setEnabled(False)
self.ControlLayout.addWidget(self.RefreshButton)
self.ControlLayout.addWidget(self.DownloadButton)
self.ControlLayout.addWidget(self.DeleteButton)
self.ControlLayout.addWidget(self.CancelButton)
self.ControlLayout.addWidget(self.ConfirmButton)
self.MainLayout.addLayout(self.ControlLayout)
def connectSignals(
self
):
self.RefreshButton.clicked.connect(self.onRefreshButtonClicked)
self.DownloadButton.clicked.connect(self.onDownloadButtonClicked)
self.DeleteButton.clicked.connect(self.onDeleteButtonClicked)
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
self.DriverComboBox.currentIndexChanged.connect(self.onDriverComboBoxChanged)
def initializeDriverManager(
self
):
try:
self.__driver_manager = webdriver_manager_instance(self.__driver_dir)
except ValueError as e:
QMessageBox.warning(self, "初始化失败", f"WebDriverManager 初始化失败:\n{str(e)}")
self.reject()
def refreshDriverList(
self
):
if not self.__driver_manager:
return
self.__driver_manager.refresh()
self.__driver_infos = self.__driver_manager.getDriverInfos()
self.DriverComboBox.clear()
for driver_info in self.__driver_infos:
display_text = f"{driver_info.driver_type.value} - {driver_info.browser_version}"
self.DriverComboBox.addItem(display_text)
count = len(self.__driver_infos)
self.BrowserCountLabel.setText(f"检测到 {count} 个可用浏览器:")
if self.__driver_infos:
self.onDriverComboBoxChanged(0)
def onDriverComboBoxChanged(
self,
index: int
):
if not self.__driver_infos or index < 0 or index >= len(self.__driver_infos):
return
driver_info = self.__driver_infos[index]
self.updateDriverInfoDisplay(driver_info)
self.updateButtonStates(driver_info)
@Slot()
def onRefreshButtonClicked(
self
):
self.refreshDriverList()
@Slot()
def onDeleteButtonClicked(
self
):
index = self.DriverComboBox.currentIndex()
if index < 0 or index >= len(self.__driver_infos):
return
driver_info = self.__driver_infos[index]
if driver_info.driver_status.name != "INSTALLED":
QMessageBox.information(self, "提示 - AutoLibrary", "该驱动未安装, 无需删除")
return
reply = QMessageBox.question(
self,
"确认删除 - AutoLibrary",
f"确定要删除 {driver_info.driver_type.value} 驱动吗 ?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.No:
return
try:
self.__driver_manager.uninstallDriver(driver_info)
self.refreshDriverList()
QMessageBox.information(self, "删除成功 - AutoLibrary", "驱动已成功删除")
except Exception as e:
QMessageBox.critical(self, "删除失败 - AutoLibrary", f"删除驱动时出错:\n{str(e)}")
@Slot()
def onDownloadButtonClicked(
self
):
index = self.DriverComboBox.currentIndex()
if index < 0 or index >= len(self.__driver_infos):
return
driver_info = self.__driver_infos[index]
if driver_info.driver_status.name == "INSTALLED":
return
self.StatusLabel.status = ALStatusLabel.Status.RUNNING
self.DownloadButton.setEnabled(False)
self.RefreshButton.setEnabled(False)
self.DriverComboBox.setEnabled(False)
self.ProgressBar.setValue(0)
self.ProgressText.setText("正在下载驱动...")
self.__download_thread = DownloadWorker(self.__driver_manager, driver_info)
self.__download_thread.progress.connect(self.onDownloadProgress)
self.__download_thread.finished.connect(self.onDownloadFinished)
self.__download_thread.error.connect(self.onDownloadError)
self.__download_thread.cancelled.connect(self.onDownloadCancelled)
self.__download_thread.start()
@Slot()
def onDownloadProgress(
self,
downloaded: float,
total: int,
speed: float,
message: str
):
progress = downloaded
self.ProgressBar.setValue(progress)
if speed >= 1024:
speed_text = f"{speed/1024:.1f} MB/s"
else:
speed_text = f"{speed:.1f} KB/s"
progress_text = f"{message}... {downloaded:.1f}% - {speed_text}"
self.ProgressText.setText(progress_text)
@Slot()
def onDownloadFinished(
self
):
self.ProgressBar.setValue(100)
self.ProgressText.setText("下载完成 !")
self.StatusLabel.status = ALStatusLabel.Status.SUCCESS
index = self.DriverComboBox.currentIndex()
if 0 <= index < len(self.__driver_infos):
driver_info = self.__driver_infos[index]
self.updateDriverInfoDisplay(driver_info)
self.__download_thread = None
self.ConfirmButton.setEnabled(True)
self.DownloadButton.setEnabled(False)
self.RefreshButton.setEnabled(True)
self.DriverComboBox.setEnabled(True)
self.DeleteButton.setEnabled(True)
@Slot()
def onDownloadError(
self,
error_message: str
):
self.StatusLabel.status = ALStatusLabel.Status.FAILURE
QMessageBox.critical(self, "下载失败 - AutoLibrary", error_message)
self.DownloadButton.setEnabled(True)
self.RefreshButton.setEnabled(True)
self.DriverComboBox.setEnabled(True)
self.CancelButton.setEnabled(True)
@Slot()
def onDownloadCancelled(
self
):
if self.__download_thread:
self.__download_thread.wait(3000)
self.__download_thread = None
index = self.DriverComboBox.currentIndex()
if 0 <= index < len(self.__driver_infos):
driver_info = self.__driver_infos[index]
self.__driver_manager.cancelDriverDownload(driver_info)
self.updateDriverInfoDisplay(driver_info)
self.ProgressText.setText("下载已取消")
self.ProgressBar.setValue(0)
self.StatusLabel.status = ALStatusLabel.Status.WAITING
self.DownloadButton.setEnabled(True)
self.RefreshButton.setEnabled(True)
self.DriverComboBox.setEnabled(True)
self.CancelButton.setEnabled(True)
self.DeleteButton.setEnabled(False)
@Slot()
def onConfirmButtonClicked(
self
):
index = self.DriverComboBox.currentIndex()
if index < 0 or index >= len(self.__driver_infos):
return
driver_info = self.__driver_infos[index]
if driver_info.driver_status.name != "INSTALLED":
return
self.__selected_driver_info = driver_info
self.__confirmed = True
self.accept()
@Slot()
def onCancelButtonClicked(
self
):
if self.__download_thread:
reply = QMessageBox.question(
self,
"确认取消 - AutoLibrary",
"正在下载中, 确定要取消下载吗 ?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self.__download_thread.cancel()
else:
self.__confirmed = False
self.__selected_driver_info = None
self.reject()
def closeEvent(
self,
event: QCloseEvent
):
if self.__download_thread and self.__download_thread.isRunning():
reply = QMessageBox.question(
self,
"确认关闭 - AutoLibrary",
"驱动正在下载中, 确定要取消并关闭对话框吗 ?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.No:
event.ignore()
return
self.__download_thread.cancel()
self.__download_thread.wait(5000)
if not self.__confirmed:
self.__selected_driver_info = None
event.accept()
super().closeEvent(event)
def getSelectedDriverInfo(
self
) -> Optional[WebDriverInfo]:
return self.__selected_driver_info
def updateDriverInfoDisplay(
self,
driver_info: WebDriverInfo
):
if driver_info.driver_type == WebDriverType.CHROME:
driver_type = "Google Chrome"
elif driver_info.driver_type == WebDriverType.FIREFOX:
driver_type = "Mozilla Firefox"
elif driver_info.driver_type == WebDriverType.EDGE:
driver_type = "Microsoft Edge"
else:
driver_type = "未知"
self.BrowserTypeLabel.setText(f"类型:{driver_type}")
self.VersionLabel.setText(f"版本:{driver_info.driver_version}")
if driver_info.driver_path:
self.PathLabel.setText(str(driver_info.driver_path))
else:
self.PathLabel.setText("未安装")
match driver_info.driver_status.name:
case "NOT_INSTALLED":
self.StatusLabel.status = ALStatusLabel.Status.WAITING
case "INSTALLED":
self.StatusLabel.status = ALStatusLabel.Status.SUCCESS
case "DOWNLOADING":
self.StatusLabel.status = ALStatusLabel.Status.RUNNING
case "ERROR":
self.StatusLabel.status = ALStatusLabel.Status.FAILURE
def updateButtonStates(
self,
driver_info: WebDriverInfo
):
if driver_info.driver_status.name == "INSTALLED":
self.DownloadButton.setEnabled(False)
self.DeleteButton.setEnabled(True)
self.ConfirmButton.setEnabled(True)
else:
self.DeleteButton.setEnabled(False)
self.DownloadButton.setEnabled(True)
self.ConfirmButton.setEnabled(False)