diff --git a/.gitignore b/.gitignore index c676a0b..76be2ea 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,8 @@ src/gui/AutoLibraryResources.py src/gui/AutoLibraryResource.py src/gui/Ui_ALMainWindow.py src/gui/Ui_ALConfigWidget.py +src/gui/Ui_ALTimerTaskWidget.py +src/gui/Ui_ALAddTimerTaskDialog.py +src/gui/Ui_ALAboutDialog.py + Main.spec diff --git a/src/gui/ALAboutDialog.py b/src/gui/ALAboutDialog.py new file mode 100644 index 0000000..7a52e94 --- /dev/null +++ b/src/gui/ALAboutDialog.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +""" +Copyright (c) 2025 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 sys +import platform + +from PySide6.QtGui import ( + QIcon +) +from PySide6.QtWidgets import ( + QDialog, QApplication +) +from PySide6.QtCore import ( + QTimer, Qt +) + +from gui.AppInfo import AL_VERSION +from gui.Ui_ALAboutDialog import Ui_ALAboutDialog + +from gui import AutoLibraryResource + + +class ALAboutDialog(QDialog, Ui_ALAboutDialog): + + def __init__( + self, + parent=None + ): + super().__init__(parent) + + self.setupUi(self) + self.modifyUi() + self.connectSignals() + + + def modifyUi( + self + ): + + self.LogoIconLabel.setPixmap(QIcon(":/res/icon/icons/AutoLibrary.ico").pixmap(48, 48)) + info_text = self.generateAboutText() + self.AboutInfoEdit.setHtml(info_text) + self.AboutInfoEdit.setTextInteractionFlags(Qt.TextBrowserInteraction) + + + def connectSignals( + self + ): + + self.CopyButton.clicked.connect(self.copyAboutInfo) + + + def generateAboutText( + self + ): + + os_info = self.getOSInfo() + about_text = f""" +

Version Information:

+Version: {AL_VERSION}
+Python version: {platform.python_version()}
+Qt version: {self.getQtVersion()}
+ +

Author Information:

+Developer: KenanZhu
+Contact: nanoki_zh@163.com
+GitHub: https://www.github.com/KenanZhu
+ +

Project Information:

+License: MIT License
+Project repository: https://www.github.com/KenanZhu/AutoLibrary
+Project website: https://www.autolibrary.cv/
+ +

System Information:

