diff --git a/src/base/MsgBase.py b/src/base/MsgBase.py
index 68c9ae0..7e10ebf 100644
--- a/src/base/MsgBase.py
+++ b/src/base/MsgBase.py
@@ -11,7 +11,7 @@ import logging
import queue
import datetime
-import managers.log.LogManager as LogManager
+from managers.log.LogManager import getLogger
class MsgBase:
@@ -54,7 +54,7 @@ class MsgBase:
self._input_queue = input_queue
self._output_queue = output_queue
try:
- self._logger = LogManager.getLogger(self._class_name)
+ self._logger = getLogger(self._class_name)
except RuntimeError:
self._logger = None
diff --git a/src/boot/AppInitializer.py b/src/boot/AppInitializer.py
index 039a163..365df4c 100644
--- a/src/boot/AppInitializer.py
+++ b/src/boot/AppInitializer.py
@@ -73,6 +73,8 @@ def _initializeWebDriverManager(
def _initializeAppearance(
):
+ logger = logInstance().getLogger("AppInitializer")
+
app = QApplication.instance()
if not app:
return
@@ -82,7 +84,6 @@ def _initializeAppearance(
saved_custom_theme = cfg.get(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "")
app.setStyle(saved_style)
setActiveStyle(saved_style)
- logger = logInstance().getLogger("AppInitializer")
if saved_custom_theme:
try:
themeInstance().applyTheme(saved_custom_theme)
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..66a74cc 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(
@@ -104,6 +104,7 @@ class _DateInputContainer(QWidget):
):
super().__init__(parent)
+
self.setupUi()
def setupUi(
@@ -113,36 +114,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 +154,25 @@ class _TimeInputContainer(QWidget):
):
super().__init__(parent)
- self._TimeEdit = QTimeEdit(self)
- self._TimeEdit.setDisplayFormat("HH:mm")
- self._TimeEdit.setFixedHeight(25)
+ self.setupUi()
+
+ def setupUi(
+ self
+ ):
+
+ 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 +183,25 @@ class _DateOffsetContainer(QWidget):
):
super().__init__(parent)
- 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.setupUi()
+
+ def setupUi(
+ 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)
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 +214,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 +233,20 @@ class _TimeOffsetContainer(QWidget):
):
super().__init__(parent)
- self._SpinBox = QSpinBox(self)
- self._SpinBox.setRange(0, 99999)
- self._SpinBox.setSuffix(" 小时")
- self._SpinBox.setFixedHeight(25)
+ self.setupUi()
+
+ def setupUi(
+ self
+ ):
+
+ 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 +258,7 @@ class _TimeOffsetContainer(QWidget):
self
) -> int:
- return self._SpinBox.value()
+ return self.SpinBox.value()
class VariableManager(QObject):
@@ -251,7 +270,7 @@ class VariableManager(QObject):
super().__init__(parent)
self._vars = []
- self._nameMap = {}
+ self._name_map = {}
self.initPresetVars()
@@ -262,21 +281,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 +303,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 +318,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 +359,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 +395,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 +408,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
new file mode 100644
index 0000000..df823c0
--- /dev/null
+++ b/src/gui/ALBulletinDialog.py
@@ -0,0 +1,352 @@
+# -*- 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, timedelta
+from typing import Any
+
+from PySide6.QtCore import (
+ Qt,
+ Signal,
+ Slot,
+ QTimer
+)
+from PySide6.QtGui import (
+ QFont
+)
+from PySide6.QtWidgets import (
+ QDialog,
+ QHBoxLayout,
+ QLabel,
+ QListWidgetItem,
+ QMessageBox,
+ QVBoxLayout,
+ QWidget
+)
+
+from gui.ALBulletinWorker import ALBulletinFetchWorker
+from gui.ALStatusLabel import ALStatusLabel
+from gui.resources.ui.Ui_ALBulletinDialog import Ui_ALBulletinDialog
+from managers.bulletin.BulletinManager import instance as bulletinInstance
+
+
+class 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", "无标题"))
+ 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("新 !")
+ 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()
+ 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)
+ 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
+ )
+ 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;")
+ 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
+ ):
+ """
+ Set last sync time to update the UI status.
+ """
+
+ 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", "无标题"))
+ 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):
+ 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
+ ):
+
+ 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)
+ 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, 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
+ ):
+
+ 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
+ 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):
+ self.__bulletin_mgr.markBulletinAsRead(bulletin["id"])
+ widget = self.BulletinListWidget.itemWidget(item)
+ if widget:
+ widget.markAsRead()
+ item.setData(Qt.UserRole, bulletin)
+ self.showBulletin(bulletin)
diff --git a/src/gui/ALBulletinPoller.py b/src/gui/ALBulletinPoller.py
new file mode 100644
index 0000000..34d9b0d
--- /dev/null
+++ b/src/gui/ALBulletinPoller.py
@@ -0,0 +1,188 @@
+# -*- 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.ALBulletinWorker 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.__stopped = False
+ self.__mgr = bulletinInstance()
+
+ def start(
+ self
+ ):
+
+ self.__stopped = False
+ interval_ms = self.__mgr.syncInterval()*60*1000
+ self.__timer.start(interval_ms)
+
+ def stop(
+ self
+ ):
+
+ self.__stopped = True
+ 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 __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(500)
+ self.__worker.deleteLater()
+ self.__worker = None
+
+ 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
+ ):
+
+ worker = self.sender()
+ if worker is not self.__worker:
+ return
+ self.__disconnectWorker(worker)
+ worker.wait(500)
+ worker.deleteLater()
+ self.__worker = None
+ 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())
+ 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 and not self.__dialog_open:
+ self.newBulletinsDetected.emit(len(new_ids))
+ if not self.__stopped:
+ interval_ms = self.__mgr.syncInterval()*60*1000
+ self.__timer.start(interval_ms)
+
+ @Slot(str)
+ def __onError(
+ self,
+ error_message: str
+ ):
+
+ worker = self.sender()
+ if worker is not self.__worker:
+ return
+ self.__disconnectWorker(worker)
+ worker.wait(500)
+ 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/ALBulletinWorker.py b/src/gui/ALBulletinWorker.py
new file mode 100644
index 0000000..0a44dbd
--- /dev/null
+++ b/src/gui/ALBulletinWorker.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+"""
+Copyright (c) 2026 KenanZhu.
+All rights reserved.
+
+This software is provided "as is", without any warranty of any kind.
+You may use, modify, and distribute this file under the terms of the MIT License.
+See the LICENSE file for details.
+"""
+import requests
+
+from PySide6.QtCore import (
+ QThread,
+ Signal
+)
+
+
+class ALBulletinFetchWorker(QThread):
+ """
+ Worker thread for fetching bulletins from the server.
+ """
+
+ fetchWorkerIsFinished = Signal(dict)
+ fetchWorkerFinishedWithError = Signal(str)
+
+ def __init__(
+ self,
+ parent=None,
+ request_url: str = "",
+ params: dict = None
+ ):
+
+ super().__init__(parent)
+ self.__request_url = request_url.rstrip("/") if request_url else ""
+ self.__params = params or {}
+
+ def run(
+ self
+ ):
+
+ try:
+ response = requests.get(
+ self.__request_url, params=self.__params, timeout=5
+ )
+ response.raise_for_status()
+ r = response.json()
+ if r.get("code") == 200:
+ data = r.get("data", {})
+ self.fetchWorkerIsFinished.emit(data)
+ else:
+ raise Exception(
+ f"服务器返回错误: [{r.get('code', '未知代码')}] {r.get('msg', '未知错误')}"
+ )
+ except requests.RequestException as e:
+ self.fetchWorkerFinishedWithError.emit(f"获取公告数据时发生网络错误: \n{e}")
+ except Exception as e:
+ self.fetchWorkerFinishedWithError.emit(f"获取公告数据时发生未知错误: \n{e}")
diff --git a/src/gui/ALCheckUpdateDialog.py b/src/gui/ALCheckUpdateDialog.py
new file mode 100644
index 0000000..17ba6a3
--- /dev/null
+++ b/src/gui/ALCheckUpdateDialog.py
@@ -0,0 +1,192 @@
+# -*- coding: utf-8 -*-
+"""
+Copyright (c) 2026 KenanZhu.
+All rights reserved.
+
+This software is provided "as is", without any warranty of any kind.
+You may use, modify, and distribute this file under the terms of the MIT License.
+See the LICENSE file for details.
+"""
+from PySide6.QtCore import Qt
+from PySide6.QtGui import QDesktopServices
+from PySide6.QtCore import QUrl
+from PySide6.QtWidgets import (
+ QDialog,
+ QDialogButtonBox,
+ QLabel,
+ QVBoxLayout
+)
+
+
+class ALCheckUpdateDialog(QDialog):
+ """
+ Dialog for showing update check result.
+
+ Supports two layouts:
+ - has_update=True : version comparison with green text + action buttons
+ - has_update=False: "already up-to-date" message + close button
+ """
+
+ def __init__(
+ self,
+ parent=None,
+ has_update: bool = False,
+ current_version: str = "",
+ latest_version: str = "",
+ tag_name: str = "",
+ html_url: str = ""
+ ):
+
+ super().__init__(parent)
+ self.__has_update = has_update
+ self.__html_url = html_url
+ self.__current_version = current_version
+ self.__latest_version = latest_version
+ self.__tag_name = tag_name
+
+ self.modifyUi()
+ self.connectSignals()
+
+ def modifyUi(
+ self
+ ):
+
+ self.setWindowTitle("检查更新 - AutoLibrary")
+ self.setMinimumWidth(360)
+ Layout = QVBoxLayout(self)
+ Layout.setSpacing(12)
+ Layout.setContentsMargins(20, 20, 20, 20)
+ if self.__has_update:
+ self.buildUpdateAvailableUi(Layout)
+ else:
+ self.buildUpToDateUi(Layout)
+
+ def buildUpdateAvailableUi(
+ self,
+ layout: QVBoxLayout
+ ):
+
+ TitleLabel = QLabel()
+ TitleLabel.setStyleSheet(
+ "font-size: 14px;"
+ "font-weight: bold;"
+ )
+ TitleLabel.setTextFormat(Qt.TextFormat.RichText)
+ TitleLabel.setText(
+ f"检测到新版本: "
+ f"{self.__current_version} "
+ f"> "
+ f"{self.__latest_version}"
+ )
+ layout.addWidget(TitleLabel)
+ InfoLabel = QLabel()
+ InfoLabel.setTextFormat(Qt.TextFormat.RichText)
+ InfoLabel.setWordWrap(True)
+ InfoLabel.setText(
+ f"最新版本: {self.__tag_name}
"
+ )
+ InfoLabel.setOpenExternalLinks(True)
+ layout.addWidget(InfoLabel)
+ layout.addSpacing(8)
+ self.__button_box = QDialogButtonBox()
+ self.__btn_github = self.__button_box.addButton(
+ "前往 GitHub",
+ QDialogButtonBox.ButtonRole.AcceptRole
+ )
+ self.__btn_github.setMinimumWidth(100)
+ self.__btn_github.setMaximumWidth(100)
+ self.__btn_github.setMinimumHeight(25)
+ self.__btn_github.setMaximumHeight(25)
+ self.__btn_download = self.__button_box.addButton(
+ "官网下载",
+ QDialogButtonBox.ButtonRole.ActionRole
+ )
+ self.__btn_download.setMinimumWidth(80)
+ self.__btn_download.setMaximumWidth(80)
+ self.__btn_download.setMinimumHeight(25)
+ self.__btn_download.setMaximumHeight(25)
+ self.__btn_cancel = self.__button_box.addButton(
+ "取消",
+ QDialogButtonBox.ButtonRole.RejectRole
+ )
+ self.__btn_cancel.setMinimumWidth(80)
+ self.__btn_cancel.setMaximumWidth(80)
+ self.__btn_cancel.setMinimumHeight(25)
+ self.__btn_cancel.setMaximumHeight(25)
+ layout.addWidget(self.__button_box)
+
+ def buildUpToDateUi(
+ self,
+ layout: QVBoxLayout
+ ):
+
+ TitleLabel = QLabel()
+ TitleLabel.setStyleSheet(
+ "font-size: 14px;"
+ "font-weight: bold;"
+ )
+ TitleLabel.setText(f"已是最新版本 !")
+ layout.addWidget(TitleLabel)
+ InfoLabel = QLabel()
+ InfoLabel.setText(
+ f"当前版本: {self.__current_version}
"
+ )
+ layout.addWidget(InfoLabel)
+ layout.addStretch()
+ self.__button_box = QDialogButtonBox()
+ self.__btn_close = self.__button_box.addButton(
+ "确定",
+ QDialogButtonBox.ButtonRole.AcceptRole
+ )
+ self.__btn_close.setMinimumWidth(80)
+ self.__btn_close.setMaximumWidth(80)
+ self.__btn_close.setMinimumHeight(25)
+ self.__btn_close.setMaximumHeight(25)
+ layout.addWidget(self.__button_box)
+
+ def connectSignals(
+ self
+ ):
+
+ if self.__has_update:
+ self.__btn_github.clicked.connect(self.onGoToGitHub)
+ self.__btn_download.clicked.connect(self.onGoToDownload)
+ self.__btn_cancel.clicked.connect(self.reject)
+ else:
+ self.__btn_close.clicked.connect(self.accept)
+
+ def onGoToGitHub(
+ self
+ ):
+
+ QDesktopServices.openUrl(QUrl(self.__html_url))
+ self.accept()
+
+ def onGoToDownload(
+ self
+ ):
+
+ QDesktopServices.openUrl(
+ QUrl("https://www.autolibrary.kenanzhu.com/downloads")
+ )
+ self.accept()
+
+ @staticmethod
+ def showResult(
+ parent,
+ has_update: bool,
+ current_version: str,
+ latest_version: str = "",
+ tag_name: str = "",
+ html_url: str = ""
+ ):
+
+ dialog = ALCheckUpdateDialog(
+ parent,
+ has_update=has_update,
+ current_version=current_version,
+ latest_version=latest_version,
+ tag_name=tag_name,
+ html_url=html_url
+ )
+ dialog.exec()
diff --git a/src/gui/ALCheckUpdateWorker.py b/src/gui/ALCheckUpdateWorker.py
new file mode 100644
index 0000000..3ce0e9d
--- /dev/null
+++ b/src/gui/ALCheckUpdateWorker.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+"""
+Copyright (c) 2026 KenanZhu.
+All rights reserved.
+
+This software is provided "as is", without any warranty of any kind.
+You may use, modify, and distribute this file under the terms of the MIT License.
+See the LICENSE file for details.
+"""
+import requests
+
+from PySide6.QtCore import (
+ QThread,
+ Signal
+)
+
+
+class ALCheckUpdateWorker(QThread):
+ """
+ Worker thread for checking latest release from GitHub API.
+ """
+
+ checkUpdateWorkerIsFinished = Signal(dict)
+ checkUpdateWorkerFinishedWithError = Signal(str)
+
+ def __init__(
+ self,
+ parent=None
+ ):
+
+ super().__init__(parent)
+ self.__api_url = "https://api.github.com/repos/KenanZhu/AutoLibrary/releases/latest"
+
+ def run(
+ self
+ ):
+
+ try:
+ response = requests.get(self.__api_url, timeout=10)
+ response.raise_for_status()
+ data = response.json()
+ self.checkUpdateWorkerIsFinished.emit({
+ "tag_name": data.get("tag_name", ""),
+ "name": data.get("name", ""),
+ "html_url": data.get("html_url", ""),
+ "body": data.get("body", ""),
+ })
+ except requests.RequestException as e:
+ self.checkUpdateWorkerFinishedWithError.emit(f"网络请求失败: \n{e}")
+ except Exception as e:
+ self.checkUpdateWorkerFinishedWithError.emit(f"检查更新时发生未知错误: \n{e}")
diff --git a/src/gui/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 5d805c6..46af84a 100644
--- a/src/gui/ALMainWindow.py
+++ b/src/gui/ALMainWindow.py
@@ -9,11 +9,12 @@ See the LICENSE file for details.
"""
import queue
+import packaging.version as ver
+
from PySide6.QtCore import (
QTimer,
QUrl,
Qt,
- Signal,
Slot
)
from PySide6.QtGui import (
@@ -32,46 +33,54 @@ from PySide6.QtWidgets import (
from base.MsgBase import MsgBase
from gui.ALAboutDialog import ALAboutDialog
+from gui.ALBulletinDialog import ALBulletinDialog
+from gui.ALCheckUpdateWorker import ALCheckUpdateWorker
+from gui.ALCheckUpdateDialog import ALCheckUpdateDialog
from gui.ALConfigWidget import ALConfigWidget
from gui.ALSettingsWidget import ALSettingsWidget
-from gui.ALMainWorkers import (
- AutoLibWorker,
- TimerTaskWorker
-)
from gui.ALTimerTaskManageWidget import ALTimerTaskManageWidget
+from gui.ALMainWorker import AutoLibWorker
+from gui.ALBulletinPoller import ALBulletinPoller
+from gui.ALTimerTaskPoller import ALTimerTaskPoller
+from gui.ALVersionInfo import AL_VERSION
from gui.resources import ALResource
from gui.resources.ui.Ui_ALMainWindow import Ui_ALMainWindow
+from managers.bulletin.BulletinManager import instance as bulletinInstance
from managers.config.ConfigUtils import ConfigUtils
class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
- # signal : timer task
- timerTaskIsRunning = Signal(dict)
- timerTaskIsExecuted = Signal(dict)
- timerTaskIsError = Signal(dict)
-
def __init__(
self
):
MsgBase.__init__(self, queue.Queue(), queue.Queue())
QMainWindow.__init__(self)
- self.__timer_task_queue = queue.Queue()
self.__config_paths = ConfigUtils.getAutomationConfigPaths()
- self.__alTimerTaskManageWidget = None
- self.__alConfigWidget = None
- self.__alSettingsWidget = None
+ self.__ALTimerTaskManageWidget = None
+ self.__ALConfigWidget = None
+ self.__ALSettingsWidget = None
+ self.__ALBulletinDialog = None
self.__auto_lib_thread = None
- self.__current_timer_task_thread = None
- self.__is_running_timer_task = False
+ self.__bulletin_poller = ALBulletinPoller(self)
+ self.__timer_task_poller = ALTimerTaskPoller(
+ self,
+ self._input_queue,
+ self._output_queue,
+ self.__config_paths
+ )
+ self.__notification_type = ""
self.setupUi(self)
self.modifyUi()
self.setupTray()
self.connectSignals()
self.startMsgPolling()
- self.startTimerTaskPolling()
+ self.__timer_task_poller.start()
+ self.__bulletin_poller.start()
+ if bulletinInstance().autoFetch():
+ QTimer.singleShot(1000, self.__bulletin_poller.fetchNow)
self._showLog("主窗口初始化完成")
def modifyUi(
@@ -82,42 +91,31 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.setWindowIcon(self.Icon)
self.MessageIOTextEdit.setFont(QFont("Courier New", 10))
self.ManualAction.triggered.connect(self.onManualActionTriggered)
+ self.CheckUpdateAction.triggered.connect(self.onCheckUpdateActionTriggered)
self.AboutAction.triggered.connect(self.onAboutActionTriggered)
self.SettingsAction.triggered.connect(self.onSettingsActionTriggered)
-
+ self.BulletinAction.triggered.connect(self.onBulletinActionTriggered)
# 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)
-
- def onAboutActionTriggered(
- self
- ):
-
- AboutDialog = ALAboutDialog(self)
- AboutDialog.exec()
-
- def onManualActionTriggered(
- self
- ):
-
- Url = QUrl("https://www.autolibrary.kenanzhu.com/manuals")
- QDesktopServices.openUrl(Url)
+ self.__timer_task_poller.taskRunning.connect(self.onTimerTaskRunning)
+ self.__timer_task_poller.taskFinished.connect(self.onTimerTaskFinished)
+ self.__timer_task_poller.taskRunning.connect(self.__ALTimerTaskManageWidget.onTimerTaskIsRunning)
+ self.__timer_task_poller.taskExecuted.connect(self.__ALTimerTaskManageWidget.onTimerTaskIsExecuted)
+ self.__timer_task_poller.taskError.connect(self.__ALTimerTaskManageWidget.onTimerTaskIsError)
+ self.__ALTimerTaskManageWidget.timerTaskIsReady.connect(self.__timer_task_poller.enqueue)
+ self.__ALTimerTaskManageWidget.timerTaskManageWidgetIsClosed.connect(self.onTimerTaskManageWidgetClosed)
+ self.__ALTimerTaskManageWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint)
def setupTray(
self
@@ -135,8 +133,8 @@ 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.onTrayMessageClicked)
self.TrayIcon.show()
def hideToTray(
@@ -144,6 +142,9 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
):
self.hide()
+ self.__notification_type = ""
+ if not hasattr(self, "TrayIcon"):
+ return
self.TrayIcon.showMessage(
"AutoLibrary",
"\n已最小化到托盘",
@@ -151,14 +152,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
2000
)
- def onTrayIconActivated(
- self,
- reason: QSystemTrayIcon.ActivationReason
- ):
-
- if reason == QSystemTrayIcon.DoubleClick:
- self.showNormal()
-
def connectSignals(
self
):
@@ -169,6 +162,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.StopButton.clicked.connect(self.onStopButtonClicked)
self.SendButton.clicked.connect(self.onSendButtonClicked)
self.MessageEdit.returnPressed.connect(self.onSendButtonClicked)
+ self.__bulletin_poller.newBulletinsDetected.connect(self.onBulletinPollerNewBulletins)
def closeEvent(
self,
@@ -181,20 +175,22 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
return
if self.__msg_queue_timer and self.__msg_queue_timer.isActive():
self.__msg_queue_timer.stop()
- if self.__timer_task_timer and self.__timer_task_timer.isActive():
- self.__timer_task_timer.stop()
- if self.__is_running_timer_task:
- self.__current_timer_task_thread.wait(2000)
- self.__current_timer_task_thread.deleteLater()
- if self.__alTimerTaskManageWidget:
- self.__alTimerTaskManageWidget.close()
- self.__alTimerTaskManageWidget.deleteLater()
- if self.__alConfigWidget:
- self.__alConfigWidget.close()
+ if self.__timer_task_poller:
+ self.__timer_task_poller.stop()
+ 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()
+ # the bulletin dialog is already deleted in the 'self.onBulletinDialogClosed'
+ if self.__bulletin_poller:
+ self.__bulletin_poller.stop()
self._showLog("主窗口关闭")
QMainWindow.closeEvent(self, event)
@@ -219,47 +215,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.__msg_queue_timer.timeout.connect(self.pollMsgQueue)
self.__msg_queue_timer.start(100)
- def startTimerTaskPolling(
- self
- ):
-
- self.__timer_task_timer = QTimer()
- self.__timer_task_timer.timeout.connect(self.pollTimerTaskQueue)
- self.__timer_task_timer.start(500)
-
- def pollTimerTaskQueue(
- self
- ):
-
- if self.__is_running_timer_task:
- return
- try:
- while not self.__is_running_timer_task:
- timer_task = self.__timer_task_queue.get_nowait()
- self.timerTaskIsRunning.emit(timer_task)
- self.__timer_task_timer.stop()
- self.__is_running_timer_task = True
- self.setControlButtons(None, True, False)
- if not timer_task["silent"]:
- self.TrayIcon.showMessage(
- "定时任务 - AutoLibrary",
- f"\n已开始执行定时任务: \n{timer_task['name']}",
- QSystemTrayIcon.MessageIcon.Information,
- 1000
- )
- self.showNormal()
- self.__current_timer_task_thread = TimerTaskWorker(
- timer_task,
- self._input_queue,
- self._output_queue,
- self.__config_paths
- )
- self.__current_timer_task_thread.timerTaskWorkerIsFinished.connect(self.onTimerTaskFinished)
- self.__current_timer_task_thread.start()
- except queue.Empty:
- self.__is_running_timer_task = False
- pass
-
def setControlButtons(
self,
config_button_enabled: bool,
@@ -287,6 +242,145 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
except queue.Empty:
pass
+ @Slot(int)
+ def onBulletinPollerNewBulletins(
+ self,
+ count: int
+ ):
+
+ if not hasattr(self, "TrayIcon"):
+ return
+ self.__notification_type = "bulletin"
+ self.TrayIcon.showMessage(
+ "公告栏 - AutoLibrary",
+ f"有 {count} 条新公告,点击查看详情。",
+ QSystemTrayIcon.MessageIcon.Information,
+ 3000
+ )
+
+ @Slot(dict)
+ def onTimerTaskRunning(
+ self,
+ timer_task: dict
+ ):
+
+ self.setControlButtons(None, True, False)
+ self.__notification_type = ""
+ if not hasattr(self, "TrayIcon"):
+ return
+ if not timer_task.get("silent", False):
+ self.TrayIcon.showMessage(
+ "定时任务 - AutoLibrary",
+ f"\n已开始执行定时任务: \n{timer_task['name']}",
+ QSystemTrayIcon.MessageIcon.Information,
+ 1000
+ )
+ self.showNormal()
+
+ @Slot(bool, dict)
+ def onTimerTaskFinished(
+ self,
+ is_error: bool,
+ timer_task: dict
+ ):
+
+ self.setControlButtons(None, False, True)
+ self.__notification_type = ""
+ if not hasattr(self, "TrayIcon"):
+ return
+ self.TrayIcon.showMessage(
+ "定时任务 - AutoLibrary",
+ f"\n定时任务 '{timer_task['name']}' 执行{'失败' if is_error else '完成'}",
+ QSystemTrayIcon.MessageIcon.Warning if is_error else QSystemTrayIcon.MessageIcon.Information,
+ 1000
+ )
+ self._showTrace(
+ f"定时任务 {timer_task['name']} 执行{'失败' if is_error else '完成'}, uuid: {timer_task['uuid']}"
+ )
+
+ @Slot(dict)
+ def onCheckUpdateIsFinished(
+ self,
+ data: dict
+ ):
+
+ worker = self.sender()
+ if worker is not self.__check_update_worker:
+ return
+ worker.checkUpdateWorkerIsFinished.disconnect(self.onCheckUpdateIsFinished)
+ worker.checkUpdateWorkerFinishedWithError.disconnect(self.onCheckUpdateFinishedWithError)
+ worker.wait(3000)
+ worker.deleteLater()
+ self.__check_update_worker = None
+ tag_name = data.get("tag_name", "")
+ html_url = data.get("html_url", "")
+ latest_version = tag_name.lstrip("v")
+ try:
+ local_ver = ver.Version(AL_VERSION)
+ remote_ver = ver.Version(latest_version)
+ except ver.InvalidVersion:
+ self._showTrace("版本号解析失败, 无法比较版本", self.TraceLevel.WARNING)
+ return
+ if remote_ver > local_ver:
+ ALCheckUpdateDialog.showResult(
+ self,
+ has_update=True,
+ current_version=AL_VERSION,
+ latest_version=latest_version,
+ tag_name=tag_name,
+ html_url=html_url
+ )
+ else:
+ ALCheckUpdateDialog.showResult(
+ self,
+ has_update=False,
+ current_version=AL_VERSION
+ )
+ self._showLog("检查更新完成")
+
+ @Slot(str)
+ def onCheckUpdateFinishedWithError(
+ self,
+ error_message: str
+ ):
+
+ worker = self.sender()
+ if worker is not self.__check_update_worker:
+ return
+ worker.checkUpdateWorkerIsFinished.disconnect(self.onCheckUpdateIsFinished)
+ worker.checkUpdateWorkerFinishedWithError.disconnect(self.onCheckUpdateFinishedWithError)
+ worker.wait(3000)
+ worker.deleteLater()
+ self.__check_update_worker = None
+ QMessageBox.warning(
+ self,
+ "检查更新 - AutoLibrary",
+ f"检查更新失败: \n{error_message}",
+ )
+ self._showLog("检查更新失败")
+
+ @Slot()
+ def onBulletinDialogClosed(
+ self
+ ):
+
+ if self.__ALBulletinDialog:
+ self.__ALBulletinDialog.finished.disconnect(self.onBulletinDialogClosed)
+ self.__ALBulletinDialog.deleteLater()
+ self.__ALBulletinDialog = None
+ self.__bulletin_poller.setDialogOpen(False)
+
+ @Slot()
+ def onSettingsWidgetClosed(
+ self
+ ):
+
+ if self.__ALSettingsWidget:
+ self.__ALSettingsWidget.settingsWidgetIsClosed.disconnect(self.onSettingsWidgetClosed)
+ self.__ALSettingsWidget.deleteLater()
+ self.__ALSettingsWidget = None
+ self.SettingsAction.setEnabled(True)
+
@Slot()
def onTimerTaskManageWidgetClosed(
self
@@ -299,84 +393,106 @@ 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.__timer_task_poller.updateConfigPaths(self.__config_paths)
self.setControlButtons(True, None, None)
self._showLog("配置窗口已关闭,配置文件路径已更新")
@Slot()
- def onSettingsWidgetClosed(
+ def onBulletinActionTriggered(
self
):
- if self.__alSettingsWidget:
- self.__alSettingsWidget.settingsWidgetIsClosed.disconnect(self.onSettingsWidgetClosed)
- self.__alSettingsWidget.deleteLater()
- self.__alSettingsWidget = None
- self.SettingsAction.setEnabled(True)
+ if self.__ALBulletinDialog is None:
+ self.__ALBulletinDialog = ALBulletinDialog(self)
+ self.__ALBulletinDialog.finished.connect(self.onBulletinDialogClosed)
+ self.__bulletin_poller.setDialogOpen(True)
+ self.__ALBulletinDialog.show()
+ self.__ALBulletinDialog.raise_()
+ self.__ALBulletinDialog.activateWindow()
+ self._showLog("打开公告栏窗口")
@Slot()
def onSettingsActionTriggered(
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.openBulletinRequested.connect(self.onBulletinActionTriggered)
+ self.__ALSettingsWidget.show()
+ self.__ALSettingsWidget.raise_()
+ self.__ALSettingsWidget.activateWindow()
self.SettingsAction.setEnabled(False)
self._showLog("打开全局设置窗口")
- @Slot(dict)
- def onTimerTaskIsReady(
- self,
- timer_task: dict
+ @Slot()
+ def onAboutActionTriggered(
+ self
):
- self.__timer_task_queue.put(timer_task)
+ AboutDialog = ALAboutDialog(self)
+ AboutDialog.exec()
- @Slot(dict)
- def onTimerTaskFinished(
- self,
- is_error: bool,
- timer_task: dict
+ @Slot()
+ def onManualActionTriggered(
+ self
):
- self.__current_timer_task_thread.wait(1000)
- self.__current_timer_task_thread.timerTaskWorkerIsFinished.disconnect(self.onTimerTaskFinished)
- self.__current_timer_task_thread.deleteLater()
- self.__current_timer_task_thread = None
- self.setControlButtons(None, False, True)
- self.__is_running_timer_task = False
- self.__timer_task_timer.start(500)
- timer_task["executed"] = True
- self.TrayIcon.showMessage(
- "定时任务 - AutoLibrary",
- f"\n定时任务 '{timer_task['name']}' 执行{'失败' if is_error else '完成'}",
- QSystemTrayIcon.MessageIcon.Warning if is_error else QSystemTrayIcon.MessageIcon.Information,
- 1000
- )
- self._showTrace(
- f"定时任务 {timer_task['name']} 执行{'失败' if is_error else '完成'}, uuid: {timer_task['uuid']}"
- )
- if not is_error:
- self.timerTaskIsExecuted.emit(timer_task)
- else:
- self.timerTaskIsError.emit(timer_task)
+ url = QUrl("https://manuals.autolibrary.kenanzhu.com")
+ QDesktopServices.openUrl(url)
+
+ @Slot()
+ def onCheckUpdateActionTriggered(
+ self
+ ):
+
+ if hasattr(self, '__check_update_worker') and self.__check_update_worker is not None:
+ return
+ self.__check_update_worker = ALCheckUpdateWorker(self)
+ self.__check_update_worker.checkUpdateWorkerIsFinished.connect(self.onCheckUpdateIsFinished)
+ self.__check_update_worker.checkUpdateWorkerFinishedWithError.connect(self.onCheckUpdateFinishedWithError)
+ self.__check_update_worker.start()
+ self._showLog("正在检查更新...")
+
+ @Slot()
+ def onTrayMessageClicked(
+ self
+ ):
+
+ if self.__notification_type == "bulletin":
+ self.__notification_type = ""
+ self.onBulletinActionTriggered()
+
+ @Slot(QSystemTrayIcon.ActivationReason)
+ def onTrayIconActivated(
+ self,
+ reason: QSystemTrayIcon.ActivationReason
+ ):
+
+ if reason == QSystemTrayIcon.DoubleClick:
+ self.showNormal()
@Slot()
def onTimerTaskManageWidgetButtonClicked(
self
):
- self.__alTimerTaskManageWidget.show()
- self.__alTimerTaskManageWidget.raise_()
- self.__alTimerTaskManageWidget.activateWindow()
+ if self.__ALTimerTaskManageWidget is None:
+ QMessageBox.warning(
+ self,
+ "警告 - AutoLibrary",
+ "定时任务功能初始化失败, 请检查配置文件。"
+ )
+ return
+ self.__ALTimerTaskManageWidget.show()
+ self.__ALTimerTaskManageWidget.raise_()
+ self.__ALTimerTaskManageWidget.activateWindow()
self.TimerTaskManageWidgetButton.setEnabled(False)
self._showLog("打开定时任务管理窗口")
@@ -385,12 +501,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/ALMainWorkers.py b/src/gui/ALMainWorker.py
similarity index 100%
rename from src/gui/ALMainWorkers.py
rename to src/gui/ALMainWorker.py
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/ALSettingsWidget.py b/src/gui/ALSettingsWidget.py
index 7dab1f1..5b72447 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,
@@ -37,6 +41,7 @@ from managers.theme.ThemeManager import(
instance as themeInstance
)
+from gui.ALBulletinWorker import ALBulletinFetchWorker
from gui.ALWidgetMixin import CenterOnParentMixin
from gui.resources.ui.Ui_ALSettingsWidget import Ui_ALSettingsWidget
from interfaces.ConfigProvider import (
@@ -86,6 +91,7 @@ def _restartApp(
class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
settingsWidgetIsClosed = Signal()
+ openBulletinRequested = Signal()
def __init__(
self,
@@ -96,6 +102,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()
@@ -107,6 +116,8 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
event: QCloseEvent
):
+ if hasattr(self, '__bulletin_test_worker') and self.__bulletin_test_worker is not None:
+ self.__bulletin_test_worker.wait(3000)
self.settingsWidgetIsClosed.emit()
super().closeEvent(event)
@@ -130,6 +141,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 +151,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 +191,8 @@ 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.BulletinOpenButton.clicked.connect(self.openBulletinRequested.emit)
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
self.ApplyButton.clicked.connect(self.onApplyButtonClicked)
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
@@ -184,6 +201,7 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self
):
+ # appearance settings
theme = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.THEME, "system")
style = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.STYLE, "Fusion")
custom_theme = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "")
@@ -206,6 +224,14 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self.CustomThemeComboBox.setCurrentIndex(idx)
self.updateCustomThemeInfo()
self.updateCustomThemeStatus()
+ # bulletin settings
+ bulletin_mgr = bulletinInstance()
+ self.__original_bulletin_url = bulletin_mgr.serverUrl()
+ self.__original_bulletin_auto_fetch = bulletin_mgr.autoFetch()
+ 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
@@ -254,10 +280,17 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
elif need_theme == "dark":
self.DarkThemeRadio.setChecked(True)
- def collectSettings(
+ def clearBulletinTestStatus(
self
):
+ self.BulletinTestStatusLabel.setText("")
+ self.BulletinTestStatusLabel.setStyleSheet("")
+
+ def collectAppearanceSettings(
+ self
+ ) -> tuple[str, str, str]:
+
if self.LightThemeRadio.isChecked():
theme = "light"
elif self.DarkThemeRadio.isChecked():
@@ -270,20 +303,29 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
custom_theme = ""
return theme, style, custom_theme
- def saveAndApply(
+ def collectBulletinSettings(
+ self
+ ) -> tuple[str, bool, int]:
+
+ url = self.BulletinServerUrlEdit.text().strip()
+ auto_fetch = self.BulletinAutoFetchCheck.isChecked()
+ sync_interval = self.BulletinSyncIntervalSpin.value()
+ if sync_interval < 1:
+ sync_interval = 5
+ return url, auto_fetch, sync_interval
+
+ def saveAppearanceSettings(
self
):
- theme, style, custom_theme = self.collectSettings()
+ theme, style, custom_theme = self.collectAppearanceSettings()
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.STYLE, style)
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, custom_theme)
setActiveStyle(style)
if not _applyCustomTheme(custom_theme, theme):
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "")
self.syncRadioFromNeedTheme(custom_theme)
- # Re-read theme after syncRadioFromNeedTheme — the radio may have
- # changed to match the custom theme's need_theme
- theme, _, _ = self.collectSettings()
+ theme, _, _ = self.collectAppearanceSettings()
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.THEME, theme)
self.setNavigationIcons()
self.updateCustomThemeStatus()
@@ -292,6 +334,25 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self.__original_custom_theme = custom_theme if custom_theme else ""
self.__original_style = getActiveStyle()
+ def saveBulletinSettings(
+ self
+ ):
+
+ url, auto_fetch, sync_interval = self.collectBulletinSettings()
+ self.__cfg_mgr.set(CfgKey.GLOBAL.BULLETIN.SERVER_URL, url)
+ self.__cfg_mgr.set(CfgKey.GLOBAL.BULLETIN.AUTO_FETCH, auto_fetch)
+ self.__cfg_mgr.set(CfgKey.GLOBAL.BULLETIN.SYNC_INTERVAL, sync_interval)
+ self.__original_bulletin_url = url
+ self.__original_bulletin_auto_fetch = auto_fetch
+ self.__original_bulletin_sync_interval = sync_interval
+
+ def saveSettings(
+ self
+ ):
+
+ self.saveBulletinSettings()
+ self.saveAppearanceSettings()
+
def maybeRestart(
self
) -> bool:
@@ -308,6 +369,33 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
return True
return False
+ @Slot()
+ def onImportCustomThemeButtonClicked(
+ self
+ ):
+
+ file_path, _ = QFileDialog.getOpenFileName(
+ self,
+ "导入主题 - AutoLibrary",
+ "",
+ "主题文件 (*.altheme *.qss);;所有文件 (*)"
+ )
+ if not file_path:
+ return
+ try:
+ file_id = themeInstance().importTheme(file_path)
+ self.populateCustomThemes()
+ idx = self.CustomThemeComboBox.findData(file_id)
+ if idx >= 0:
+ self.CustomThemeComboBox.setCurrentIndex(idx)
+ self.updateCustomThemeInfo()
+ except Exception as e:
+ QMessageBox.warning(
+ self,
+ "导入失败 - AutoLibrary",
+ f"无法导入主题文件:{e}"
+ )
+
@Slot()
def onRemoveCustomThemeButtonClicked(
self
@@ -345,34 +433,6 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
f"无法删除主题:{e}"
)
- @Slot()
- def onImportCustomThemeButtonClicked(
- self
- ):
-
- file_path, _ = QFileDialog.getOpenFileName(
- self,
- "导入主题 - AutoLibrary",
- "",
- "主题文件 (*.altheme *.qss);;所有文件 (*)"
- )
- if not file_path:
- return
- try:
- file_id = themeInstance().importTheme(file_path)
- self.populateCustomThemes()
- idx = self.CustomThemeComboBox.findData(file_id)
- if idx >= 0:
- self.CustomThemeComboBox.setCurrentIndex(idx)
- self.updateCustomThemeStatus()
- self.updateCustomThemeInfo()
- except Exception as e:
- QMessageBox.warning(
- self,
- "导入失败 - AutoLibrary",
- f"无法导入主题文件:{e}"
- )
-
@Slot()
def onCustomThemeComboBoxChanged(
self,
@@ -380,7 +440,6 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
):
self.updateCustomThemeInfo()
- # no status update, because custom theme is not applied yet.
@Slot()
def onResetCustomThemeButtonClicked(
@@ -406,6 +465,76 @@ 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
+ 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("")
+ self.__bulletin_test_t0 = time.monotonic()
+ 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.onBulletinTestIsFinished
+ )
+ self.__bulletin_test_worker.fetchWorkerFinishedWithError.connect(
+ self.onBulletinTestFinishedWithError
+ )
+ self.__bulletin_test_worker.start()
+
+ @Slot(dict)
+ def onBulletinTestIsFinished(
+ self,
+ data: dict
+ ):
+
+ self.__bulletin_test_worker.fetchWorkerIsFinished.disconnect(
+ self.onBulletinTestIsFinished
+ )
+ self.__bulletin_test_worker.fetchWorkerFinishedWithError.disconnect(
+ self.onBulletinTestFinishedWithError
+ )
+ self.__bulletin_test_worker.wait(3000)
+ 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, self.clearBulletinTestStatus)
+
+ @Slot(str)
+ def onBulletinTestFinishedWithError(
+ self,
+ error_message: str
+ ):
+
+ self.__bulletin_test_worker.fetchWorkerIsFinished.disconnect(
+ self.onBulletinTestIsFinished
+ )
+ self.__bulletin_test_worker.fetchWorkerFinishedWithError.disconnect(
+ self.onBulletinTestFinishedWithError
+ )
+ self.__bulletin_test_worker.wait(3000)
+ 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(
self
@@ -418,9 +547,9 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self
):
- _, style, _ = self.collectSettings()
+ _, style, _ = self.collectAppearanceSettings()
style_changed = self.__original_style != style
- self.saveAndApply()
+ self.saveSettings()
if style_changed:
self.maybeRestart()
@@ -429,5 +558,5 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self
):
- self.onApplyButtonClicked() # virtually call apply button clicked
+ self.onApplyButtonClicked()
self.close()
diff --git a/src/gui/ALStatusLabel.py b/src/gui/ALStatusLabel.py
index 5a08e59..537a86d 100644
--- a/src/gui/ALStatusLabel.py
+++ b/src/gui/ALStatusLabel.py
@@ -118,35 +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.setBrush(Qt.BrushStyle.NoBrush)
- 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),
@@ -154,102 +153,99 @@ class ALStatusLabel(QLabel):
)
case self.Status.SUCCESS:
# draw the success green circle
- Pen = Painter.pen()
- Pen.setWidth(2)
- Pen.setBrush(Qt.BrushStyle.NoBrush)
- Pen.setCapStyle(Qt.PenCapStyle.RoundCap)
- Pen.setColor(QColor("#4CAF50" if self.isDarkMode() else "#00AF50")) # green
- Painter.setPen(Pen)
- Painter.drawEllipse(
+ 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.setBrush(Qt.BrushStyle.NoBrush)
- 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.setBrush(Qt.BrushStyle.NoBrush)
- 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/ALTimerTaskAddDialog.py b/src/gui/ALTimerTaskAddDialog.py
index 1807898..d2d717e 100644
--- a/src/gui/ALTimerTaskAddDialog.py
+++ b/src/gui/ALTimerTaskAddDialog.py
@@ -322,5 +322,5 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
):
QDesktopServices.openUrl(
- QUrl("https://www.autolibrary.kenanzhu.com/manuals/autoscript")
+ QUrl("https://manuals.autolibrary.kenanzhu.com/zh/autoscript")
)
diff --git a/src/gui/ALTimerTaskHistoryDialog.py b/src/gui/ALTimerTaskHistoryDialog.py
index 40eec21..5220fb0 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()
@@ -135,4 +135,4 @@ class ALTimerTaskHistoryDialog(QDialog):
self.__history.clear()
self.HistoryTableWidget.setRowCount(0)
- self.__task_data["repeat_history"] = self.__history
+ self.__task_data["repeat_history"] = self.__history # = []
diff --git a/src/gui/ALTimerTaskManageWidget.py b/src/gui/ALTimerTaskManageWidget.py
index 8b9ac30..6c10251 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)
@@ -515,6 +515,11 @@ class ALTimerTaskManageWidget(CenterOnParentMixin, QWidget, Ui_ALTimerTaskManage
task: dict
):
+ # Here we use reference to task data, not copy.
+ # So any change of task data in history dialog will be
+ # reflected in the timer task list
+ # Thus we can emit timerTasksChanged signal to
+ # update config file
Dialog = ALTimerTaskHistoryDialog(self, task)
if Dialog.exec() == QDialog.DialogCode.Accepted:
self.timerTasksChanged.emit()
diff --git a/src/gui/ALTimerTaskPoller.py b/src/gui/ALTimerTaskPoller.py
new file mode 100644
index 0000000..0c25377
--- /dev/null
+++ b/src/gui/ALTimerTaskPoller.py
@@ -0,0 +1,142 @@
+# -*- coding: utf-8 -*-
+"""
+Copyright (c) 2026 KenanZhu.
+All rights reserved.
+
+This software is provided "as is", without any warranty of any kind.
+You may use, modify, and distribute this file under the terms of the MIT License.
+See the LICENSE file for details.
+"""
+import queue
+
+from PySide6.QtCore import (
+ QObject,
+ QTimer,
+ Signal,
+ Slot
+)
+
+from gui.ALMainWorker import TimerTaskWorker
+
+
+class ALTimerTaskPoller(QObject):
+
+ taskRunning = Signal(dict)
+ taskFinished = Signal(bool, dict)
+ taskExecuted = Signal(dict)
+ taskError = Signal(dict)
+
+ def __init__(
+ self,
+ parent=None,
+ input_queue: queue.Queue = None,
+ output_queue: queue.Queue = None,
+ config_paths: list = None
+ ):
+
+ super().__init__(parent)
+ self.__input_queue = input_queue or queue.Queue()
+ self.__output_queue = output_queue or queue.Queue()
+ self.__config_paths = config_paths or []
+ self.__task_queue = queue.Queue()
+ self.__timer = QTimer(self)
+ self.__timer.timeout.connect(self.__poll)
+ self.__worker = None
+ self.__stopped = False
+
+ def start(
+ self
+ ):
+
+ self.__stopped = False
+ self.__timer.start(500)
+
+ def stop(
+ self
+ ):
+
+ self.__stopped = True
+ self.__timer.stop()
+ self.__cleanupWorker()
+
+ def enqueue(
+ self,
+ task: dict
+ ):
+
+ self.__task_queue.put(task)
+
+ def isRunning(
+ self
+ ) -> bool:
+
+ return self.__worker is not None
+
+ def updateConfigPaths(
+ self,
+ config_paths: list
+ ):
+
+ self.__config_paths = config_paths
+
+ @Slot()
+ def __poll(
+ self
+ ):
+
+ if self.__worker is not None:
+ return
+ try:
+ task = self.__task_queue.get_nowait()
+ except queue.Empty:
+ return
+ self.__timer.stop()
+ self.taskRunning.emit(task)
+ try:
+ self.__worker = TimerTaskWorker(
+ task,
+ self.__input_queue,
+ self.__output_queue,
+ self.__config_paths
+ )
+ self.__worker.timerTaskWorkerIsFinished.connect(self.__onFinished)
+ self.__worker.start()
+ except Exception:
+ self.__worker = None
+ if not self.__stopped:
+ self.__timer.start(500)
+
+ @Slot(bool, dict)
+ def __onFinished(
+ self,
+ is_error: bool,
+ task: dict
+ ):
+
+ self.__worker.timerTaskWorkerIsFinished.disconnect(self.__onFinished)
+ self.__worker.wait(1000)
+ self.__worker.deleteLater()
+ self.__worker = None
+ try:
+ self.taskFinished.emit(is_error, task)
+ if not is_error:
+ self.taskExecuted.emit(task)
+ else:
+ self.taskError.emit(task)
+ finally:
+ if not self.__stopped:
+ self.__timer.start(500)
+
+ def __cleanupWorker(
+ self
+ ):
+
+ if self.__worker is None:
+ return
+ try:
+ self.__worker.timerTaskWorkerIsFinished.disconnect(self.__onFinished)
+ except (TypeError, RuntimeError):
+ pass
+ self.__worker.wait(500)
+ self.__worker.deleteLater()
+ self.__worker = None
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..9f03409 100644
--- a/src/gui/ALWebDriverDownloadDialog.py
+++ b/src/gui/ALWebDriverDownloadDialog.py
@@ -8,6 +8,7 @@ You may use, modify, and distribute this file under the terms of the MIT License
See the LICENSE file for details.
"""
import threading
+from pathlib import Path
from typing import Optional
from PySide6.QtCore import (
@@ -33,7 +34,7 @@ from PySide6.QtGui import QCloseEvent
from managers.driver.WebDriverManager import (
instance as webdriverInstance,
WebDriverManager,
- WebDriverInfo,
+ WebDriverCtx,
WebDriverType,
WebDriverStatus
)
@@ -54,12 +55,12 @@ class DownloadWorker(QThread):
def __init__(
self,
driver_manager: WebDriverManager,
- driver_info: WebDriverInfo
+ driver_ctx: WebDriverCtx
):
super().__init__()
self.__driver_manager = driver_manager
- self.__driver_info = driver_info
- self.__driver_path = None
+ self.__driver_ctx = driver_ctx
+ self.__driver_path: Optional[Path] = None
self.__cancelled = False
self.__cancel_event = threading.Event()
@@ -81,7 +82,7 @@ class DownloadWorker(QThread):
self.downloadCancelled.emit()
return
self.__driver_path = self.__driver_manager.installDriver(
- self.__driver_info,
+ self.__driver_ctx,
progress_callback=self.onProgress,
cancel_event=self.__cancel_event
)
@@ -140,12 +141,11 @@ class ALWebDriverDownloadDialog(CenterOnParentMixin, QDialog):
"""
super().__init__(parent)
-
self.__driver_dir = driver_dir
self.__driver_manager: Optional[WebDriverManager] = None
self.__confirmed = False
- self.__selected_driver_info: Optional[WebDriverInfo] = None
- self.__driver_infos: list[WebDriverInfo] = []
+ self.__selected_driver_ctx: Optional[WebDriverCtx] = None
+ self.__driver_ctxs: list[WebDriverCtx] = []
self.__download_thread: Optional[DownloadWorker] = None
self.setupUi()
@@ -153,6 +153,27 @@ class ALWebDriverDownloadDialog(CenterOnParentMixin, QDialog):
self.initializeDriverManager()
self.refreshDriverList()
+ def closeEvent(
+ self,
+ event: QCloseEvent
+ ):
+
+ if self.__download_thread and self.__download_thread.isRunning():
+ reply = QMessageBox.question(
+ self,
+ "确认关闭 - AutoLibrary",
+ "驱动正在下载中, 确定要取消并关闭对话框吗 ?",
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
+ )
+ if reply == QMessageBox.StandardButton.No:
+ event.ignore()
+ return
+ self.__download_thread.stop()
+ if not self.__confirmed:
+ self.__selected_driver_ctx = None
+ event.accept()
+ super().closeEvent(event)
+
def setupUi(
self
):
@@ -246,89 +267,42 @@ class ALWebDriverDownloadDialog(CenterOnParentMixin, QDialog):
if not self.__driver_manager:
return
self.__driver_manager.refresh()
- self.__driver_infos = self.__driver_manager.getDriverInfos()
+ self.__driver_ctxs = self.__driver_manager.getDriverCtxs()
self.DriverComboBox.clear()
installed = 0
installed_idx = 0
- for i, driver_info in enumerate(self.__driver_infos):
- display_text = f"{driver_info.driver_type.value} - {driver_info.browser_version}"
- if driver_info.driver_status == WebDriverStatus.INSTALLED:
+ for i, driver_ctx in enumerate(self.__driver_ctxs):
+ display_text = f"{driver_ctx.driver_type.value} - {driver_ctx.browser_version}"
+ if driver_ctx.driver_status == WebDriverStatus.INSTALLED:
installed += 1
installed_idx = i # get the installed driver index
- display_text += " : 已安装"
+ display_text += " - 已安装"
self.DriverComboBox.addItem(display_text)
- count = len(self.__driver_infos)
+ count = len(self.__driver_ctxs)
self.BrowserCountLabel.setText(f"检测到 {count} 个可用浏览器,{installed} 个已安装驱动:")
- if self.__driver_infos:
+ if self.__driver_ctxs:
self.DriverComboBox.setCurrentIndex(installed_idx)
- def onDriverComboBoxChanged(
- self,
- index: int
- ):
-
- if not self.__driver_infos or index < 0 or index >= len(self.__driver_infos):
- return
- driver_info = self.__driver_infos[index]
- self.updateDriverInfoDisplay(driver_info)
- self.updateProgressBarStates(driver_info)
- self.updateButtonStates(driver_info)
-
- def closeEvent(
- self,
- event: QCloseEvent
- ):
-
- if self.__download_thread and self.__download_thread.isRunning():
- reply = QMessageBox.question(
- self,
- "确认关闭 - AutoLibrary",
- "驱动正在下载中, 确定要取消并关闭对话框吗 ?",
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
- )
- if reply == QMessageBox.StandardButton.No:
- event.ignore()
- return
- self.__download_thread.stop()
- if not self.__confirmed:
- self.__selected_driver_info = None
- event.accept()
- super().closeEvent(event)
-
- def onThreadFinished(
- self
- ):
-
- if self.__download_thread:
- self.__download_thread.deleteLater()
- self.__download_thread = None
-
- def getSelectedDriverInfo(
- self
- ) -> Optional[WebDriverInfo]:
-
- return self.__selected_driver_info
-
def updateDriverInfoDisplay(
self,
- driver_info: WebDriverInfo
+ driver_ctx: WebDriverCtx
):
- if driver_info.driver_type == WebDriverType.CHROME:
+ if driver_ctx.driver_type == WebDriverType.CHROME:
driver_type = "Google Chrome"
- elif driver_info.driver_type == WebDriverType.FIREFOX:
+ elif driver_ctx.driver_type == WebDriverType.FIREFOX:
driver_type = "Mozilla Firefox"
- elif driver_info.driver_type == WebDriverType.EDGE:
+ elif driver_ctx.driver_type == WebDriverType.EDGE:
driver_type = "Microsoft Edge"
else:
driver_type = "未知"
self.BrowserTypeLabel.setText(f"类型:{driver_type}")
- self.VersionLabel.setText(f"版本:{driver_info.driver_version}")
- if driver_info.driver_path:
- self.PathLabel.setText(str(driver_info.driver_path))
+ self.VersionLabel.setText(f"版本:{driver_ctx.driver_version}")
+ if driver_ctx.driver_path:
+ self.PathLabel.setText(str(driver_ctx.driver_path))
else:
self.PathLabel.setText("未安装")
- match driver_info.driver_status:
+ match driver_ctx.driver_status:
case WebDriverStatus.NOT_INSTALLED:
self.StatusLabel.status = ALStatusLabel.Status.WAITING
case WebDriverStatus.INSTALLED:
@@ -340,51 +314,70 @@ class ALWebDriverDownloadDialog(CenterOnParentMixin, QDialog):
def updateProgressBarStates(
self,
- driver_info: WebDriverInfo
+ driver_ctx: WebDriverCtx
):
- if driver_info.driver_status == WebDriverStatus.NOT_INSTALLED:
+ if driver_ctx.driver_status == WebDriverStatus.NOT_INSTALLED:
self.ProgressBar.setValue(0)
self.ProgressText.setText("未安装")
- elif driver_info.driver_status == WebDriverStatus.INSTALLED:
+ elif driver_ctx.driver_status == WebDriverStatus.INSTALLED:
self.ProgressBar.setValue(100)
self.ProgressText.setText("已安装")
- elif driver_info.driver_status == WebDriverStatus.DOWNLOADING:
+ elif driver_ctx.driver_status == WebDriverStatus.DOWNLOADING:
pass # update by worker thread
- elif driver_info.driver_status == WebDriverStatus.ERROR:
+ elif driver_ctx.driver_status == WebDriverStatus.ERROR:
self.ProgressBar.setValue(0)
self.ProgressText.setText("下载失败")
def updateButtonStates(
self,
- driver_info: WebDriverInfo
+ driver_ctx: WebDriverCtx
):
- if driver_info.driver_status == WebDriverStatus.NOT_INSTALLED:
+ if driver_ctx.driver_status == WebDriverStatus.NOT_INSTALLED:
self.RefreshButton.setEnabled(True)
self.DeleteButton.setEnabled(False)
self.DownloadButton.setEnabled(True)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(False)
- elif driver_info.driver_status == WebDriverStatus.INSTALLED:
+ elif driver_ctx.driver_status == WebDriverStatus.INSTALLED:
self.RefreshButton.setEnabled(True)
self.DownloadButton.setEnabled(False)
self.DeleteButton.setEnabled(True)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(True)
- elif driver_info.driver_status == WebDriverStatus.DOWNLOADING:
+ elif driver_ctx.driver_status == WebDriverStatus.DOWNLOADING:
self.RefreshButton.setEnabled(False)
self.DownloadButton.setEnabled(False)
self.DeleteButton.setEnabled(False)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(False)
- elif driver_info.driver_status == WebDriverStatus.ERROR:
+ elif driver_ctx.driver_status == WebDriverStatus.ERROR:
self.RefreshButton.setEnabled(True)
self.DownloadButton.setEnabled(True)
self.DeleteButton.setEnabled(False)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(False)
+ def getSelectedDriverInfo(
+ self
+ ) -> Optional[WebDriverCtx]:
+
+ return self.__selected_driver_ctx
+
+ @Slot(int)
+ def onDriverComboBoxChanged(
+ self,
+ index: int
+ ):
+
+ if not self.__driver_ctxs or index < 0 or index >= len(self.__driver_ctxs):
+ return
+ driver_ctx = self.__driver_ctxs[index]
+ self.updateDriverInfoDisplay(driver_ctx)
+ self.updateProgressBarStates(driver_ctx)
+ self.updateButtonStates(driver_ctx)
+
@Slot()
def onRefreshButtonClicked(
self
@@ -398,9 +391,9 @@ class ALWebDriverDownloadDialog(CenterOnParentMixin, QDialog):
):
index = self.DriverComboBox.currentIndex()
- if index < 0 or index >= len(self.__driver_infos):
+ if index < 0 or index >= len(self.__driver_ctxs):
return
- driver_info = self.__driver_infos[index]
+ driver_info = self.__driver_ctxs[index]
if driver_info.driver_status.name != "INSTALLED":
QMessageBox.information(self, "提示 - AutoLibrary", "该驱动未安装, 无需删除")
return
@@ -426,9 +419,9 @@ class ALWebDriverDownloadDialog(CenterOnParentMixin, QDialog):
self.DriverComboBox.setEnabled(False)
index = self.DriverComboBox.currentIndex()
- if index < 0 or index >= len(self.__driver_infos):
+ if index < 0 or index >= len(self.__driver_ctxs):
return
- driver_info = self.__driver_infos[index]
+ driver_info = self.__driver_ctxs[index]
if driver_info.driver_status == WebDriverStatus.INSTALLED:
return
driver_info.driver_status = WebDriverStatus.DOWNLOADING # we set this only to update
@@ -444,7 +437,7 @@ class ALWebDriverDownloadDialog(CenterOnParentMixin, QDialog):
self.__download_thread.downloadFinished.connect(self.onDownloadFinished)
self.__download_thread.downloadError.connect(self.onDownloadError)
self.__download_thread.downloadCancelled.connect(self.onDownloadCancelled)
- self.__download_thread.finished.connect(self.onThreadFinished)
+ self.__download_thread.finished.connect(self.onDownloadThreadFinished)
self.__download_thread.start()
@Slot()
@@ -472,9 +465,11 @@ class ALWebDriverDownloadDialog(CenterOnParentMixin, QDialog):
self.DriverComboBox.setEnabled(True)
index = self.DriverComboBox.currentIndex()
- if 0 <= index < len(self.__driver_infos):
- driver_info = self.__driver_infos[index]
+ if 0 <= index < len(self.__driver_ctxs):
+ driver_info = self.__driver_ctxs[index]
driver_info.driver_status = WebDriverStatus.INSTALLED
+ current_text = self.DriverComboBox.itemText(index)
+ self.DriverComboBox.setItemText(index, current_text + " - 已安装")
self.updateDriverInfoDisplay(driver_info)
self.updateProgressBarStates(driver_info)
self.updateButtonStates(driver_info)
@@ -487,8 +482,8 @@ class ALWebDriverDownloadDialog(CenterOnParentMixin, QDialog):
self.DriverComboBox.setEnabled(True)
index = self.DriverComboBox.currentIndex()
- if 0 <= index < len(self.__driver_infos):
- driver_info = self.__driver_infos[index]
+ if 0 <= index < len(self.__driver_ctxs):
+ driver_info = self.__driver_ctxs[index]
driver_info.driver_status = WebDriverStatus.ERROR
self.updateDriverInfoDisplay(driver_info)
self.updateProgressBarStates(driver_info)
@@ -502,8 +497,8 @@ class ALWebDriverDownloadDialog(CenterOnParentMixin, QDialog):
self.DriverComboBox.setEnabled(True)
index = self.DriverComboBox.currentIndex()
- if 0 <= index < len(self.__driver_infos):
- driver_info = self.__driver_infos[index]
+ if 0 <= index < len(self.__driver_ctxs):
+ driver_info = self.__driver_ctxs[index]
self.__driver_manager.cancelDriverDownload(driver_info)
driver_info.driver_status = WebDriverStatus.NOT_INSTALLED
self.updateDriverInfoDisplay(driver_info)
@@ -511,18 +506,27 @@ class ALWebDriverDownloadDialog(CenterOnParentMixin, QDialog):
self.updateButtonStates(driver_info)
self.ProgressText.setText("下载已取消")
+ @Slot()
+ def onDownloadThreadFinished(
+ self
+ ):
+
+ if self.__download_thread:
+ self.__download_thread.deleteLater()
+ self.__download_thread = None
+
@Slot()
def onConfirmButtonClicked(
self
):
index = self.DriverComboBox.currentIndex()
- if index < 0 or index >= len(self.__driver_infos):
+ if index < 0 or index >= len(self.__driver_ctxs):
return
- driver_info = self.__driver_infos[index]
- if driver_info.driver_status != WebDriverStatus.INSTALLED:
+ driver_ctx = self.__driver_ctxs[index]
+ if driver_ctx.driver_status != WebDriverStatus.INSTALLED:
return
- self.__selected_driver_info = driver_info
+ self.__selected_driver_ctx = driver_ctx
self.__confirmed = True
self.accept()
@@ -542,5 +546,5 @@ class ALWebDriverDownloadDialog(CenterOnParentMixin, QDialog):
self.__download_thread.cancel()
else:
self.__confirmed = False
- self.__selected_driver_info = None
+ self.__selected_driver_ctx = None
self.reject()
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/ALMainWindow.ui b/src/gui/resources/ui/ALMainWindow.ui
index 84aa38c..e075fc0 100644
--- a/src/gui/resources/ui/ALMainWindow.ui
+++ b/src/gui/resources/ui/ALMainWindow.ui
@@ -286,6 +286,7 @@ font: 700 9pt;
工具
+
@@ -310,6 +312,11 @@ font: 700 9pt;
在线手册
+
+
+ 检查更新
+
+
关于
@@ -320,6 +327,11 @@ font: 700 9pt;
全局设置
+
+
+ 公告栏
+
+
diff --git a/src/gui/resources/ui/ALSettingsWidget.ui b/src/gui/resources/ui/ALSettingsWidget.ui
index 45ed0e8..4eed503 100644
--- a/src/gui/resources/ui/ALSettingsWidget.ui
+++ b/src/gui/resources/ui/ALSettingsWidget.ui
@@ -101,340 +101,589 @@
+ -
+
+ 公告
+
+
+
+
+
-
-
-
- QFrame::Shape::NoFrame
+
+
+ 0
-
- 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
+ 409
+ 383
+
-
- 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
+
+
+
+ 测试连接
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+
+
+
+ true
+
+
+
+ -
+
+
+
+ 100
+ 25
+
+
+
+
+ 100
+ 25
+
+
+
+ 查看公告栏
+
+
+
+
+
+
+ -
+
+
+ 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..dba14bb
--- /dev/null
+++ b/src/managers/bulletin/BulletinManager.py
@@ -0,0 +1,340 @@
+# -*- 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, timezone
+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 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
+
+ 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"
+
+ @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:
+ """
+ Check whether this is the first sync.
+
+ Returns:
+ bool: True if no cached bulletins or no last sync time.
+ """
+
+ with self.__lock:
+ 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.
+ """
+
+ 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
+ ) -> dict:
+ """
+ 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.
+ """
+
+ 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"),
+ "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.
+ - Bulletins missing an "id" field are skipped.
+
+ 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(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.get("id")
+ if bid is None:
+ continue
+ bid = str(bid)
+ if bid in delete_set:
+ bulletins_dict.pop(bid, None)
+ continue
+ 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: str(x.get("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 str(b.get("id", "")) == str(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..024ce6a 100644
--- a/src/managers/config/ConfigManager.py
+++ b/src/managers/config/ConfigManager.py
@@ -7,6 +7,7 @@ This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
+import copy
import os
import threading
@@ -18,7 +19,7 @@ from interfaces.ConfigProvider import ConfigType, ConfigPath
# This config manager class only responsible for global and other
-# unconfigurable config files.
+# config files. NOT include run and user config files.
class ConfigTemplate:
@@ -59,6 +60,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:
@@ -82,8 +88,8 @@ class ConfigManager:
):
self.__config_dir = os.path.abspath(config_dir)
- self.__config_lock = threading.Lock()
self.__config_data = {}
+ self.__lock = threading.Lock()
self.initialize()
@@ -105,7 +111,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])
@@ -116,16 +122,16 @@ class ConfigManager:
default: Optional[Any] = None
) -> Any:
- with self.__config_lock:
+ with self.__lock:
config_data = self.__config_data[key.config_type.value]
if key.key == "":
- return config_data
+ return copy.deepcopy(config_data)
keys = key.key.split('.')
for k in keys[:-1]:
config_data = config_data.get(k, None)
if config_data is None:
return default
- return config_data.get(keys[-1], default)
+ return copy.deepcopy(config_data.get(keys[-1], default))
def set(
self,
@@ -133,7 +139,7 @@ class ConfigManager:
value: Any = None
):
- with self.__config_lock:
+ with self.__lock:
root_data = self.__config_data[key.config_type.value]
if key.key == "":
self.__config_data[key.config_type.value] = value
diff --git a/src/managers/driver/WebDriverDownloader.py b/src/managers/driver/WebDriverDownloader.py
index 681d9c8..af11ae7 100644
--- a/src/managers/driver/WebDriverDownloader.py
+++ b/src/managers/driver/WebDriverDownloader.py
@@ -181,7 +181,7 @@ class WebDriverFileName:
raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}")
-class WebDriverURL:
+class WebDriverDownloadURL:
"""
Web driver download URL
"""
@@ -242,7 +242,7 @@ class WebDriverDownloader:
self.driver_type = driver_type
self.arch = driver_arch
self.version = driver_version
- self.download_url = str(WebDriverURL(self.version, self.driver_type, self.arch))
+ self.download_url = str(WebDriverDownloadURL(self.version, self.driver_type, self.arch))
self.download_dir = Path(download_dir)/self.driver_type.value/self.version/self.arch.value
self.download_dir.mkdir(mode=0o0755, parents=True, exist_ok=True)
self.download_path = self.download_dir/str(WebDriverFileName(self.version, self.driver_type, self.arch))
diff --git a/src/managers/driver/WebDriverManager.py b/src/managers/driver/WebDriverManager.py
index 7ab845d..ff5d7a3 100644
--- a/src/managers/driver/WebDriverManager.py
+++ b/src/managers/driver/WebDriverManager.py
@@ -8,6 +8,7 @@ You may use, modify, and distribute this file under the terms of the MIT License
See the LICENSE file for details.
"""
import os
+import shutil
import threading
import packaging.version as ver
@@ -19,7 +20,7 @@ from managers.driver.WebBrowserDetector import (
WebBrowserType, WebBrowserArch, WebBrowserInfo, WebBrowserDetector
)
from managers.driver.WebDriverDownloader import (
- WebDriverArch, WebDriverType,
+ WebDriverArch, WebDriverType, WebDriverExecName,
ChromeDriverDownloader, FirefoxDriverDownloader, EdgeDriverDownloader
)
@@ -35,9 +36,9 @@ class WebDriverStatus(Enum):
ERROR = 3
-class WebDriverInfo:
+class WebDriverCtx:
"""
- Web driver information.
+ Web browser and driver context.
Attributes:
driver_type (WebDriverType): Web driver type
@@ -52,8 +53,8 @@ class WebDriverInfo:
self
):
- self.driver_type = None
- self.driver_arch = None
+ self.driver_type : Optional[WebDriverType] = None
+ self.driver_arch : Optional[WebDriverArch] = None
self.driver_version = ""
self.browser_version = ""
self.driver_path: Optional[Path] = None
@@ -75,7 +76,7 @@ class WebDriverManager:
self.__driver_dir = os.path.abspath(driver_dir)
self.__browser_detector = WebBrowserDetector()
- self.__driver_infos: list[WebDriverInfo] = []
+ self.__driver_ctxs: list[WebDriverCtx] = []
self.__initialized = False
self.__lock = threading.Lock()
@@ -98,8 +99,8 @@ class WebDriverManager:
with self.__lock:
browser_infos = self.__browser_detector.detect()
- self.__driver_infos = [
- self._getDriverInfo(info)
+ self.__driver_ctxs = [
+ self._getDriverCtx(info)
for info in browser_infos
]
@@ -108,7 +109,7 @@ class WebDriverManager:
):
with self.__lock:
- for driver_info in self.__driver_infos:
+ for driver_info in self.__driver_ctxs:
driver_path = self._getDriverPath(driver_info)
if driver_path and driver_path.exists() and driver_path.is_file():
# Repair missing execute permission on Unix
@@ -198,7 +199,7 @@ class WebDriverManager:
else:
raise ValueError(f"不支持的 Web 浏览器类型 : {browser_type}")
- def _mapFirefoxDriverVersion(
+ def _mapFirefoxBrowserVersionToDriver(
self,
version: str
) -> str:
@@ -238,46 +239,30 @@ class WebDriverManager:
except Exception as e:
raise ValueError(f"无效的 Firefox 版本格式 : {version}") from e
- def _getDriverInfo(
+ def _getDriverCtx(
self,
browser_info: WebBrowserInfo
- ) -> WebDriverInfo:
+ ) -> WebDriverCtx:
- driver_info = WebDriverInfo()
- driver_info.driver_type = self._mapWebBrowserTypeToDriver(browser_info.browser_type)
- driver_info.driver_arch = self._mapWebBrowserArchToDriver(browser_info.browser_type, browser_info.browser_arch)
+ driver_ctx = WebDriverCtx()
+ driver_ctx.driver_type = self._mapWebBrowserTypeToDriver(browser_info.browser_type)
+ driver_ctx.driver_arch = self._mapWebBrowserArchToDriver(browser_info.browser_type, browser_info.browser_arch)
if browser_info.browser_type == WebBrowserType.FIREFOX:
- driver_info.driver_version = self._mapFirefoxDriverVersion(browser_info.browser_version)
+ driver_ctx.driver_version = self._mapFirefoxBrowserVersionToDriver(browser_info.browser_version)
else:
- driver_info.driver_version = browser_info.browser_version
- driver_info.browser_version = browser_info.browser_version
- return driver_info
+ driver_ctx.driver_version = browser_info.browser_version
+ driver_ctx.browser_version = browser_info.browser_version
+ return driver_ctx
def _getDriverPath(
self,
- driver_info: WebDriverInfo
+ driver_ctx: WebDriverCtx
) -> Optional[Path]:
- driver_type = driver_info.driver_type
- driver_arch = driver_info.driver_arch
- driver_version = driver_info.driver_version
- if driver_type == WebDriverType.CHROME:
- driver_name = "chromedriver"
- elif driver_type == WebDriverType.FIREFOX:
- driver_name = "geckodriver"
- elif driver_type == WebDriverType.EDGE:
- driver_name = "msedgedriver"
- else:
- return None
- is_win = driver_arch in [
- WebDriverArch.Chrome.WINX86_32,
- WebDriverArch.Chrome.WINX86_64,
- WebDriverArch.Firefox.WINX86_32,
- WebDriverArch.Firefox.WINX86_64,
- WebDriverArch.Edge.WINX86_32,
- WebDriverArch.Edge.WINX86_64,
- ]
- exe_name = f"{driver_name}.exe" if is_win else driver_name
+ driver_type = driver_ctx.driver_type
+ driver_arch = driver_ctx.driver_arch
+ driver_version = driver_ctx.driver_version
+ exe_name = str(WebDriverExecName(driver_type, driver_arch))
driver_dir = Path(self.__driver_dir)/driver_type.value/driver_version/driver_arch.value
driver_path = driver_dir/exe_name
return driver_path
@@ -289,28 +274,28 @@ class WebDriverManager:
self._detectBrowsers()
self._checkDriverStatus()
- def getDriverInfos(
+ def getDriverCtxs(
self
- ) -> list[WebDriverInfo]:
+ ) -> list[WebDriverCtx]:
with self.__lock:
- return self.__driver_infos.copy()
+ return self.__driver_ctxs.copy()
- def getDriverInfo(
+ def getDriverCtx(
self,
driver_type: WebDriverType
- ) -> list[WebDriverInfo]:
+ ) -> list[WebDriverCtx]:
with self.__lock:
return [
info
- for info in self.__driver_infos
+ for info in self.__driver_ctxs
if info.driver_type == driver_type
]
def getDriverPath(
self,
- driver_info: WebDriverInfo
+ driver_info: WebDriverCtx
) -> Optional[Path]:
if driver_info and driver_info.driver_status == WebDriverStatus.INSTALLED:
@@ -319,28 +304,28 @@ class WebDriverManager:
def installDriver(
self,
- driver_info: WebDriverInfo,
+ driver_ctx: WebDriverCtx,
progress_callback: Optional[Callable[[float, int, float, str], None]] = None,
cancel_event: Optional[threading.Event] = None
) -> Optional[Path]:
with self.__lock:
- if not driver_info:
+ if not driver_ctx:
if progress_callback:
progress_callback(0, 0, 0, "未找到浏览器信息")
else:
raise ValueError("未找到浏览器信息")
- if driver_info and driver_info.driver_status == WebDriverStatus.DOWNLOADING:
+ if driver_ctx and driver_ctx.driver_status == WebDriverStatus.DOWNLOADING:
if progress_callback:
- progress_callback(0, 0, 0, f"{driver_info.driver_type} 驱动正在下载中")
+ progress_callback(0, 0, 0, f"{driver_ctx.driver_type} 驱动正在下载中")
else:
- raise ValueError(f"{driver_info.driver_type} 驱动正在下载中")
+ raise ValueError(f"{driver_ctx.driver_type} 驱动正在下载中")
try:
- if not driver_info:
+ if not driver_ctx:
raise ValueError("未找到浏览器信息")
- driver_arch = driver_info.driver_arch
- driver_type = driver_info.driver_type
- driver_version = driver_info.driver_version
+ driver_arch = driver_ctx.driver_arch
+ driver_type = driver_ctx.driver_type
+ driver_version = driver_ctx.driver_version
downloader = None
if driver_type == WebDriverType.CHROME:
downloader = ChromeDriverDownloader(
@@ -366,68 +351,66 @@ class WebDriverManager:
else:
raise ValueError(f"不支持的 Web Driver 类型")
with self.__lock:
- driver_info.driver_status = WebDriverStatus.DOWNLOADING
+ driver_ctx.driver_status = WebDriverStatus.DOWNLOADING
driver_path = downloader.download(progress_callback=progress_callback, cancel_event=cancel_event)
with self.__lock:
if driver_path:
- driver_info.driver_path = driver_path
- driver_info.driver_version = driver_version
- driver_info.driver_status = WebDriverStatus.INSTALLED
+ driver_ctx.driver_path = driver_path
+ driver_ctx.driver_version = driver_version
+ driver_ctx.driver_status = WebDriverStatus.INSTALLED
else:
- driver_info.driver_status = WebDriverStatus.ERROR
+ driver_ctx.driver_status = WebDriverStatus.ERROR
return driver_path
except Exception as e:
with self.__lock:
- driver_info.driver_status = WebDriverStatus.ERROR
+ driver_ctx.driver_status = WebDriverStatus.ERROR
raise e
def cancelDriverDownload(
self,
- driver_info: WebDriverInfo
+ driver_ctx: WebDriverCtx
) -> bool:
- import shutil
-
try:
- driver_path = self._getDriverPath(driver_info)
+ driver_path = self._getDriverPath(driver_ctx)
if driver_path:
download_dir = driver_path.parent
if download_dir.exists():
shutil.rmtree(download_dir, ignore_errors=True)
with self.__lock:
- driver_info.driver_path = None
- driver_info.driver_status = WebDriverStatus.NOT_INSTALLED
+ driver_ctx.driver_path = None
+ driver_ctx.driver_status = WebDriverStatus.NOT_INSTALLED
return True
except Exception:
return False
def uninstallDriver(
self,
- driver_info: WebDriverInfo,
+ driver_ctx: WebDriverCtx,
progress_callback: Optional[Callable[[int, int, float, str], None]] = None
) -> bool:
with self.__lock:
- if not driver_info:
+ if not driver_ctx:
if progress_callback:
progress_callback(0, 0, 0, "未找到浏览器信息")
else:
raise ValueError("未找到浏览器信息")
- if driver_info.driver_status != WebDriverStatus.INSTALLED:
+ if driver_ctx.driver_status != WebDriverStatus.INSTALLED:
if progress_callback:
- progress_callback(0, 0, 0, f"{driver_info.driver_type} 驱动未安装")
+ progress_callback(0, 0, 0, f"{driver_ctx.driver_type} 驱动未安装")
else:
- raise ValueError(f"{driver_info.driver_type} 驱动未安装")
+ raise ValueError(f"{driver_ctx.driver_type} 驱动未安装")
try:
- driver_path = driver_info.driver_path
+ driver_path = driver_ctx.driver_path
driver_path.unlink()
with self.__lock:
- driver_info.driver_path = None
- driver_info.driver_status = WebDriverStatus.NOT_INSTALLED
+ driver_ctx.driver_path = None
+ driver_ctx.driver_status = WebDriverStatus.NOT_INSTALLED
return True
except Exception:
with self.__lock:
- driver_info.driver_status = WebDriverStatus.ERROR
+ driver_ctx.driver_status = WebDriverStatus.ERROR
raise
def driverDir(
diff --git a/src/managers/log/LogManager.py b/src/managers/log/LogManager.py
index 767a951..8e5b075 100644
--- a/src/managers/log/LogManager.py
+++ b/src/managers/log/LogManager.py
@@ -99,17 +99,14 @@ class LogManager:
self.__logger = logging.getLogger("AutoLibrary")
self.__logger.setLevel(logging.DEBUG)
self.__logger.handlers.clear()
-
formatter = CallerInfoFormatter(
'[%(asctime)s] - [%(name)s] - [%(levelname)s] - [%(filename)s:%(lineno)s] - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
-
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
self.__logger.addHandler(console_handler)
-
all_log_file = os.path.join(self.__log_dir, "all.log")
file_handler_all = TimedRotatingFileHandler(
all_log_file,
@@ -122,7 +119,6 @@ class LogManager:
file_handler_all.setLevel(logging.DEBUG)
file_handler_all.setFormatter(formatter)
self.__logger.addHandler(file_handler_all)
-
error_log_file = os.path.join(self.__log_dir, "error.log")
file_handler_error = TimedRotatingFileHandler(
error_log_file,
@@ -135,7 +131,6 @@ class LogManager:
file_handler_error.setLevel(logging.ERROR)
file_handler_error.setFormatter(formatter)
self.__logger.addHandler(file_handler_error)
-
self.__initialized = True
def getLogger(
diff --git a/src/managers/theme/ThemeManager.py b/src/managers/theme/ThemeManager.py
index 755c67b..957fb45 100644
--- a/src/managers/theme/ThemeManager.py
+++ b/src/managers/theme/ThemeManager.py
@@ -28,6 +28,8 @@ from utils.ThemeUtils import (
)
+# we use Fusion as the default active style name
+# because it's cross-platform.
_active_style_name = "Fusion"
@@ -61,8 +63,10 @@ class ThemeManager:
):
self.__themes_dir = os.path.abspath(themes_dir)
- self.__lock = threading.Lock()
self.__current_theme_name = ""
+ self.__lock = threading.Lock()
+
+ # directory must exist
os.makedirs(self.__themes_dir, exist_ok=True)
@staticmethod
@@ -144,18 +148,6 @@ class ThemeManager:
)
return alt_path
- def themesDir(
- self
- ) -> str:
- """
- Get the themes directory path.
-
- Returns:
- str: The absolute path to the themes storage directory.
- """
-
- return self.__themes_dir
-
def importTheme(
self,
source_path: str
@@ -321,6 +313,18 @@ class ThemeManager:
)
app.setStyle(QStyleFactory.create(_active_style_name))
+ def themesDir(
+ self
+ ) -> str:
+ """
+ Get the themes directory path.
+
+ Returns:
+ str: The absolute path to the themes storage directory.
+ """
+
+ return self.__themes_dir
+
# ThemeManager singleton instance.
_theme_manager_instance = None
diff --git a/src/pages/flows/ReserveFlow.py b/src/pages/flows/ReserveFlow.py
index 9d0c04b..51c447d 100644
--- a/src/pages/flows/ReserveFlow.py
+++ b/src/pages/flows/ReserveFlow.py
@@ -22,6 +22,7 @@ from pages.strategies.TimeSelectMaker import TimeSelectMaker
from pages.ReserveView import ReserveView
from pages.components.ReserveResultDialog import ReserveResultDialog
from pages.components.TimeSelectDialog import TimeSelectDialog
+from pages.components.SeatMapDialog import SeatMapDialog
@dataclass
@@ -150,7 +151,7 @@ class ReserveFlow(MsgBase):
def _selectSeatAndSubmit(
self,
view: ReserveView,
- seat_map,
+ seat_map: SeatMapDialog,
ctx: ReserveContext,
) -> tuple[bool, bool]: