From a716e5b945556f95f003e2032f1f82ee3cac674d Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Wed, 24 Jun 2026 10:18:30 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(bulletin):=20=E5=85=AC=E5=91=8A?= =?UTF-8?q?=E6=A0=8F=E7=B3=BB=E7=BB=9F=E5=AE=8C=E6=95=B4=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- src/gui/ALBulletinDialog.py | 382 ++++++++++ src/gui/ALBulletinPoller.py | 170 +++++ src/gui/ALMainWindow.py | 63 ++ src/gui/ALSettingsWidget.py | 93 ++- src/gui/resources/ui/ALBulletinDialog.ui | 385 ++++++++++ src/gui/resources/ui/ALSettingsWidget.ui | 865 ++++++++++++++--------- src/interfaces/ConfigProvider.py | 6 + src/managers/bulletin/BulletinManager.py | 292 ++++++++ src/managers/bulletin/__init__.py | 9 + src/managers/config/ConfigManager.py | 5 + 10 files changed, 1938 insertions(+), 332 deletions(-) create mode 100644 src/gui/ALBulletinDialog.py create mode 100644 src/gui/ALBulletinPoller.py create mode 100644 src/gui/resources/ui/ALBulletinDialog.ui create mode 100644 src/managers/bulletin/BulletinManager.py create mode 100644 src/managers/bulletin/__init__.py diff --git a/src/gui/ALBulletinDialog.py b/src/gui/ALBulletinDialog.py new file mode 100644 index 0000000..c252681 --- /dev/null +++ b/src/gui/ALBulletinDialog.py @@ -0,0 +1,382 @@ +# -*- 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 datetime import datetime, timedelta +from typing import Any + +from PySide6.QtCore import ( + Qt, + QThread, + Signal, + Slot, + QTimer +) +from PySide6.QtGui import ( + QFont +) +from PySide6.QtWidgets import ( + QDialog, + QHBoxLayout, + QLabel, + QListWidgetItem, + QMessageBox, + QVBoxLayout, + QWidget +) + +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. + """ + + def __init__( + self, + parent=None, + bulletin: dict[str, Any] = None + ): + + super().__init__(parent) + self.__bulletin = bulletin.copy() if bulletin else {} + self.modifyUi() + + def modifyUi( + self + ): + + self.ItemWidgetLayout = QVBoxLayout() + self.ItemWidgetLayout.setSpacing(10) + self.ItemWidgetLayout.setContentsMargins(10, 5, 10, 5) + self.BulletinTitleLayout = QHBoxLayout() + self.BulletinTitleLabel = QLabel(self.__bulletin.get("title", "无标题")) + titleFont = QFont() + titleFont.setBold(True) + self.BulletinTitleLabel.setFont(titleFont) + self.BulletinTitleLayout.addWidget(self.BulletinTitleLabel) + if self.__bulletin.get("isNew", False): + self.NewIndicatorLabel = QLabel("新 !") + self.NewIndicatorLabel.setStyleSheet( + "color: #DC0000;"\ + "font-size: 10px;"\ + "font-weight: bold;"\ + "font-style: italic;" + ) + self.NewIndicatorLabel.setFixedSize(25, 25) + self.BulletinTitleLayout.addWidget(self.NewIndicatorLabel) + else: + self.NewIndicatorLabel = None + if self.__bulletin.get("isEdited", False): + self.BulletinIsEditedLabel = QLabel("(已编辑)") + self.BulletinIsEditedLabel.setStyleSheet( + "color: #FF9800;"\ + "font-size: 10px;" + ) + self.BulletinTitleLayout.addWidget(self.BulletinIsEditedLabel) + self.BulletinTitleLayout.addStretch() + self.ItemWidgetLayout.addLayout(self.BulletinTitleLayout) + self.BulletinInfoLayout = QHBoxLayout() + self.BulletinDateLabel = QLabel() + date_time = datetime.fromisoformat(self.__bulletin.get("dateTime", "")) + self.BulletinDateLabel.setText(date_time.strftime("%Y-%m-%d %H:%M:%S")) + self.BulletinDateLabel.setStyleSheet("color: #969696; font-size: 11px;") + self.BulletinInfoLayout.addWidget(self.BulletinDateLabel) + self.BulletinAuthorLabel = QLabel(self.__bulletin.get("author", "未知")) + self.BulletinAuthorLabel.setStyleSheet("color: #969696; font-size: 11px;") + self.BulletinInfoLayout.addWidget(self.BulletinAuthorLabel) + self.BulletinInfoLayout.addStretch() + self.ItemWidgetLayout.addLayout(self.BulletinInfoLayout) + self.setLayout(self.ItemWidgetLayout) + self.__bulletin = None + + def markAsRead( + self + ): + + if self.NewIndicatorLabel: + self.NewIndicatorLabel.hide() + + +class ALBulletinDialog(QDialog, Ui_ALBulletinDialog): + """ + Bulletin viewer dialog. + """ + + def __init__( + self, + parent=None + ): + super().__init__(parent) + self.__sync_timer: QTimer | None = None + self.__fetch_worker: ALBulletinFetchWorker | None = None + self.__bulletin_mgr = bulletinInstance() + + self.setupUi(self) + self.modifyUi() + self.connectSignals() + self.setupTimer() + if self.shouldAutoSync(): + self.syncBulletin() + + def modifyUi( + self + ): + + self.ALSyncStatusLabel = ALStatusLabel(self) + self.ALSyncStatusLabel.setFixedSize(30, 30) + self.SyncLayout.replaceWidget( + self.SyncStatusPlaceholder, self.ALSyncStatusLabel + ) + + titleFont = QFont() + titleFont.setBold(True) + titleFont.setPointSize(15) + self.BulletinTitleLabel.setFont(titleFont) + self.BulletinDateLabel.setStyleSheet("color: #969696;") + self.BulletinAuthorLabel.setStyleSheet("color: #969696;") + self.BulletinIsEditedLabel.setStyleSheet("color: #FF9800;") + self.BulletinIsEditedLabel.hide() + last_time = self.__bulletin_mgr.lastSyncTime() + if last_time: + self.setLastSyncTime(last_time) + self.updateBulletinList(self.__bulletin_mgr.bulletins()) + + def shouldAutoSync( + self + ) -> bool: + + last = self.__bulletin_mgr.lastSyncTime() + if last is None: + return True + try: + last_dt = datetime.fromisoformat(last) + interval_min = max(self.__bulletin_mgr.syncInterval()//2, 1) + threshold = timedelta(minutes=interval_min) + return (datetime.now().astimezone() - last_dt) > threshold + except (ValueError, TypeError): + return True + + def setLastSyncTime( + self, + iso_str: str + ): + + try: + dt = datetime.fromisoformat(iso_str) + self.LastSyncDateTimeEdit.setDateTime(dt) + except (ValueError, TypeError): + pass + + def connectSignals( + self + ): + + self.BulletinListWidget.itemClicked.connect(self.onBulletinListWidgetItemClicked) + self.SyncButton.clicked.connect(self.syncBulletin) + + def setupTimer( + self + ): + + self.__sync_timer = QTimer(self) + self.__sync_timer.timeout.connect(self.syncBulletin) + self.__sync_timer.start(self.__bulletin_mgr.syncInterval()*60*1000) + + def updateBulletinList( + self, + bulletins: list[dict] + ): + + self.BulletinListWidget.clear() + sorted_list = sorted( + bulletins, key=lambda x: x.get("dateTime", ""), reverse=True + ) + for bulletin in sorted_list: + item = QListWidgetItem() + item.setData(Qt.UserRole, bulletin) + widget = ALBulletinItemWidget(self, bulletin) + item.setSizeHint(widget.sizeHint()) + self.BulletinListWidget.addItem(item) + self.BulletinListWidget.setItemWidget(item, widget) + + def showBulletin( + self, + bulletin: dict + ): + + self.BulletinTitleLabel.setText(bulletin.get("title", "无标题")) + date_time = datetime.fromisoformat(bulletin.get("dateTime", "")) + self.BulletinDateLabel.setText(date_time.strftime("%Y-%m-%d %H:%M:%S")) + self.BulletinAuthorLabel.setText(bulletin.get("author", "未知")) + if bulletin.get("isEdited", False): + self.BulletinIsEditedLabel.show() + else: + self.BulletinIsEditedLabel.hide() + self.BulletinContentTextBrowser.setHtml( + "
{content}
".format( + content=bulletin.get("content", "无内容") + ) + ) + + def clearBulletin( + self + ): + + self.BulletinTitleLabel.setText("") + self.BulletinDateLabel.setText("") + self.BulletinAuthorLabel.setText("") + self.BulletinContentTextBrowser.setText("") + self.BulletinIsEditedLabel.hide() + + def syncBulletin( + self + ): + + if self.__fetch_worker is not None: + return + self.SyncButton.setEnabled(False) + self.SyncButton.setText("同步中...") + self.ALSyncStatusLabel.status = ALStatusLabel.Status.RUNNING + self.SyncStatusDetailLabel.setText("") + self.SyncStatusDetailLabel.setStyleSheet("") + params = self.__bulletin_mgr.getSyncDateTimeAndRange() + self.__fetch_worker = ALBulletinFetchWorker( + self, self.__bulletin_mgr.apiUrl(), params + ) + self.__fetch_worker.fetchWorkerIsFinished.connect(self.onBulletinsFetched) + self.__fetch_worker.fetchWorkerFinishedWithError.connect(self.onBulletinsFetchError) + self.__fetch_worker.start() + + @Slot(dict) + def onBulletinsFetched( + self, + data: dict + ): + + if self.__fetch_worker: + self.__fetch_worker.wait(2000) + self.__fetch_worker.fetchWorkerIsFinished.disconnect(self.onBulletinsFetched) + self.__fetch_worker.fetchWorkerFinishedWithError.disconnect(self.onBulletinsFetchError) + self.__fetch_worker.deleteLater() + self.__fetch_worker = None + bulletins = data.get("bulletins", []) + delete_ids = data.get("delete_ids", []) + merged = self.__bulletin_mgr.updateAndMergeBulletins(bulletins, delete_ids) + self.__bulletin_mgr.setLastSyncTime(datetime.now().astimezone().isoformat()) + self.SyncButton.setEnabled(True) + self.SyncButton.setText("同步") + self.ALSyncStatusLabel.status = ALStatusLabel.Status.SUCCESS + self.SyncStatusDetailLabel.setText("同步成功") + self.SyncStatusDetailLabel.setStyleSheet("color: green;") + QTimer.singleShot(3000, self.clearSyncStatus) + + self.setLastSyncTime(self.__bulletin_mgr.lastSyncTime()) + self.updateBulletinList(merged) + self.clearBulletin() + interval_ms = self.__bulletin_mgr.syncInterval()*60*1000 + if self.__sync_timer: + self.__sync_timer.start(interval_ms) + + @Slot(str) + def onBulletinsFetchError( + self, + error_message: str + ): + + if self.__fetch_worker: + self.__fetch_worker.wait(2000) + self.__fetch_worker.fetchWorkerIsFinished.disconnect(self.onBulletinsFetched) + self.__fetch_worker.fetchWorkerFinishedWithError.disconnect(self.onBulletinsFetchError) + self.__fetch_worker.deleteLater() + self.__fetch_worker = None + self.SyncButton.setEnabled(True) + self.SyncButton.setText("重试") + self.ALSyncStatusLabel.status = ALStatusLabel.Status.FAILURE + self.SyncStatusDetailLabel.setText("同步失败,请检查网络连接") + self.SyncStatusDetailLabel.setStyleSheet("color: red;") + QMessageBox.warning( + self, + "警告 - AutoLibrary", + f"同步失败:{error_message}" + ) + interval_ms = self.__bulletin_mgr.syncInterval()*60*1000 + if self.__sync_timer: + self.__sync_timer.start(interval_ms) + + def clearSyncStatus( + self + ): + + self.ALSyncStatusLabel.status = ALStatusLabel.Status.WAITING + self.SyncStatusDetailLabel.setText("") + self.SyncStatusDetailLabel.setStyleSheet("") + + @Slot(QListWidgetItem) + def onBulletinListWidgetItemClicked( + self, + item: QListWidgetItem + ): + + if item is None or item.data(Qt.UserRole) is None: + return + bulletin = item.data(Qt.UserRole) + if bulletin.get("isNew", False): + bulletin["isNew"] = False + widget = self.BulletinListWidget.itemWidget(item) + widget.markAsRead() + item.setData(Qt.UserRole, bulletin) + self.__bulletin_mgr.markBulletinAsRead(bulletin["id"]) + self.showBulletin(bulletin) diff --git a/src/gui/ALBulletinPoller.py b/src/gui/ALBulletinPoller.py new file mode 100644 index 0000000..fcf05e6 --- /dev/null +++ b/src/gui/ALBulletinPoller.py @@ -0,0 +1,170 @@ +# -*- 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 datetime import datetime + +from PySide6.QtCore import ( + QObject, + QTimer, + Signal, + Slot +) + +from gui.ALBulletinDialog import ALBulletinFetchWorker +from managers.bulletin.BulletinManager import instance as bulletinInstance + + +class ALBulletinPoller(QObject): + """ + Background bulletin poller. + + Owns the periodic poll timer and the ALBulletinFetchWorker + lifecycle. Emits signals when new bulletins are detected so + the owner can show tray notifications without knowing the + fetch / merge details. + """ + + newBulletinsDetected = Signal(int) + + def __init__( + self, + parent=None + ): + + super().__init__(parent) + self.__timer = QTimer(self) + self.__timer.timeout.connect(self.__poll) + self.__worker = None + self.__dialog_open = False + self.__mgr = bulletinInstance() + + def start( + self + ): + + interval_ms = self.__mgr.syncInterval()*60*1000 + self.__timer.start(interval_ms) + + def stop( + self + ): + + self.__timer.stop() + self.__cleanupWorker() + + def restart( + self + ): + + self.stop() + self.start() + + def fetchNow( + self + ): + + if self.__worker is not None: + return + if self.__dialog_open: + return + self.__doFetch() + + def isPolling( + self + ) -> bool: + + return self.__timer.isActive() + + def setDialogOpen( + self, + open: bool + ): + + self.__dialog_open = open + + def __doFetch( + self + ): + + params = self.__mgr.getSyncDateTimeAndRange() + self.__worker = ALBulletinFetchWorker( + self, self.__mgr.apiUrl(), params + ) + self.__worker.fetchWorkerIsFinished.connect(self.__onFetched) + self.__worker.fetchWorkerFinishedWithError.connect(self.__onError) + self.__worker.start() + + @Slot() + def __poll( + self + ): + + if self.__worker is not None: + return + if self.__dialog_open: + return + self.__doFetch() + + @Slot(dict) + def __onFetched( + self, + data: dict + ): + + if self.__worker is None: + return + self.__worker.fetchWorkerIsFinished.disconnect(self.__onFetched) + self.__worker.fetchWorkerFinishedWithError.disconnect(self.__onError) + self.__worker.wait(2000) + self.__worker.deleteLater() + self.__worker = None + old_ids = {b["id"] for b in self.__mgr.bulletins()} + bulletins = data.get("bulletins", []) + delete_ids = data.get("delete_ids", []) + self.__mgr.updateAndMergeBulletins(bulletins, delete_ids) + self.__mgr.setLastSyncTime(datetime.now().astimezone().isoformat()) + new_ids = {b["id"] for b in bulletins if b["id"] not in old_ids} + if new_ids: + self.newBulletinsDetected.emit(len(new_ids)) + interval_ms = self.__mgr.syncInterval()*60*1000 + self.__timer.start(interval_ms) + + @Slot(str) + def __onError( + self, + error_message: str + ): + + if self.__worker is None: + return + self.__worker.fetchWorkerIsFinished.disconnect(self.__onFetched) + self.__worker.fetchWorkerFinishedWithError.disconnect(self.__onError) + self.__worker.wait(2000) + self.__worker.deleteLater() + self.__worker = None + interval_ms = self.__mgr.syncInterval()*60*1000 + self.__timer.start(interval_ms) + + def __cleanupWorker( + self + ): + + if self.__worker is None: + return + try: + self.__worker.fetchWorkerIsFinished.disconnect() + except (TypeError, RuntimeError): + pass + try: + self.__worker.fetchWorkerFinishedWithError.disconnect() + except (TypeError, RuntimeError): + pass + self.__worker.wait(2000) + self.__worker.deleteLater() + self.__worker = None diff --git a/src/gui/ALMainWindow.py b/src/gui/ALMainWindow.py index 5d805c6..96f1472 100644 --- a/src/gui/ALMainWindow.py +++ b/src/gui/ALMainWindow.py @@ -32,6 +32,8 @@ 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 ( @@ -41,6 +43,7 @@ from gui.ALMainWorkers import ( from gui.ALTimerTaskManageWidget import ALTimerTaskManageWidget from gui.resources import ALResource from gui.resources.ui.Ui_ALMainWindow import Ui_ALMainWindow +from managers.bulletin.BulletinManager import instance as bulletinInstance from managers.config.ConfigUtils import ConfigUtils @@ -62,6 +65,8 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): 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 @@ -72,6 +77,11 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self.connectSignals() self.startMsgPolling() self.startTimerTaskPolling() + + self.__bulletin_poller.start() + if bulletinInstance().autoFetch(): + QTimer.singleShot(1000, self.__bulletin_poller.fetchNow) + self._showLog("主窗口初始化完成") def modifyUi( @@ -84,6 +94,8 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self.ManualAction.triggered.connect(self.onManualActionTriggered) self.AboutAction.triggered.connect(self.onAboutActionTriggered) 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: @@ -131,12 +143,14 @@ 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.show() def hideToTray( @@ -170,6 +184,10 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self.SendButton.clicked.connect(self.onSendButtonClicked) self.MessageEdit.returnPressed.connect(self.onSendButtonClicked) + self.__bulletin_poller.newBulletinsDetected.connect( + self._onBulletinPollerNewBulletins + ) + def closeEvent( self, event: QCloseEvent @@ -195,6 +213,11 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): if self.__alSettingsWidget: self.__alSettingsWidget.close() # the settings widget is already deleted in the 'self.onSettingsWidgetClosed' + if self.__alBulletinDialog: + self.__alBulletinDialog.close() + # the bulletin dialog is already deleted in the 'self.onBulletinDialogClosed' + if self.__bulletin_poller: + self.__bulletin_poller.stop() self._showLog("主窗口关闭") QMainWindow.closeEvent(self, event) @@ -318,6 +341,46 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self.__alSettingsWidget = None self.SettingsAction.setEnabled(True) + @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.__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 diff --git a/src/gui/ALSettingsWidget.py b/src/gui/ALSettingsWidget.py index 7dab1f1..05bf166 100644 --- a/src/gui/ALSettingsWidget.py +++ b/src/gui/ALSettingsWidget.py @@ -9,12 +9,15 @@ See the LICENSE file for details. """ import os import sys +import time +import requests import qtawesome as qta from PySide6.QtCore import ( QProcess, Qt, + QTimer, Signal, Slot ) @@ -30,6 +33,7 @@ from PySide6.QtWidgets import ( ) import managers.config.ConfigManager as ConfigManager +from managers.bulletin.BulletinManager import instance as bulletinInstance from managers.log.LogManager import instance as logInstance from managers.theme.ThemeManager import( getActiveStyle, @@ -96,6 +100,9 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget): self.__original_theme: str = "" self.__original_custom_theme: str = "" self.__original_style: str = "" + self.__original_bulletin_url: str = "" + self.__original_bulletin_auto_fetch: bool = False + self.__original_bulletin_sync_interval: int = 10 self.setupUi(self) self.modifyUi() @@ -130,6 +137,7 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget): "padding: 5px;" ) self.NavigationList.setCurrentRow(0) + self.NavigationList.currentRowChanged.connect(self.PageStack.setCurrentIndex) self.populateStyles() self.populateCustomThemes() @@ -139,9 +147,12 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget): app : QApplication | None = QApplication.instance() color = app.palette().color(app.palette().ColorRole.WindowText).name() - item = self.NavigationList.item(0) - if item: - item.setIcon(qta.icon("fa6s.palette", color=color)) + item0 = self.NavigationList.item(0) + if item0: + item0.setIcon(qta.icon("fa6s.palette", color=color)) + item1 = self.NavigationList.item(1) + if item1: + item1.setIcon(qta.icon("fa6s.bullhorn", color=color)) def populateStyles( self @@ -176,6 +187,7 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget): self.RemoveCustomThemeButton.clicked.connect(self.onRemoveCustomThemeButtonClicked) self.CustomThemeComboBox.currentIndexChanged.connect(self.onCustomThemeComboBoxChanged) self.ResetCustomThemeButton.clicked.connect(self.onResetCustomThemeButtonClicked) + self.BulletinTestButton.clicked.connect(self.onBulletinTestButtonClicked) self.CancelButton.clicked.connect(self.onCancelButtonClicked) self.ApplyButton.clicked.connect(self.onApplyButtonClicked) self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked) @@ -207,6 +219,14 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget): self.updateCustomThemeInfo() self.updateCustomThemeStatus() + bulletin_mgr = bulletinInstance() + self.__original_bulletin_url = bulletin_mgr.serverUrl() + self.__original_bulletin_auto_fetch = bulletin_mgr.autoFetch() + self.__original_bulletin_sync_interval = bulletin_mgr.syncInterval() + self.BulletinServerUrlEdit.setText(self.__original_bulletin_url) + self.BulletinAutoFetchCheck.setChecked(self.__original_bulletin_auto_fetch) + self.BulletinSyncIntervalSpin.setValue(self.__original_bulletin_sync_interval) + def updateCustomThemeInfo( self ): @@ -270,6 +290,29 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget): custom_theme = "" return theme, style, custom_theme + def clearBulletinTestStatus( + self + ): + + self.BulletinTestStatusLabel.setText("") + self.BulletinTestStatusLabel.setStyleSheet("") + + def saveBulletinSettings( + self + ): + + 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 + def saveAndApply( self ): @@ -281,8 +324,6 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget): if not _applyCustomTheme(custom_theme, theme): self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "") self.syncRadioFromNeedTheme(custom_theme) - # Re-read theme after syncRadioFromNeedTheme — the radio may have - # changed to match the custom theme's need_theme theme, _, _ = self.collectSettings() self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.THEME, theme) self.setNavigationIcons() @@ -292,6 +333,8 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget): self.__original_custom_theme = custom_theme if custom_theme else "" self.__original_style = getActiveStyle() + self.saveBulletinSettings() + def maybeRestart( self ) -> bool: @@ -380,7 +423,6 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget): ): self.updateCustomThemeInfo() - # no status update, because custom theme is not applied yet. @Slot() def onResetCustomThemeButtonClicked( @@ -406,6 +448,43 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget): self.updateCustomThemeInfo() self.updateCustomThemeStatus() + @Slot() + def onBulletinTestButtonClicked( + self + ): + + url = self.BulletinServerUrlEdit.text().strip() + if not url: + self.BulletinTestStatusLabel.setText("请先输入服务器地址。") + self.BulletinTestStatusLabel.setStyleSheet("color: red;") + return + self.BulletinTestButton.setEnabled(False) + self.BulletinTestStatusLabel.setText("正在测试连接...") + self.BulletinTestStatusLabel.setStyleSheet("") + try: + api_url = url.rstrip("/") + "/bulletins" + t0 = time.monotonic() + response = requests.get(api_url, timeout=5) + elapsed_ms = (time.monotonic() - t0) * 1000 + response.raise_for_status() + data = response.json() + if data.get("code") == 200: + self.BulletinTestStatusLabel.setText( + f"连接成功!响应延迟 {elapsed_ms:.0f} ms" + ) + self.BulletinTestStatusLabel.setStyleSheet("color: green;") + else: + self.BulletinTestStatusLabel.setText( + f"服务器返回异常: [{data.get('code', '?')}] {data.get('msg', '未知错误')}" + ) + self.BulletinTestStatusLabel.setStyleSheet("color: red;") + except Exception as e: + self.BulletinTestStatusLabel.setText(f"连接失败:{e}") + self.BulletinTestStatusLabel.setStyleSheet("color: red;") + finally: + self.BulletinTestButton.setEnabled(True) + QTimer.singleShot(3000, self.clearBulletinTestStatus) + @Slot() def onCancelButtonClicked( self @@ -429,5 +508,5 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget): self ): - self.onApplyButtonClicked() # virtually call apply button clicked + self.onApplyButtonClicked() self.close() diff --git a/src/gui/resources/ui/ALBulletinDialog.ui b/src/gui/resources/ui/ALBulletinDialog.ui new file mode 100644 index 0000000..40fdba5 --- /dev/null +++ b/src/gui/resources/ui/ALBulletinDialog.ui @@ -0,0 +1,385 @@ + + + ALBulletinDialog + + + Qt::WindowModality::NonModal + + + + 0 + 0 + 640 + 450 + + + + + 400 + 450 + + + + + 800 + 450 + + + + 公告栏 - AutoLibrary + + + + 5 + + + 5 + + + 5 + + + 5 + + + 5 + + + + + 10 + + + + + true + + + + 80 + 25 + + + + + 80 + 25 + + + + Qt::FocusPolicy::NoFocus + + + 同步 + + + false + + + + + + + + 30 + 30 + + + + + 30 + 30 + + + + + + + + + + + + 80 + 25 + + + + + 200 + 25 + + + + + + + + + + + + 0 + 25 + + + + + 800 + 25 + + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Plain + + + 0 + + + + + + + + 50 + 25 + + + + + 50 + 25 + + + + 上次同步: + + + + + + + + 130 + 25 + + + + + 130 + 25 + + + + false + + + true + + + QAbstractSpinBox::ButtonSymbols::NoButtons + + + false + + + yyyy/MM/dd HH:mm:ss + + + Qt::TimeSpec::LocalTime + + + + + + + + + Qt::Orientation::Horizontal + + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Plain + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::Shape::NoFrame + + + QAbstractItemView::EditTrigger::NoEditTriggers + + + QAbstractItemView::SelectionMode::SingleSelection + + + + + + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Plain + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + 5 + + + + + + + + + + + + + + + + + + + (已编辑) + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + true + + + true + + + + + + + + + + + + 0 + 25 + + + + + 16777215 + 25 + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + + + BulletinDialogButtonBox + accepted() + ALBulletinDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + BulletinDialogButtonBox + rejected() + ALBulletinDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/gui/resources/ui/ALSettingsWidget.ui b/src/gui/resources/ui/ALSettingsWidget.ui index 45ed0e8..03ac1f4 100644 --- a/src/gui/resources/ui/ALSettingsWidget.ui +++ b/src/gui/resources/ui/ALSettingsWidget.ui @@ -101,340 +101,555 @@ + + + 公告 + + + + + - - - QFrame::Shape::NoFrame - - - true - - - - - 0 - -51 - 397 - 434 - + + + + QFrame::Shape::NoFrame - - - 5 + + true + + + + + 0 + 0 + 397 + 434 + - - 3 + + + 5 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + 主题模式 + + + + 5 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + 浅色 + + + + + + + 深色 + + + + + + + 跟随系统 + + + true + + + + + + + + + + 界面风格 + + + + 5 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + 5 + + + + + + 100 + 25 + + + + + 100 + 25 + + + + 应用程序样式: + + + + + + + + 160 + 25 + + + + + + + + + + 更改样式将在下次启动应用程序时生效。 + + + + + + + + + + 自定义外观 + + + + 5 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + 选择一个主题,或导入新的主题文件: + + + true + + + + + + + 5 + + + + + + 160 + 25 + + + + + + + + + 0 + 25 + + + + false + + + 选择或输入 QSS 样式表文件路径... + + + + + + + + 25 + 25 + + + + + 25 + 25 + + + + + + + + + + + + + 25 + 25 + + + + + 25 + 25 + + + + - + + + + + + + + + + 0 + 60 + + + + + + + Qt::TextFormat::RichText + + + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop + + + true + + + + + + + 5 + + + + + + 80 + 25 + + + + 重置主题 + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + 当前使用程序 默认 外观。 + + + true + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + QFrame::Shape::NoFrame + + + true + + + + + 0 + 0 + 397 + 434 + - - 3 - - - 3 - - - 3 - - - - - 主题模式 - - - - 5 + + + 5 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + 公告设置 - - 3 + + + 5 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + 5 + + + + + + 100 + 25 + + + + + 100 + 25 + + + + 服务器地址: + + + + + + + + 200 + 25 + + + + https://api.autolibrary.kenanzhu.com + + + + + + + + + 软件启动时自动获取公告 + + + + + + + 5 + + + + + + 100 + 25 + + + + + 100 + 25 + + + + 自动同步间隔: + + + + + + + + 80 + 25 + + + + 1 + + + 1440 + + + 分钟 + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + 100 + 25 + + + + + 100 + 25 + + + + 测试连接 + + + + + + + + + + true + + + + + + + + + + Qt::Orientation::Vertical - - 3 + + + 20 + 40 + - - 3 - - - 3 - - - - - 浅色 - - - - - - - 深色 - - - - - - - 跟随系统 - - - true - - - - - - - - - - 界面风格 - - - - 5 - - - 3 - - - 3 - - - 3 - - - 3 - - - - - 5 - - - - - - 100 - 25 - - - - - 100 - 25 - - - - 应用程序样式: - - - - - - - - 160 - 25 - - - - - - - - - - 更改样式将在下次启动应用程序时生效。 - - - - - - - - - - 自定义外观 - - - - 5 - - - 3 - - - 3 - - - 3 - - - 3 - - - - - 选择一个主题,或导入新的主题文件: - - - true - - - - - - - 5 - - - - - - 160 - 25 - - - - - - - - - 0 - 25 - - - - false - - - 选择或输入 QSS 样式表文件路径... - - - - - - - - 25 - 25 - - - - - 25 - 25 - - - - + - - - - - - - - 25 - 25 - - - - - 25 - 25 - - - - - - - - - - - - - - - 0 - 60 - - - - - - - Qt::TextFormat::RichText - - - Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop - - - true - - - - - - - 5 - - - - - - 80 - 25 - - - - 重置主题 - - - - - - - Qt::Orientation::Horizontal - - - - 40 - 20 - - - - - - - - - - 当前使用程序 默认 外观。 - - - true - - - - - - - - - - Qt::Orientation::Vertical - - - - 20 - 40 - - - - - + + + + diff --git a/src/interfaces/ConfigProvider.py b/src/interfaces/ConfigProvider.py index d4f180a..ef9c889 100644 --- a/src/interfaces/ConfigProvider.py +++ b/src/interfaces/ConfigProvider.py @@ -72,6 +72,12 @@ class CfgKey: STYLE = ConfigPath(ConfigType.GLOBAL, "appearance.style") CUSTOM_THEME = ConfigPath(ConfigType.GLOBAL, "appearance.custom_theme") + class BULLETIN: + ROOT = ConfigPath(ConfigType.GLOBAL, "bulletin") + AUTO_FETCH = ConfigPath(ConfigType.GLOBAL, "bulletin.auto_fetch") + SERVER_URL = ConfigPath(ConfigType.GLOBAL, "bulletin.server_url") + SYNC_INTERVAL = ConfigPath(ConfigType.GLOBAL, "bulletin.sync_interval") + class TIMERTASK: ROOT = ConfigPath(ConfigType.TIMERTASK, "") TIMER_TASKS = ConfigPath(ConfigType.TIMERTASK, "timer_tasks") diff --git a/src/managers/bulletin/BulletinManager.py b/src/managers/bulletin/BulletinManager.py new file mode 100644 index 0000000..316ff67 --- /dev/null +++ b/src/managers/bulletin/BulletinManager.py @@ -0,0 +1,292 @@ +# -*- 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 datetime import datetime, timedelta +from typing import Optional + +from interfaces.ConfigProvider import ( + CfgKey, + ConfigProvider +) +from managers.config.ConfigManager import instance as configInstance + + +class BulletinManager: + """ + Bulletin Manager Singleton Class + + Manages bulletin data persistence via ConfigManager and provides + merge, read-status and sync-parameter logic. The HTTP fetch and + UI concerns are handled by the GUI layer. + """ + + def __init__( + self + ): + + self.__lock = threading.Lock() + self.__cfg: ConfigProvider = configInstance() + + def bulletins( + self + ) -> list[dict]: + """ + Get cached bulletins. + + Returns: + list[dict]: Cached bulletin list. + """ + + return self.__cfg.get(CfgKey.BULLETIN.BULLETIN, []) + + def setBulletins( + self, + bulletins: list[dict] + ): + """ + Replace the bulletin cache. + + Args: + bulletins (list[dict]): Bulletin list to store. + """ + + self.__cfg.set(CfgKey.BULLETIN.BULLETIN, bulletins) + + def lastSyncTime( + self + ) -> Optional[str]: + """ + Get last sync time string. + + Returns: + Optional[str]: ISO-format last sync time, or None. + """ + + return self.__cfg.get(CfgKey.BULLETIN.LAST_SYNC_TIME, None) + + def setLastSyncTime( + self, + value: Optional[str] + ): + """ + Set last sync time. + + Args: + value (Optional[str]): ISO-format datetime string. + """ + + self.__cfg.set(CfgKey.BULLETIN.LAST_SYNC_TIME, value) + + def autoFetch( + self + ) -> bool: + """ + Get auto-fetch-on-startup setting. + + Returns: + bool: Whether to fetch bulletins on application startup. + """ + + return self.__cfg.get(CfgKey.GLOBAL.BULLETIN.AUTO_FETCH, False) + + def serverUrl( + self + ) -> str: + """ + Get bulletin server URL. + + Returns: + str: Server base URL. + """ + + return self.__cfg.get( + CfgKey.GLOBAL.BULLETIN.SERVER_URL, + "https://api.autolibrary.kenanzhu.com" + ) + + def syncInterval( + self + ) -> int: + """ + Get auto-sync interval in minutes. + + Values below 1 are clamped to 5 minutes. + + Returns: + int: Sync interval (minutes), minimum 1. + """ + + interval = self.__cfg.get(CfgKey.GLOBAL.BULLETIN.SYNC_INTERVAL, 10) + if interval < 1: + return 5 + return interval + + def apiUrl( + self + ) -> str: + """ + Build the full bulletin API endpoint URL. + + Returns: + str: Full API URL (base + /bulletins). + """ + + base = self.serverUrl().rstrip("/") + return f"{base}/bulletins" + + def isFirstSync( + self + ) -> bool: + """ + Check whether this is the first sync. + + Returns: + bool: True if no cached bulletins or no last sync time. + """ + + last = self.lastSyncTime() + bulletins = self.bulletins() + return last is None or not bulletins + + def shouldFullSync( + self + ) -> bool: + """ + Check whether a full sync is needed. + + A full sync is triggered when the last sync time is more than + one hour ago. + + Returns: + bool: True if a full sync should be performed. + """ + + last = self.lastSyncTime() + if last is None: + return True + try: + last_dt = datetime.fromisoformat(last) + return (datetime.now().astimezone() - last_dt) > timedelta(hours=1) + except (ValueError, TypeError): + return True + + def getSyncDateTimeAndRange( + self + ) -> dict: + """ + Calculate the date / time / range_hour query parameters. + + Returns: + dict: Keys "date", "time", "range_hour" for the API request. + """ + + if self.isFirstSync(): + start_date = datetime.now() - timedelta(days=7) + range_hour = str(24 * (7 + 1)) + elif self.shouldFullSync(): + bulletins = self.bulletins() + earliest = min(bulletins, key=lambda x: x.get("dateTime", "")) + start_date = datetime.fromisoformat(earliest["dateTime"]) + diff = datetime.now().astimezone() - start_date + range_hour = str(int(diff.total_seconds() / 3600) + 1) + else: + bulletins = self.bulletins() + latest = max(bulletins, key=lambda x: x.get("dateTime", "")) + start_date = datetime.fromisoformat(latest["dateTime"]) + diff = datetime.now().astimezone() - start_date + range_hour = str(int(diff.total_seconds() / 3600) + 1) + return { + "date": start_date.strftime("%Y-%m-%d"), + "time": start_date.strftime("%H:%M:%S"), + "range_hour": range_hour, + } + + def updateAndMergeBulletins( + self, + new_bulletins: list[dict], + delete_ids: list[str] + ) -> list[dict]: + """ + 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. + + Args: + new_bulletins (list[dict]): Incoming bulletin list. + delete_ids (list[str]): IDs to remove. + + Returns: + list[dict]: Merged bulletin list sorted by id. + """ + + with self.__lock: + delete_set = set(delete_ids) + bulletins_dict = {b["id"]: b for b in self.bulletins()} + for bulletin in new_bulletins: + bid = bulletin["id"] + if bid in delete_set: + bulletins_dict.pop(bid, None) + continue + if bid not in bulletins_dict: + bulletin["isNew"] = True + else: + bulletin["isNew"] = 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: int(x["id"])) + self.setBulletins(result) + return result + + def markBulletinAsRead( + self, + bulletin_id: str + ): + """ + Mark a bulletin as read by its id. + + Args: + bulletin_id (str): The bulletin id. + """ + + with self.__lock: + bulletins = self.bulletins() + for b in bulletins: + if b["id"] == bulletin_id: + b["isNew"] = False + self.setBulletins(bulletins) + break + + +# BulletinManager singleton instance. +_bulletin_manager_instance = None + +# Singleton instance lock. +_instance_lock = threading.Lock() + + +def instance( +) -> BulletinManager: + """ + Get the BulletinManager singleton instance. + + Returns: + BulletinManager: The singleton BulletinManager instance. + """ + + global _bulletin_manager_instance + with _instance_lock: + if _bulletin_manager_instance is None: + _bulletin_manager_instance = BulletinManager() + return _bulletin_manager_instance diff --git a/src/managers/bulletin/__init__.py b/src/managers/bulletin/__init__.py new file mode 100644 index 0000000..5cb1dac --- /dev/null +++ b/src/managers/bulletin/__init__.py @@ -0,0 +1,9 @@ +# -*- 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. +""" diff --git a/src/managers/config/ConfigManager.py b/src/managers/config/ConfigManager.py index 0f54f13..7010d46 100644 --- a/src/managers/config/ConfigManager.py +++ b/src/managers/config/ConfigManager.py @@ -59,6 +59,11 @@ class ConfigTemplate: "theme": "system", "style": "Fusion", "custom_theme": "" + }, + "bulletin": { + "auto_fetch": False, + "server_url": "https://api.autolibrary.kenanzhu.com", + "sync_interval": 10 } } case ConfigType.BULLETIN: From 6f32923865137e46a4be83972e4400258a3aee45 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Wed, 24 Jun 2026 12:17:26 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix(bulletin):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=85=AC=E5=91=8A=E6=A0=8F=E7=B3=BB=E7=BB=9F=E5=A4=9A=E9=A1=B9?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E7=BC=BA=E9=99=B7=E4=B8=8E=E7=BA=BF=E7=A8=8B?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E9=9A=90=E6=82=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- src/gui/ALBulletinDialog.py | 45 +++++--- src/gui/ALBulletinPoller.py | 86 ++++++++++------ src/gui/ALSettingsWidget.py | 78 +++++++++----- src/managers/bulletin/BulletinManager.py | 126 ++++++++++++++++------- src/managers/config/ConfigManager.py | 2 +- 5 files changed, 226 insertions(+), 111 deletions(-) diff --git a/src/gui/ALBulletinDialog.py b/src/gui/ALBulletinDialog.py index c252681..9bdcf02 100644 --- a/src/gui/ALBulletinDialog.py +++ b/src/gui/ALBulletinDialog.py @@ -130,7 +130,11 @@ class ALBulletinItemWidget(QWidget): self.ItemWidgetLayout.addLayout(self.BulletinTitleLayout) self.BulletinInfoLayout = QHBoxLayout() self.BulletinDateLabel = QLabel() - date_time = datetime.fromisoformat(self.__bulletin.get("dateTime", "")) + try: + raw_dt = self.__bulletin.get("dateTime", "") + date_time = datetime.fromisoformat(raw_dt) if raw_dt else datetime.now() + except (ValueError, TypeError): + date_time = datetime.now() self.BulletinDateLabel.setText(date_time.strftime("%Y-%m-%d %H:%M:%S")) self.BulletinDateLabel.setStyleSheet("color: #969696; font-size: 11px;") self.BulletinInfoLayout.addWidget(self.BulletinDateLabel) @@ -258,7 +262,11 @@ class ALBulletinDialog(QDialog, Ui_ALBulletinDialog): ): self.BulletinTitleLabel.setText(bulletin.get("title", "无标题")) - date_time = datetime.fromisoformat(bulletin.get("dateTime", "")) + try: + raw_dt = bulletin.get("dateTime", "") + date_time = datetime.fromisoformat(raw_dt) if raw_dt else datetime.now() + except (ValueError, TypeError): + date_time = datetime.now() self.BulletinDateLabel.setText(date_time.strftime("%Y-%m-%d %H:%M:%S")) self.BulletinAuthorLabel.setText(bulletin.get("author", "未知")) if bulletin.get("isEdited", False): @@ -306,12 +314,15 @@ class ALBulletinDialog(QDialog, Ui_ALBulletinDialog): data: dict ): - if self.__fetch_worker: - self.__fetch_worker.wait(2000) - self.__fetch_worker.fetchWorkerIsFinished.disconnect(self.onBulletinsFetched) - self.__fetch_worker.fetchWorkerFinishedWithError.disconnect(self.onBulletinsFetchError) - self.__fetch_worker.deleteLater() - self.__fetch_worker = None + worker = self.sender() + if worker is not self.__fetch_worker: + return + worker.fetchWorkerIsFinished.disconnect(self.onBulletinsFetched) + worker.fetchWorkerFinishedWithError.disconnect(self.onBulletinsFetchError) + worker.wait(2000) + worker.deleteLater() + self.__fetch_worker = None + bulletins = data.get("bulletins", []) delete_ids = data.get("delete_ids", []) merged = self.__bulletin_mgr.updateAndMergeBulletins(bulletins, delete_ids) @@ -336,12 +347,15 @@ class ALBulletinDialog(QDialog, Ui_ALBulletinDialog): error_message: str ): - if self.__fetch_worker: - self.__fetch_worker.wait(2000) - self.__fetch_worker.fetchWorkerIsFinished.disconnect(self.onBulletinsFetched) - self.__fetch_worker.fetchWorkerFinishedWithError.disconnect(self.onBulletinsFetchError) - self.__fetch_worker.deleteLater() - self.__fetch_worker = None + worker = self.sender() + if worker is not self.__fetch_worker: + return + worker.fetchWorkerIsFinished.disconnect(self.onBulletinsFetched) + worker.fetchWorkerFinishedWithError.disconnect(self.onBulletinsFetchError) + worker.wait(2000) + worker.deleteLater() + self.__fetch_worker = None + self.SyncButton.setEnabled(True) self.SyncButton.setText("重试") self.ALSyncStatusLabel.status = ALStatusLabel.Status.FAILURE @@ -376,7 +390,8 @@ class ALBulletinDialog(QDialog, Ui_ALBulletinDialog): if bulletin.get("isNew", False): bulletin["isNew"] = False widget = self.BulletinListWidget.itemWidget(item) - widget.markAsRead() + if widget: + widget.markAsRead() item.setData(Qt.UserRole, bulletin) self.__bulletin_mgr.markBulletinAsRead(bulletin["id"]) self.showBulletin(bulletin) diff --git a/src/gui/ALBulletinPoller.py b/src/gui/ALBulletinPoller.py index fcf05e6..5a328c3 100644 --- a/src/gui/ALBulletinPoller.py +++ b/src/gui/ALBulletinPoller.py @@ -42,12 +42,14 @@ class ALBulletinPoller(QObject): self.__timer.timeout.connect(self.__poll) self.__worker = None self.__dialog_open = False + self.__stopped = False self.__mgr = bulletinInstance() def start( self ): + self.__stopped = False interval_ms = self.__mgr.syncInterval()*60*1000 self.__timer.start(interval_ms) @@ -55,6 +57,7 @@ class ALBulletinPoller(QObject): self ): + self.__stopped = True self.__timer.stop() self.__cleanupWorker() @@ -88,6 +91,31 @@ class ALBulletinPoller(QObject): self.__dialog_open = open + def __disconnectWorker( + self, + worker: ALBulletinFetchWorker + ): + + try: + worker.fetchWorkerIsFinished.disconnect(self.__onFetched) + except (TypeError, RuntimeError): + pass + try: + worker.fetchWorkerFinishedWithError.disconnect(self.__onError) + except (TypeError, RuntimeError): + pass + + def __cleanupWorker( + self + ): + + if self.__worker is None: + return + self.__disconnectWorker(self.__worker) + self.__worker.wait(2000) + self.__worker.deleteLater() + self.__worker = None + def __doFetch( self ): @@ -117,23 +145,30 @@ class ALBulletinPoller(QObject): data: dict ): - if self.__worker is None: + worker = self.sender() + if worker is not self.__worker: return - self.__worker.fetchWorkerIsFinished.disconnect(self.__onFetched) - self.__worker.fetchWorkerFinishedWithError.disconnect(self.__onError) - self.__worker.wait(2000) - self.__worker.deleteLater() + self.__disconnectWorker(worker) + worker.wait(2000) + worker.deleteLater() self.__worker = None - old_ids = {b["id"] for b in self.__mgr.bulletins()} + old_ids = {str(b.get("id", "")) for b in self.__mgr.bulletins()} bulletins = data.get("bulletins", []) delete_ids = data.get("delete_ids", []) self.__mgr.updateAndMergeBulletins(bulletins, delete_ids) self.__mgr.setLastSyncTime(datetime.now().astimezone().isoformat()) - new_ids = {b["id"] for b in bulletins if b["id"] not in old_ids} + delete_id_set = {str(d) for d in delete_ids} + new_ids = { + str(b.get("id", "")) + for b in bulletins + if str(b.get("id", "")) and str(b.get("id", "")) not in old_ids + } + new_ids -= delete_id_set if new_ids: self.newBulletinsDetected.emit(len(new_ids)) - interval_ms = self.__mgr.syncInterval()*60*1000 - self.__timer.start(interval_ms) + if not self.__stopped: + interval_ms = self.__mgr.syncInterval()*60*1000 + self.__timer.start(interval_ms) @Slot(str) def __onError( @@ -141,30 +176,13 @@ class ALBulletinPoller(QObject): error_message: str ): - if self.__worker is None: + worker = self.sender() + if worker is not self.__worker: return - self.__worker.fetchWorkerIsFinished.disconnect(self.__onFetched) - self.__worker.fetchWorkerFinishedWithError.disconnect(self.__onError) - self.__worker.wait(2000) - self.__worker.deleteLater() - self.__worker = None - interval_ms = self.__mgr.syncInterval()*60*1000 - self.__timer.start(interval_ms) - - def __cleanupWorker( - self - ): - - if self.__worker is None: - return - try: - self.__worker.fetchWorkerIsFinished.disconnect() - except (TypeError, RuntimeError): - pass - try: - self.__worker.fetchWorkerFinishedWithError.disconnect() - except (TypeError, RuntimeError): - pass - self.__worker.wait(2000) - self.__worker.deleteLater() + self.__disconnectWorker(worker) + worker.wait(2000) + worker.deleteLater() self.__worker = None + if not self.__stopped: + interval_ms = self.__mgr.syncInterval()*60*1000 + self.__timer.start(interval_ms) diff --git a/src/gui/ALSettingsWidget.py b/src/gui/ALSettingsWidget.py index 05bf166..aef6584 100644 --- a/src/gui/ALSettingsWidget.py +++ b/src/gui/ALSettingsWidget.py @@ -458,32 +458,64 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget): self.BulletinTestStatusLabel.setText("请先输入服务器地址。") self.BulletinTestStatusLabel.setStyleSheet("color: red;") return + if hasattr(self, '__bulletin_test_worker') and self.__bulletin_test_worker is not None: + return self.BulletinTestButton.setEnabled(False) self.BulletinTestStatusLabel.setText("正在测试连接...") self.BulletinTestStatusLabel.setStyleSheet("") - try: - api_url = url.rstrip("/") + "/bulletins" - t0 = time.monotonic() - response = requests.get(api_url, timeout=5) - elapsed_ms = (time.monotonic() - t0) * 1000 - response.raise_for_status() - data = response.json() - if data.get("code") == 200: - self.BulletinTestStatusLabel.setText( - f"连接成功!响应延迟 {elapsed_ms:.0f} ms" - ) - self.BulletinTestStatusLabel.setStyleSheet("color: green;") - else: - self.BulletinTestStatusLabel.setText( - f"服务器返回异常: [{data.get('code', '?')}] {data.get('msg', '未知错误')}" - ) - self.BulletinTestStatusLabel.setStyleSheet("color: red;") - except Exception as e: - self.BulletinTestStatusLabel.setText(f"连接失败:{e}") - self.BulletinTestStatusLabel.setStyleSheet("color: red;") - finally: - self.BulletinTestButton.setEnabled(True) - QTimer.singleShot(3000, self.clearBulletinTestStatus) + 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.__bulletin_test_worker.fetchWorkerFinishedWithError.connect( + self.__onBulletinTestError + ) + self.__bulletin_test_worker.start() + + @Slot(dict) + def __onBulletinTestFetched( + self, + data: dict + ): + + self.__bulletin_test_worker.fetchWorkerIsFinished.disconnect( + self.__onBulletinTestFetched + ) + self.__bulletin_test_worker.fetchWorkerFinishedWithError.disconnect( + self.__onBulletinTestError + ) + self.__bulletin_test_worker.deleteLater() + self.__bulletin_test_worker = None + elapsed_ms = (time.monotonic() - self.__bulletin_test_t0) * 1000 + self.BulletinTestStatusLabel.setText( + f"连接成功!响应延迟 {elapsed_ms:.0f} ms" + ) + self.BulletinTestStatusLabel.setStyleSheet("color: green;") + self.BulletinTestButton.setEnabled(True) + QTimer.singleShot(3000, self.clearBulletinTestStatus) + + @Slot(str) + def __onBulletinTestError( + self, + error_message: str + ): + + self.__bulletin_test_worker.fetchWorkerIsFinished.disconnect( + self.__onBulletinTestFetched + ) + self.__bulletin_test_worker.fetchWorkerFinishedWithError.disconnect( + self.__onBulletinTestError + ) + self.__bulletin_test_worker.deleteLater() + self.__bulletin_test_worker = None + self.BulletinTestStatusLabel.setText(f"连接失败:{error_message}") + self.BulletinTestStatusLabel.setStyleSheet("color: red;") + self.BulletinTestButton.setEnabled(True) @Slot() def onCancelButtonClicked( diff --git a/src/managers/bulletin/BulletinManager.py b/src/managers/bulletin/BulletinManager.py index 316ff67..49a75c2 100644 --- a/src/managers/bulletin/BulletinManager.py +++ b/src/managers/bulletin/BulletinManager.py @@ -9,7 +9,7 @@ See the LICENSE file for details. """ import threading -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Optional from interfaces.ConfigProvider import ( @@ -118,13 +118,18 @@ class BulletinManager: """ Get auto-sync interval in minutes. - Values below 1 are clamped to 5 minutes. + Values below 1 are clamped upward. Non-integer or missing + values fall back to the default of 10 minutes. Returns: int: Sync interval (minutes), minimum 1. """ interval = self.__cfg.get(CfgKey.GLOBAL.BULLETIN.SYNC_INTERVAL, 10) + try: + interval = int(interval) + except (ValueError, TypeError): + return 10 if interval < 1: return 5 return interval @@ -142,6 +147,26 @@ class BulletinManager: base = self.serverUrl().rstrip("/") return f"{base}/bulletins" + @staticmethod + def _ensureAware( + dt_value: datetime + ) -> datetime: + + if dt_value.tzinfo is None: + return dt_value.replace(tzinfo=timezone.utc) + return dt_value + + @staticmethod + def _parseBulletinDateTime( + b: dict + ) -> datetime: + + raw = b.get("dateTime", "") + if not raw: + raise ValueError("empty dateTime") + dt_value = datetime.fromisoformat(raw) + return BulletinManager._ensureAware(dt_value) + def isFirstSync( self ) -> bool: @@ -152,9 +177,10 @@ class BulletinManager: bool: True if no cached bulletins or no last sync time. """ - last = self.lastSyncTime() - bulletins = self.bulletins() - return last is None or not bulletins + with self.__lock: + last = self.lastSyncTime() + bulletins = self.bulletins() + return last is None or not bulletins def shouldFullSync( self @@ -169,14 +195,17 @@ class BulletinManager: bool: True if a full sync should be performed. """ - last = self.lastSyncTime() - if last is None: - return True - try: - last_dt = datetime.fromisoformat(last) - return (datetime.now().astimezone() - last_dt) > timedelta(hours=1) - except (ValueError, TypeError): - return True + with self.__lock: + last = self.lastSyncTime() + if last is None: + return True + try: + last_dt = datetime.fromisoformat(last) + last_dt = self._ensureAware(last_dt) + now = datetime.now(timezone.utc).astimezone() + return (now - last_dt) > timedelta(hours=1) + except (ValueError, TypeError): + return True def getSyncDateTimeAndRange( self @@ -184,25 +213,35 @@ class BulletinManager: """ Calculate the date / time / range_hour query parameters. + Falls back to a first-sync (7-day window) on any parsing error. + Returns: dict: Keys "date", "time", "range_hour" for the API request. """ - if self.isFirstSync(): - start_date = datetime.now() - timedelta(days=7) - range_hour = str(24 * (7 + 1)) - elif self.shouldFullSync(): - bulletins = self.bulletins() - earliest = min(bulletins, key=lambda x: x.get("dateTime", "")) - start_date = datetime.fromisoformat(earliest["dateTime"]) - diff = datetime.now().astimezone() - start_date - range_hour = str(int(diff.total_seconds() / 3600) + 1) - else: - bulletins = self.bulletins() - latest = max(bulletins, key=lambda x: x.get("dateTime", "")) - start_date = datetime.fromisoformat(latest["dateTime"]) - diff = datetime.now().astimezone() - start_date - range_hour = str(int(diff.total_seconds() / 3600) + 1) + now = datetime.now(timezone.utc).astimezone() + try: + if self.isFirstSync(): + start_date = now - timedelta(days=7) + 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) + 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) + except (ValueError, TypeError, KeyError): + start_date = now - timedelta(days=7) + range_hour = str(24 * 8) + return { "date": start_date.strftime("%Y-%m-%d"), "time": start_date.strftime("%H:%M:%S"), @@ -219,7 +258,7 @@ class BulletinManager: New bulletins are added with isNew=True. Existing bulletins keep their current isNew state. Entries listed in delete_ids - are removed. + are removed. Bulletins missing an "id" field are skipped. Args: new_bulletins (list[dict]): Incoming bulletin list. @@ -230,22 +269,33 @@ class BulletinManager: """ with self.__lock: - delete_set = set(delete_ids) - bulletins_dict = {b["id"]: b for b in self.bulletins()} + delete_set = set(str(d) for d in delete_ids) + bulletins_dict = {} + for b in self.bulletins(): + bid = b.get("id") + if bid is not None: + bulletins_dict[str(bid)] = b + for bulletin in new_bulletins: - bid = bulletin["id"] + bid = bulletin.get("id") + if bid is None: + continue + bid = str(bid) if bid in delete_set: bulletins_dict.pop(bid, None) continue - if bid not in bulletins_dict: - bulletin["isNew"] = True - else: - bulletin["isNew"] = bulletins_dict[bid].get("isNew", True) + bulletin["isNew"] = ( + True + if bid not in bulletins_dict + 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: int(x["id"])) + result.sort(key=lambda x: str(x.get("id", ""))) self.setBulletins(result) return result @@ -263,7 +313,7 @@ class BulletinManager: with self.__lock: bulletins = self.bulletins() for b in bulletins: - if b["id"] == bulletin_id: + if str(b.get("id", "")) == str(bulletin_id): b["isNew"] = False self.setBulletins(bulletins) break diff --git a/src/managers/config/ConfigManager.py b/src/managers/config/ConfigManager.py index 7010d46..c26b7bf 100644 --- a/src/managers/config/ConfigManager.py +++ b/src/managers/config/ConfigManager.py @@ -110,7 +110,7 @@ class ConfigManager: config_data = JSONReader(config_path).data() self.__config_data[config_type.value] = config_data return - except: + except Exception: pass self.__config_data[config_type.value] = ConfigTemplate(config_type).template() JSONWriter(config_path, self.__config_data[config_type.value]) From 6a9c662287b8b9c66639107b6912935d5fea8721 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Thu, 25 Jun 2026 11:32:58 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix(bulletin):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=A0=87=E7=AD=BE=E5=9C=86=E5=9C=88=E4=B8=8D?= =?UTF-8?q?=E5=8F=AF=E8=A7=81=E5=92=8C=E5=AE=9A=E6=97=B6=E5=99=A8=E6=82=AC?= =?UTF-8?q?=E7=A9=BA=E5=BC=95=E7=94=A8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/ALBulletinDialog.py | 2 +- src/gui/ALSettingsWidget.py | 2 +- src/gui/ALStatusLabel.py | 4 ---- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/gui/ALBulletinDialog.py b/src/gui/ALBulletinDialog.py index 9bdcf02..b5350e3 100644 --- a/src/gui/ALBulletinDialog.py +++ b/src/gui/ALBulletinDialog.py @@ -332,7 +332,7 @@ class ALBulletinDialog(QDialog, Ui_ALBulletinDialog): self.ALSyncStatusLabel.status = ALStatusLabel.Status.SUCCESS self.SyncStatusDetailLabel.setText("同步成功") self.SyncStatusDetailLabel.setStyleSheet("color: green;") - QTimer.singleShot(3000, self.clearSyncStatus) + QTimer.singleShot(3000, self, self.clearSyncStatus) self.setLastSyncTime(self.__bulletin_mgr.lastSyncTime()) self.updateBulletinList(merged) diff --git a/src/gui/ALSettingsWidget.py b/src/gui/ALSettingsWidget.py index aef6584..36eeaa3 100644 --- a/src/gui/ALSettingsWidget.py +++ b/src/gui/ALSettingsWidget.py @@ -497,7 +497,7 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget): ) self.BulletinTestStatusLabel.setStyleSheet("color: green;") self.BulletinTestButton.setEnabled(True) - QTimer.singleShot(3000, self.clearBulletinTestStatus) + QTimer.singleShot(3000, self, self.clearBulletinTestStatus) @Slot(str) def __onBulletinTestError( diff --git a/src/gui/ALStatusLabel.py b/src/gui/ALStatusLabel.py index 5a08e59..c1668a5 100644 --- a/src/gui/ALStatusLabel.py +++ b/src/gui/ALStatusLabel.py @@ -127,7 +127,6 @@ class ALStatusLabel(QLabel): 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) @@ -156,7 +155,6 @@ class ALStatusLabel(QLabel): # 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) @@ -193,7 +191,6 @@ class ALStatusLabel(QLabel): # 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) @@ -223,7 +220,6 @@ class ALStatusLabel(QLabel): # 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) From 0069d5274b29cdebd6940f8fd3d441d5fa546e2c Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Thu, 25 Jun 2026 13:17:36 +0800 Subject: [PATCH 4/4] =?UTF-8?q?style(gui):=20=E7=BB=9F=E4=B8=80=E9=9D=9EQt?= =?UTF-8?q?=E6=8E=A7=E4=BB=B6=E5=8F=98=E9=87=8F=E4=B8=BA=E8=9B=87=E5=BD=A2?= =?UTF-8?q?=E5=91=BD=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/ALAboutDialog.py | 8 +- src/gui/ALAutoScriptEditDialog.py | 218 +++++++++---------- src/gui/ALAutoScriptOrchDialog/_blocks.py | 102 ++++----- src/gui/ALAutoScriptOrchDialog/_dialog.py | 4 +- src/gui/ALAutoScriptOrchDialog/_helpers.py | 240 ++++++++++----------- src/gui/ALAutoScriptOrchDialog/_widgets.py | 178 +++++++-------- src/gui/ALBulletinDialog.py | 17 +- src/gui/ALConfigWidget.py | 149 +++++++------ src/gui/ALMainWindow.py | 114 +++++----- src/gui/ALSeatMapSelectDialog.py | 4 - src/gui/ALSeatMapView.py | 2 - src/gui/ALStatusLabel.py | 126 +++++------ src/gui/ALTimerTaskHistoryDialog.py | 26 +-- src/gui/ALTimerTaskManageWidget.py | 14 +- src/gui/ALUserTreeWidget.py | 20 +- src/gui/ALWebDriverDownloadDialog.py | 1 - 16 files changed, 606 insertions(+), 617 deletions(-) diff --git a/src/gui/ALAboutDialog.py b/src/gui/ALAboutDialog.py index 34a5490..aa18971 100644 --- a/src/gui/ALAboutDialog.py +++ b/src/gui/ALAboutDialog.py @@ -55,9 +55,9 @@ class ALAboutDialog(QDialog, Ui_ALAboutDialog): AboutBrowser.setOpenExternalLinks(True) AboutBrowser.setLineWrapMode(QTextBrowser.LineWrapMode.NoWrap) AboutBrowser.setTextInteractionFlags(Qt.TextBrowserInteraction) - BrowserFont = AboutBrowser.font() - BrowserFont.setFamilies(["Courier New", "Consolas", "Menlo", "DejaVu Sans Mono", "monospace"]) - AboutBrowser.setFont(BrowserFont) + browser_font = AboutBrowser.font() + browser_font.setFamilies(["Courier New", "Consolas", "Menlo", "DejaVu Sans Mono", "monospace"]) + AboutBrowser.setFont(browser_font) self.TabWidget.addTab(AboutBrowser, "关于") LicenseBrowser = QTextBrowser() LicenseBrowser.setHtml(self.generateLicenseText()) @@ -135,7 +135,6 @@ THE SOFTWARE.