+Processor: {platform.processor()}
+Operating system: {os_info['system']}
+System version: {os_info['version']}
+System architecture: {os_info['architecture']}
+""" + return about_text + + + def getOSInfo( + self + ): + + system = platform.system() + version = platform.version() + architecture = platform.architecture()[0] + + if system == "Windows": + try: + version = platform.win32_ver()[1] + except: + pass + elif system == "Darwin": + try: + version = platform.mac_ver()[0] + except: + pass + elif system == "Linux": + try: + import distro # try to get Linux distro info + version = f"{distro.name()} {distro.version()}" + except ImportError: + pass + + return { + 'system': system, + 'version': version, + 'architecture': architecture + } + + + def getQtVersion( + self + ): + + try: + from PySide6.QtCore import qVersion + return qVersion() + except: + return "Unknown" + + + def copyAboutInfo( + self + ): + + about_text = self.AboutInfoEdit.toPlainText() + clipboard = QApplication.clipboard() + clipboard.setText(about_text) + original_text = self.CopyButton.text() + self.CopyButton.setText("已复制") + QTimer.singleShot(2000, lambda: self.CopyButton.setText(original_text)) \ No newline at end of file diff --git a/src/gui/ALAboutDialog.ui b/src/gui/ALAboutDialog.ui new file mode 100644 index 0000000..74b2b9c --- /dev/null +++ b/src/gui/ALAboutDialog.ui @@ -0,0 +1,140 @@ + + + ALAboutDialog + + + + 0 + 0 + 300 + 300 + + + + + 300 + 300 + + + + + 800 + 300 + + + + 关于 - AutoLibrary + + + + 5 + + + 5 + + + 5 + + + 5 + + + 5 + + + + + 5 + + + + + + 56 + 56 + + + + + 56 + 56 + + + + + + + true + + + 0 + + + + + + + + 24 + true + + + + 0 + + + AutoLibrary + + + 0 + + + 0 + + + + + + + + + + Courier New + false + + + + QTextEdit::LineWrapMode::NoWrap + + + true + + + Qt::TextInteractionFlag::TextBrowserInteraction + + + + + + + + 80 + 25 + + + + + 80 + 25 + + + + 复制 + + + + + + + + diff --git a/src/gui/ALAddTimerTaskDialog.py b/src/gui/ALAddTimerTaskDialog.py new file mode 100644 index 0000000..4620a4f --- /dev/null +++ b/src/gui/ALAddTimerTaskDialog.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +""" +Copyright (c) 2025 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 os +import sys +import time +import uuid +import queue + +from enum import Enum +from datetime import datetime, timedelta + +from PySide6.QtCore import ( + Qt, Signal, Slot, QDateTime +) +from PySide6.QtWidgets import ( + QLabel, QDialog, QWidget, QSpinBox, QVBoxLayout, + QHBoxLayout, QGridLayout, QDateTimeEdit +) +from PySide6.QtGui import ( + QCloseEvent +) + +from gui.Ui_ALAddTimerTaskDialog import Ui_ALAddTimerTaskDialog + + +class TimerTaskStatus(Enum): + PENDING = "等待中" + READY = "已就绪" + RUNNING = "执行中" + EXECUTED = "已执行" + OUTDATED = "已过期" + + +class ALAddTimerTaskWidget(QDialog, Ui_ALAddTimerTaskDialog): + + def __init__( + self, + parent = None + ): + + super().__init__(parent) + + self.setupUi(self) + self.connectSignals() + self.modifyUi() + + + def modifyUi( + self + ): + + self.TimerTypeComboBox.setCurrentIndex(0) + self.SpecificTimerWidget = QWidget() + self.SpecificTimerLayout = QHBoxLayout(self.SpecificTimerWidget) + self.SpecificTimerLayout.addWidget(QLabel("定时时间:")) + self.SpecificDateTimeEdit = QDateTimeEdit() + self.SpecificDateTimeEdit.setCalendarPopup(True) + self.SpecificDateTimeEdit.setDisplayFormat("yyyy-MM-dd HH:mm:ss") + self.SpecificDateTimeEdit.setMinimumDateTime(QDateTime.currentDateTime()) + self.SpecificDateTimeEdit.setDateTime(QDateTime.currentDateTime().addSecs(60)) + self.SpecificTimerLayout.addWidget(self.SpecificDateTimeEdit) + self.TimerConfigLayout.addWidget(self.SpecificTimerWidget) + + self.RelativeTimerWidget = QWidget() + self.RelativeTimerLayout = QGridLayout(self.RelativeTimerWidget) + self.RelativeTimerLayout.addWidget(QLabel("相对时间:"), 0, 0) + self.RelativeDaySpinBox = QSpinBox() + self.RelativeDaySpinBox.setMinimum(0) + self.RelativeDaySpinBox.setMaximum(365) + self.RelativeDaySpinBox.setSuffix("天") + self.RelativeTimerLayout.addWidget(self.RelativeDaySpinBox, 1, 0) + self.RelativeHourSpinBox = QSpinBox() + self.RelativeHourSpinBox.setMinimum(0) + self.RelativeHourSpinBox.setMaximum(23) + self.RelativeHourSpinBox.setSuffix("时") + self.RelativeTimerLayout.addWidget(self.RelativeHourSpinBox, 1, 1) + self.RelativeMinuteSpinBox = QSpinBox() + self.RelativeMinuteSpinBox.setMinimum(0) + self.RelativeMinuteSpinBox.setMaximum(59) + self.RelativeMinuteSpinBox.setSuffix("分") + self.RelativeTimerLayout.addWidget(self.RelativeMinuteSpinBox, 1, 2) + self.RelativeSecondSpinBox = QSpinBox() + self.RelativeSecondSpinBox.setMinimum(0) + self.RelativeSecondSpinBox.setMaximum(59) + self.RelativeSecondSpinBox.setSuffix("秒") + self.RelativeTimerLayout.addWidget(self.RelativeSecondSpinBox, 1, 3) + self.TimerConfigLayout.addWidget(self.RelativeTimerWidget) + self.RelativeTimerWidget.setVisible(False) + + + def connectSignals( + self + ): + + self.CancelButton.clicked.connect(self.reject) + self.ConfirmButton.clicked.connect(self.accept) + self.TimerTypeComboBox.currentIndexChanged.connect(self.onTimerTypeComboBoxIndexChanged) + + + def getTimerTask( + self + ) -> dict: + + added_time = datetime.now() + if not self.TaskNameLineEdit.text(): + name = f"未命名任务-{added_time.strftime("%Y%m%d%H%M%S")}" + else: + name = self.TaskNameLineEdit.text() + timer_type_index = self.TimerTypeComboBox.currentIndex() + silent = not self.ShowBeforeRunRadioButton.isChecked() + if timer_type_index == 0: + execute_time = self.SpecificDateTimeEdit.dateTime() + tmp_time_str = execute_time.toString("yyyy-MM-dd HH:mm:ss") + execute_time = datetime.strptime(tmp_time_str, "%Y-%m-%d %H:%M:%S") + else: + execute_time = datetime.now() + timedelta( + days = self.RelativeDaySpinBox.value(), + hours = self.RelativeHourSpinBox.value(), + minutes = self.RelativeMinuteSpinBox.value(), + seconds = self.RelativeSecondSpinBox.value() + ) + return { + "name": name, + "task_uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}", + "time_type": self.TimerTypeComboBox.currentText(), + "execute_time": execute_time, + "silent": silent, + "add_time": added_time, + "status": TimerTaskStatus.PENDING, + "executed": False + } + + + @Slot(int) + def onTimerTypeComboBoxIndexChanged( + self, + index: int + ): + + self.SpecificTimerWidget.setVisible(index == 0) + self.RelativeTimerWidget.setVisible(index == 1) \ No newline at end of file diff --git a/src/gui/ALAddTimerTaskDialog.ui b/src/gui/ALAddTimerTaskDialog.ui new file mode 100644 index 0000000..c0ef6db --- /dev/null +++ b/src/gui/ALAddTimerTaskDialog.ui @@ -0,0 +1,249 @@ + + + ALAddTimerTaskDialog + + + + 0 + 0 + 300 + 300 + + + + + 0 + 300 + + + + + 500 + 300 + + + + 添加定时任务 - AutoLibrary + + + true + + + true + + + + 5 + + + 5 + + + 5 + + + 5 + + + 5 + + + + + 5 + + + + + + 60 + 25 + + + + + 16777215 + 25 + + + + 任务名称: + + + + + + + + + + + + 定时设置 + + + + 5 + + + 5 + + + 5 + + + 5 + + + 5 + + + + + 5 + + + + + 定时类型: + + + + + + + + 特定时间 + + + + + 相对时间 + + + + + + + + + + + + + 运行设置 + + + + 5 + + + 5 + + + 5 + + + 5 + + + 5 + + + + + 静默运行 + + + true + + + true + + + false + + + true + + + + + + + 运行前提示 + + + + + + + + + + 5 + + + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Plain + + + 0 + + + + + + + + 80 + 25 + + + + + 80 + 25 + + + + 取消 + + + + + + + + 80 + 25 + + + + + 80 + 25 + + + + 添加 + + + true + + + true + + + + + + + + + + diff --git a/src/gui/ALConfigWidget.ui b/src/gui/ALConfigWidget.ui index 0947db5..e507a52 100644 --- a/src/gui/ALConfigWidget.ui +++ b/src/gui/ALConfigWidget.ui @@ -1829,6 +1829,9 @@ 导出配置文件 + + + @@ -1924,6 +1927,9 @@ 新建配置 + + + @@ -1943,6 +1949,9 @@ 加载配置 + + + diff --git a/src/gui/ALMainWindow.py b/src/gui/ALMainWindow.py index 466f627..6f2d5dd 100644 --- a/src/gui/ALMainWindow.py +++ b/src/gui/ALMainWindow.py @@ -13,19 +13,21 @@ import time import queue from PySide6.QtCore import ( - Qt, Signal, Slot, QDir, QFileInfo, QTimer, QThread + Qt, Signal, Slot, QDir, QFileInfo, QTimer, QThread, QUrl, ) from PySide6.QtWidgets import ( - QMainWindow, QMenu + QMainWindow, QMenu, QSystemTrayIcon ) from PySide6.QtGui import ( - QTextCursor, QCloseEvent, QFont, QIcon + QTextCursor, QCloseEvent, QFont, QIcon, QDesktopServices ) -from .Ui_ALMainWindow import Ui_ALMainWindow -from .ALConfigWidget import ALConfigWidget +from gui.Ui_ALMainWindow import Ui_ALMainWindow +from gui.ALConfigWidget import ALConfigWidget +from gui.ALTimerTaskWidget import ALTimerTaskWidget +from gui.ALAboutDialog import ALAboutDialog -from . import AutoLibraryResource +from gui import AutoLibraryResource from operators.AutoLib import AutoLib from utils.ConfigReader import ConfigReader @@ -49,7 +51,6 @@ class AutoLibWorker(QThread): self.__input_queue = input_queue self.__output_queue = output_queue self.__config_paths = config_paths - self.__stopped = False def checkTimeAvailable( @@ -110,15 +111,46 @@ class AutoLibWorker(QThread): self.finishedSignal.emit() - def stop( +class TimerTaskWorker(AutoLibWorker): + + finishedSignal_TimerWorker = Signal(dict) + + def __init__( + self, + timer_task: dict, + input_queue: queue.Queue, + output_queue: queue.Queue, + config_paths: dict + ): + + super().__init__( + input_queue, + output_queue, + config_paths, + ) + + self.__timer_task = timer_task + self.__stopped = False + + def run( self ): - self.__stopped = True + self.showTraceSignal.emit( + f"定时任务 {self.__timer_task['name']} 开始运行" + ) + super().run() + self.showTraceSignal.emit( + f"定时任务 {self.__timer_task['name']} 运行结束" + ) + self.finishedSignal_TimerWorker.emit(self.__timer_task) class ALMainWindow(QMainWindow, Ui_ALMainWindow): + timerTaskIsRunning = Signal(dict) + timerTaskIsExecuted = Signal(dict) + def __init__( self ): @@ -129,27 +161,100 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow): self.setupUi(self) self.__input_queue = queue.Queue() self.__output_queue = queue.Queue() + self.__timer_task_queue = queue.Queue() script_path = sys.executable script_dir = QFileInfo(script_path).absoluteDir() self.__config_paths = { "system": QDir.toNativeSeparators(script_dir.absoluteFilePath("system.json")), "users": QDir.toNativeSeparators(script_dir.absoluteFilePath("users.json")), } + self.__alTimerTaskWidget = None self.__alConfigWidget = None + self.__alAboutDialog = None self.__auto_lib_thread = None + self.__current_timer_task_thread = None + self.__is_running_timer_task = False self.modifyUi() + self.setupTray() self.connectSignals() self.startMsgPolling() + self.startTimerTaskPolling() def modifyUi( self ): - icon = QIcon(":/res/icon/icons/AutoLibrary.ico") - self.setWindowIcon(icon) + self.icon = QIcon(":/res/icon/icons/AutoLibrary.ico") + self.setWindowIcon(self.icon) self.MessageIOTextEdit.setFont(QFont("Courier New", 10)) + self.ManualAction.triggered.connect(self.onManualActionTriggered) + self.AboutAction.triggered.connect(self.onAboutActionTriggered) + + + def onAboutActionTriggered( + self + ): + + if self.__alAboutDialog is None: + self.__alAboutDialog = ALAboutDialog(self) + self.__alAboutDialog.show() + + + def onManualActionTriggered( + self + ): + + url = QUrl("https://www.autolibrary.cv/docs/manual_lists.html") + QDesktopServices.openUrl(url) + + + def setupTray( + self + ): + + if not QSystemTrayIcon.isSystemTrayAvailable(): + self.showTraceSignal.emit( + "系统不支持系统托盘功能, 无法创建系统托盘图标。" + ) + return + self.TrayIcon = QSystemTrayIcon(self.icon, self) + self.TrayIcon.setToolTip("AutoLibrary") + + self.TrayMenu = QMenu() + self.TrayMenu.addAction("显示主窗口", self.showNormal) + self.TrayMenu.addAction("显示定时窗口", self.onTimerTaskWidgetButtonClicked) + self.TrayMenu.addAction("最小化到托盘", self.hideToTray) + self.TrayMenu.addSeparator() + self.TrayMenu.addAction("退出", self.close) + self.TrayIcon.setContextMenu(self.TrayMenu) + + self.TrayIcon.setContextMenu(self.TrayMenu) + self.TrayIcon.activated.connect(self.onTrayIconActivated) + self.TrayIcon.show() + + + def hideToTray( + self + ): + + self.hide() + self.TrayIcon.showMessage( + "AutoLibrary", + "\n已最小化到托盘", + QSystemTrayIcon.MessageIcon.Information, + 2000 + ) + + + def onTrayIconActivated( + self, + reason: QSystemTrayIcon.ActivationReason + ): + + if reason == QSystemTrayIcon.DoubleClick: + self.showNormal() def connectSignals( @@ -157,6 +262,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow): ): self.ConfigButton.clicked.connect(self.onConfigButtonClicked) + self.TimerTaskWidgetButton.clicked.connect(self.onTimerTaskWidgetButtonClicked) self.StartButton.clicked.connect(self.onStartButtonClicked) self.StopButton.clicked.connect(self.onStopButtonClicked) self.SendButton.clicked.connect(self.onSendButtonClicked) @@ -170,8 +276,17 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow): if self.__timer and self.__timer.isActive(): self.__timer.stop() + if self.__timer_task_timer and self.__timer_task_timer.isActive(): + self.__timer_task_timer.stop() + if self.__is_running_timer_task: + self.__current_timer_task_thread.wait(2000) + self.__current_timer_task_thread.deleteLater() + if self.__alTimerTaskWidget: + self.__alTimerTaskWidget.close() + self.__alTimerTaskWidget.deleteLater() if self.__alConfigWidget: self.__alConfigWidget.close() + self.__alConfigWidget.deleteLater() super().closeEvent(event) @@ -198,6 +313,51 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow): self.__timer.start(100) + def startTimerTaskPolling( + self + ): + + self.__timer_task_timer = QTimer() + self.__timer_task_timer.timeout.connect(self.pollTimerTaskQueue) + self.__timer_task_timer.start(500) + + + def pollTimerTaskQueue( + self + ): + + if self.__is_running_timer_task: + return + try: + while not self.__is_running_timer_task: + timer_task = self.__timer_task_queue.get_nowait() + self.timerTaskIsRunning.emit(timer_task) + self.__timer_task_timer.stop() + self.__is_running_timer_task = True + self.setControlButtons(False, False, True) + if not timer_task["silent"]: + self.TrayIcon.showMessage( + "定时任务 - AutoLibrary", + f"\n已开始执行定时任务: \n{timer_task['name']}", + QSystemTrayIcon.MessageIcon.Information, + 1000 + ) + self.showNormal() + self.__current_timer_task_thread = TimerTaskWorker( + timer_task, + self.__input_queue, + self.__output_queue, + self.__config_paths + ) + self.__current_timer_task_thread.finishedSignal_TimerWorker.connect(self.onTimerTaskFinished) + self.__current_timer_task_thread.showTraceSignal.connect(self.showTrace) + self.__current_timer_task_thread.showMsgSignal.connect(self.showMsg) + self.__current_timer_task_thread.start() + except queue.Empty: + self.__is_running_timer_task = False + pass + + def setControlButtons( self, config_button_enabled: bool, @@ -238,6 +398,15 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow): except queue.Empty: pass + + @Slot() + def onTimerTaskWidgetClosed( + self + ): + + self.TimerTaskWidgetButton.setEnabled(True) + + @Slot(dict) def onConfigWidgetClosed( self, @@ -253,6 +422,55 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow): self.StopButton.setEnabled(False) self.__config_paths = config_paths + @Slot(dict) + def onTimerTaskReady( + self, + timer_task: dict + ): + + self.__timer_task_queue.put(timer_task) + + @Slot(dict) + def onTimerTaskFinished( + self, + timer_task: dict + ): + + self.__current_timer_task_thread.wait(1000) + self.__current_timer_task_thread.finishedSignal_TimerWorker.disconnect(self.onTimerTaskFinished) + self.__current_timer_task_thread.showTraceSignal.disconnect(self.showTrace) + self.__current_timer_task_thread.showMsgSignal.disconnect(self.showMsg) + self.__current_timer_task_thread.deleteLater() + self.__current_timer_task_thread = None + self.setControlButtons(True, True, False) + self.__is_running_timer_task = False + self.__timer_task_timer.start(500) + timer_task["executed"] = True + self.TrayIcon.showMessage( + "定时任务 - AutoLibrary", + f"\n定时任务 '{timer_task['name']}' 执行完成", + QSystemTrayIcon.MessageIcon.Information, + 1000 + ) + self.showTrace(f"定时任务 {timer_task['name']} 执行完成, uuid: {timer_task['task_uuid']}") + self.timerTaskIsExecuted.emit(timer_task) + + @Slot() + def onTimerTaskWidgetButtonClicked( + self + ): + if self.__alTimerTaskWidget is None: + self.__alTimerTaskWidget = ALTimerTaskWidget(self) + self.timerTaskIsRunning.connect(self.__alTimerTaskWidget.onTimerTaskIsRunning) + self.timerTaskIsExecuted.connect(self.__alTimerTaskWidget.onTimerTaskIsExecuted) + self.__alTimerTaskWidget.timerTaskReady.connect(self.onTimerTaskReady) + self.__alTimerTaskWidget.timerTaskWidgetClosed.connect(self.onTimerTaskWidgetClosed) + self.__alTimerTaskWidget.setWindowFlags(Qt.Window) + self.__alTimerTaskWidget.show() + self.__alTimerTaskWidget.raise_() + self.__alTimerTaskWidget.activateWindow() + self.TimerTaskWidgetButton.setEnabled(False) + @Slot() def onConfigButtonClicked( self @@ -281,7 +499,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow): self.__auto_lib_thread = AutoLibWorker( self.__input_queue, self.__output_queue, - self.__config_paths, + self.__config_paths ) self.__auto_lib_thread.finishedSignal.connect(self.onStopButtonClicked) self.__auto_lib_thread.showMsgSignal.connect(self.showMsg) @@ -295,8 +513,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow): if self.__auto_lib_thread: self.showTrace("正在停止操作......") - self.__auto_lib_thread.stop() - self.__auto_lib_thread.wait() + self.__auto_lib_thread.wait(2000) self.showTrace("操作已停止") self.__auto_lib_thread.showMsgSignal.disconnect(self.showMsg) self.__auto_lib_thread.showTraceSignal.disconnect(self.showTrace) diff --git a/src/gui/ALMainWindow.ui b/src/gui/ALMainWindow.ui index 7a43677..c2916f4 100644 --- a/src/gui/ALMainWindow.ui +++ b/src/gui/ALMainWindow.ui @@ -50,11 +50,33 @@ 5 + + + + + 25 + 25 + + + + + 25 + 25 + + + + + + + + + + - 1280 + 1000 0 @@ -237,6 +259,9 @@ font: 700 9pt "Microsoft YaHei UI"; 发送 + + + @@ -245,7 +270,7 @@ font: 700 9pt "Microsoft YaHei UI"; - false + true @@ -258,6 +283,17 @@ font: 700 9pt "Microsoft YaHei UI"; true + + + true + + + 帮助 + + + + + diff --git a/src/gui/ALTimerTaskWidget.py b/src/gui/ALTimerTaskWidget.py new file mode 100644 index 0000000..3920e37 --- /dev/null +++ b/src/gui/ALTimerTaskWidget.py @@ -0,0 +1,313 @@ +# -*- coding: utf-8 -*- +""" +Copyright (c) 2025 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 os +import sys +import time +import queue + +from enum import Enum +from datetime import datetime, timedelta + +from PySide6.QtCore import ( + Qt, Signal, Slot, QTimer +) +from PySide6.QtWidgets import ( + QDialog, QWidget, QListWidgetItem, QMessageBox, + QHBoxLayout, QVBoxLayout, QLabel, QPushButton +) +from PySide6.QtGui import ( + QCloseEvent +) + +from gui.Ui_ALTimerTaskWidget import Ui_ALTimerTaskWidget +from gui.ALAddTimerTaskDialog import ALAddTimerTaskWidget, TimerTaskStatus + + +class TimerTaskItemWidget(QWidget): + + def __init__( + self, + parent = None, + timer_task: dict = None + ): + + super().__init__(parent) + + self.__timer_task = timer_task + self.modifyUi() + + + def modifyUi( + self + ): + + self.ItemWidgetLayout = QHBoxLayout(self) + self.ItemWidgetLayout.setSpacing(10) + self.ItemWidgetLayout.setContentsMargins(10, 5, 10, 5) + + self.TaskInfoLayout = QVBoxLayout() + self.TaskInfoLayout.setSpacing(5) + TaskNameLabel = QLabel(self.__timer_task["name"]) + TaskNameLabelFont = TaskNameLabel.font() + TaskNameLabelFont.setBold(True) + TaskNameLabel.setFont(TaskNameLabelFont) + TaskNameLabel.setFixedHeight(25) + self.TaskInfoLayout.addWidget(TaskNameLabel) + + ExecuteTimeStr = self.__timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S") + ExecuteTimeLabel = QLabel(f"执行时间: {ExecuteTimeStr}") + ExecuteTimeLabel.setStyleSheet("color: gray;") + ExecuteTimeLabel.setFixedHeight(20) + self.TaskInfoLayout.addWidget(ExecuteTimeLabel) + + self.ItemWidgetLayout.addLayout(self.TaskInfoLayout) + self.ItemWidgetLayout.addStretch() + + match self.__timer_task["status"]: + case TimerTaskStatus.PENDING: + TaskStatusText = "等待中" + TaskStatusColor = "#FF9800" + case TimerTaskStatus.READY: + TaskStatusText = "已就绪" + TaskStatusColor = "#316BFF" + case TimerTaskStatus.RUNNING: + TaskStatusText = "执行中" + TaskStatusColor = "#2294FF" + case TimerTaskStatus.EXECUTED: + TaskStatusText = "已执行" + TaskStatusColor = "#4CAF50" + case TimerTaskStatus.OUTDATED: + TaskStatusText = "已过期" + TaskStatusColor = "#FF5722" + TaskStatusLabel = QLabel(TaskStatusText) + TaskStatusLabel.setStyleSheet(f""" + QLabel {{ + background-color: {TaskStatusColor}; + color: white; + border-radius: 5px; + font-weight: bold; + }} + """) + TaskStatusLabel.setAlignment(Qt.AlignmentFlag.AlignCenter) + TaskStatusLabel.setFixedSize(80, 25) + self.ItemWidgetLayout.addWidget(TaskStatusLabel) + + TaskModeText = "静默" if self.__timer_task["silent"] else "显示" + TaskModeColor = "#6325FF" if self.__timer_task["silent"] else "#2294FF" + TaskModeLabel = QLabel(TaskModeText) + TaskModeLabel.setStyleSheet(f""" + QLabel {{ + background-color: {TaskModeColor}; + color: white; + border-radius: 5px; + font-weight: bold; + }} + """) + TaskModeLabel.setAlignment(Qt.AlignmentFlag.AlignCenter) + TaskModeLabel.setFixedSize(60, 25) + self.ItemWidgetLayout.addWidget(TaskModeLabel) + + self.DeleteButton = QPushButton("删除") + self.DeleteButton.setFixedSize(80, 25) + self.ItemWidgetLayout.addWidget(self.DeleteButton) + if self.__timer_task["status"] == TimerTaskStatus.READY\ + or self.__timer_task["status"] == TimerTaskStatus.RUNNING: + self.DeleteButton.setEnabled(False) + self.setFixedHeight(55) + + +class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget): + + timerTasksChanged = Signal(list) + timerTaskReady = Signal(dict) + timerTaskWidgetClosed = Signal() + + def __init__( + self, + parent = None + ): + + super().__init__(parent) + + self.__timer_tasks = [] + self.__check_timer = None + self.setupUi(self) + self.connectSignals() + self.setupTimer() + + + def setupTimer( + self + ): + + self.__check_timer = QTimer(self) + self.__check_timer.timeout.connect(self.checkTasks) + self.__check_timer.start(500) + + + def connectSignals( + self + ): + + self.AddTimerTaskButton.clicked.connect(self.addTask) + self.ClearAllTimerTasksButton.clicked.connect(self.clearAllTasks) + + + def closeEvent( + self, + event: QCloseEvent + ): + + self.hide() + self.timerTaskWidgetClosed.emit() + event.ignore() + + + def updateStat( + self + ): + + pending = 0 + in_queue = 0 + executed = 0 + total = len(self.__timer_tasks) + for timer_task in self.__timer_tasks: + if timer_task["status"] == TimerTaskStatus.PENDING: + pending += 1 + elif timer_task["status"] == TimerTaskStatus.READY\ + or timer_task["status"] == TimerTaskStatus.RUNNING: + in_queue += 1 + elif timer_task["status"] == TimerTaskStatus.EXECUTED: + executed += 1 + self.TotalTaskLabel.setText(f"总任务:{total}") + self.PendingTaskLabel.setText(f"待执行:{pending}") + self.InQueueTaskLabel.setText(f"队列中:{in_queue}") + self.ExecutedTaskLabel.setText(f"已执行:{executed}") + + + def updateTimerTaskList( + self + ): + + self.TimerTasksListWidget.clear() + self.__timer_tasks.sort( + key = lambda x: x["execute_time"] + ) + for timer_task in self.__timer_tasks: + item = QListWidgetItem() + item.setData(Qt.UserRole, timer_task) + widget = TimerTaskItemWidget(self, timer_task) + widget.DeleteButton.clicked.connect( + lambda _, uuid = timer_task["task_uuid"]: self.deleteTask(uuid) + ) + item.setSizeHint(widget.size()) + self.TimerTasksListWidget.addItem(item) + self.TimerTasksListWidget.setItemWidget(item, widget) + + + def addTask( + self + ): + + dialog = ALAddTimerTaskWidget(self) + if dialog.exec() == QDialog.DialogCode.Accepted: + timer_task = dialog.getTimerTask() + self.__timer_tasks.append(timer_task) + self.updateTimerTaskList() + self.updateStat() + + + def deleteTask( + self, + task_uuid: str + ): + + self.__timer_tasks = [ + x for x in self.__timer_tasks + if x["task_uuid"] != task_uuid + ] + self.updateTimerTaskList() + self.updateStat() + + + def clearAllTasks( + self + ): + + if not self.__timer_tasks: + return + result = QMessageBox.question( + self, + "确认 - AutoLibrary", + "是否要清除所有定时任务 ?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + if result is QMessageBox.StandardButton.No: + return + in_queue_tasks = [ + x for x in self.__timer_tasks + if x["status"] == TimerTaskStatus.READY + or x["status"] == TimerTaskStatus.RUNNING + ] + in_queue_count = len(in_queue_tasks) + if in_queue_count > 0: + QMessageBox.warning( + self, + "警告 - AutoLibrary", + "存在正在执行或已就绪的队列任务,无法清除所有定时任务 !" + ) + self.__timer_tasks = in_queue_tasks + self.updateTimerTaskList() + self.updateStat() + + + def checkTasks( + self + ): + + now = datetime.now() + for timer_task in self.__timer_tasks: + if timer_task["execute_time"] > now: + continue + if timer_task["status"] is not TimerTaskStatus.PENDING: + continue + if timer_task["execute_time"] <= now + timedelta(seconds = -5): + timer_task["status"] = TimerTaskStatus.OUTDATED + else: + timer_task["status"] = TimerTaskStatus.READY + self.timerTaskReady.emit(timer_task) + self.updateTimerTaskList() + self.updateStat() + + + @Slot(dict) + def onTimerTaskIsRunning( + self, + timer_task: dict + ): + + for task in self.__timer_tasks: + if task["task_uuid"] == timer_task["task_uuid"]: + task["status"] = TimerTaskStatus.RUNNING + self.updateTimerTaskList() + self.updateStat() + + + @Slot(dict) + def onTimerTaskIsExecuted( + self, + timer_task: dict + ): + + for task in self.__timer_tasks: + if task["task_uuid"] == timer_task["task_uuid"]: + task["status"] = TimerTaskStatus.EXECUTED + self.updateTimerTaskList() + self.updateStat() diff --git a/src/gui/ALTimerTaskWidget.ui b/src/gui/ALTimerTaskWidget.ui new file mode 100644 index 0000000..fcfa810 --- /dev/null +++ b/src/gui/ALTimerTaskWidget.ui @@ -0,0 +1,239 @@ + + + ALTimerTaskWidget + + + + 0 + 0 + 400 + 400 + + + + + 400 + 400 + + + + + 600 + 400 + + + + 定时任务 - AutoLibrary + + + + 5 + + + 5 + + + 5 + + + 5 + + + 5 + + + + + 5 + + + + + + 70 + 25 + + + + + 70 + 25 + + + + 总任务:0 + + + + + + + + 70 + 25 + + + + + 70 + 25 + + + + QLabel { + color: #FF9800 +} + + + 待执行:0 + + + + + + + + 70 + 25 + + + + + 70 + 25 + + + + QLabel { + color: #2294FF +} + + + 队列中:0 + + + + + + + + 70 + 25 + + + + + 70 + 25 + + + + QLabel { + color: #4CAF50 +} + + + 已执行:0 + + + + + + + + 0 + 0 + + + + + 600 + 16777215 + + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Plain + + + + + + + + + true + + + + + + + 5 + + + + + + 80 + 25 + + + + + 80 + 25 + + + + 清除全部 + + + + + + + + 80 + 25 + + + + + 80 + 25 + + + + 添加任务 + + + + + + + + 16777215 + 16777215 + + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Plain + + + 0 + + + + + + + + + + diff --git a/src/gui/AppInfo.py b/src/gui/AppInfo.py new file mode 100644 index 0000000..2ffa437 --- /dev/null +++ b/src/gui/AppInfo.py @@ -0,0 +1 @@ +AL_VERSION = "1.0.0-beta"