1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-08-01 21:59:36 +08:00

Compare commits

...

10 Commits

17 changed files with 463 additions and 295 deletions
+2 -2
View File
@@ -11,7 +11,7 @@ import logging
import queue
import datetime
import managers.log.LogManager as LogManager
from managers.log.LogManager import getLogger
class MsgBase:
@@ -54,7 +54,7 @@ class MsgBase:
self._input_queue = input_queue
self._output_queue = output_queue
try:
self._logger = LogManager.getLogger(self._class_name)
self._logger = getLogger(self._class_name)
except RuntimeError:
self._logger = None
+2 -1
View File
@@ -73,6 +73,8 @@ def _initializeWebDriverManager(
def _initializeAppearance(
):
logger = logInstance().getLogger("AppInitializer")
app = QApplication.instance()
if not app:
return
@@ -82,7 +84,6 @@ def _initializeAppearance(
saved_custom_theme = cfg.get(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "")
app.setStyle(saved_style)
setActiveStyle(saved_style)
logger = logInstance().getLogger("AppInitializer")
if saved_custom_theme:
try:
themeInstance().applyTheme(saved_custom_theme)
+5 -47
View File
@@ -7,14 +7,11 @@ 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 datetime import datetime, timedelta
from typing import Any
from PySide6.QtCore import (
Qt,
QThread,
Signal,
Slot,
QTimer
@@ -32,53 +29,12 @@ from PySide6.QtWidgets import (
QWidget
)
from gui.ALBulletinWorker import ALBulletinFetchWorker
from gui.ALStatusLabel import ALStatusLabel
from gui.resources.ui.Ui_ALBulletinDialog import Ui_ALBulletinDialog
from managers.bulletin.BulletinManager import instance as bulletinInstance
class ALBulletinFetchWorker(QThread):
"""
Worker thread for fetching bulletins from the server.
"""
fetchWorkerIsFinished = Signal(dict)
fetchWorkerFinishedWithError = Signal(str)
def __init__(
self,
parent=None,
request_url: str = "",
params: dict = None
):
super().__init__(parent)
self.__request_url = request_url.rstrip("/") if request_url else ""
self.__params = params or {}
def run(
self
):
try:
response = requests.get(
self.__request_url, params=self.__params, timeout=5
)
response.raise_for_status()
r = response.json()
if r.get("code") == 200:
data = r.get("data", {})
self.fetchWorkerIsFinished.emit(data)
else:
raise Exception(
f"服务器返回错误: [{r.get('code', '未知代码')}] {r.get('msg', '未知错误')}"
)
except requests.RequestException as e:
self.fetchWorkerFinishedWithError.emit(f"获取公告数据时发生网络错误: \n{e}")
except Exception as e:
self.fetchWorkerFinishedWithError.emit(f"获取公告数据时发生未知错误: \n{e}")
class ALBulletinItemWidget(QWidget):
"""
Single bulletin item widget for the bulletin list.
@@ -216,6 +172,9 @@ class ALBulletinDialog(QDialog, Ui_ALBulletinDialog):
self,
iso_str: str
):
"""
Set last sync time to update the UI status.
"""
try:
dt = datetime.fromisoformat(iso_str)
@@ -385,10 +344,9 @@ class ALBulletinDialog(QDialog, Ui_ALBulletinDialog):
return
bulletin = item.data(Qt.UserRole)
if bulletin.get("isNew", False):
bulletin["isNew"] = False
self.__bulletin_mgr.markBulletinAsRead(bulletin["id"])
widget = self.BulletinListWidget.itemWidget(item)
if widget:
widget.markAsRead()
item.setData(Qt.UserRole, bulletin)
self.__bulletin_mgr.markBulletinAsRead(bulletin["id"])
self.showBulletin(bulletin)
+5 -5
View File
@@ -16,7 +16,7 @@ from PySide6.QtCore import (
Slot
)
from gui.ALBulletinDialog import ALBulletinFetchWorker
from gui.ALBulletinWorker import ALBulletinFetchWorker
from managers.bulletin.BulletinManager import instance as bulletinInstance
@@ -112,7 +112,7 @@ class ALBulletinPoller(QObject):
if self.__worker is None:
return
self.__disconnectWorker(self.__worker)
self.__worker.wait(2000)
self.__worker.wait(500)
self.__worker.deleteLater()
self.__worker = None
@@ -149,7 +149,7 @@ class ALBulletinPoller(QObject):
if worker is not self.__worker:
return
self.__disconnectWorker(worker)
worker.wait(2000)
worker.wait(500)
worker.deleteLater()
self.__worker = None
old_ids = {str(b.get("id", "")) for b in self.__mgr.bulletins()}
@@ -164,7 +164,7 @@ class ALBulletinPoller(QObject):
if str(b.get("id", "")) and str(b.get("id", "")) not in old_ids
}
new_ids -= delete_id_set
if new_ids:
if new_ids and not self.__dialog_open:
self.newBulletinsDetected.emit(len(new_ids))
if not self.__stopped:
interval_ms = self.__mgr.syncInterval()*60*1000
@@ -180,7 +180,7 @@ class ALBulletinPoller(QObject):
if worker is not self.__worker:
return
self.__disconnectWorker(worker)
worker.wait(2000)
worker.wait(500)
worker.deleteLater()
self.__worker = None
if not self.__stopped:
+57
View File
@@ -0,0 +1,57 @@
# -*- 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 ALBulletinFetchWorker(QThread):
"""
Worker thread for fetching bulletins from the server.
"""
fetchWorkerIsFinished = Signal(dict)
fetchWorkerFinishedWithError = Signal(str)
def __init__(
self,
parent=None,
request_url: str = "",
params: dict = None
):
super().__init__(parent)
self.__request_url = request_url.rstrip("/") if request_url else ""
self.__params = params or {}
def run(
self
):
try:
response = requests.get(
self.__request_url, params=self.__params, timeout=5
)
response.raise_for_status()
r = response.json()
if r.get("code") == 200:
data = r.get("data", {})
self.fetchWorkerIsFinished.emit(data)
else:
raise Exception(
f"服务器返回错误: [{r.get('code', '未知代码')}] {r.get('msg', '未知错误')}"
)
except requests.RequestException as e:
self.fetchWorkerFinishedWithError.emit(f"获取公告数据时发生网络错误: \n{e}")
except Exception as e:
self.fetchWorkerFinishedWithError.emit(f"获取公告数据时发生未知错误: \n{e}")
+118 -145
View File
@@ -13,7 +13,6 @@ from PySide6.QtCore import (
QTimer,
QUrl,
Qt,
Signal,
Slot
)
from PySide6.QtGui import (
@@ -33,14 +32,12 @@ from PySide6.QtWidgets import (
from base.MsgBase import MsgBase
from gui.ALAboutDialog import ALAboutDialog
from gui.ALBulletinDialog import ALBulletinDialog
from gui.ALBulletinPoller import ALBulletinPoller
from gui.ALConfigWidget import ALConfigWidget
from gui.ALSettingsWidget import ALSettingsWidget
from gui.ALMainWorkers import (
AutoLibWorker,
TimerTaskWorker
)
from gui.ALTimerTaskManageWidget import ALTimerTaskManageWidget
from gui.ALMainWorker import AutoLibWorker
from gui.ALBulletinPoller import ALBulletinPoller
from gui.ALTimerTaskPoller import ALTimerTaskPoller
from gui.resources import ALResource
from gui.resources.ui.Ui_ALMainWindow import Ui_ALMainWindow
from managers.bulletin.BulletinManager import instance as bulletinInstance
@@ -49,39 +46,36 @@ from managers.config.ConfigUtils import ConfigUtils
class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
# signal : timer task
timerTaskIsRunning = Signal(dict)
timerTaskIsExecuted = Signal(dict)
timerTaskIsError = Signal(dict)
def __init__(
self
):
MsgBase.__init__(self, queue.Queue(), queue.Queue())
QMainWindow.__init__(self)
self.__timer_task_queue = queue.Queue()
self.__config_paths = ConfigUtils.getAutomationConfigPaths()
self.__ALTimerTaskManageWidget = None
self.__ALConfigWidget = None
self.__ALSettingsWidget = None
self.__ALBulletinDialog = None
self.__bulletin_poller = ALBulletinPoller(self)
self.__auto_lib_thread = None
self.__current_timer_task_thread = None
self.__is_running_timer_task = False
self.__bulletin_poller = ALBulletinPoller(self)
self.__timer_task_poller = ALTimerTaskPoller(
self,
self._input_queue,
self._output_queue,
self.__config_paths
)
self.__notification_type = ""
self.setupUi(self)
self.modifyUi()
self.setupTray()
self.connectSignals()
self.startMsgPolling()
self.startTimerTaskPolling()
self.__timer_task_poller.start()
self.__bulletin_poller.start()
if bulletinInstance().autoFetch():
QTimer.singleShot(1000, self.__bulletin_poller.fetchNow)
self._showLog("主窗口初始化完成")
def modifyUi(
@@ -96,7 +90,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.SettingsAction.triggered.connect(self.onSettingsActionTriggered)
if hasattr(self, 'BulletinAction'):
self.BulletinAction.triggered.connect(self.onBulletinActionTriggered)
# initialize timer task widget, but not show it
try:
self.__ALTimerTaskManageWidget = ALTimerTaskManageWidget(self)
@@ -110,10 +103,12 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.TimerTaskManageWidgetButton.setEnabled(False)
self.TimerTaskManageWidgetButton.setToolTip("定时任务功能初始化失败, 请检查配置文件。")
return
self.timerTaskIsRunning.connect(self.__ALTimerTaskManageWidget.onTimerTaskIsRunning)
self.timerTaskIsExecuted.connect(self.__ALTimerTaskManageWidget.onTimerTaskIsExecuted)
self.timerTaskIsError.connect(self.__ALTimerTaskManageWidget.onTimerTaskIsError)
self.__ALTimerTaskManageWidget.timerTaskIsReady.connect(self.onTimerTaskIsReady)
self.__timer_task_poller.taskRunning.connect(self.onTimerTaskRunning)
self.__timer_task_poller.taskFinished.connect(self.onTimerTaskFinished)
self.__timer_task_poller.taskRunning.connect(self.__ALTimerTaskManageWidget.onTimerTaskIsRunning)
self.__timer_task_poller.taskExecuted.connect(self.__ALTimerTaskManageWidget.onTimerTaskIsExecuted)
self.__timer_task_poller.taskError.connect(self.__ALTimerTaskManageWidget.onTimerTaskIsError)
self.__ALTimerTaskManageWidget.timerTaskIsReady.connect(self.__timer_task_poller.enqueue)
self.__ALTimerTaskManageWidget.timerTaskManageWidgetIsClosed.connect(self.onTimerTaskManageWidgetClosed)
self.__ALTimerTaskManageWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint)
@@ -128,7 +123,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self
):
url = QUrl("https://www.autolibrary.kenanzhu.com/manuals")
url = QUrl("https://manuals.autolibrary.kenanzhu.com")
QDesktopServices.openUrl(url)
def setupTray(
@@ -143,13 +138,12 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.TrayMenu = QMenu()
self.TrayMenu.addAction("显示主窗口", self.showNormal)
self.TrayMenu.addAction("显示定时窗口", self.onTimerTaskManageWidgetButtonClicked)
self.TrayMenu.addAction("公告栏", self.onBulletinActionTriggered)
self.TrayMenu.addAction("最小化到托盘", self.hideToTray)
self.TrayMenu.addSeparator()
self.TrayMenu.addAction("退出", self.close)
self.TrayIcon.setContextMenu(self.TrayMenu)
self.TrayIcon.activated.connect(self.onTrayIconActivated)
self.TrayIcon.messageClicked.connect(self.onBulletinActionTriggered)
self.TrayIcon.messageClicked.connect(self.onTrayMessageClicked)
self.TrayIcon.show()
def hideToTray(
@@ -157,6 +151,9 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
):
self.hide()
self.__notification_type = ""
if not hasattr(self, "TrayIcon"):
return
self.TrayIcon.showMessage(
"AutoLibrary",
"\n已最小化到托盘",
@@ -182,9 +179,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.StopButton.clicked.connect(self.onStopButtonClicked)
self.SendButton.clicked.connect(self.onSendButtonClicked)
self.MessageEdit.returnPressed.connect(self.onSendButtonClicked)
self.__bulletin_poller.newBulletinsDetected.connect(
self._onBulletinPollerNewBulletins
)
self.__bulletin_poller.newBulletinsDetected.connect(self.onBulletinPollerNewBulletins)
def closeEvent(
self,
@@ -197,11 +192,8 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
return
if self.__msg_queue_timer and self.__msg_queue_timer.isActive():
self.__msg_queue_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.__timer_task_poller:
self.__timer_task_poller.stop()
if self.__ALTimerTaskManageWidget:
self.__ALTimerTaskManageWidget.close()
self.__ALTimerTaskManageWidget.deleteLater()
@@ -240,47 +232,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.__msg_queue_timer.timeout.connect(self.pollMsgQueue)
self.__msg_queue_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(None, True, False)
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.timerTaskWorkerIsFinished.connect(self.onTimerTaskFinished)
self.__current_timer_task_thread.start()
except queue.Empty:
self.__is_running_timer_task = False
pass
def setControlButtons(
self,
config_button_enabled: bool,
@@ -308,6 +259,84 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
except queue.Empty:
pass
@Slot(int)
def onBulletinPollerNewBulletins(
self,
count: int
):
if not hasattr(self, "TrayIcon"):
return
self.__notification_type = "bulletin"
self.TrayIcon.showMessage(
"公告栏 - AutoLibrary",
f"{count} 条新公告,点击查看详情。",
QSystemTrayIcon.MessageIcon.Information,
3000
)
@Slot(dict)
def onTimerTaskRunning(
self,
timer_task: dict
):
self.setControlButtons(None, True, False)
self.__notification_type = ""
if not hasattr(self, "TrayIcon"):
return
if not timer_task.get("silent", False):
self.TrayIcon.showMessage(
"定时任务 - AutoLibrary",
f"\n已开始执行定时任务: \n{timer_task['name']}",
QSystemTrayIcon.MessageIcon.Information,
1000
)
self.showNormal()
@Slot(bool, dict)
def onTimerTaskFinished(
self,
is_error: bool,
timer_task: dict
):
self.setControlButtons(None, False, True)
self.__notification_type = ""
if not hasattr(self, "TrayIcon"):
return
self.TrayIcon.showMessage(
"定时任务 - AutoLibrary",
f"\n定时任务 '{timer_task['name']}' 执行{'失败' if is_error else '完成'}",
QSystemTrayIcon.MessageIcon.Warning if is_error else QSystemTrayIcon.MessageIcon.Information,
1000
)
self._showTrace(
f"定时任务 {timer_task['name']} 执行{'失败' if is_error else '完成'}, uuid: {timer_task['uuid']}"
)
@Slot()
def onBulletinDialogClosed(
self
):
if self.__ALBulletinDialog:
self.__ALBulletinDialog.finished.disconnect(self.onBulletinDialogClosed)
self.__ALBulletinDialog.deleteLater()
self.__ALBulletinDialog = None
self.__bulletin_poller.setDialogOpen(False)
@Slot()
def onSettingsWidgetClosed(
self
):
if self.__ALSettingsWidget:
self.__ALSettingsWidget.settingsWidgetIsClosed.disconnect(self.onSettingsWidgetClosed)
self.__ALSettingsWidget.deleteLater()
self.__ALSettingsWidget = None
self.SettingsAction.setEnabled(True)
@Slot()
def onTimerTaskManageWidgetClosed(
self
@@ -325,60 +354,33 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.__ALConfigWidget.deleteLater()
self.__ALConfigWidget = None
self.__config_paths = ConfigUtils.getAutomationConfigPaths()
self.__timer_task_poller.updateConfigPaths(self.__config_paths)
self.setControlButtons(True, None, None)
self._showLog("配置窗口已关闭,配置文件路径已更新")
@Slot()
def onSettingsWidgetClosed(
def onTrayMessageClicked(
self
):
if self.__ALSettingsWidget:
self.__ALSettingsWidget.settingsWidgetIsClosed.disconnect(self.onSettingsWidgetClosed)
self.__ALSettingsWidget.deleteLater()
self.__ALSettingsWidget = None
self.SettingsAction.setEnabled(True)
if self.__notification_type == "bulletin":
self.__notification_type = ""
self.onBulletinActionTriggered()
@Slot()
def onBulletinActionTriggered(
self
):
self.__bulletin_poller.setDialogOpen(True)
if self.__ALBulletinDialog is None:
self.__ALBulletinDialog = ALBulletinDialog(self)
self.__ALBulletinDialog.finished.connect(self.onBulletinDialogClosed)
self.__bulletin_poller.setDialogOpen(True)
self.__ALBulletinDialog.show()
self.__ALBulletinDialog.raise_()
self.__ALBulletinDialog.activateWindow()
self._showLog("打开公告栏窗口")
@Slot()
def onBulletinDialogClosed(
self
):
if self.__ALBulletinDialog:
self.__ALBulletinDialog.finished.disconnect(self.onBulletinDialogClosed)
self.__ALBulletinDialog.deleteLater()
self.__ALBulletinDialog = None
self.__bulletin_poller.setDialogOpen(False)
@Slot(int)
def _onBulletinPollerNewBulletins(
self,
count: int
):
if not hasattr(self, 'TrayIcon'):
return
self.TrayIcon.showMessage(
"公告栏 - AutoLibrary",
f"{count} 条新公告,点击查看详情。",
QSystemTrayIcon.MessageIcon.Information,
3000
)
@Slot()
def onSettingsActionTriggered(
self
@@ -387,54 +389,25 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
if self.__ALSettingsWidget is None:
self.__ALSettingsWidget = ALSettingsWidget(self)
self.__ALSettingsWidget.settingsWidgetIsClosed.connect(self.onSettingsWidgetClosed)
self.__ALSettingsWidget.openBulletinRequested.connect(self.onBulletinActionTriggered)
self.__ALSettingsWidget.show()
self.__ALSettingsWidget.raise_()
self.__ALSettingsWidget.activateWindow()
self.SettingsAction.setEnabled(False)
self._showLog("打开全局设置窗口")
@Slot(dict)
def onTimerTaskIsReady(
self,
timer_task: dict
):
self.__timer_task_queue.put(timer_task)
@Slot(dict)
def onTimerTaskFinished(
self,
is_error: bool,
timer_task: dict
):
self.__current_timer_task_thread.wait(1000)
self.__current_timer_task_thread.timerTaskWorkerIsFinished.disconnect(self.onTimerTaskFinished)
self.__current_timer_task_thread.deleteLater()
self.__current_timer_task_thread = None
self.setControlButtons(None, False, True)
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']}' 执行{'失败' if is_error else '完成'}",
QSystemTrayIcon.MessageIcon.Warning if is_error else QSystemTrayIcon.MessageIcon.Information,
1000
)
self._showTrace(
f"定时任务 {timer_task['name']} 执行{'失败' if is_error else '完成'}, uuid: {timer_task['uuid']}"
)
if not is_error:
self.timerTaskIsExecuted.emit(timer_task)
else:
self.timerTaskIsError.emit(timer_task)
@Slot()
def onTimerTaskManageWidgetButtonClicked(
self
):
if self.__ALTimerTaskManageWidget is None:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
"定时任务功能初始化失败, 请检查配置文件。"
)
return
self.__ALTimerTaskManageWidget.show()
self.__ALTimerTaskManageWidget.raise_()
self.__ALTimerTaskManageWidget.activateWindow()
+74 -59
View File
@@ -41,6 +41,7 @@ from managers.theme.ThemeManager import(
instance as themeInstance
)
from gui.ALBulletinWorker import ALBulletinFetchWorker
from gui.ALWidgetMixin import CenterOnParentMixin
from gui.resources.ui.Ui_ALSettingsWidget import Ui_ALSettingsWidget
from interfaces.ConfigProvider import (
@@ -90,6 +91,7 @@ def _restartApp(
class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
settingsWidgetIsClosed = Signal()
openBulletinRequested = Signal()
def __init__(
self,
@@ -188,6 +190,7 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self.CustomThemeComboBox.currentIndexChanged.connect(self.onCustomThemeComboBoxChanged)
self.ResetCustomThemeButton.clicked.connect(self.onResetCustomThemeButtonClicked)
self.BulletinTestButton.clicked.connect(self.onBulletinTestButtonClicked)
self.BulletinOpenButton.clicked.connect(self.openBulletinRequested.emit)
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
self.ApplyButton.clicked.connect(self.onApplyButtonClicked)
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
@@ -196,6 +199,7 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self
):
# appearance settings
theme = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.THEME, "system")
style = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.STYLE, "Fusion")
custom_theme = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "")
@@ -218,7 +222,7 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self.CustomThemeComboBox.setCurrentIndex(idx)
self.updateCustomThemeInfo()
self.updateCustomThemeStatus()
# bulletin settings
bulletin_mgr = bulletinInstance()
self.__original_bulletin_url = bulletin_mgr.serverUrl()
self.__original_bulletin_auto_fetch = bulletin_mgr.autoFetch()
@@ -274,10 +278,17 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
elif need_theme == "dark":
self.DarkThemeRadio.setChecked(True)
def collectSettings(
def clearBulletinTestStatus(
self
):
self.BulletinTestStatusLabel.setText("")
self.BulletinTestStatusLabel.setStyleSheet("")
def collectAppearanceSettings(
self
) -> tuple[str, str, str]:
if self.LightThemeRadio.isChecked():
theme = "light"
elif self.DarkThemeRadio.isChecked():
@@ -290,41 +301,29 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
custom_theme = ""
return theme, style, custom_theme
def clearBulletinTestStatus(
def collectBulletinSettings(
self
):
self.BulletinTestStatusLabel.setText("")
self.BulletinTestStatusLabel.setStyleSheet("")
def saveBulletinSettings(
self
):
) -> tuple[str, bool, int]:
url = self.BulletinServerUrlEdit.text().strip()
auto_fetch = self.BulletinAutoFetchCheck.isChecked()
sync_interval = self.BulletinSyncIntervalSpin.value()
if sync_interval < 1:
sync_interval = 5
self.__cfg_mgr.set(CfgKey.GLOBAL.BULLETIN.SERVER_URL, url)
self.__cfg_mgr.set(CfgKey.GLOBAL.BULLETIN.AUTO_FETCH, auto_fetch)
self.__cfg_mgr.set(CfgKey.GLOBAL.BULLETIN.SYNC_INTERVAL, sync_interval)
self.__original_bulletin_url = url
self.__original_bulletin_auto_fetch = auto_fetch
self.__original_bulletin_sync_interval = sync_interval
return url, auto_fetch, sync_interval
def saveAndApply(
def saveAppearanceSettings(
self
):
theme, style, custom_theme = self.collectSettings()
theme, style, custom_theme = self.collectAppearanceSettings()
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.STYLE, style)
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, custom_theme)
setActiveStyle(style)
if not _applyCustomTheme(custom_theme, theme):
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "")
self.syncRadioFromNeedTheme(custom_theme)
theme, _, _ = self.collectSettings()
theme, _, _ = self.collectAppearanceSettings()
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.THEME, theme)
self.setNavigationIcons()
self.updateCustomThemeStatus()
@@ -333,7 +332,24 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self.__original_custom_theme = custom_theme if custom_theme else ""
self.__original_style = getActiveStyle()
def saveBulletinSettings(
self
):
url, auto_fetch, sync_interval = self.collectBulletinSettings()
self.__cfg_mgr.set(CfgKey.GLOBAL.BULLETIN.SERVER_URL, url)
self.__cfg_mgr.set(CfgKey.GLOBAL.BULLETIN.AUTO_FETCH, auto_fetch)
self.__cfg_mgr.set(CfgKey.GLOBAL.BULLETIN.SYNC_INTERVAL, sync_interval)
self.__original_bulletin_url = url
self.__original_bulletin_auto_fetch = auto_fetch
self.__original_bulletin_sync_interval = sync_interval
def saveSettings(
self
):
self.saveBulletinSettings()
self.saveAppearanceSettings()
def maybeRestart(
self
@@ -351,6 +367,34 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
return True
return False
@Slot()
def onImportCustomThemeButtonClicked(
self
):
file_path, _ = QFileDialog.getOpenFileName(
self,
"导入主题 - AutoLibrary",
"",
"主题文件 (*.altheme *.qss);;所有文件 (*)"
)
if not file_path:
return
try:
file_id = themeInstance().importTheme(file_path)
self.populateCustomThemes()
idx = self.CustomThemeComboBox.findData(file_id)
if idx >= 0:
self.CustomThemeComboBox.setCurrentIndex(idx)
self.updateCustomThemeStatus()
self.updateCustomThemeInfo()
except Exception as e:
QMessageBox.warning(
self,
"导入失败 - AutoLibrary",
f"无法导入主题文件:{e}"
)
@Slot()
def onRemoveCustomThemeButtonClicked(
self
@@ -388,34 +432,6 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
f"无法删除主题:{e}"
)
@Slot()
def onImportCustomThemeButtonClicked(
self
):
file_path, _ = QFileDialog.getOpenFileName(
self,
"导入主题 - AutoLibrary",
"",
"主题文件 (*.altheme *.qss);;所有文件 (*)"
)
if not file_path:
return
try:
file_id = themeInstance().importTheme(file_path)
self.populateCustomThemes()
idx = self.CustomThemeComboBox.findData(file_id)
if idx >= 0:
self.CustomThemeComboBox.setCurrentIndex(idx)
self.updateCustomThemeStatus()
self.updateCustomThemeInfo()
except Exception as e:
QMessageBox.warning(
self,
"导入失败 - AutoLibrary",
f"无法导入主题文件:{e}"
)
@Slot()
def onCustomThemeComboBoxChanged(
self,
@@ -464,30 +480,29 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self.BulletinTestStatusLabel.setText("正在测试连接...")
self.BulletinTestStatusLabel.setStyleSheet("")
self.__bulletin_test_t0 = time.monotonic()
from gui.ALBulletinDialog import ALBulletinFetchWorker
api_url = url.rstrip("/") + "/bulletins"
self.__bulletin_test_worker = ALBulletinFetchWorker(
self, api_url, {"date": "", "time": "", "range_hour": "1"}
)
self.__bulletin_test_worker.fetchWorkerIsFinished.connect(
self.__onBulletinTestFetched
self.onBulletinTestFetched
)
self.__bulletin_test_worker.fetchWorkerFinishedWithError.connect(
self.__onBulletinTestError
self.onBulletinTestError
)
self.__bulletin_test_worker.start()
@Slot(dict)
def __onBulletinTestFetched(
def onBulletinTestFetched(
self,
data: dict
):
self.__bulletin_test_worker.fetchWorkerIsFinished.disconnect(
self.__onBulletinTestFetched
self.onBulletinTestFetched
)
self.__bulletin_test_worker.fetchWorkerFinishedWithError.disconnect(
self.__onBulletinTestError
self.onBulletinTestError
)
self.__bulletin_test_worker.deleteLater()
self.__bulletin_test_worker = None
@@ -500,16 +515,16 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
QTimer.singleShot(3000, self, self.clearBulletinTestStatus)
@Slot(str)
def __onBulletinTestError(
def onBulletinTestError(
self,
error_message: str
):
self.__bulletin_test_worker.fetchWorkerIsFinished.disconnect(
self.__onBulletinTestFetched
self.onBulletinTestFetched
)
self.__bulletin_test_worker.fetchWorkerFinishedWithError.disconnect(
self.__onBulletinTestError
self.onBulletinTestError
)
self.__bulletin_test_worker.deleteLater()
self.__bulletin_test_worker = None
@@ -529,9 +544,9 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self
):
_, style, _ = self.collectSettings()
_, style, _ = self.collectAppearanceSettings()
style_changed = self.__original_style != style
self.saveAndApply()
self.saveSettings()
if style_changed:
self.maybeRestart()
+1 -1
View File
@@ -322,5 +322,5 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
):
QDesktopServices.openUrl(
QUrl("https://www.autolibrary.kenanzhu.com/manuals/autoscript")
QUrl("https://manuals.autolibrary.kenanzhu.com/zh/autoscript")
)
+1 -1
View File
@@ -135,4 +135,4 @@ class ALTimerTaskHistoryDialog(QDialog):
self.__history.clear()
self.HistoryTableWidget.setRowCount(0)
self.__task_data["repeat_history"] = self.__history
self.__task_data["repeat_history"] = self.__history # = []
+5
View File
@@ -515,6 +515,11 @@ class ALTimerTaskManageWidget(CenterOnParentMixin, QWidget, Ui_ALTimerTaskManage
task: dict
):
# Here we use reference to task data, not copy.
# So any change of task data in history dialog will be
# reflected in the timer task list
# Thus we can emit timerTasksChanged signal to
# update config file
Dialog = ALTimerTaskHistoryDialog(self, task)
if Dialog.exec() == QDialog.DialogCode.Accepted:
self.timerTasksChanged.emit()
+142
View File
@@ -0,0 +1,142 @@
# -*- 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 queue
from PySide6.QtCore import (
QObject,
QTimer,
Signal,
Slot
)
from gui.ALMainWorker import TimerTaskWorker
class ALTimerTaskPoller(QObject):
taskRunning = Signal(dict)
taskFinished = Signal(bool, dict)
taskExecuted = Signal(dict)
taskError = Signal(dict)
def __init__(
self,
parent=None,
input_queue: queue.Queue = None,
output_queue: queue.Queue = None,
config_paths: list = None
):
super().__init__(parent)
self.__input_queue = input_queue or queue.Queue()
self.__output_queue = output_queue or queue.Queue()
self.__config_paths = config_paths or []
self.__task_queue = queue.Queue()
self.__timer = QTimer(self)
self.__timer.timeout.connect(self.__poll)
self.__worker = None
self.__stopped = False
def start(
self
):
self.__stopped = False
self.__timer.start(500)
def stop(
self
):
self.__stopped = True
self.__timer.stop()
self.__cleanupWorker()
def enqueue(
self,
task: dict
):
self.__task_queue.put(task)
def isRunning(
self
) -> bool:
return self.__worker is not None
def updateConfigPaths(
self,
config_paths: list
):
self.__config_paths = config_paths
@Slot()
def __poll(
self
):
if self.__worker is not None:
return
try:
task = self.__task_queue.get_nowait()
except queue.Empty:
return
self.__timer.stop()
self.taskRunning.emit(task)
try:
self.__worker = TimerTaskWorker(
task,
self.__input_queue,
self.__output_queue,
self.__config_paths
)
self.__worker.timerTaskWorkerIsFinished.connect(self.__onFinished)
self.__worker.start()
except Exception:
self.__worker = None
if not self.__stopped:
self.__timer.start(500)
@Slot(bool, dict)
def __onFinished(
self,
is_error: bool,
task: dict
):
self.__worker.timerTaskWorkerIsFinished.disconnect(self.__onFinished)
self.__worker.wait(1000)
self.__worker.deleteLater()
self.__worker = None
try:
self.taskFinished.emit(is_error, task)
if not is_error:
self.taskExecuted.emit(task)
else:
self.taskError.emit(task)
finally:
if not self.__stopped:
self.__timer.start(500)
def __cleanupWorker(
self
):
if self.__worker is None:
return
try:
self.__worker.timerTaskWorkerIsFinished.disconnect(self.__onFinished)
except (TypeError, RuntimeError):
pass
self.__worker.wait(500)
self.__worker.deleteLater()
self.__worker = None
+19
View File
@@ -632,6 +632,25 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="BulletinOpenButton">
<property name="minimumSize">
<size>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>查看公告栏</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
+8 -10
View File
@@ -223,24 +223,24 @@ class BulletinManager:
try:
if self.isFirstSync():
start_date = now - timedelta(days=7)
range_hour = str(24 * 8)
range_hour = str(24*8)
elif self.shouldFullSync():
with self.__lock:
bulletins = self.bulletins()
earliest = min(bulletins, key=self._parseBulletinDateTime)
start_date = self._parseBulletinDateTime(earliest)
diff = now - start_date
range_hour = str(int(diff.total_seconds() / 3600) + 1)
range_hour = str(int(diff.total_seconds()/3600) + 1)
else:
with self.__lock:
bulletins = self.bulletins()
latest = max(bulletins, key=self._parseBulletinDateTime)
start_date = self._parseBulletinDateTime(latest)
diff = now - start_date
range_hour = str(int(diff.total_seconds() / 3600) + 1)
range_hour = str(int(diff.total_seconds()/3600) + 1)
except (ValueError, TypeError, KeyError):
start_date = now - timedelta(days=7)
range_hour = str(24 * 8)
range_hour = str(24*8)
return {
"date": start_date.strftime("%Y-%m-%d"),
@@ -256,9 +256,10 @@ class BulletinManager:
"""
Merge incoming bulletins into the cache.
New bulletins are added with isNew=True. Existing bulletins
keep their current isNew state. Entries listed in delete_ids
are removed. Bulletins missing an "id" field are skipped.
- New bulletins are added with isNew=True.
- Existing bulletins keep their current isNew state.
- Entries listed in delete_ids are removed.
- Bulletins missing an "id" field are skipped.
Args:
new_bulletins (list[dict]): Incoming bulletin list.
@@ -275,7 +276,6 @@ class BulletinManager:
bid = b.get("id")
if bid is not None:
bulletins_dict[str(bid)] = b
for bulletin in new_bulletins:
bid = bulletin.get("id")
if bid is None:
@@ -290,10 +290,8 @@ class BulletinManager:
else bulletins_dict[bid].get("isNew", True)
)
bulletins_dict[bid] = bulletin
for bid in delete_set:
bulletins_dict.pop(bid, None)
result = list(bulletins_dict.values())
result.sort(key=lambda x: str(x.get("id", "")))
self.setBulletins(result)
+7 -6
View File
@@ -7,6 +7,7 @@ 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 copy
import os
import threading
@@ -18,7 +19,7 @@ from interfaces.ConfigProvider import ConfigType, ConfigPath
# This config manager class only responsible for global and other
# unconfigurable config files.
# config files. NOT include run and user config files.
class ConfigTemplate:
@@ -87,8 +88,8 @@ class ConfigManager:
):
self.__config_dir = os.path.abspath(config_dir)
self.__config_lock = threading.Lock()
self.__config_data = {}
self.__lock = threading.Lock()
self.initialize()
@@ -121,16 +122,16 @@ class ConfigManager:
default: Optional[Any] = None
) -> Any:
with self.__config_lock:
with self.__lock:
config_data = self.__config_data[key.config_type.value]
if key.key == "":
return config_data
return copy.deepcopy(config_data)
keys = key.key.split('.')
for k in keys[:-1]:
config_data = config_data.get(k, None)
if config_data is None:
return default
return config_data.get(keys[-1], default)
return copy.deepcopy(config_data.get(keys[-1], default))
def set(
self,
@@ -138,7 +139,7 @@ class ConfigManager:
value: Any = None
):
with self.__config_lock:
with self.__lock:
root_data = self.__config_data[key.config_type.value]
if key.key == "":
self.__config_data[key.config_type.value] = value
-5
View File
@@ -99,17 +99,14 @@ class LogManager:
self.__logger = logging.getLogger("AutoLibrary")
self.__logger.setLevel(logging.DEBUG)
self.__logger.handlers.clear()
formatter = CallerInfoFormatter(
'[%(asctime)s] - [%(name)s] - [%(levelname)s] - [%(filename)s:%(lineno)s] - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
self.__logger.addHandler(console_handler)
all_log_file = os.path.join(self.__log_dir, "all.log")
file_handler_all = TimedRotatingFileHandler(
all_log_file,
@@ -122,7 +119,6 @@ class LogManager:
file_handler_all.setLevel(logging.DEBUG)
file_handler_all.setFormatter(formatter)
self.__logger.addHandler(file_handler_all)
error_log_file = os.path.join(self.__log_dir, "error.log")
file_handler_error = TimedRotatingFileHandler(
error_log_file,
@@ -135,7 +131,6 @@ class LogManager:
file_handler_error.setLevel(logging.ERROR)
file_handler_error.setFormatter(formatter)
self.__logger.addHandler(file_handler_error)
self.__initialized = True
def getLogger(
+17 -13
View File
@@ -28,6 +28,8 @@ from utils.ThemeUtils import (
)
# we use Fusion as the default active style name
# because it's cross-platform.
_active_style_name = "Fusion"
@@ -61,8 +63,10 @@ class ThemeManager:
):
self.__themes_dir = os.path.abspath(themes_dir)
self.__lock = threading.Lock()
self.__current_theme_name = ""
self.__lock = threading.Lock()
# directory must exist
os.makedirs(self.__themes_dir, exist_ok=True)
@staticmethod
@@ -144,18 +148,6 @@ class ThemeManager:
)
return alt_path
def themesDir(
self
) -> str:
"""
Get the themes directory path.
Returns:
str: The absolute path to the themes storage directory.
"""
return self.__themes_dir
def importTheme(
self,
source_path: str
@@ -321,6 +313,18 @@ class ThemeManager:
)
app.setStyle(QStyleFactory.create(_active_style_name))
def themesDir(
self
) -> str:
"""
Get the themes directory path.
Returns:
str: The absolute path to the themes storage directory.
"""
return self.__themes_dir
# ThemeManager singleton instance.
_theme_manager_instance = None