""" system = platform.system() version = platform.version() architecture = platform.architecture()[0] - if system == "Windows": try: version = platform.win32_ver()[1] @@ -152,7 +151,6 @@ THE SOFTWARE.

""" version = f"{distro.name()} {distro.version()}" except ImportError: pass - return { 'system': system, 'version': version, diff --git a/src/gui/ALAutoScriptEditDialog.py b/src/gui/ALAutoScriptEditDialog.py index 2a32510..8040994 100644 --- a/src/gui/ALAutoScriptEditDialog.py +++ b/src/gui/ALAutoScriptEditDialog.py @@ -75,54 +75,54 @@ class ALScriptHighlighter(QSyntaxHighlighter): super().__init__(parent) self._rules = [] - KeywordFmt = QTextCharFormat() - KeywordFmt.setForeground(QColor("#569CD6")) - KeywordFmt.setFontWeight(QFont.Weight.Bold) + keyword_fmt = QTextCharFormat() + keyword_fmt.setForeground(QColor("#569CD6")) + keyword_fmt.setFontWeight(QFont.Weight.Bold) for kw in [ "if", "elseif", "else", "end", "then", "and", "or", "not", "local", "function", "return", "nil", ]: - self._rules.append((r"\b" + kw + r"\b", KeywordFmt)) - BoolFmt = QTextCharFormat() - BoolFmt.setForeground(QColor("#4FC1FF")) - BoolFmt.setFontWeight(QFont.Weight.Bold) - self._rules.append((r"\btrue\b", BoolFmt)) - self._rules.append((r"\bfalse\b", BoolFmt)) - CmpFmt = QTextCharFormat() - CmpFmt.setForeground(QColor("#C586C0")) - CmpFmt.setFontWeight(QFont.Weight.Normal) + self._rules.append((r"\b" + kw + r"\b", keyword_fmt)) + bool_fmt = QTextCharFormat() + bool_fmt.setForeground(QColor("#4FC1FF")) + bool_fmt.setFontWeight(QFont.Weight.Bold) + self._rules.append((r"\btrue\b", bool_fmt)) + self._rules.append((r"\bfalse\b", bool_fmt)) + cmp_fmt = QTextCharFormat() + cmp_fmt.setForeground(QColor("#C586C0")) + cmp_fmt.setFontWeight(QFont.Weight.Normal) for op in [r"==", r"~=", r">=", r"<=", r">", r"<"]: - self._rules.append((op, CmpFmt)) - ArithFmt = QTextCharFormat() - ArithFmt.setForeground(QColor("#C586C0")) - ArithFmt.setFontWeight(QFont.Weight.Normal) + self._rules.append((op, cmp_fmt)) + arith_fmt = QTextCharFormat() + arith_fmt.setForeground(QColor("#C586C0")) + arith_fmt.setFontWeight(QFont.Weight.Normal) for op in [r"\+", r"-", r"\*", r"/", r"\.\."]: - self._rules.append((op, ArithFmt)) - FuncFmt = QTextCharFormat() - FuncFmt.setForeground(QColor("#DCDCAA")) - FuncFmt.setFontWeight(QFont.Weight.Normal) + self._rules.append((op, arith_fmt)) + func_fmt = QTextCharFormat() + func_fmt.setForeground(QColor("#DCDCAA")) + func_fmt.setFontWeight(QFont.Weight.Normal) for fn in [ "time", "date", "datenow", "timenow", "dateadd", "timeadd"]: - self._rules.append((r"\b" + fn + r"\b", FuncFmt)) - VarFmt = QTextCharFormat() - VarFmt.setForeground(QColor("#9CDCFE")) - VarFmt.setFontWeight(QFont.Weight.Normal) + self._rules.append((r"\b" + fn + r"\b", func_fmt)) + var_fmt = QTextCharFormat() + var_fmt.setForeground(QColor("#9CDCFE")) + var_fmt.setFontWeight(QFont.Weight.Normal) var_names = [name for _, (name, _) in createAllVariablesTable().items()] for var in var_names: - self._rules.append((r"\b" + var + r"\b", VarFmt)) - StrFmt = QTextCharFormat() - StrFmt.setForeground(QColor("#CE9178")) - StrFmt.setFontWeight(QFont.Weight.Normal) - self._rules.append((r'"[^"]*"', StrFmt)) - self._rules.append((r"'[^']*'", StrFmt)) - NumFmt = QTextCharFormat() - NumFmt.setForeground(QColor("#B5CEA8")) - NumFmt.setFontWeight(QFont.Weight.Normal) - self._rules.append((r"\b\d+(?:\.\d+)?\b", NumFmt)) - CommentFmt = QTextCharFormat() - CommentFmt.setForeground(QColor("#6A9955")) - CommentFmt.setFontItalic(True) - self._rules.append((r"--[^\n]*", CommentFmt)) + self._rules.append((r"\b" + var + r"\b", var_fmt)) + st_fmt = QTextCharFormat() + st_fmt.setForeground(QColor("#CE9178")) + st_fmt.setFontWeight(QFont.Weight.Normal) + self._rules.append((r'"[^"]*"', st_fmt)) + self._rules.append((r"'[^']*'", st_fmt)) + num_fmt = QTextCharFormat() + num_fmt.setForeground(QColor("#B5CEA8")) + num_fmt.setFontWeight(QFont.Weight.Normal) + self._rules.append((r"\b\d+(?:\.\d+)?\b", num_fmt)) + comment_fmt = QTextCharFormat() + comment_fmt.setForeground(QColor("#6A9955")) + comment_fmt.setFontItalic(True) + self._rules.append((r"--[^\n]*", comment_fmt)) def highlightBlock( self, @@ -189,13 +189,13 @@ class ALAutoScriptEditDialog(QDialog): ): super().__init__(parent) - self._fontSize = 21 - self._mockWidgets = {} + self._font_size = 21 + self._mock_widgets = {} self.setupUi() self.connectSignals() self.TextEdit.setPlainText(script) - self._Highlighter = ALScriptHighlighter( + self._high_lighter = ALScriptHighlighter( self.TextEdit.document() ) if mockData: @@ -224,7 +224,7 @@ class ALAutoScriptEditDialog(QDialog): self.ZoomResetBtn.setIconSize(QSize(14, 14)) self.ZoomResetBtn.setFixedSize(25, 25) self.ZoomResetBtn.setToolTip("重置缩放") - self.ZoomLabel = QLabel(f"{self._fontSize}px") + self.ZoomLabel = QLabel(f"{self._font_size}px") self.ZoomLabel.setFixedHeight(25) self.OrchBtn = QPushButton("编排") self.OrchBtn.setFixedSize(80, 25) @@ -259,7 +259,7 @@ class ALAutoScriptEditDialog(QDialog): self.TextEdit.setStyleSheet( "QPlainTextEdit {" " font-family: 'Courier New', 'Consolas', monospace;" - f" font-size: {self._fontSize}px;" + f" font-size: {self._font_size}px;" "}" ) Layout.addWidget(self.TextEdit) @@ -287,30 +287,30 @@ class ALAutoScriptEditDialog(QDialog): BasicLayout.setSpacing(4) BasicLayout.setContentsMargins(4, 4, 4, 4) BasicLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) - controlButtons = [ + control_buttons = [ ("如果 (if...)", "if then\n \nend"), ("再如果 (elseif...)", "elseif then\n "), ("否则 (else)", "else"), ("结束 (end)", "end"), ("跳过 (pass)", "-- pass"), ] - self.addButtonsToGrid(BasicLayout, controlButtons, 0, 0, 3) - assignButtons = [ + self.addButtonsToGrid(BasicLayout, control_buttons, 0, 0, 3) + assign_buttons = [ ("赋值 (=)", " = "), ] - self.addButtonsToGrid(BasicLayout, assignButtons, 1, 2, 3) + self.addButtonsToGrid(BasicLayout, assign_buttons, 1, 2, 3) TabWidget.addTab(BasicWidget, "基本语法") OperatorWidget = QWidget() OperatorLayout = QGridLayout(OperatorWidget) OperatorLayout.setSpacing(4) OperatorLayout.setContentsMargins(4, 4, 4, 4) OperatorLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) - arithmeticButtons = [ + arithmetic_buttons = [ ("加 (+)", " + "), ("减 (-)", " - "), ] - self.addButtonsToGrid(OperatorLayout, arithmeticButtons, 0, 0, 3) - compareButtons = [ + self.addButtonsToGrid(OperatorLayout, arithmetic_buttons, 0, 0, 3) + compare_buttons = [ ("等于 (==)", " == "), ("不等于 (~=)", " ~= "), ("大于 (>)", " > "), @@ -318,7 +318,7 @@ class ALAutoScriptEditDialog(QDialog): ("大于等于 (>=)", " >= "), ("小于等于 (<=)", " <= "), ] - self.addButtonsToGrid(OperatorLayout, compareButtons, 1, 0, 3) + self.addButtonsToGrid(OperatorLayout, compare_buttons, 1, 0, 3) logic_buttons = [ ("且 (and)", " and "), ("或 (or)", " or "), @@ -335,40 +335,40 @@ class ALAutoScriptEditDialog(QDialog): ("假 (false)", "false"), ] self.addButtonsToGrid(LiteralLayout, bool_buttons, 0, 0, 3) - dateTimeButtons = [ + date_time_buttons = [ ("日期", 'date(2026, 1, 1)'), ("时间", 'time(0, 0)'), ] - self.addButtonsToGrid(LiteralLayout, dateTimeButtons, 1, 0, 3) - hintButtons = [ + self.addButtonsToGrid(LiteralLayout, date_time_buttons, 1, 0, 3) + hint_buttons = [ ("字符串", '"请输入文本"'), ("数字", "123"), ("注释", "-- 请输入注释"), ] - self.addButtonsToGrid(LiteralLayout, hintButtons, 2, 0, 3) + self.addButtonsToGrid(LiteralLayout, hint_buttons, 2, 0, 3) TabWidget.addTab(LiteralWidget, "字面量") VarWidget = QWidget() VarLayout = QGridLayout(VarWidget) VarLayout.setSpacing(4) VarLayout.setContentsMargins(4, 4, 4, 4) VarLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) - varButtons = [ + var_buttons = [ (display_name, name) for display_name, (name, _) in createAllVariablesTable().items() ] - self.addButtonsToGrid(VarLayout, varButtons, 0, 0, 3) + self.addButtonsToGrid(VarLayout, var_buttons, 0, 0, 3) TabWidget.addTab(VarWidget, "变量") FuncWidget = QWidget() FuncLayout = QGridLayout(FuncWidget) FuncLayout.setSpacing(4) FuncLayout.setContentsMargins(4, 4, 4, 4) FuncLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) - funcButtons = [ + func_buttons = [ ("datenow()", "datenow()", "返回当前日期的 Unix 时间戳"), ("timenow()", "timenow()", "返回当前时间在一天中的分钟数"), ("dateadd(day, n)", "dateadd(, )", "日期偏移: dateadd(日期时间戳, 天数)"), ("timeadd(time, n)", "timeadd(, )", "时间偏移: timeadd(分钟数, 分钟数)"), ] - for i, (text, template, tooltip) in enumerate(funcButtons): + for i, (text, template, tooltip) in enumerate(func_buttons): Btn = QPushButton(text) Btn.setProperty("template", template) Btn.clicked.connect(self.insertTemplate) @@ -419,17 +419,17 @@ class ALAutoScriptEditDialog(QDialog): Form = QFormLayout(Group) Form.setSpacing(4) Form.setContentsMargins(5, 10, 5, 5) - self._mockWidgets = {} - mockData = createMockTargetData() + self._mock_widgets = {} + mock_data = createMockTargetData() for name, var_type, key_path, display_name in createTargetVarDefs(): - d = mockData + d = mock_data for key in key_path: d = d[key] default = d Widget = self.makeMockInput(var_type, default) Label = QLabel(f"{display_name}: {name}({var_type})") Form.addRow(Label, Widget) - self._mockWidgets[name] = (Widget, var_type, key_path) + self._mock_widgets[name] = (Widget, var_type, key_path) return Group def makeMockInput( @@ -439,49 +439,49 @@ class ALAutoScriptEditDialog(QDialog): ) -> QWidget: if var_type == "String": - W = QLineEdit() - W.setText(str(default)) - return W + widget = QLineEdit() + widget.setText(str(default)) + return widget if var_type == "Boolean": - W = QComboBox() - W.addItems(["是", "否"]) - W.setCurrentIndex(0 if default else 1) - return W + widget = QComboBox() + widget.addItems(["是", "否"]) + widget.setCurrentIndex(0 if default else 1) + return widget if var_type == "Date": - W = QDateEdit() - W.setCalendarPopup(True) - W.setDisplayFormat("yyyy-MM-dd") - W.setDate(QDate.fromString(str(default), "yyyy-MM-dd")) - return W + widget = QDateEdit() + widget.setCalendarPopup(True) + widget.setDisplayFormat("yyyy-MM-dd") + widget.setDate(QDate.fromString(str(default), "yyyy-MM-dd")) + return widget if var_type == "Time": - W = QTimeEdit() - W.setDisplayFormat("HH:mm") - W.setTime(QTime.fromString(str(default), "HH:mm")) - return W + widget = QTimeEdit() + widget.setDisplayFormat("HH:mm") + widget.setTime(QTime.fromString(str(default), "HH:mm")) + return widget if var_type == "Int": - W = QSpinBox() - W.setMinimum(-999999) - W.setMaximum(999999) - W.setValue(int(default) if default else 0) - return W + widget = QSpinBox() + widget.setMinimum(-999999) + widget.setMaximum(999999) + widget.setValue(int(default) if default else 0) + return widget if var_type == "Float": - W = QDoubleSpinBox() - W.setMinimum(-999999.0) - W.setMaximum(999999.0) - W.setDecimals(2) - W.setValue(float(default) if default else 0.0) - return W - W = QLineEdit() - W.setText(str(default)) - return W + widget = QDoubleSpinBox() + widget.setMinimum(-999999.0) + widget.setMaximum(999999.0) + widget.setDecimals(2) + widget.setValue(float(default) if default else 0.0) + return widget + widget = QLineEdit() + widget.setText(str(default)) + return widget def getMockData( self ) -> dict: data = {} - for name, var_type, key_path, display_name in createTargetVarDefs(): - widget, _, _ = self._mockWidgets[name] + for name, var_type, key_path, _ in createTargetVarDefs(): + widget, _, _ = self._mock_widgets[name] value = self.getMockValue(widget, var_type) d = data for key in key_path[:-1]: @@ -496,14 +496,14 @@ class ALAutoScriptEditDialog(QDialog): if not data: return - for name, var_type, key_path, display_name in createTargetVarDefs(): + for name, var_type, key_path, _ in createTargetVarDefs(): d = data try: for key in key_path: d = d[key] except (KeyError, TypeError): continue - widget, _, _ = self._mockWidgets[name] + widget, _, _ = self._mock_widgets[name] self.setMockValue(widget, var_type, d) def getMockValue( @@ -578,10 +578,10 @@ class ALAutoScriptEditDialog(QDialog): self.TextEdit.setStyleSheet( "QPlainTextEdit {" " font-family: 'Courier New', 'Consolas', monospace;" - f" font-size: {self._fontSize}px;" + f" font-size: {self._font_size}px;" "}" ) - self.ZoomLabel.setText(f"{self._fontSize}px") + self.ZoomLabel.setText(f"{self._font_size}px") @Slot() def insertTemplate( @@ -602,7 +602,7 @@ class ALAutoScriptEditDialog(QDialog): self ): - self._fontSize = min(self._fontSize + 2, 40) + self._font_size = min(self._font_size + 2, 40) self.updateFontSize() @Slot() @@ -610,7 +610,7 @@ class ALAutoScriptEditDialog(QDialog): self ): - self._fontSize = max(self._fontSize - 2, 8) + self._font_size = max(self._font_size - 2, 8) self.updateFontSize() @Slot() @@ -618,7 +618,7 @@ class ALAutoScriptEditDialog(QDialog): self ): - self._fontSize = 21 + self._font_size = 21 self.updateFontSize() @Slot() @@ -639,13 +639,13 @@ class ALAutoScriptEditDialog(QDialog): ): from gui.ALAutoScriptOrchDialog import ALAutoScriptOrchDialog - Dlg = ALAutoScriptOrchDialog(self) - if Dlg.exec() == QDialog.DialogCode.Accepted: - script = Dlg.getScript() + Dialpg = ALAutoScriptOrchDialog(self) + if Dialpg.exec() == QDialog.DialogCode.Accepted: + script = Dialpg.getScript() if script: cursor = self.TextEdit.textCursor() cursor.insertText(script) - Dlg.deleteLater() + Dialpg.deleteLater() @Slot() def onDebugRun( @@ -679,6 +679,6 @@ class ALAutoScriptEditDialog(QDialog): if not changes: QMessageBox.information(self, "调试运行", "目标变量未发生变化。") return - Dlg = _DebugResultDialog(changes, self) - Dlg.exec() - Dlg.deleteLater() + Dialpg = _DebugResultDialog(changes, self) + Dialpg.exec() + Dialpg.deleteLater() diff --git a/src/gui/ALAutoScriptOrchDialog/_blocks.py b/src/gui/ALAutoScriptOrchDialog/_blocks.py index ac82f26..ee4c09a 100644 --- a/src/gui/ALAutoScriptOrchDialog/_blocks.py +++ b/src/gui/ALAutoScriptOrchDialog/_blocks.py @@ -32,16 +32,16 @@ class ConditionalBlock(QGroupBox): def __init__( self, - blockIndex: int, - varMgr = None, + block_index: int, + var_mgr = None, parent = None ): super().__init__(parent) - self.blockIndex = blockIndex - self._varMgr = varMgr - self._actionWidgets = [] - self._conditionRows = [] + self._block_index = block_index + self._var_mgr = var_mgr + self._action_widgets = [] + self._condition_rows = [] self.setupUi() self.connectSignals() @@ -53,8 +53,12 @@ class ConditionalBlock(QGroupBox): self.setUpdatesEnabled(False) self.setStyleSheet( - "QGroupBox { font-weight: bold; border: 1px solid #ccc; " - "margin-top: 5px; padding-top: 5px; }" + "QGroupBox { "\ + "font-weight: bold;"\ + "border: 1px solid #ccc;"\ + "margin-top: 5px;"\ + "padding-top: 5px; "\ + "}" ) self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) MainLayout = QVBoxLayout(self) @@ -67,7 +71,7 @@ class ConditionalBlock(QGroupBox): self.TypeCombo.addItem("ELSE IF", "ELSE IF") self.TypeCombo.addItem("ELSE", "ELSE") self.TypeCombo.setFixedHeight(25) - if self.blockIndex == 0: + if self._block_index == 0: self.TypeCombo.setEnabled(False) HeaderLayout.addWidget(QLabel("类型:", self)) HeaderLayout.addWidget(self.TypeCombo) @@ -115,10 +119,10 @@ class ConditionalBlock(QGroupBox): ): Row = ConditionRowFrame( - self._varMgr, self.blockIndex, - isFirst=True, parent=self + self._var_mgr, self._block_index, + is_first=True, parent=self ) - self._conditionRows.append(Row) + self._condition_rows.append(Row) self.CondRowsLayout.addWidget(Row) def addConditionRow( @@ -126,11 +130,11 @@ class ConditionalBlock(QGroupBox): ): Row = ConditionRowFrame( - self._varMgr, self.blockIndex, - isFirst=False, parent=self + self._var_mgr, self._block_index, + is_first=False, parent=self ) Row.DeleteBtn.clicked.connect(lambda: self.removeConditionRow(Row)) - self._conditionRows.append(Row) + self._condition_rows.append(Row) self.CondRowsLayout.addWidget(Row) def removeConditionRow( @@ -138,8 +142,8 @@ class ConditionalBlock(QGroupBox): row: ConditionRowFrame ): - if row in self._conditionRows and len(self._conditionRows) > 1: - self._conditionRows.remove(row) + if row in self._condition_rows and len(self._condition_rows) > 1: + self._condition_rows.remove(row) self.CondRowsLayout.removeWidget(row) row.hide() row.deleteLater() @@ -148,9 +152,9 @@ class ConditionalBlock(QGroupBox): self ): - Step = ActionStepFrame(self._varMgr, self.blockIndex, parent=self) + Step = ActionStepFrame(self._var_mgr, self._block_index, parent=self) Step.DeleteBtn.clicked.connect(lambda: self.removeActionStep(Step)) - self._actionWidgets.append(Step) + self._action_widgets.append(Step) self.ActionsLayout.addWidget(Step) def removeActionStep( @@ -158,8 +162,8 @@ class ConditionalBlock(QGroupBox): step: ActionStepFrame ): - if step in self._actionWidgets: - self._actionWidgets.remove(step) + if step in self._action_widgets: + self._action_widgets.remove(step) self.ActionsLayout.removeWidget(step) step.hide() step.deleteLater() @@ -174,19 +178,19 @@ class ConditionalBlock(QGroupBox): self ): - return list(self._conditionRows) + return list(self._condition_rows) def getActionSteps( self ): - return list(self._actionWidgets) + return list(self._action_widgets) def countActionSteps( self ) -> int: - return len(self._actionWidgets) + return len(self._action_widgets) def toScript( self @@ -195,48 +199,48 @@ class ConditionalBlock(QGroupBox): Generate Lua script lines for this conditional block. """ - blockType = self.getBlockType() + block_type = self.getBlockType() lines = [] - if blockType in ("IF", "ELSE IF"): - condTexts = [ - r.toScript() for r in self._conditionRows if r.toScript() + if block_type in ("IF", "ELSE IF"): + cond_texts = [ + r.toScript() for r in self._condition_rows if r.toScript() ] - if not condTexts: - condTexts = ["true"] - if len(condTexts) == 1: - combined = condTexts[0] + if not cond_texts: + cond_texts = ["true"] + if len(cond_texts) == 1: + combined = cond_texts[0] else: parts = [] - for i, ct in enumerate(condTexts): + for i, ct in enumerate(cond_texts): if i > 0: - logic = self._conditionRows[i].getLogic() or "and" + logic = self._condition_rows[i].getLogic() or "and" parts.append(f" {logic} ") parts.append(f"({ct})") combined = "".join(parts) - if blockType == "IF": + if block_type == "IF": lines.append(f"if {combined} then") else: lines.append(f"elseif {combined} then") else: lines.append("else") - for step in self._actionWidgets: - scriptLine = step.toScript() - if scriptLine: - lines.append(scriptLine) + for step in self._action_widgets: + script_line = step.toScript() + if script_line: + lines.append(script_line) return lines def refreshVarCombos( self ): - for row in self._conditionRows: + for row in self._condition_rows: row.refreshVarCombos() - for step in self._actionWidgets: + for step in self._action_widgets: step.refreshVarCombos() def setPrevBlockType( self, - prevType: str | None + prev_type: str | None ): model = self.TypeCombo.model() @@ -247,19 +251,19 @@ class ConditionalBlock(QGroupBox): if idx < 0: continue item = model.item(idx) - shouldEnable = prevType != "ELSE" - item.setEnabled(shouldEnable) - if prevType == "ELSE" and self.TypeCombo.currentData() in ("ELSE IF", "ELSE"): + should_enable = prev_type != "ELSE" + item.setEnabled(should_enable) + if prev_type == "ELSE" and self.TypeCombo.currentData() in ("ELSE IF", "ELSE"): self.TypeCombo.setCurrentIndex(0) @Slot(int) def onTypeChanged( self, - _idx + idx: int ): - isCond = self.TypeCombo.currentData() in ("IF", "ELSE IF") - self.ConditionWidget.setVisible(isCond) + is_cond = self.TypeCombo.currentData() in ("IF", "ELSE IF") + self.ConditionWidget.setVisible(is_cond) self.ActionLabel.setText( - "执行步骤:" if isCond else "ELSE 执行步骤:" + "执行步骤:" if is_cond else "ELSE 执行步骤:" ) diff --git a/src/gui/ALAutoScriptOrchDialog/_dialog.py b/src/gui/ALAutoScriptOrchDialog/_dialog.py index 55d8213..188018c 100644 --- a/src/gui/ALAutoScriptOrchDialog/_dialog.py +++ b/src/gui/ALAutoScriptOrchDialog/_dialog.py @@ -35,7 +35,7 @@ class ALAutoScriptOrchDialog(QDialog): super().__init__(parent) self._blocks = [] - self._varMgr = VariableManager(self) + self._var_mgr = VariableManager(self) self.setupUi() self.connectSignals() @@ -91,7 +91,7 @@ class ALAutoScriptOrchDialog(QDialog): ): Block = ConditionalBlock( - len(self._blocks), self._varMgr, parent=self + len(self._blocks), self._var_mgr, parent=self ) Block.DeleteBlockBtn.clicked.connect(lambda: self.removeBlock(Block)) Block.TypeCombo.currentIndexChanged.connect(self.updateBlockTypeRestrictions) diff --git a/src/gui/ALAutoScriptOrchDialog/_helpers.py b/src/gui/ALAutoScriptOrchDialog/_helpers.py index d019d93..4000335 100644 --- a/src/gui/ALAutoScriptOrchDialog/_helpers.py +++ b/src/gui/ALAutoScriptOrchDialog/_helpers.py @@ -46,11 +46,11 @@ def getTypeOrder( return [t for t, _ in VARTYPE_INFOS] def getArithType( - varType: str + var_type: str ) -> bool: for t, a in VARTYPE_INFOS: - if t == varType: + if t == var_type: return a def getPresetVars( @@ -113,36 +113,36 @@ class _DateInputContainer(QWidget): Layout = QHBoxLayout(self) Layout.setContentsMargins(0, 0, 0, 0) Layout.setSpacing(4) - self._ModeCombo = QComboBox(self) - self._ModeCombo.addItem("相对日期", "relative") - self._ModeCombo.addItem("绝对日期", "absolute") - self._ModeCombo.setFixedHeight(25) - self._Stack = QStackedWidget(self) - self._RelCombo = QComboBox(self) + self.ModeCombo = QComboBox(self) + self.ModeCombo.addItem("相对日期", "relative") + self.ModeCombo.addItem("绝对日期", "absolute") + self.ModeCombo.setFixedHeight(25) + self.Stack = QStackedWidget(self) + self.RelCombo = QComboBox(self) for display, data in DATE_OPTIONS: - self._RelCombo.addItem(display, data) - self._RelCombo.setFixedHeight(25) - self._Stack.addWidget(self._RelCombo) - self._DateEdit = QDateEdit(self) - self._DateEdit.setDisplayFormat("yyyy-MM-dd") - self._DateEdit.setCalendarPopup(True) - self._DateEdit.setFixedHeight(25) - self._Stack.addWidget(self._DateEdit) - self._ModeCombo.currentIndexChanged.connect( - lambda i: self._Stack.setCurrentIndex(i) + self.RelCombo.addItem(display, data) + self.RelCombo.setFixedHeight(25) + self.Stack.addWidget(self.RelCombo) + self.DateEdit = QDateEdit(self) + self.DateEdit.setDisplayFormat("yyyy-MM-dd") + self.DateEdit.setCalendarPopup(True) + self.DateEdit.setFixedHeight(25) + self.Stack.addWidget(self.DateEdit) + self.ModeCombo.currentIndexChanged.connect( + lambda i: self.Stack.setCurrentIndex(i) ) - Layout.addWidget(self._ModeCombo) - Layout.addWidget(self._Stack) + Layout.addWidget(self.ModeCombo) + Layout.addWidget(self.Stack) Layout.addStretch() def getValue( self ) -> str: - mode = self._ModeCombo.currentData() + mode = self.ModeCombo.currentData() if mode == "relative": - return self._RelCombo.currentText() - return self._DateEdit.date().toString("yyyy-MM-dd") + return self.RelCombo.currentText() + return self.DateEdit.date().toString("yyyy-MM-dd") class _TimeInputContainer(QWidget): @@ -153,19 +153,19 @@ class _TimeInputContainer(QWidget): ): super().__init__(parent) - self._TimeEdit = QTimeEdit(self) - self._TimeEdit.setDisplayFormat("HH:mm") - self._TimeEdit.setFixedHeight(25) + self.TimeEdit = QTimeEdit(self) + self.TimeEdit.setDisplayFormat("HH:mm") + self.TimeEdit.setFixedHeight(25) Layout = QHBoxLayout(self) Layout.setContentsMargins(0, 0, 0, 0) - Layout.addWidget(self._TimeEdit) + Layout.addWidget(self.TimeEdit) def getValue( self ) -> str: - return self._TimeEdit.time().toString("HH:mm") + return self.TimeEdit.time().toString("HH:mm") class _DateOffsetContainer(QWidget): @@ -176,19 +176,19 @@ class _DateOffsetContainer(QWidget): ): super().__init__(parent) - self._SpinBox = QSpinBox(self) - self._SpinBox.setRange(0, 99999) - self._SpinBox.setFixedHeight(25) - self._UnitCombo = QComboBox(self) + self.SpinBox = QSpinBox(self) + self.SpinBox.setRange(0, 99999) + self.SpinBox.setFixedHeight(25) + self.UnitCombo = QComboBox(self) for display, data in DATE_OFFSET_OPTIONS: - self._UnitCombo.addItem(display, data) - self._UnitCombo.setFixedHeight(25) + self.UnitCombo.addItem(display, data) + self.UnitCombo.setFixedHeight(25) Layout = QHBoxLayout(self) Layout.setContentsMargins(0, 0, 0, 0) Layout.setSpacing(4) - Layout.addWidget(self._SpinBox) - Layout.addWidget(self._UnitCombo) + Layout.addWidget(self.SpinBox) + Layout.addWidget(self.UnitCombo) Layout.addStretch() def getValue( @@ -201,8 +201,8 @@ class _DateOffsetContainer(QWidget): self ) -> int: - val = self._SpinBox.value() - unit = self._UnitCombo.currentData() + val = self.SpinBox.value() + unit = self.UnitCombo.currentData() if unit == "weeks": return val*7 if unit == "months": @@ -220,14 +220,14 @@ class _TimeOffsetContainer(QWidget): ): super().__init__(parent) - self._SpinBox = QSpinBox(self) - self._SpinBox.setRange(0, 99999) - self._SpinBox.setSuffix(" 小时") - self._SpinBox.setFixedHeight(25) + self.SpinBox = QSpinBox(self) + self.SpinBox.setRange(0, 99999) + self.SpinBox.setSuffix(" 小时") + self.SpinBox.setFixedHeight(25) Layout = QHBoxLayout(self) Layout.setContentsMargins(0, 0, 0, 0) - Layout.addWidget(self._SpinBox) + Layout.addWidget(self.SpinBox) def getValue( self @@ -239,7 +239,7 @@ class _TimeOffsetContainer(QWidget): self ) -> int: - return self._SpinBox.value() + return self.SpinBox.value() class VariableManager(QObject): @@ -251,7 +251,7 @@ class VariableManager(QObject): super().__init__(parent) self._vars = [] - self._nameMap = {} + self._name_map = {} self.initPresetVars() @@ -262,21 +262,21 @@ class VariableManager(QObject): for p in getPresetVars(): entry = {"name": p["name"], "type": p["type"], "display": p["display"]} self._vars.append(entry) - self._nameMap[p["name"]] = entry + self._name_map[p["name"]] = entry def getInfoByName( self, name: str ): - return self._nameMap.get(name.upper().strip()) + return self._name_map.get(name.upper().strip()) def populateCombo( self, combo: QComboBox ): - currentData = combo.currentData() + current_data = combo.currentData() combo.blockSignals(True) combo.clear() for entry in self._vars: @@ -284,10 +284,10 @@ class VariableManager(QObject): entry["display"], (entry["name"], entry["type"]) ) - if currentData: + if current_data: for i in range(combo.count()): d = combo.itemData(i) - if d and d[0] == currentData[0]: + if d and d[0] == current_data[0]: combo.setCurrentIndex(i) break combo.blockSignals(False) @@ -299,40 +299,40 @@ def makeValueWidget( ) -> QWidget: if var_type == "Int": - w = QSpinBox(parent) - w.setRange(-999999, 999999) - w.setFixedHeight(25) - w.setMinimumWidth(100) - return w + Widget = QSpinBox(parent) + Widget.setRange(-999999, 999999) + Widget.setFixedHeight(25) + Widget.setMinimumWidth(100) + return Widget if var_type == "Float": - w = QDoubleSpinBox(parent) - w.setRange(-999999.0, 999999.0) - w.setDecimals(2) - w.setFixedHeight(25) - w.setMinimumWidth(100) - return w + Widget = QDoubleSpinBox(parent) + Widget.setRange(-999999.0, 999999.0) + Widget.setDecimals(2) + Widget.setFixedHeight(25) + Widget.setMinimumWidth(100) + return Widget if var_type == "String": - w = QLineEdit(parent) - w.setPlaceholderText("输入值") - w.setFixedHeight(25) - w.setMinimumWidth(120) - return w + Widget = QLineEdit(parent) + Widget.setPlaceholderText("输入值") + Widget.setFixedHeight(25) + Widget.setMinimumWidth(120) + return Widget if var_type == "Boolean": - w = QComboBox(parent) - w.addItem("是 (true)", "true") - w.addItem("否 (false)", "false") - w.setFixedHeight(25) - w.setMinimumWidth(100) - return w + Widget = QComboBox(parent) + Widget.addItem("是 (true)", "true") + Widget.addItem("否 (false)", "false") + Widget.setFixedHeight(25) + Widget.setMinimumWidth(100) + return Widget if var_type == "Date": return _DateInputContainer(parent) if var_type == "Time": return _TimeInputContainer(parent) - w = QLineEdit(parent) - w.setPlaceholderText("输入值") - w.setFixedHeight(25) - w.setMinimumWidth(120) - return w + Widget = QLineEdit(parent) + Widget.setPlaceholderText("输入值") + Widget.setFixedHeight(25) + Widget.setMinimumWidth(120) + return Widget def makeOffsetWidget( var_type: str, @@ -340,35 +340,35 @@ def makeOffsetWidget( ) -> QWidget: if var_type == "Int": - w = QSpinBox(parent) - w.setRange(-999999, 999999) - w.setFixedHeight(25) - w.setMinimumWidth(100) - return w + Widget = QSpinBox(parent) + Widget.setRange(-999999, 999999) + Widget.setFixedHeight(25) + Widget.setMinimumWidth(100) + return Widget if var_type == "Float": - w = QDoubleSpinBox(parent) - w.setRange(-999999.0, 999999.0) - w.setDecimals(2) - w.setFixedHeight(25) - w.setMinimumWidth(100) - return w + Widget = QDoubleSpinBox(parent) + Widget.setRange(-999999.0, 999999.0) + Widget.setDecimals(2) + Widget.setFixedHeight(25) + Widget.setMinimumWidth(100) + return Widget if var_type == "Date": return _DateOffsetContainer(parent) if var_type == "Time": return _TimeOffsetContainer(parent) - w = QLabel("(不支持该操作)", parent) - w.setFixedHeight(25) - return w + Widget = QLabel("(不支持该操作)", parent) + Widget.setFixedHeight(25) + return Widget def makeVarRefCombo( parent: QWidget = None ) -> QComboBox: - Cb = QComboBox(parent) - Cb.setFixedHeight(25) - Cb.setMinimumWidth(120) - Cb.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - return Cb + ComboBox = QComboBox(parent) + ComboBox.setFixedHeight(25) + ComboBox.setMinimumWidth(120) + ComboBox.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + return ComboBox def makeComboWidget( items, @@ -376,12 +376,12 @@ def makeComboWidget( parent: QWidget = None ) -> QComboBox: - Cb = QComboBox(parent) + ComboBox = QComboBox(parent) for display, data in items: - Cb.addItem(display, data) - Cb.setFixedHeight(25) - Cb.setMinimumWidth(min_width) - return Cb + ComboBox.addItem(display, data) + ComboBox.setFixedHeight(25) + ComboBox.setMinimumWidth(min_width) + return ComboBox def makeLabel( text: str, @@ -389,30 +389,30 @@ def makeLabel( width: int = None ) -> QLabel: - Lbl = QLabel(text, parent) - Lbl.setFixedHeight(25) + Label = QLabel(text, parent) + Label.setFixedHeight(25) if width: - Lbl.setFixedWidth(width) - return Lbl + Label.setFixedWidth(width) + return Label def getValueFromWidget( - w: QWidget + widget: QWidget ) -> str: - if hasattr(w, "getValue"): - return w.getValue() - if isinstance(w, QTimeEdit): - return w.time().toString("HH:mm") - if isinstance(w, QDateEdit): - return w.date().toString("yyyy-MM-dd") - if isinstance(w, QComboBox): - return w.currentData() or w.currentText() - if isinstance(w, QSpinBox): - return str(w.value()) - if isinstance(w, QDoubleSpinBox): - return str(w.value()) - if isinstance(w, QLineEdit): - return w.text() + if hasattr(widget, "getValue"): + return widget.getValue() + if isinstance(widget, QTimeEdit): + return widget.time().toString("HH:mm") + if isinstance(widget, QDateEdit): + return widget.date().toString("yyyy-MM-dd") + if isinstance(widget, QComboBox): + return widget.currentData() or widget.currentText() + if isinstance(widget, QSpinBox): + return str(widget.value()) + if isinstance(widget, QDoubleSpinBox): + return str(widget.value()) + if isinstance(widget, QLineEdit): + return widget.text() return "" def encodeValueStr( diff --git a/src/gui/ALAutoScriptOrchDialog/_widgets.py b/src/gui/ALAutoScriptOrchDialog/_widgets.py index 5b7b7bd..f6e1c77 100644 --- a/src/gui/ALAutoScriptOrchDialog/_widgets.py +++ b/src/gui/ALAutoScriptOrchDialog/_widgets.py @@ -42,18 +42,18 @@ class ConditionRowFrame(QFrame): def __init__( self, - varMgr, - parentBlockIndex: int = 0, - isFirst: bool = False, + var_mgr, + parent_block_index: int = 0, + is_first: bool = False, parent = None ): super().__init__(parent) - self._varMgr = varMgr - self._blockIndex = parentBlockIndex - self._isFirst = isFirst - self._isBoolMode = False - self._rawRhsExpr = "" + self._var_mgr = var_mgr + self._block_index = parent_block_index + self._is_first = is_first + self._is_bool_mode = False + self._raw_rhs_expr = "" self.setupUi() self.connectSignals() @@ -69,7 +69,7 @@ class ConditionRowFrame(QFrame): Layout = QHBoxLayout(self) Layout.setContentsMargins(2, 2, 2, 2) Layout.setSpacing(4) - if self._isFirst: + if self._is_first: self.LogicCombo = None else: self.LogicCombo = makeComboWidget(LOGIC_OPTIONS, min_width=110, parent=self) @@ -82,11 +82,11 @@ class ConditionRowFrame(QFrame): Layout.addWidget(self.LeftVarCombo) self.OpCombo = makeComboWidget(COMPARE_OPTIONS, min_width=80, parent=self) Layout.addWidget(self.OpCombo) - self._CompTypeCombo = makeComboWidget([ + self.CompTypeCombo = makeComboWidget([ ("特定值", "literal"), ("变量", "variable"), ], min_width=70, parent=self) - Layout.addWidget(self._CompTypeCombo) + Layout.addWidget(self.CompTypeCombo) self.RhsStack = QStackedWidget(self) self.RhsStack.setFixedHeight(25) self.initLiteralStack() @@ -94,7 +94,7 @@ class ConditionRowFrame(QFrame): self.RhsStack.addWidget(self.RhsVarCombo) self.RhsStack.setCurrentIndex(0) Layout.addWidget(self.RhsStack) - if not self._isFirst: + if not self._is_first: self.DeleteBtn = QPushButton("×", self) self.DeleteBtn.setFixedSize(25, 25) self.DeleteBtn.setStyleSheet("color: red; font-weight: bold;") @@ -108,21 +108,21 @@ class ConditionRowFrame(QFrame): self ): - wasBool = self._isBoolMode - boolName = None - if wasBool: + was_bool = self._is_bool_mode + bool_name = None + if was_bool: data = self.LeftVarCombo.currentData() if data: - boolName = data[0] - self._varMgr.populateCombo(self.LeftVarCombo) + bool_name = data[0] + self._var_mgr.populateCombo(self.LeftVarCombo) # Append boolean literal sentinels at the end self.LeftVarCombo.insertSeparator(self.LeftVarCombo.count()) self.LeftVarCombo.addItem("true", ("true", "Boolean")) self.LeftVarCombo.addItem("false", ("false", "Boolean")) - if wasBool and boolName: + if was_bool and bool_name: for ci in range(self.LeftVarCombo.count()): d = self.LeftVarCombo.itemData(ci) - if d and d[0] == boolName: + if d and d[0] == bool_name: self.LeftVarCombo.setCurrentIndex(ci) break @@ -130,7 +130,7 @@ class ConditionRowFrame(QFrame): self ): - self._varMgr.populateCombo(self.RhsVarCombo) + self._var_mgr.populateCombo(self.RhsVarCombo) def initLiteralStack( self @@ -138,12 +138,12 @@ class ConditionRowFrame(QFrame): self.LiteralStack = QStackedWidget(self) self.LiteralStack.setFixedHeight(25) - self._literalWidgets = {} + self.literal_widgets = {} for vt in getTypeOrder(): W = makeValueWidget(vt, self.LiteralStack) - self._literalWidgets[vt] = W + self.literal_widgets[vt] = W self.LiteralStack.addWidget(W) - self.LiteralStack.setCurrentWidget(self._literalWidgets.get("String")) + self.LiteralStack.setCurrentWidget(self.literal_widgets.get("String")) self.RhsStack.addWidget(self.LiteralStack) def connectSignals( @@ -151,7 +151,7 @@ class ConditionRowFrame(QFrame): ): self.LeftVarCombo.currentIndexChanged.connect(self.onLeftVarChanged) - self._CompTypeCombo.currentIndexChanged.connect(self.onCompTypeChanged) + self.CompTypeCombo.currentIndexChanged.connect(self.onCompTypeChanged) def getLogic( self @@ -164,16 +164,16 @@ class ConditionRowFrame(QFrame): vartype: str ): - if vartype not in self._literalWidgets: + if vartype not in self.literal_widgets: vartype = "String" - self.LiteralStack.setCurrentWidget(self._literalWidgets[vartype]) + self.LiteralStack.setCurrentWidget(self.literal_widgets[vartype]) def toScript( self ) -> str: data = self.LeftVarCombo.currentData() - if self._isBoolMode and data: + if self._is_bool_mode and data: return data[0] if not data: return "" @@ -183,28 +183,28 @@ class ConditionRowFrame(QFrame): name = "datenow()" elif name == "CURRENT_TIME": name = "timenow()" - opSym = self.OpCombo.currentData() - if self._rawRhsExpr: - return f"{name} {opSym} {self._rawRhsExpr}" - isVarRef = (self._CompTypeCombo.currentData() == "variable") - if isVarRef: + op_sym = self.OpCombo.currentData() + if self._raw_rhs_expr: + return f"{name} {op_sym} {self._raw_rhs_expr}" + is_var_ref = (self.CompTypeCombo.currentData() == "variable") + if is_var_ref: rd = self.RhsVarCombo.currentData() if rd: - rhsName = rd[0] - if rhsName == "CURRENT_DATE": - rhsName = "datenow()" - elif rhsName == "CURRENT_TIME": - rhsName = "timenow()" - return f"{name} {opSym} {rhsName}" - rhsText = self.RhsVarCombo.currentText().strip() - if rhsText: - return f"{name} {opSym} {rhsText}" + rhs_name = rd[0] + if rhs_name == "CURRENT_DATE": + rhs_name = "datenow()" + elif rhs_name == "CURRENT_TIME": + rhs_name = "timenow()" + return f"{name} {op_sym} {rhs_name}" + rhs_text = self.RhsVarCombo.currentText().strip() + if rhs_text: + return f"{name} {op_sym} {rhs_text}" return "" - w = self._literalWidgets.get(vartype) + w = self.literal_widgets.get(vartype) if w: - rawVal = getValueFromWidget(w) - encoded = encodeValueStr(rawVal, vartype) - return f"{name} {opSym} {encoded}" + raw_val = getValueFromWidget(w) + encoded = encodeValueStr(raw_val, vartype) + return f"{name} {op_sym} {encoded}" return "" def refreshVarCombos( @@ -220,19 +220,19 @@ class ConditionRowFrame(QFrame): idx ): - self._rawRhsExpr = "" + self._raw_rhs_expr = "" if idx < 0: return data = self.LeftVarCombo.itemData(idx) if not data: return name, vartype = data - isBool = name in ("true", "false") - self._isBoolMode = isBool - self.OpCombo.setVisible(not isBool) - self._CompTypeCombo.setVisible(not isBool) - self.RhsStack.setVisible(not isBool) - if not isBool: + is_bool = name in ("true", "false") + self._is_bool_mode = is_bool + self.OpCombo.setVisible(not is_bool) + self.CompTypeCombo.setVisible(not is_bool) + self.RhsStack.setVisible(not is_bool) + if not is_bool: self.updateRHSLiteralWidget(vartype) @Slot(int) @@ -241,8 +241,8 @@ class ConditionRowFrame(QFrame): idx ): - self._rawRhsExpr = "" - isVar = (self._CompTypeCombo.currentData() == "variable") + self._raw_rhs_expr = "" + isVar = (self.CompTypeCombo.currentData() == "variable") self.RhsStack.setCurrentIndex(1 if isVar else 0) if isVar: self.populateRHSVarCombo() @@ -252,15 +252,15 @@ class ActionStepFrame(QFrame): def __init__( self, - varMgr, - parentBlockIndex: int = 0, + var_mgr, + parent_block_index: int = 0, parent = None ): super().__init__(parent) - self._varMgr = varMgr - self._blockIndex = parentBlockIndex - self._currentTargetType = "String" + self._var_mgr = var_mgr + self._block_index = parent_block_index + self._current_target_type = "String" self.setupUi() self.connectSignals() @@ -312,7 +312,7 @@ class ActionStepFrame(QFrame): for p in getPresetVars(): if p["name"] in ("CURRENT_TIME", "CURRENT_DATE"): continue - info = self._varMgr.getInfoByName(p["name"]) + info = self._var_mgr.getInfoByName(p["name"]) if info: self.TargetCombo.addItem( info["display"], @@ -324,18 +324,18 @@ class ActionStepFrame(QFrame): self ): - self._literalWidgets = {} - self._offsetWidgets = {} + self.literal_widgets = {} + self.offset_widgets = {} for vt in getTypeOrder(): - self._literalWidgets[vt] = makeValueWidget(vt, self.ValueStack) - self.ValueStack.addWidget(self._literalWidgets[vt]) + self.literal_widgets[vt] = makeValueWidget(vt, self.ValueStack) + self.ValueStack.addWidget(self.literal_widgets[vt]) if getArithType(vt): - self._offsetWidgets[vt] = makeOffsetWidget(vt, self.ValueStack) - self.ValueStack.addWidget(self._offsetWidgets[vt]) + self.offset_widgets[vt] = makeOffsetWidget(vt, self.ValueStack) + self.ValueStack.addWidget(self.offset_widgets[vt]) else: Lbl = QLabel("(不支持该操作)", self.ValueStack) Lbl.setFixedHeight(25) - self._offsetWidgets[vt] = Lbl + self.offset_widgets[vt] = Lbl self.ValueStack.addWidget(Lbl) def connectSignals( @@ -358,14 +358,14 @@ class ActionStepFrame(QFrame): ): op = self.OpTypeCombo.currentData() - isArith = (op in ("add", "sub")) - actualType = self._currentTargetType - if isArith and actualType in self._offsetWidgets: - self.ValueStack.setCurrentWidget(self._offsetWidgets[actualType]) - elif actualType in self._literalWidgets: - self.ValueStack.setCurrentWidget(self._literalWidgets[actualType]) + is_arith = (op in ("add", "sub")) + actual_type = self._current_target_type + if is_arith and actual_type in self.offset_widgets: + self.ValueStack.setCurrentWidget(self.offset_widgets[actual_type]) + elif actual_type in self.literal_widgets: + self.ValueStack.setCurrentWidget(self.literal_widgets[actual_type]) else: - self.ValueStack.setCurrentWidget(self._literalWidgets.get("String")) + self.ValueStack.setCurrentWidget(self.literal_widgets.get("String")) def toScript( self @@ -380,10 +380,10 @@ class ActionStepFrame(QFrame): return " -- pass" if not target: return "" - rawVal = self.getValueRaw() - vartype = self._currentTargetType + raw_val = self.getValueRaw() + vartype = self._current_target_type if op == "set": - encoded = encodeValueStr(rawVal, vartype) + encoded = encodeValueStr(raw_val, vartype) return f" {target} = {encoded}" elif op == "add": if vartype == "Date" and hasattr(self.ValueStack.currentWidget(), "getOffsetDays"): @@ -392,7 +392,7 @@ class ActionStepFrame(QFrame): if vartype == "Time" and hasattr(self.ValueStack.currentWidget(), "getOffsetHours"): hours = self.ValueStack.currentWidget().getOffsetHours() return f" {target} = timeadd({target}, {hours})" - return f" {target} = {target} + {rawVal}" + return f" {target} = {target} + {raw_val}" elif op == "sub": if vartype == "Date" and hasattr(self.ValueStack.currentWidget(), "getOffsetDays"): days = self.ValueStack.currentWidget().getOffsetDays() @@ -400,7 +400,7 @@ class ActionStepFrame(QFrame): if vartype == "Time" and hasattr(self.ValueStack.currentWidget(), "getOffsetHours"): hours = self.ValueStack.currentWidget().getOffsetHours() return f" {target} = timeadd({target}, -{hours})" - return f" {target} = {target} - {rawVal}" + return f" {target} = {target} - {raw_val}" return "" def getValueRaw( @@ -419,15 +419,15 @@ class ActionStepFrame(QFrame): self ): - currentData = self.TargetCombo.currentData() + current_data = self.TargetCombo.currentData() self.populateTargetCombo() - if currentData: + if current_data: for i in range(self.TargetCombo.count()): d = self.TargetCombo.itemData(i) - if d and d[0] == currentData[0]: + if d and d[0] == current_data[0]: self.TargetCombo.setCurrentIndex(i) break - self._varMgr.populateCombo(self.ExistingVarCombo) + self._var_mgr.populateCombo(self.ExistingVarCombo) @Slot(int) def onTargetChanged( @@ -441,7 +441,7 @@ class ActionStepFrame(QFrame): if not data: return _, vartype = data - self._currentTargetType = vartype + self._current_target_type = vartype self.updateValueWidget() self.onValueSrcChanged(self.ValueSrcCombo.currentIndex()) @@ -459,10 +459,10 @@ class ActionStepFrame(QFrame): idx ): - isVar = (self.ValueSrcCombo.currentData() == "variable") - self.ValueStack.setVisible(not isVar) - self.ExistingVarCombo.setVisible(isVar) - if isVar: - self._varMgr.populateCombo(self.ExistingVarCombo) + is_var = (self.ValueSrcCombo.currentData() == "variable") + self.ValueStack.setVisible(not is_var) + self.ExistingVarCombo.setVisible(is_var) + if is_var: + self._var_mgr.populateCombo(self.ExistingVarCombo) else: self.updateValueWidget() diff --git a/src/gui/ALBulletinDialog.py b/src/gui/ALBulletinDialog.py index b5350e3..ef66430 100644 --- a/src/gui/ALBulletinDialog.py +++ b/src/gui/ALBulletinDialog.py @@ -103,9 +103,9 @@ class ALBulletinItemWidget(QWidget): self.ItemWidgetLayout.setContentsMargins(10, 5, 10, 5) self.BulletinTitleLayout = QHBoxLayout() self.BulletinTitleLabel = QLabel(self.__bulletin.get("title", "无标题")) - titleFont = QFont() - titleFont.setBold(True) - self.BulletinTitleLabel.setFont(titleFont) + title_font = QFont() + title_font.setBold(True) + self.BulletinTitleLabel.setFont(title_font) self.BulletinTitleLayout.addWidget(self.BulletinTitleLabel) if self.__bulletin.get("isNew", False): self.NewIndicatorLabel = QLabel("新 !") @@ -184,11 +184,10 @@ class ALBulletinDialog(QDialog, Ui_ALBulletinDialog): self.SyncLayout.replaceWidget( self.SyncStatusPlaceholder, self.ALSyncStatusLabel ) - - titleFont = QFont() - titleFont.setBold(True) - titleFont.setPointSize(15) - self.BulletinTitleLabel.setFont(titleFont) + title_font = QFont() + title_font.setBold(True) + title_font.setPointSize(15) + self.BulletinTitleLabel.setFont(title_font) self.BulletinDateLabel.setStyleSheet("color: #969696;") self.BulletinAuthorLabel.setStyleSheet("color: #969696;") self.BulletinIsEditedLabel.setStyleSheet("color: #FF9800;") @@ -322,7 +321,6 @@ class ALBulletinDialog(QDialog, Ui_ALBulletinDialog): worker.wait(2000) worker.deleteLater() self.__fetch_worker = None - bulletins = data.get("bulletins", []) delete_ids = data.get("delete_ids", []) merged = self.__bulletin_mgr.updateAndMergeBulletins(bulletins, delete_ids) @@ -355,7 +353,6 @@ class ALBulletinDialog(QDialog, Ui_ALBulletinDialog): worker.wait(2000) worker.deleteLater() self.__fetch_worker = None - self.SyncButton.setEnabled(True) self.SyncButton.setText("重试") self.ALSyncStatusLabel.status = ALStatusLabel.Status.FAILURE diff --git a/src/gui/ALConfigWidget.py b/src/gui/ALConfigWidget.py index e06727b..b0b4c84 100644 --- a/src/gui/ALConfigWidget.py +++ b/src/gui/ALConfigWidget.py @@ -364,18 +364,18 @@ class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget): user_config = self.defaultUserConfig() for i in range(self.UserTreeWidget.topLevelItemCount()): - GroupItem = self.UserTreeWidget.topLevelItem(i) + group_item = self.UserTreeWidget.topLevelItem(i) group_config = { - "name": GroupItem.text(0), - "enabled": GroupItem.checkState(1) == Qt.CheckState.Checked, + "name": group_item.text(0), + "enabled": group_item.checkState(1) == Qt.CheckState.Checked, "users": [] } - for j in range(GroupItem.childCount()): - UserItem = GroupItem.child(j) - user = UserItem.data(0, Qt.UserRole) + for j in range(group_item.childCount()): + user_item = group_item.child(j) + user = user_item.data(0, Qt.UserRole) if not user: continue - user["enabled"] = UserItem.checkState(1) == Qt.CheckState.Checked + user["enabled"] = user_item.checkState(1) == Qt.CheckState.Checked group_config["users"].append(user) user_config["groups"].append(group_config) return user_config @@ -431,18 +431,18 @@ class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget): try: if "groups" in users: for group_config in users["groups"]: - GroupItem = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value) - GroupItem.setText(0, group_config["name"]) - GroupItem.setFlags(GroupItem.flags() | Qt.ItemIsEditable) - GroupItem.setCheckState(1, Qt.Checked if group_config.get("enabled", True) else Qt.Unchecked) + group_item = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value) + group_item.setText(0, group_config["name"]) + group_item.setFlags(group_item.flags() | Qt.ItemIsEditable) + group_item.setCheckState(1, Qt.Checked if group_config.get("enabled", True) else Qt.Unchecked) for user_config in group_config["users"]: - UserItem = QTreeWidgetItem(GroupItem, ALUserTreeItemType.USER.value) - UserItem.setText(0, user_config["username"]) - UserItem.setText(1, "" if user_config.get("enabled", True) else "跳过") - UserItem.setData(0, Qt.UserRole, user_config) - UserItem.setCheckState(1, Qt.Checked if user_config.get("enabled", True) else Qt.Unchecked) - UserItem.setDisabled(not group_config.get("enabled", True)) - GroupItem.setExpanded(True) + user_item = QTreeWidgetItem(group_item, ALUserTreeItemType.USER.value) + user_item.setText(0, user_config["username"]) + user_item.setText(1, "" if user_config.get("enabled", True) else "跳过") + user_item.setData(0, Qt.UserRole, user_config) + user_item.setCheckState(1, Qt.Checked if user_config.get("enabled", True) else Qt.Unchecked) + user_item.setDisabled(not group_config.get("enabled", True)) + group_item.setExpanded(True) except KeyError as e: QMessageBox.warning( self, @@ -616,43 +616,43 @@ class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget): ) -> QTreeWidgetItem: self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged) - GroupItem = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value) + group_item = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value) if not group_name: group_name = f"新分组-{self.UserTreeWidget.topLevelItemCount()}" - GroupItem.setText(0, group_name) - GroupItem.setFlags(GroupItem.flags() | Qt.ItemIsEditable) - GroupItem.setCheckState(1, Qt.Checked) - self.UserTreeWidget.setCurrentItem(GroupItem) + group_item.setText(0, group_name) + group_item.setFlags(group_item.flags() | Qt.ItemIsEditable) + group_item.setCheckState(1, Qt.Checked) + self.UserTreeWidget.setCurrentItem(group_item) self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged) - return GroupItem + return group_item def delGroup( self, - GroupItem: QTreeWidgetItem = None + group_item: QTreeWidgetItem = None ): - if GroupItem is None: + if group_item is None: return - if GroupItem.type() != ALUserTreeItemType.GROUP.value: + if group_item.type() != ALUserTreeItemType.GROUP.value: return - index = self.UserTreeWidget.indexOfTopLevelItem(GroupItem) + index = self.UserTreeWidget.indexOfTopLevelItem(group_item) self.UserTreeWidget.takeTopLevelItem(index) def addUser( self, - GroupItem: QTreeWidgetItem = None + group_item: QTreeWidgetItem = None ) -> QTreeWidgetItem: - if GroupItem is None: - CurrentItem = self.UserTreeWidget.currentItem() - if CurrentItem is None: - GroupItem = self.addGroup() - if GroupItem.type() == ALUserTreeItemType.USER.value: - GroupItem = GroupItem.parent() - if GroupItem.checkState(1) == Qt.CheckState.Unchecked: + if group_item is None: + current_item = self.UserTreeWidget.currentItem() + if current_item is None: + group_item = self.addGroup() + if group_item.type() == ALUserTreeItemType.USER.value: + group_item = group_item.parent() + if group_item.checkState(1) == Qt.CheckState.Unchecked: return None new_user = { - "username": f"新用户-{GroupItem.childCount()}", + "username": f"新用户-{group_item.childCount()}", "password": "000000", "enabled": True, "reserve_info": { @@ -681,30 +681,30 @@ class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget): } } self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged) - UserItem = QTreeWidgetItem(GroupItem, ALUserTreeItemType.USER.value) - UserItem.setText(0, new_user["username"]) - UserItem.setText(1, "") - UserItem.setData(0, Qt.UserRole, new_user) - UserItem.setCheckState(1, Qt.CheckState.Checked) - GroupItem.setExpanded(True) - self.UserTreeWidget.setCurrentItem(UserItem) + user_item = QTreeWidgetItem(group_item, ALUserTreeItemType.USER.value) + user_item.setText(0, new_user["username"]) + user_item.setText(1, "") + user_item.setData(0, Qt.UserRole, new_user) + user_item.setCheckState(1, Qt.CheckState.Checked) + group_item.setExpanded(True) + self.UserTreeWidget.setCurrentItem(user_item) self.setUserToWidget(new_user) self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged) - return UserItem + return user_item def delUser( self, - UserItem: QTreeWidgetItem = None + user_item: QTreeWidgetItem = None ): - if UserItem is None: + if user_item is None: return - if UserItem.type() != ALUserTreeItemType.USER.value: + if user_item.type() != ALUserTreeItemType.USER.value: return - ParentItem = UserItem.parent() - index = ParentItem.indexOfChild(UserItem) - ParentItem.takeChild(index) - if ParentItem.childCount() == 0: + parent_item = user_item.parent() + index = parent_item.indexOfChild(user_item) + parent_item.takeChild(index) + if parent_item.childCount() == 0: self.UserTreeWidget.setCurrentItem(None) def renameItem( @@ -822,10 +822,10 @@ class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget): if item.type() == ALUserTreeItemType.GROUP.value: is_checked = item.checkState(1) == Qt.CheckState.Checked for i in range(item.childCount()): - Child = item.child(i) - if self.UserTreeWidget.currentItem() == Child: + child = item.child(i) + if self.UserTreeWidget.currentItem() == child: self.UserTreeWidget.setCurrentItem(item) - Child.setDisabled(not is_checked) + child.setDisabled(not is_checked) else: is_checked = item.checkState(1) == Qt.CheckState.Checked item.setText(1, "" if is_checked else "跳过") @@ -842,32 +842,32 @@ class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget): def showGroupMenu( self, menu: QMenu, - GroupItem: QTreeWidgetItem = None + group_item: QTreeWidgetItem = None ): AddUserAction = QAction("添加用户", menu) RenameGroupAction = QAction("重命名分组", menu) DelGroupAction = QAction("删除分组", menu) - AddUserAction.triggered.connect(lambda: self.addUser(GroupItem)) - RenameGroupAction.triggered.connect(lambda: self.renameItem(GroupItem)) - DelGroupAction.triggered.connect(lambda: self.delGroup(GroupItem)) + AddUserAction.triggered.connect(lambda: self.addUser(group_item)) + RenameGroupAction.triggered.connect(lambda: self.renameItem(group_item)) + DelGroupAction.triggered.connect(lambda: self.delGroup(group_item)) menu.addAction(AddUserAction) menu.addSeparator() menu.addAction(RenameGroupAction) menu.addAction(DelGroupAction) - if GroupItem.checkState(1) == Qt.CheckState.Unchecked: + if group_item.checkState(1) == Qt.CheckState.Unchecked: AddUserAction.setEnabled(False) def showUserMenu( self, menu: QMenu, - UserItem: QTreeWidgetItem = None + user_item: QTreeWidgetItem = None ): RenameUserAction = QAction("重命名用户", menu) DelUserAction = QAction("删除用户", menu) - RenameUserAction.triggered.connect(lambda: self.renameItem(UserItem)) - DelUserAction.triggered.connect(lambda: self.delUser(UserItem)) + RenameUserAction.triggered.connect(lambda: self.renameItem(user_item)) + DelUserAction.triggered.connect(lambda: self.delUser(user_item)) menu.addAction(RenameUserAction) menu.addAction(DelUserAction) @@ -877,14 +877,14 @@ class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget): pos ): - CurrentItem = self.UserTreeWidget.itemAt(pos) + current_item = self.UserTreeWidget.itemAt(pos) Menu = QMenu(self.UserTreeWidget) - if CurrentItem is None: + if current_item is None: self.showTreeMenu(Menu) - elif CurrentItem.type() == ALUserTreeItemType.GROUP.value: - self.showGroupMenu(Menu, CurrentItem) + elif current_item.type() == ALUserTreeItemType.GROUP.value: + self.showGroupMenu(Menu, current_item) else: - self.showUserMenu(Menu, CurrentItem) + self.showUserMenu(Menu, current_item) Menu.exec_(self.UserTreeWidget.mapToGlobal(pos)) @Slot() @@ -892,16 +892,16 @@ class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget): self ): - CurrentItem = self.UserTreeWidget.currentItem() - self.addUser(CurrentItem) + current_item = self.UserTreeWidget.currentItem() + self.addUser(current_item) @Slot() def onDelUserButtonClicked( self ): - CurrentItem = self.UserTreeWidget.currentItem() - self.delUser(CurrentItem) + current_item = self.UserTreeWidget.currentItem() + self.delUser(current_item) @Slot() def onBrowseBrowserDriverButtonClicked( @@ -1034,7 +1034,6 @@ class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget): ): msg = "" - run_config_path = self.ExportRunConfigEdit.text() user_config_path = self.ExportUserConfigEdit.text() if run_config_path: @@ -1111,8 +1110,8 @@ class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget): self ): - CurrentItem = self.UserTreeWidget.currentItem() - if CurrentItem and CurrentItem.type() == ALUserTreeItemType.USER.value: + current_item = self.UserTreeWidget.currentItem() + if current_item and current_item.type() == ALUserTreeItemType.USER.value: self.UserTreeWidget.setCurrentItem(None) if self.saveConfigs( self.__config_paths["run"], diff --git a/src/gui/ALMainWindow.py b/src/gui/ALMainWindow.py index 96f1472..6de44dc 100644 --- a/src/gui/ALMainWindow.py +++ b/src/gui/ALMainWindow.py @@ -62,10 +62,10 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): 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.__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 @@ -99,23 +99,23 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): # initialize timer task widget, but not show it try: - self.__alTimerTaskManageWidget = ALTimerTaskManageWidget(self) + self.__ALTimerTaskManageWidget = ALTimerTaskManageWidget(self) except Exception as e: QMessageBox.critical( self, "错误 - AutoLibrary", f"初始化定时任务功能失败: \n{e}" ) - self.__alTimerTaskManageWidget = None + self.__ALTimerTaskManageWidget = None 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.__alTimerTaskManageWidget.timerTaskManageWidgetIsClosed.connect(self.onTimerTaskManageWidgetClosed) - self.__alTimerTaskManageWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint) + 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.__ALTimerTaskManageWidget.timerTaskManageWidgetIsClosed.connect(self.onTimerTaskManageWidgetClosed) + self.__ALTimerTaskManageWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint) def onAboutActionTriggered( self @@ -128,8 +128,8 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self ): - Url = QUrl("https://www.autolibrary.kenanzhu.com/manuals") - QDesktopServices.openUrl(Url) + url = QUrl("https://www.autolibrary.kenanzhu.com/manuals") + QDesktopServices.openUrl(url) def setupTray( self @@ -148,7 +148,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): 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.show() @@ -183,7 +182,6 @@ 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 ) @@ -204,17 +202,17 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): if self.__is_running_timer_task: self.__current_timer_task_thread.wait(2000) self.__current_timer_task_thread.deleteLater() - if self.__alTimerTaskManageWidget: - self.__alTimerTaskManageWidget.close() - self.__alTimerTaskManageWidget.deleteLater() - if self.__alConfigWidget: - self.__alConfigWidget.close() + if self.__ALTimerTaskManageWidget: + self.__ALTimerTaskManageWidget.close() + self.__ALTimerTaskManageWidget.deleteLater() + if self.__ALConfigWidget: + self.__ALConfigWidget.close() # the config widget is already deleted in the 'self.onConfigWidgetClosed' - if self.__alSettingsWidget: - self.__alSettingsWidget.close() + if self.__ALSettingsWidget: + self.__ALSettingsWidget.close() # the settings widget is already deleted in the 'self.onSettingsWidgetClosed' - if self.__alBulletinDialog: - self.__alBulletinDialog.close() + if self.__ALBulletinDialog: + self.__ALBulletinDialog.close() # the bulletin dialog is already deleted in the 'self.onBulletinDialogClosed' if self.__bulletin_poller: self.__bulletin_poller.stop() @@ -322,10 +320,10 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self ): - if self.__alConfigWidget: - self.__alConfigWidget.configWidgetIsClosed.disconnect(self.onConfigWidgetClosed) - self.__alConfigWidget.deleteLater() - self.__alConfigWidget = None + if self.__ALConfigWidget: + self.__ALConfigWidget.configWidgetIsClosed.disconnect(self.onConfigWidgetClosed) + self.__ALConfigWidget.deleteLater() + self.__ALConfigWidget = None self.__config_paths = ConfigUtils.getAutomationConfigPaths() self.setControlButtons(True, None, None) self._showLog("配置窗口已关闭,配置文件路径已更新") @@ -335,10 +333,10 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self ): - if self.__alSettingsWidget: - self.__alSettingsWidget.settingsWidgetIsClosed.disconnect(self.onSettingsWidgetClosed) - self.__alSettingsWidget.deleteLater() - self.__alSettingsWidget = None + if self.__ALSettingsWidget: + self.__ALSettingsWidget.settingsWidgetIsClosed.disconnect(self.onSettingsWidgetClosed) + self.__ALSettingsWidget.deleteLater() + self.__ALSettingsWidget = None self.SettingsAction.setEnabled(True) @Slot() @@ -347,12 +345,12 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): ): self.__bulletin_poller.setDialogOpen(True) - if self.__alBulletinDialog is None: - self.__alBulletinDialog = ALBulletinDialog(self) - self.__alBulletinDialog.finished.connect(self.onBulletinDialogClosed) - self.__alBulletinDialog.show() - self.__alBulletinDialog.raise_() - self.__alBulletinDialog.activateWindow() + if self.__ALBulletinDialog is None: + self.__ALBulletinDialog = ALBulletinDialog(self) + self.__ALBulletinDialog.finished.connect(self.onBulletinDialogClosed) + self.__ALBulletinDialog.show() + self.__ALBulletinDialog.raise_() + self.__ALBulletinDialog.activateWindow() self._showLog("打开公告栏窗口") @Slot() @@ -360,10 +358,10 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self ): - if self.__alBulletinDialog: - self.__alBulletinDialog.finished.disconnect(self.onBulletinDialogClosed) - self.__alBulletinDialog.deleteLater() - self.__alBulletinDialog = None + if self.__ALBulletinDialog: + self.__ALBulletinDialog.finished.disconnect(self.onBulletinDialogClosed) + self.__ALBulletinDialog.deleteLater() + self.__ALBulletinDialog = None self.__bulletin_poller.setDialogOpen(False) @Slot(int) @@ -386,12 +384,12 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self ): - if self.__alSettingsWidget is None: - self.__alSettingsWidget = ALSettingsWidget(self) - self.__alSettingsWidget.settingsWidgetIsClosed.connect(self.onSettingsWidgetClosed) - self.__alSettingsWidget.show() - self.__alSettingsWidget.raise_() - self.__alSettingsWidget.activateWindow() + if self.__ALSettingsWidget is None: + self.__ALSettingsWidget = ALSettingsWidget(self) + self.__ALSettingsWidget.settingsWidgetIsClosed.connect(self.onSettingsWidgetClosed) + self.__ALSettingsWidget.show() + self.__ALSettingsWidget.raise_() + self.__ALSettingsWidget.activateWindow() self.SettingsAction.setEnabled(False) self._showLog("打开全局设置窗口") @@ -437,9 +435,9 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self ): - self.__alTimerTaskManageWidget.show() - self.__alTimerTaskManageWidget.raise_() - self.__alTimerTaskManageWidget.activateWindow() + self.__ALTimerTaskManageWidget.show() + self.__ALTimerTaskManageWidget.raise_() + self.__ALTimerTaskManageWidget.activateWindow() self.TimerTaskManageWidgetButton.setEnabled(False) self._showLog("打开定时任务管理窗口") @@ -448,12 +446,12 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self ): - if self.__alConfigWidget is None: - self.__alConfigWidget = ALConfigWidget(self) - self.__alConfigWidget.configWidgetIsClosed.connect(self.onConfigWidgetClosed) - self.__alConfigWidget.show() - self.__alConfigWidget.raise_() - self.__alConfigWidget.activateWindow() + if self.__ALConfigWidget is None: + self.__ALConfigWidget = ALConfigWidget(self) + self.__ALConfigWidget.configWidgetIsClosed.connect(self.onConfigWidgetClosed) + self.__ALConfigWidget.show() + self.__ALConfigWidget.raise_() + self.__ALConfigWidget.activateWindow() self.ConfigButton.setEnabled(False) self._showLog("打开配置窗口") diff --git a/src/gui/ALSeatMapSelectDialog.py b/src/gui/ALSeatMapSelectDialog.py index 67c812b..342d60e 100644 --- a/src/gui/ALSeatMapSelectDialog.py +++ b/src/gui/ALSeatMapSelectDialog.py @@ -55,7 +55,6 @@ class ALSeatMapSelectDialog(CenterOnParentMixin, QDialog): self.setMinimumSize(800, 600) self.resize(800, 600) self.setWindowTitle(f"选择楼层座位 - AutoLibrary") - self.SeatMapWidgetMainLayout = QVBoxLayout(self) self.SeatMapWidgetMainLayout.setContentsMargins(5, 5, 5, 5) self.SeatMapWidgetMainLayout.setSpacing(5) @@ -63,10 +62,8 @@ class ALSeatMapSelectDialog(CenterOnParentMixin, QDialog): self.TitleLabel.setAlignment(Qt.AlignmentFlag.AlignCenter) self.TitleLabel.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;") self.SeatMapWidgetMainLayout.addWidget(self.TitleLabel) - self.SeatMapGraphicsView = ALSeatMapView(None, self.__seats_data) self.SeatMapWidgetMainLayout.addWidget(self.SeatMapGraphicsView) - self.TipsLabel = QLabel( " 点击座位进行选择/取消选择, 最多选择1个座位 \n" " [操作方法: Ctrl+鼠标滚轮缩放 | 滚轮/拖拽/方向键 移动]" @@ -74,7 +71,6 @@ class ALSeatMapSelectDialog(CenterOnParentMixin, QDialog): self.TipsLabel.setAlignment(Qt.AlignmentFlag.AlignLeft) self.TipsLabel.setStyleSheet("color: #666; margin: 5px;") self.SeatMapWidgetMainLayout.addWidget(self.TipsLabel) - self.ConfirmButton = QPushButton("确认") self.ConfirmButton.setFixedSize(80, 25) self.ConfirmButton.setAutoDefault(True) diff --git a/src/gui/ALSeatMapView.py b/src/gui/ALSeatMapView.py index 0379bb0..56b6878 100644 --- a/src/gui/ALSeatMapView.py +++ b/src/gui/ALSeatMapView.py @@ -85,11 +85,9 @@ class ALSeatMapView(QGraphicsView): self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) self.viewport().installEventFilter(self) - self.SeatsContainerWidget = QWidget() self.SeatsContainerLayout = QGridLayout(self.SeatsContainerWidget) self.setupSeatMap() - self.ContainerProxy = self.SeatMapGraphicsScene.addWidget(self.SeatsContainerWidget) self.ContainerProxy.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, False) diff --git a/src/gui/ALStatusLabel.py b/src/gui/ALStatusLabel.py index c1668a5..537a86d 100644 --- a/src/gui/ALStatusLabel.py +++ b/src/gui/ALStatusLabel.py @@ -118,34 +118,34 @@ class ALStatusLabel(QLabel): event ): - Painter = QPainter(self) - Painter.setRenderHint(QPainter.RenderHint.Antialiasing) + 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.setCapStyle(Qt.PenCapStyle.RoundCap) - Pen.setColor(QColor("#969696")) # grey - Painter.setPen(Pen) - Painter.drawEllipse( + pen = painter.pen() + pen.setWidth(2) + 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( + 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), @@ -153,99 +153,99 @@ class ALStatusLabel(QLabel): ) case self.Status.SUCCESS: # draw the success green circle - Pen = Painter.pen() - Pen.setWidth(2) - Pen.setCapStyle(Qt.PenCapStyle.RoundCap) - Pen.setColor(QColor("#4CAF50" if self.isDarkMode() else "#00AF50")) # green - Painter.setPen(Pen) - Painter.drawEllipse( + pen = painter.pen() + pen.setWidth(2) + 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) + 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) + 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( + painter.drawLine( int(mark_path[0][0]),int(mark_path[0][1]), int(mark_path[1][0]),int(mark_path[1][1]) ) - Painter.drawLine( + 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.setCapStyle(Qt.PenCapStyle.RoundCap) - Pen.setColor(QColor("#FF9800")) # orange - Painter.setPen(Pen) - Painter.drawEllipse( + pen = painter.pen() + pen.setWidth(2) + 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) + 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( + 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( + 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.setCapStyle(Qt.PenCapStyle.RoundCap) - Pen.setColor(QColor("#DC0000")) # red - Painter.setPen(Pen) - Painter.drawEllipse( + pen = painter.pen() + pen.setWidth(2) + 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) + 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) + pen.setColor(self.getMarkColor()) + painter.setPen(pen) mark_size = radius/3 - Painter.drawLine( + painter.drawLine( int(center_x - mark_size), int(center_y - mark_size), int(center_x + mark_size), int(center_y + mark_size) ) - Painter.drawLine( + painter.drawLine( int(center_x + mark_size), int(center_y - mark_size), int(center_x - mark_size), int(center_y + mark_size) ) - Painter.end() + painter.end() super().paintEvent(event) diff --git a/src/gui/ALTimerTaskHistoryDialog.py b/src/gui/ALTimerTaskHistoryDialog.py index 40eec21..2267bf2 100644 --- a/src/gui/ALTimerTaskHistoryDialog.py +++ b/src/gui/ALTimerTaskHistoryDialog.py @@ -108,24 +108,24 @@ class ALTimerTaskHistoryDialog(QDialog): execute_time = record.get("execute_time", "") result = record.get("result", ALTimerTaskStatus.UNKNOWN) duration = record.get("duration", 0) - ExecuteTimeItem = QTableWidgetItem(execute_time) - ExecuteTimeItem.setTextAlignment(Qt.AlignmentFlag.AlignCenter) - self.HistoryTableWidget.setItem(row, 0, ExecuteTimeItem) - ResultItem = QTableWidgetItem(result.value) - ResultItem.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + execute_time_item = QTableWidgetItem(execute_time) + execute_time_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + self.HistoryTableWidget.setItem(row, 0, execute_time_item) + result_item = QTableWidgetItem(result.value) + result_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) match result: case ALTimerTaskStatus.EXECUTED: - ResultItem.setForeground(Qt.GlobalColor.green) + result_item.setForeground(Qt.GlobalColor.green) case ALTimerTaskStatus.ERROR: - ResultItem.setForeground(Qt.GlobalColor.red) + result_item.setForeground(Qt.GlobalColor.red) case ALTimerTaskStatus.OUTDATED: - ResultItem.setForeground(Qt.GlobalColor.red) + result_item.setForeground(Qt.GlobalColor.red) case _: - ResultItem.setForeground(Qt.GlobalColor.black) - self.HistoryTableWidget.setItem(row, 1, ResultItem) - DurationItem = QTableWidgetItem(f"{duration:.2f}") - DurationItem.setTextAlignment(Qt.AlignmentFlag.AlignCenter) - self.HistoryTableWidget.setItem(row, 2, DurationItem) + result_item.setForeground(Qt.GlobalColor.black) + self.HistoryTableWidget.setItem(row, 1, result_item) + duration_item = QTableWidgetItem(f"{duration:.2f}") + duration_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + self.HistoryTableWidget.setItem(row, 2, duration_item) self.HistoryTableWidget.setRowHeight(row, 25) @Slot() diff --git a/src/gui/ALTimerTaskManageWidget.py b/src/gui/ALTimerTaskManageWidget.py index 8b9ac30..d97ef6b 100644 --- a/src/gui/ALTimerTaskManageWidget.py +++ b/src/gui/ALTimerTaskManageWidget.py @@ -81,12 +81,12 @@ class ALTimerTaskItemWidget(QWidget): self.TaskInfoLayout = QVBoxLayout() self.TaskInfoLayout.setSpacing(5) TaskNameLabel = QLabel(self.__timer_task["name"]) - TaskNameLabelFont = TaskNameLabel.font() - TaskNameLabelFont.setBold(True) - TaskNameLabel.setFont(TaskNameLabelFont) + task_name_label_font = TaskNameLabel.font() + task_name_label_font.setBold(True) + TaskNameLabel.setFont(task_name_label_font) TaskNameLabel.setFixedHeight(25) self.TaskInfoLayout.addWidget(TaskNameLabel) - ExecuteTimeStr = self.__timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S") + execute_time_str = self.__timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S") if self.__timer_task.get("repeat", False): repeat_days = self.__timer_task.get("repeat_days", []) repeat_hour = self.__timer_task.get("repeat_hour", 0) @@ -94,14 +94,14 @@ class ALTimerTaskItemWidget(QWidget): repeat_second = self.__timer_task.get("repeat_second", 0) if len(repeat_days) == 7: time_str = f"{repeat_hour:02d}:{repeat_minute:02d}:{repeat_second:02d}" - ExecuteTimeLabel = QLabel(f"下次执行时间: {ExecuteTimeStr} (每日 {time_str})") + ExecuteTimeLabel = QLabel(f"下次执行时间: {execute_time_str} (每日 {time_str})") else: day_names = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"] selected_days = [day_names[d] for d in repeat_days] time_str = f"{repeat_hour:02d}:{repeat_minute:02d}:{repeat_second:02d}" - ExecuteTimeLabel = QLabel(f"下次执行时间: {ExecuteTimeStr} (每{','.join(selected_days)} {time_str})") + ExecuteTimeLabel = QLabel(f"下次执行时间: {execute_time_str} (每{','.join(selected_days)} {time_str})") else: - ExecuteTimeLabel = QLabel(f"执行时间: {ExecuteTimeStr}") + ExecuteTimeLabel = QLabel(f"执行时间: {execute_time_str}") ExecuteTimeLabel.setStyleSheet("color: #969696;") ExecuteTimeLabel.setFixedHeight(20) self.TaskInfoLayout.addWidget(ExecuteTimeLabel) diff --git a/src/gui/ALUserTreeWidget.py b/src/gui/ALUserTreeWidget.py index 17c9ae4..53d2392 100644 --- a/src/gui/ALUserTreeWidget.py +++ b/src/gui/ALUserTreeWidget.py @@ -109,27 +109,27 @@ class ALUserTreeWidget(QTreeWidget): super().dragMoveEvent(event) - SourceItem = self.currentItem() - TargetItem = self.itemAt(event.position().toPoint()) - if SourceItem is None: + source_item = self.currentItem() + target_item = self.itemAt(event.position().toPoint()) + if source_item is None: event.ignore() return - if SourceItem.type() == ALUserTreeItemType.GROUP.value: - if TargetItem is not None: + if source_item.type() == ALUserTreeItemType.GROUP.value: + if target_item is not None: event.ignore() return - elif SourceItem.type() == ALUserTreeItemType.USER.value: - if TargetItem is None: + elif source_item.type() == ALUserTreeItemType.USER.value: + if target_item is None: event.ignore() return - if TargetItem.type() != ALUserTreeItemType.GROUP.value: + if target_item.type() != ALUserTreeItemType.GROUP.value: event.ignore() return - if TargetItem.checkState(1) == Qt.CheckState.Unchecked: + if target_item.checkState(1) == Qt.CheckState.Unchecked: event.ignore() return if not self.isDragPositionValid( - self.visualItemRect(TargetItem), + self.visualItemRect(target_item), event.position().toPoint() ): event.ignore() diff --git a/src/gui/ALWebDriverDownloadDialog.py b/src/gui/ALWebDriverDownloadDialog.py index aaf619e..4f513d9 100644 --- a/src/gui/ALWebDriverDownloadDialog.py +++ b/src/gui/ALWebDriverDownloadDialog.py @@ -140,7 +140,6 @@ class ALWebDriverDownloadDialog(CenterOnParentMixin, QDialog): """ super().__init__(parent) - self.__driver_dir = driver_dir self.__driver_manager: Optional[WebDriverManager] = None self.__confirmed = False