diff --git a/src/gui/ALAboutDialog.py b/src/gui/ALAboutDialog.py
index 34a5490..aa18971 100644
--- a/src/gui/ALAboutDialog.py
+++ b/src/gui/ALAboutDialog.py
@@ -55,9 +55,9 @@ class ALAboutDialog(QDialog, Ui_ALAboutDialog):
AboutBrowser.setOpenExternalLinks(True)
AboutBrowser.setLineWrapMode(QTextBrowser.LineWrapMode.NoWrap)
AboutBrowser.setTextInteractionFlags(Qt.TextBrowserInteraction)
- BrowserFont = AboutBrowser.font()
- BrowserFont.setFamilies(["Courier New", "Consolas", "Menlo", "DejaVu Sans Mono", "monospace"])
- AboutBrowser.setFont(BrowserFont)
+ browser_font = AboutBrowser.font()
+ browser_font.setFamilies(["Courier New", "Consolas", "Menlo", "DejaVu Sans Mono", "monospace"])
+ AboutBrowser.setFont(browser_font)
self.TabWidget.addTab(AboutBrowser, "关于")
LicenseBrowser = QTextBrowser()
LicenseBrowser.setHtml(self.generateLicenseText())
@@ -135,7 +135,6 @@ THE SOFTWARE.
"""
system = platform.system()
version = platform.version()
architecture = platform.architecture()[0]
-
if system == "Windows":
try:
version = platform.win32_ver()[1]
@@ -152,7 +151,6 @@ THE SOFTWARE."""
version = f"{distro.name()} {distro.version()}"
except ImportError:
pass
-
return {
'system': system,
'version': version,
diff --git a/src/gui/ALAutoScriptEditDialog.py b/src/gui/ALAutoScriptEditDialog.py
index 2a32510..8040994 100644
--- a/src/gui/ALAutoScriptEditDialog.py
+++ b/src/gui/ALAutoScriptEditDialog.py
@@ -75,54 +75,54 @@ class ALScriptHighlighter(QSyntaxHighlighter):
super().__init__(parent)
self._rules = []
- KeywordFmt = QTextCharFormat()
- KeywordFmt.setForeground(QColor("#569CD6"))
- KeywordFmt.setFontWeight(QFont.Weight.Bold)
+ keyword_fmt = QTextCharFormat()
+ keyword_fmt.setForeground(QColor("#569CD6"))
+ keyword_fmt.setFontWeight(QFont.Weight.Bold)
for kw in [
"if", "elseif", "else", "end", "then",
"and", "or", "not",
"local", "function", "return", "nil",
]:
- self._rules.append((r"\b" + kw + r"\b", KeywordFmt))
- BoolFmt = QTextCharFormat()
- BoolFmt.setForeground(QColor("#4FC1FF"))
- BoolFmt.setFontWeight(QFont.Weight.Bold)
- self._rules.append((r"\btrue\b", BoolFmt))
- self._rules.append((r"\bfalse\b", BoolFmt))
- CmpFmt = QTextCharFormat()
- CmpFmt.setForeground(QColor("#C586C0"))
- CmpFmt.setFontWeight(QFont.Weight.Normal)
+ self._rules.append((r"\b" + kw + r"\b", keyword_fmt))
+ bool_fmt = QTextCharFormat()
+ bool_fmt.setForeground(QColor("#4FC1FF"))
+ bool_fmt.setFontWeight(QFont.Weight.Bold)
+ self._rules.append((r"\btrue\b", bool_fmt))
+ self._rules.append((r"\bfalse\b", bool_fmt))
+ cmp_fmt = QTextCharFormat()
+ cmp_fmt.setForeground(QColor("#C586C0"))
+ cmp_fmt.setFontWeight(QFont.Weight.Normal)
for op in [r"==", r"~=", r">=", r"<=", r">", r"<"]:
- self._rules.append((op, CmpFmt))
- ArithFmt = QTextCharFormat()
- ArithFmt.setForeground(QColor("#C586C0"))
- ArithFmt.setFontWeight(QFont.Weight.Normal)
+ self._rules.append((op, cmp_fmt))
+ arith_fmt = QTextCharFormat()
+ arith_fmt.setForeground(QColor("#C586C0"))
+ arith_fmt.setFontWeight(QFont.Weight.Normal)
for op in [r"\+", r"-", r"\*", r"/", r"\.\."]:
- self._rules.append((op, ArithFmt))
- FuncFmt = QTextCharFormat()
- FuncFmt.setForeground(QColor("#DCDCAA"))
- FuncFmt.setFontWeight(QFont.Weight.Normal)
+ self._rules.append((op, arith_fmt))
+ func_fmt = QTextCharFormat()
+ func_fmt.setForeground(QColor("#DCDCAA"))
+ func_fmt.setFontWeight(QFont.Weight.Normal)
for fn in [ "time", "date", "datenow", "timenow", "dateadd", "timeadd"]:
- self._rules.append((r"\b" + fn + r"\b", FuncFmt))
- VarFmt = QTextCharFormat()
- VarFmt.setForeground(QColor("#9CDCFE"))
- VarFmt.setFontWeight(QFont.Weight.Normal)
+ self._rules.append((r"\b" + fn + r"\b", func_fmt))
+ var_fmt = QTextCharFormat()
+ var_fmt.setForeground(QColor("#9CDCFE"))
+ var_fmt.setFontWeight(QFont.Weight.Normal)
var_names = [name for _, (name, _) in createAllVariablesTable().items()]
for var in var_names:
- self._rules.append((r"\b" + var + r"\b", VarFmt))
- StrFmt = QTextCharFormat()
- StrFmt.setForeground(QColor("#CE9178"))
- StrFmt.setFontWeight(QFont.Weight.Normal)
- self._rules.append((r'"[^"]*"', StrFmt))
- self._rules.append((r"'[^']*'", StrFmt))
- NumFmt = QTextCharFormat()
- NumFmt.setForeground(QColor("#B5CEA8"))
- NumFmt.setFontWeight(QFont.Weight.Normal)
- self._rules.append((r"\b\d+(?:\.\d+)?\b", NumFmt))
- CommentFmt = QTextCharFormat()
- CommentFmt.setForeground(QColor("#6A9955"))
- CommentFmt.setFontItalic(True)
- self._rules.append((r"--[^\n]*", CommentFmt))
+ self._rules.append((r"\b" + var + r"\b", var_fmt))
+ st_fmt = QTextCharFormat()
+ st_fmt.setForeground(QColor("#CE9178"))
+ st_fmt.setFontWeight(QFont.Weight.Normal)
+ self._rules.append((r'"[^"]*"', st_fmt))
+ self._rules.append((r"'[^']*'", st_fmt))
+ num_fmt = QTextCharFormat()
+ num_fmt.setForeground(QColor("#B5CEA8"))
+ num_fmt.setFontWeight(QFont.Weight.Normal)
+ self._rules.append((r"\b\d+(?:\.\d+)?\b", num_fmt))
+ comment_fmt = QTextCharFormat()
+ comment_fmt.setForeground(QColor("#6A9955"))
+ comment_fmt.setFontItalic(True)
+ self._rules.append((r"--[^\n]*", comment_fmt))
def highlightBlock(
self,
@@ -189,13 +189,13 @@ class ALAutoScriptEditDialog(QDialog):
):
super().__init__(parent)
- self._fontSize = 21
- self._mockWidgets = {}
+ self._font_size = 21
+ self._mock_widgets = {}
self.setupUi()
self.connectSignals()
self.TextEdit.setPlainText(script)
- self._Highlighter = ALScriptHighlighter(
+ self._high_lighter = ALScriptHighlighter(
self.TextEdit.document()
)
if mockData:
@@ -224,7 +224,7 @@ class ALAutoScriptEditDialog(QDialog):
self.ZoomResetBtn.setIconSize(QSize(14, 14))
self.ZoomResetBtn.setFixedSize(25, 25)
self.ZoomResetBtn.setToolTip("重置缩放")
- self.ZoomLabel = QLabel(f"{self._fontSize}px")
+ self.ZoomLabel = QLabel(f"{self._font_size}px")
self.ZoomLabel.setFixedHeight(25)
self.OrchBtn = QPushButton("编排")
self.OrchBtn.setFixedSize(80, 25)
@@ -259,7 +259,7 @@ class ALAutoScriptEditDialog(QDialog):
self.TextEdit.setStyleSheet(
"QPlainTextEdit {"
" font-family: 'Courier New', 'Consolas', monospace;"
- f" font-size: {self._fontSize}px;"
+ f" font-size: {self._font_size}px;"
"}"
)
Layout.addWidget(self.TextEdit)
@@ -287,30 +287,30 @@ class ALAutoScriptEditDialog(QDialog):
BasicLayout.setSpacing(4)
BasicLayout.setContentsMargins(4, 4, 4, 4)
BasicLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
- controlButtons = [
+ control_buttons = [
("如果 (if...)", "if then\n \nend"),
("再如果 (elseif...)", "elseif then\n "),
("否则 (else)", "else"),
("结束 (end)", "end"),
("跳过 (pass)", "-- pass"),
]
- self.addButtonsToGrid(BasicLayout, controlButtons, 0, 0, 3)
- assignButtons = [
+ self.addButtonsToGrid(BasicLayout, control_buttons, 0, 0, 3)
+ assign_buttons = [
("赋值 (=)", " = "),
]
- self.addButtonsToGrid(BasicLayout, assignButtons, 1, 2, 3)
+ self.addButtonsToGrid(BasicLayout, assign_buttons, 1, 2, 3)
TabWidget.addTab(BasicWidget, "基本语法")
OperatorWidget = QWidget()
OperatorLayout = QGridLayout(OperatorWidget)
OperatorLayout.setSpacing(4)
OperatorLayout.setContentsMargins(4, 4, 4, 4)
OperatorLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
- arithmeticButtons = [
+ arithmetic_buttons = [
("加 (+)", " + "),
("减 (-)", " - "),
]
- self.addButtonsToGrid(OperatorLayout, arithmeticButtons, 0, 0, 3)
- compareButtons = [
+ self.addButtonsToGrid(OperatorLayout, arithmetic_buttons, 0, 0, 3)
+ compare_buttons = [
("等于 (==)", " == "),
("不等于 (~=)", " ~= "),
("大于 (>)", " > "),
@@ -318,7 +318,7 @@ class ALAutoScriptEditDialog(QDialog):
("大于等于 (>=)", " >= "),
("小于等于 (<=)", " <= "),
]
- self.addButtonsToGrid(OperatorLayout, compareButtons, 1, 0, 3)
+ self.addButtonsToGrid(OperatorLayout, compare_buttons, 1, 0, 3)
logic_buttons = [
("且 (and)", " and "),
("或 (or)", " or "),
@@ -335,40 +335,40 @@ class ALAutoScriptEditDialog(QDialog):
("假 (false)", "false"),
]
self.addButtonsToGrid(LiteralLayout, bool_buttons, 0, 0, 3)
- dateTimeButtons = [
+ date_time_buttons = [
("日期", 'date(2026, 1, 1)'),
("时间", 'time(0, 0)'),
]
- self.addButtonsToGrid(LiteralLayout, dateTimeButtons, 1, 0, 3)
- hintButtons = [
+ self.addButtonsToGrid(LiteralLayout, date_time_buttons, 1, 0, 3)
+ hint_buttons = [
("字符串", '"请输入文本"'),
("数字", "123"),
("注释", "-- 请输入注释"),
]
- self.addButtonsToGrid(LiteralLayout, hintButtons, 2, 0, 3)
+ self.addButtonsToGrid(LiteralLayout, hint_buttons, 2, 0, 3)
TabWidget.addTab(LiteralWidget, "字面量")
VarWidget = QWidget()
VarLayout = QGridLayout(VarWidget)
VarLayout.setSpacing(4)
VarLayout.setContentsMargins(4, 4, 4, 4)
VarLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
- varButtons = [
+ var_buttons = [
(display_name, name) for display_name, (name, _) in createAllVariablesTable().items()
]
- self.addButtonsToGrid(VarLayout, varButtons, 0, 0, 3)
+ self.addButtonsToGrid(VarLayout, var_buttons, 0, 0, 3)
TabWidget.addTab(VarWidget, "变量")
FuncWidget = QWidget()
FuncLayout = QGridLayout(FuncWidget)
FuncLayout.setSpacing(4)
FuncLayout.setContentsMargins(4, 4, 4, 4)
FuncLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
- funcButtons = [
+ func_buttons = [
("datenow()", "datenow()", "返回当前日期的 Unix 时间戳"),
("timenow()", "timenow()", "返回当前时间在一天中的分钟数"),
("dateadd(day, n)", "dateadd(, )", "日期偏移: dateadd(日期时间戳, 天数)"),
("timeadd(time, n)", "timeadd(, )", "时间偏移: timeadd(分钟数, 分钟数)"),
]
- for i, (text, template, tooltip) in enumerate(funcButtons):
+ for i, (text, template, tooltip) in enumerate(func_buttons):
Btn = QPushButton(text)
Btn.setProperty("template", template)
Btn.clicked.connect(self.insertTemplate)
@@ -419,17 +419,17 @@ class ALAutoScriptEditDialog(QDialog):
Form = QFormLayout(Group)
Form.setSpacing(4)
Form.setContentsMargins(5, 10, 5, 5)
- self._mockWidgets = {}
- mockData = createMockTargetData()
+ self._mock_widgets = {}
+ mock_data = createMockTargetData()
for name, var_type, key_path, display_name in createTargetVarDefs():
- d = mockData
+ d = mock_data
for key in key_path:
d = d[key]
default = d
Widget = self.makeMockInput(var_type, default)
Label = QLabel(f"{display_name}: {name}({var_type})")
Form.addRow(Label, Widget)
- self._mockWidgets[name] = (Widget, var_type, key_path)
+ self._mock_widgets[name] = (Widget, var_type, key_path)
return Group
def makeMockInput(
@@ -439,49 +439,49 @@ class ALAutoScriptEditDialog(QDialog):
) -> QWidget:
if var_type == "String":
- W = QLineEdit()
- W.setText(str(default))
- return W
+ widget = QLineEdit()
+ widget.setText(str(default))
+ return widget
if var_type == "Boolean":
- W = QComboBox()
- W.addItems(["是", "否"])
- W.setCurrentIndex(0 if default else 1)
- return W
+ widget = QComboBox()
+ widget.addItems(["是", "否"])
+ widget.setCurrentIndex(0 if default else 1)
+ return widget
if var_type == "Date":
- W = QDateEdit()
- W.setCalendarPopup(True)
- W.setDisplayFormat("yyyy-MM-dd")
- W.setDate(QDate.fromString(str(default), "yyyy-MM-dd"))
- return W
+ widget = QDateEdit()
+ widget.setCalendarPopup(True)
+ widget.setDisplayFormat("yyyy-MM-dd")
+ widget.setDate(QDate.fromString(str(default), "yyyy-MM-dd"))
+ return widget
if var_type == "Time":
- W = QTimeEdit()
- W.setDisplayFormat("HH:mm")
- W.setTime(QTime.fromString(str(default), "HH:mm"))
- return W
+ widget = QTimeEdit()
+ widget.setDisplayFormat("HH:mm")
+ widget.setTime(QTime.fromString(str(default), "HH:mm"))
+ return widget
if var_type == "Int":
- W = QSpinBox()
- W.setMinimum(-999999)
- W.setMaximum(999999)
- W.setValue(int(default) if default else 0)
- return W
+ widget = QSpinBox()
+ widget.setMinimum(-999999)
+ widget.setMaximum(999999)
+ widget.setValue(int(default) if default else 0)
+ return widget
if var_type == "Float":
- W = QDoubleSpinBox()
- W.setMinimum(-999999.0)
- W.setMaximum(999999.0)
- W.setDecimals(2)
- W.setValue(float(default) if default else 0.0)
- return W
- W = QLineEdit()
- W.setText(str(default))
- return W
+ widget = QDoubleSpinBox()
+ widget.setMinimum(-999999.0)
+ widget.setMaximum(999999.0)
+ widget.setDecimals(2)
+ widget.setValue(float(default) if default else 0.0)
+ return widget
+ widget = QLineEdit()
+ widget.setText(str(default))
+ return widget
def getMockData(
self
) -> dict:
data = {}
- for name, var_type, key_path, display_name in createTargetVarDefs():
- widget, _, _ = self._mockWidgets[name]
+ for name, var_type, key_path, _ in createTargetVarDefs():
+ widget, _, _ = self._mock_widgets[name]
value = self.getMockValue(widget, var_type)
d = data
for key in key_path[:-1]:
@@ -496,14 +496,14 @@ class ALAutoScriptEditDialog(QDialog):
if not data:
return
- for name, var_type, key_path, display_name in createTargetVarDefs():
+ for name, var_type, key_path, _ in createTargetVarDefs():
d = data
try:
for key in key_path:
d = d[key]
except (KeyError, TypeError):
continue
- widget, _, _ = self._mockWidgets[name]
+ widget, _, _ = self._mock_widgets[name]
self.setMockValue(widget, var_type, d)
def getMockValue(
@@ -578,10 +578,10 @@ class ALAutoScriptEditDialog(QDialog):
self.TextEdit.setStyleSheet(
"QPlainTextEdit {"
" font-family: 'Courier New', 'Consolas', monospace;"
- f" font-size: {self._fontSize}px;"
+ f" font-size: {self._font_size}px;"
"}"
)
- self.ZoomLabel.setText(f"{self._fontSize}px")
+ self.ZoomLabel.setText(f"{self._font_size}px")
@Slot()
def insertTemplate(
@@ -602,7 +602,7 @@ class ALAutoScriptEditDialog(QDialog):
self
):
- self._fontSize = min(self._fontSize + 2, 40)
+ self._font_size = min(self._font_size + 2, 40)
self.updateFontSize()
@Slot()
@@ -610,7 +610,7 @@ class ALAutoScriptEditDialog(QDialog):
self
):
- self._fontSize = max(self._fontSize - 2, 8)
+ self._font_size = max(self._font_size - 2, 8)
self.updateFontSize()
@Slot()
@@ -618,7 +618,7 @@ class ALAutoScriptEditDialog(QDialog):
self
):
- self._fontSize = 21
+ self._font_size = 21
self.updateFontSize()
@Slot()
@@ -639,13 +639,13 @@ class ALAutoScriptEditDialog(QDialog):
):
from gui.ALAutoScriptOrchDialog import ALAutoScriptOrchDialog
- Dlg = ALAutoScriptOrchDialog(self)
- if Dlg.exec() == QDialog.DialogCode.Accepted:
- script = Dlg.getScript()
+ Dialpg = ALAutoScriptOrchDialog(self)
+ if Dialpg.exec() == QDialog.DialogCode.Accepted:
+ script = Dialpg.getScript()
if script:
cursor = self.TextEdit.textCursor()
cursor.insertText(script)
- Dlg.deleteLater()
+ Dialpg.deleteLater()
@Slot()
def onDebugRun(
@@ -679,6 +679,6 @@ class ALAutoScriptEditDialog(QDialog):
if not changes:
QMessageBox.information(self, "调试运行", "目标变量未发生变化。")
return
- Dlg = _DebugResultDialog(changes, self)
- Dlg.exec()
- Dlg.deleteLater()
+ Dialpg = _DebugResultDialog(changes, self)
+ Dialpg.exec()
+ Dialpg.deleteLater()
diff --git a/src/gui/ALAutoScriptOrchDialog/_blocks.py b/src/gui/ALAutoScriptOrchDialog/_blocks.py
index ac82f26..ee4c09a 100644
--- a/src/gui/ALAutoScriptOrchDialog/_blocks.py
+++ b/src/gui/ALAutoScriptOrchDialog/_blocks.py
@@ -32,16 +32,16 @@ class ConditionalBlock(QGroupBox):
def __init__(
self,
- blockIndex: int,
- varMgr = None,
+ block_index: int,
+ var_mgr = None,
parent = None
):
super().__init__(parent)
- self.blockIndex = blockIndex
- self._varMgr = varMgr
- self._actionWidgets = []
- self._conditionRows = []
+ self._block_index = block_index
+ self._var_mgr = var_mgr
+ self._action_widgets = []
+ self._condition_rows = []
self.setupUi()
self.connectSignals()
@@ -53,8 +53,12 @@ class ConditionalBlock(QGroupBox):
self.setUpdatesEnabled(False)
self.setStyleSheet(
- "QGroupBox { font-weight: bold; border: 1px solid #ccc; "
- "margin-top: 5px; padding-top: 5px; }"
+ "QGroupBox { "\
+ "font-weight: bold;"\
+ "border: 1px solid #ccc;"\
+ "margin-top: 5px;"\
+ "padding-top: 5px; "\
+ "}"
)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
MainLayout = QVBoxLayout(self)
@@ -67,7 +71,7 @@ class ConditionalBlock(QGroupBox):
self.TypeCombo.addItem("ELSE IF", "ELSE IF")
self.TypeCombo.addItem("ELSE", "ELSE")
self.TypeCombo.setFixedHeight(25)
- if self.blockIndex == 0:
+ if self._block_index == 0:
self.TypeCombo.setEnabled(False)
HeaderLayout.addWidget(QLabel("类型:", self))
HeaderLayout.addWidget(self.TypeCombo)
@@ -115,10 +119,10 @@ class ConditionalBlock(QGroupBox):
):
Row = ConditionRowFrame(
- self._varMgr, self.blockIndex,
- isFirst=True, parent=self
+ self._var_mgr, self._block_index,
+ is_first=True, parent=self
)
- self._conditionRows.append(Row)
+ self._condition_rows.append(Row)
self.CondRowsLayout.addWidget(Row)
def addConditionRow(
@@ -126,11 +130,11 @@ class ConditionalBlock(QGroupBox):
):
Row = ConditionRowFrame(
- self._varMgr, self.blockIndex,
- isFirst=False, parent=self
+ self._var_mgr, self._block_index,
+ is_first=False, parent=self
)
Row.DeleteBtn.clicked.connect(lambda: self.removeConditionRow(Row))
- self._conditionRows.append(Row)
+ self._condition_rows.append(Row)
self.CondRowsLayout.addWidget(Row)
def removeConditionRow(
@@ -138,8 +142,8 @@ class ConditionalBlock(QGroupBox):
row: ConditionRowFrame
):
- if row in self._conditionRows and len(self._conditionRows) > 1:
- self._conditionRows.remove(row)
+ if row in self._condition_rows and len(self._condition_rows) > 1:
+ self._condition_rows.remove(row)
self.CondRowsLayout.removeWidget(row)
row.hide()
row.deleteLater()
@@ -148,9 +152,9 @@ class ConditionalBlock(QGroupBox):
self
):
- Step = ActionStepFrame(self._varMgr, self.blockIndex, parent=self)
+ Step = ActionStepFrame(self._var_mgr, self._block_index, parent=self)
Step.DeleteBtn.clicked.connect(lambda: self.removeActionStep(Step))
- self._actionWidgets.append(Step)
+ self._action_widgets.append(Step)
self.ActionsLayout.addWidget(Step)
def removeActionStep(
@@ -158,8 +162,8 @@ class ConditionalBlock(QGroupBox):
step: ActionStepFrame
):
- if step in self._actionWidgets:
- self._actionWidgets.remove(step)
+ if step in self._action_widgets:
+ self._action_widgets.remove(step)
self.ActionsLayout.removeWidget(step)
step.hide()
step.deleteLater()
@@ -174,19 +178,19 @@ class ConditionalBlock(QGroupBox):
self
):
- return list(self._conditionRows)
+ return list(self._condition_rows)
def getActionSteps(
self
):
- return list(self._actionWidgets)
+ return list(self._action_widgets)
def countActionSteps(
self
) -> int:
- return len(self._actionWidgets)
+ return len(self._action_widgets)
def toScript(
self
@@ -195,48 +199,48 @@ class ConditionalBlock(QGroupBox):
Generate Lua script lines for this conditional block.
"""
- blockType = self.getBlockType()
+ block_type = self.getBlockType()
lines = []
- if blockType in ("IF", "ELSE IF"):
- condTexts = [
- r.toScript() for r in self._conditionRows if r.toScript()
+ if block_type in ("IF", "ELSE IF"):
+ cond_texts = [
+ r.toScript() for r in self._condition_rows if r.toScript()
]
- if not condTexts:
- condTexts = ["true"]
- if len(condTexts) == 1:
- combined = condTexts[0]
+ if not cond_texts:
+ cond_texts = ["true"]
+ if len(cond_texts) == 1:
+ combined = cond_texts[0]
else:
parts = []
- for i, ct in enumerate(condTexts):
+ for i, ct in enumerate(cond_texts):
if i > 0:
- logic = self._conditionRows[i].getLogic() or "and"
+ logic = self._condition_rows[i].getLogic() or "and"
parts.append(f" {logic} ")
parts.append(f"({ct})")
combined = "".join(parts)
- if blockType == "IF":
+ if block_type == "IF":
lines.append(f"if {combined} then")
else:
lines.append(f"elseif {combined} then")
else:
lines.append("else")
- for step in self._actionWidgets:
- scriptLine = step.toScript()
- if scriptLine:
- lines.append(scriptLine)
+ for step in self._action_widgets:
+ script_line = step.toScript()
+ if script_line:
+ lines.append(script_line)
return lines
def refreshVarCombos(
self
):
- for row in self._conditionRows:
+ for row in self._condition_rows:
row.refreshVarCombos()
- for step in self._actionWidgets:
+ for step in self._action_widgets:
step.refreshVarCombos()
def setPrevBlockType(
self,
- prevType: str | None
+ prev_type: str | None
):
model = self.TypeCombo.model()
@@ -247,19 +251,19 @@ class ConditionalBlock(QGroupBox):
if idx < 0:
continue
item = model.item(idx)
- shouldEnable = prevType != "ELSE"
- item.setEnabled(shouldEnable)
- if prevType == "ELSE" and self.TypeCombo.currentData() in ("ELSE IF", "ELSE"):
+ should_enable = prev_type != "ELSE"
+ item.setEnabled(should_enable)
+ if prev_type == "ELSE" and self.TypeCombo.currentData() in ("ELSE IF", "ELSE"):
self.TypeCombo.setCurrentIndex(0)
@Slot(int)
def onTypeChanged(
self,
- _idx
+ idx: int
):
- isCond = self.TypeCombo.currentData() in ("IF", "ELSE IF")
- self.ConditionWidget.setVisible(isCond)
+ is_cond = self.TypeCombo.currentData() in ("IF", "ELSE IF")
+ self.ConditionWidget.setVisible(is_cond)
self.ActionLabel.setText(
- "执行步骤:" if isCond else "ELSE 执行步骤:"
+ "执行步骤:" if is_cond else "ELSE 执行步骤:"
)
diff --git a/src/gui/ALAutoScriptOrchDialog/_dialog.py b/src/gui/ALAutoScriptOrchDialog/_dialog.py
index 55d8213..188018c 100644
--- a/src/gui/ALAutoScriptOrchDialog/_dialog.py
+++ b/src/gui/ALAutoScriptOrchDialog/_dialog.py
@@ -35,7 +35,7 @@ class ALAutoScriptOrchDialog(QDialog):
super().__init__(parent)
self._blocks = []
- self._varMgr = VariableManager(self)
+ self._var_mgr = VariableManager(self)
self.setupUi()
self.connectSignals()
@@ -91,7 +91,7 @@ class ALAutoScriptOrchDialog(QDialog):
):
Block = ConditionalBlock(
- len(self._blocks), self._varMgr, parent=self
+ len(self._blocks), self._var_mgr, parent=self
)
Block.DeleteBlockBtn.clicked.connect(lambda: self.removeBlock(Block))
Block.TypeCombo.currentIndexChanged.connect(self.updateBlockTypeRestrictions)
diff --git a/src/gui/ALAutoScriptOrchDialog/_helpers.py b/src/gui/ALAutoScriptOrchDialog/_helpers.py
index d019d93..4000335 100644
--- a/src/gui/ALAutoScriptOrchDialog/_helpers.py
+++ b/src/gui/ALAutoScriptOrchDialog/_helpers.py
@@ -46,11 +46,11 @@ def getTypeOrder(
return [t for t, _ in VARTYPE_INFOS]
def getArithType(
- varType: str
+ var_type: str
) -> bool:
for t, a in VARTYPE_INFOS:
- if t == varType:
+ if t == var_type:
return a
def getPresetVars(
@@ -113,36 +113,36 @@ class _DateInputContainer(QWidget):
Layout = QHBoxLayout(self)
Layout.setContentsMargins(0, 0, 0, 0)
Layout.setSpacing(4)
- self._ModeCombo = QComboBox(self)
- self._ModeCombo.addItem("相对日期", "relative")
- self._ModeCombo.addItem("绝对日期", "absolute")
- self._ModeCombo.setFixedHeight(25)
- self._Stack = QStackedWidget(self)
- self._RelCombo = QComboBox(self)
+ self.ModeCombo = QComboBox(self)
+ self.ModeCombo.addItem("相对日期", "relative")
+ self.ModeCombo.addItem("绝对日期", "absolute")
+ self.ModeCombo.setFixedHeight(25)
+ self.Stack = QStackedWidget(self)
+ self.RelCombo = QComboBox(self)
for display, data in DATE_OPTIONS:
- self._RelCombo.addItem(display, data)
- self._RelCombo.setFixedHeight(25)
- self._Stack.addWidget(self._RelCombo)
- self._DateEdit = QDateEdit(self)
- self._DateEdit.setDisplayFormat("yyyy-MM-dd")
- self._DateEdit.setCalendarPopup(True)
- self._DateEdit.setFixedHeight(25)
- self._Stack.addWidget(self._DateEdit)
- self._ModeCombo.currentIndexChanged.connect(
- lambda i: self._Stack.setCurrentIndex(i)
+ self.RelCombo.addItem(display, data)
+ self.RelCombo.setFixedHeight(25)
+ self.Stack.addWidget(self.RelCombo)
+ self.DateEdit = QDateEdit(self)
+ self.DateEdit.setDisplayFormat("yyyy-MM-dd")
+ self.DateEdit.setCalendarPopup(True)
+ self.DateEdit.setFixedHeight(25)
+ self.Stack.addWidget(self.DateEdit)
+ self.ModeCombo.currentIndexChanged.connect(
+ lambda i: self.Stack.setCurrentIndex(i)
)
- Layout.addWidget(self._ModeCombo)
- Layout.addWidget(self._Stack)
+ Layout.addWidget(self.ModeCombo)
+ Layout.addWidget(self.Stack)
Layout.addStretch()
def getValue(
self
) -> str:
- mode = self._ModeCombo.currentData()
+ mode = self.ModeCombo.currentData()
if mode == "relative":
- return self._RelCombo.currentText()
- return self._DateEdit.date().toString("yyyy-MM-dd")
+ return self.RelCombo.currentText()
+ return self.DateEdit.date().toString("yyyy-MM-dd")
class _TimeInputContainer(QWidget):
@@ -153,19 +153,19 @@ class _TimeInputContainer(QWidget):
):
super().__init__(parent)
- self._TimeEdit = QTimeEdit(self)
- self._TimeEdit.setDisplayFormat("HH:mm")
- self._TimeEdit.setFixedHeight(25)
+ self.TimeEdit = QTimeEdit(self)
+ self.TimeEdit.setDisplayFormat("HH:mm")
+ self.TimeEdit.setFixedHeight(25)
Layout = QHBoxLayout(self)
Layout.setContentsMargins(0, 0, 0, 0)
- Layout.addWidget(self._TimeEdit)
+ Layout.addWidget(self.TimeEdit)
def getValue(
self
) -> str:
- return self._TimeEdit.time().toString("HH:mm")
+ return self.TimeEdit.time().toString("HH:mm")
class _DateOffsetContainer(QWidget):
@@ -176,19 +176,19 @@ class _DateOffsetContainer(QWidget):
):
super().__init__(parent)
- self._SpinBox = QSpinBox(self)
- self._SpinBox.setRange(0, 99999)
- self._SpinBox.setFixedHeight(25)
- self._UnitCombo = QComboBox(self)
+ self.SpinBox = QSpinBox(self)
+ self.SpinBox.setRange(0, 99999)
+ self.SpinBox.setFixedHeight(25)
+ self.UnitCombo = QComboBox(self)
for display, data in DATE_OFFSET_OPTIONS:
- self._UnitCombo.addItem(display, data)
- self._UnitCombo.setFixedHeight(25)
+ self.UnitCombo.addItem(display, data)
+ self.UnitCombo.setFixedHeight(25)
Layout = QHBoxLayout(self)
Layout.setContentsMargins(0, 0, 0, 0)
Layout.setSpacing(4)
- Layout.addWidget(self._SpinBox)
- Layout.addWidget(self._UnitCombo)
+ Layout.addWidget(self.SpinBox)
+ Layout.addWidget(self.UnitCombo)
Layout.addStretch()
def getValue(
@@ -201,8 +201,8 @@ class _DateOffsetContainer(QWidget):
self
) -> int:
- val = self._SpinBox.value()
- unit = self._UnitCombo.currentData()
+ val = self.SpinBox.value()
+ unit = self.UnitCombo.currentData()
if unit == "weeks":
return val*7
if unit == "months":
@@ -220,14 +220,14 @@ class _TimeOffsetContainer(QWidget):
):
super().__init__(parent)
- self._SpinBox = QSpinBox(self)
- self._SpinBox.setRange(0, 99999)
- self._SpinBox.setSuffix(" 小时")
- self._SpinBox.setFixedHeight(25)
+ self.SpinBox = QSpinBox(self)
+ self.SpinBox.setRange(0, 99999)
+ self.SpinBox.setSuffix(" 小时")
+ self.SpinBox.setFixedHeight(25)
Layout = QHBoxLayout(self)
Layout.setContentsMargins(0, 0, 0, 0)
- Layout.addWidget(self._SpinBox)
+ Layout.addWidget(self.SpinBox)
def getValue(
self
@@ -239,7 +239,7 @@ class _TimeOffsetContainer(QWidget):
self
) -> int:
- return self._SpinBox.value()
+ return self.SpinBox.value()
class VariableManager(QObject):
@@ -251,7 +251,7 @@ class VariableManager(QObject):
super().__init__(parent)
self._vars = []
- self._nameMap = {}
+ self._name_map = {}
self.initPresetVars()
@@ -262,21 +262,21 @@ class VariableManager(QObject):
for p in getPresetVars():
entry = {"name": p["name"], "type": p["type"], "display": p["display"]}
self._vars.append(entry)
- self._nameMap[p["name"]] = entry
+ self._name_map[p["name"]] = entry
def getInfoByName(
self,
name: str
):
- return self._nameMap.get(name.upper().strip())
+ return self._name_map.get(name.upper().strip())
def populateCombo(
self,
combo: QComboBox
):
- currentData = combo.currentData()
+ current_data = combo.currentData()
combo.blockSignals(True)
combo.clear()
for entry in self._vars:
@@ -284,10 +284,10 @@ class VariableManager(QObject):
entry["display"],
(entry["name"], entry["type"])
)
- if currentData:
+ if current_data:
for i in range(combo.count()):
d = combo.itemData(i)
- if d and d[0] == currentData[0]:
+ if d and d[0] == current_data[0]:
combo.setCurrentIndex(i)
break
combo.blockSignals(False)
@@ -299,40 +299,40 @@ def makeValueWidget(
) -> QWidget:
if var_type == "Int":
- w = QSpinBox(parent)
- w.setRange(-999999, 999999)
- w.setFixedHeight(25)
- w.setMinimumWidth(100)
- return w
+ Widget = QSpinBox(parent)
+ Widget.setRange(-999999, 999999)
+ Widget.setFixedHeight(25)
+ Widget.setMinimumWidth(100)
+ return Widget
if var_type == "Float":
- w = QDoubleSpinBox(parent)
- w.setRange(-999999.0, 999999.0)
- w.setDecimals(2)
- w.setFixedHeight(25)
- w.setMinimumWidth(100)
- return w
+ Widget = QDoubleSpinBox(parent)
+ Widget.setRange(-999999.0, 999999.0)
+ Widget.setDecimals(2)
+ Widget.setFixedHeight(25)
+ Widget.setMinimumWidth(100)
+ return Widget
if var_type == "String":
- w = QLineEdit(parent)
- w.setPlaceholderText("输入值")
- w.setFixedHeight(25)
- w.setMinimumWidth(120)
- return w
+ Widget = QLineEdit(parent)
+ Widget.setPlaceholderText("输入值")
+ Widget.setFixedHeight(25)
+ Widget.setMinimumWidth(120)
+ return Widget
if var_type == "Boolean":
- w = QComboBox(parent)
- w.addItem("是 (true)", "true")
- w.addItem("否 (false)", "false")
- w.setFixedHeight(25)
- w.setMinimumWidth(100)
- return w
+ Widget = QComboBox(parent)
+ Widget.addItem("是 (true)", "true")
+ Widget.addItem("否 (false)", "false")
+ Widget.setFixedHeight(25)
+ Widget.setMinimumWidth(100)
+ return Widget
if var_type == "Date":
return _DateInputContainer(parent)
if var_type == "Time":
return _TimeInputContainer(parent)
- w = QLineEdit(parent)
- w.setPlaceholderText("输入值")
- w.setFixedHeight(25)
- w.setMinimumWidth(120)
- return w
+ Widget = QLineEdit(parent)
+ Widget.setPlaceholderText("输入值")
+ Widget.setFixedHeight(25)
+ Widget.setMinimumWidth(120)
+ return Widget
def makeOffsetWidget(
var_type: str,
@@ -340,35 +340,35 @@ def makeOffsetWidget(
) -> QWidget:
if var_type == "Int":
- w = QSpinBox(parent)
- w.setRange(-999999, 999999)
- w.setFixedHeight(25)
- w.setMinimumWidth(100)
- return w
+ Widget = QSpinBox(parent)
+ Widget.setRange(-999999, 999999)
+ Widget.setFixedHeight(25)
+ Widget.setMinimumWidth(100)
+ return Widget
if var_type == "Float":
- w = QDoubleSpinBox(parent)
- w.setRange(-999999.0, 999999.0)
- w.setDecimals(2)
- w.setFixedHeight(25)
- w.setMinimumWidth(100)
- return w
+ Widget = QDoubleSpinBox(parent)
+ Widget.setRange(-999999.0, 999999.0)
+ Widget.setDecimals(2)
+ Widget.setFixedHeight(25)
+ Widget.setMinimumWidth(100)
+ return Widget
if var_type == "Date":
return _DateOffsetContainer(parent)
if var_type == "Time":
return _TimeOffsetContainer(parent)
- w = QLabel("(不支持该操作)", parent)
- w.setFixedHeight(25)
- return w
+ Widget = QLabel("(不支持该操作)", parent)
+ Widget.setFixedHeight(25)
+ return Widget
def makeVarRefCombo(
parent: QWidget = None
) -> QComboBox:
- Cb = QComboBox(parent)
- Cb.setFixedHeight(25)
- Cb.setMinimumWidth(120)
- Cb.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
- return Cb
+ ComboBox = QComboBox(parent)
+ ComboBox.setFixedHeight(25)
+ ComboBox.setMinimumWidth(120)
+ ComboBox.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ return ComboBox
def makeComboWidget(
items,
@@ -376,12 +376,12 @@ def makeComboWidget(
parent: QWidget = None
) -> QComboBox:
- Cb = QComboBox(parent)
+ ComboBox = QComboBox(parent)
for display, data in items:
- Cb.addItem(display, data)
- Cb.setFixedHeight(25)
- Cb.setMinimumWidth(min_width)
- return Cb
+ ComboBox.addItem(display, data)
+ ComboBox.setFixedHeight(25)
+ ComboBox.setMinimumWidth(min_width)
+ return ComboBox
def makeLabel(
text: str,
@@ -389,30 +389,30 @@ def makeLabel(
width: int = None
) -> QLabel:
- Lbl = QLabel(text, parent)
- Lbl.setFixedHeight(25)
+ Label = QLabel(text, parent)
+ Label.setFixedHeight(25)
if width:
- Lbl.setFixedWidth(width)
- return Lbl
+ Label.setFixedWidth(width)
+ return Label
def getValueFromWidget(
- w: QWidget
+ widget: QWidget
) -> str:
- if hasattr(w, "getValue"):
- return w.getValue()
- if isinstance(w, QTimeEdit):
- return w.time().toString("HH:mm")
- if isinstance(w, QDateEdit):
- return w.date().toString("yyyy-MM-dd")
- if isinstance(w, QComboBox):
- return w.currentData() or w.currentText()
- if isinstance(w, QSpinBox):
- return str(w.value())
- if isinstance(w, QDoubleSpinBox):
- return str(w.value())
- if isinstance(w, QLineEdit):
- return w.text()
+ if hasattr(widget, "getValue"):
+ return widget.getValue()
+ if isinstance(widget, QTimeEdit):
+ return widget.time().toString("HH:mm")
+ if isinstance(widget, QDateEdit):
+ return widget.date().toString("yyyy-MM-dd")
+ if isinstance(widget, QComboBox):
+ return widget.currentData() or widget.currentText()
+ if isinstance(widget, QSpinBox):
+ return str(widget.value())
+ if isinstance(widget, QDoubleSpinBox):
+ return str(widget.value())
+ if isinstance(widget, QLineEdit):
+ return widget.text()
return ""
def encodeValueStr(
diff --git a/src/gui/ALAutoScriptOrchDialog/_widgets.py b/src/gui/ALAutoScriptOrchDialog/_widgets.py
index 5b7b7bd..f6e1c77 100644
--- a/src/gui/ALAutoScriptOrchDialog/_widgets.py
+++ b/src/gui/ALAutoScriptOrchDialog/_widgets.py
@@ -42,18 +42,18 @@ class ConditionRowFrame(QFrame):
def __init__(
self,
- varMgr,
- parentBlockIndex: int = 0,
- isFirst: bool = False,
+ var_mgr,
+ parent_block_index: int = 0,
+ is_first: bool = False,
parent = None
):
super().__init__(parent)
- self._varMgr = varMgr
- self._blockIndex = parentBlockIndex
- self._isFirst = isFirst
- self._isBoolMode = False
- self._rawRhsExpr = ""
+ self._var_mgr = var_mgr
+ self._block_index = parent_block_index
+ self._is_first = is_first
+ self._is_bool_mode = False
+ self._raw_rhs_expr = ""
self.setupUi()
self.connectSignals()
@@ -69,7 +69,7 @@ class ConditionRowFrame(QFrame):
Layout = QHBoxLayout(self)
Layout.setContentsMargins(2, 2, 2, 2)
Layout.setSpacing(4)
- if self._isFirst:
+ if self._is_first:
self.LogicCombo = None
else:
self.LogicCombo = makeComboWidget(LOGIC_OPTIONS, min_width=110, parent=self)
@@ -82,11 +82,11 @@ class ConditionRowFrame(QFrame):
Layout.addWidget(self.LeftVarCombo)
self.OpCombo = makeComboWidget(COMPARE_OPTIONS, min_width=80, parent=self)
Layout.addWidget(self.OpCombo)
- self._CompTypeCombo = makeComboWidget([
+ self.CompTypeCombo = makeComboWidget([
("特定值", "literal"),
("变量", "variable"),
], min_width=70, parent=self)
- Layout.addWidget(self._CompTypeCombo)
+ Layout.addWidget(self.CompTypeCombo)
self.RhsStack = QStackedWidget(self)
self.RhsStack.setFixedHeight(25)
self.initLiteralStack()
@@ -94,7 +94,7 @@ class ConditionRowFrame(QFrame):
self.RhsStack.addWidget(self.RhsVarCombo)
self.RhsStack.setCurrentIndex(0)
Layout.addWidget(self.RhsStack)
- if not self._isFirst:
+ if not self._is_first:
self.DeleteBtn = QPushButton("×", self)
self.DeleteBtn.setFixedSize(25, 25)
self.DeleteBtn.setStyleSheet("color: red; font-weight: bold;")
@@ -108,21 +108,21 @@ class ConditionRowFrame(QFrame):
self
):
- wasBool = self._isBoolMode
- boolName = None
- if wasBool:
+ was_bool = self._is_bool_mode
+ bool_name = None
+ if was_bool:
data = self.LeftVarCombo.currentData()
if data:
- boolName = data[0]
- self._varMgr.populateCombo(self.LeftVarCombo)
+ bool_name = data[0]
+ self._var_mgr.populateCombo(self.LeftVarCombo)
# Append boolean literal sentinels at the end
self.LeftVarCombo.insertSeparator(self.LeftVarCombo.count())
self.LeftVarCombo.addItem("true", ("true", "Boolean"))
self.LeftVarCombo.addItem("false", ("false", "Boolean"))
- if wasBool and boolName:
+ if was_bool and bool_name:
for ci in range(self.LeftVarCombo.count()):
d = self.LeftVarCombo.itemData(ci)
- if d and d[0] == boolName:
+ if d and d[0] == bool_name:
self.LeftVarCombo.setCurrentIndex(ci)
break
@@ -130,7 +130,7 @@ class ConditionRowFrame(QFrame):
self
):
- self._varMgr.populateCombo(self.RhsVarCombo)
+ self._var_mgr.populateCombo(self.RhsVarCombo)
def initLiteralStack(
self
@@ -138,12 +138,12 @@ class ConditionRowFrame(QFrame):
self.LiteralStack = QStackedWidget(self)
self.LiteralStack.setFixedHeight(25)
- self._literalWidgets = {}
+ self.literal_widgets = {}
for vt in getTypeOrder():
W = makeValueWidget(vt, self.LiteralStack)
- self._literalWidgets[vt] = W
+ self.literal_widgets[vt] = W
self.LiteralStack.addWidget(W)
- self.LiteralStack.setCurrentWidget(self._literalWidgets.get("String"))
+ self.LiteralStack.setCurrentWidget(self.literal_widgets.get("String"))
self.RhsStack.addWidget(self.LiteralStack)
def connectSignals(
@@ -151,7 +151,7 @@ class ConditionRowFrame(QFrame):
):
self.LeftVarCombo.currentIndexChanged.connect(self.onLeftVarChanged)
- self._CompTypeCombo.currentIndexChanged.connect(self.onCompTypeChanged)
+ self.CompTypeCombo.currentIndexChanged.connect(self.onCompTypeChanged)
def getLogic(
self
@@ -164,16 +164,16 @@ class ConditionRowFrame(QFrame):
vartype: str
):
- if vartype not in self._literalWidgets:
+ if vartype not in self.literal_widgets:
vartype = "String"
- self.LiteralStack.setCurrentWidget(self._literalWidgets[vartype])
+ self.LiteralStack.setCurrentWidget(self.literal_widgets[vartype])
def toScript(
self
) -> str:
data = self.LeftVarCombo.currentData()
- if self._isBoolMode and data:
+ if self._is_bool_mode and data:
return data[0]
if not data:
return ""
@@ -183,28 +183,28 @@ class ConditionRowFrame(QFrame):
name = "datenow()"
elif name == "CURRENT_TIME":
name = "timenow()"
- opSym = self.OpCombo.currentData()
- if self._rawRhsExpr:
- return f"{name} {opSym} {self._rawRhsExpr}"
- isVarRef = (self._CompTypeCombo.currentData() == "variable")
- if isVarRef:
+ op_sym = self.OpCombo.currentData()
+ if self._raw_rhs_expr:
+ return f"{name} {op_sym} {self._raw_rhs_expr}"
+ is_var_ref = (self.CompTypeCombo.currentData() == "variable")
+ if is_var_ref:
rd = self.RhsVarCombo.currentData()
if rd:
- rhsName = rd[0]
- if rhsName == "CURRENT_DATE":
- rhsName = "datenow()"
- elif rhsName == "CURRENT_TIME":
- rhsName = "timenow()"
- return f"{name} {opSym} {rhsName}"
- rhsText = self.RhsVarCombo.currentText().strip()
- if rhsText:
- return f"{name} {opSym} {rhsText}"
+ rhs_name = rd[0]
+ if rhs_name == "CURRENT_DATE":
+ rhs_name = "datenow()"
+ elif rhs_name == "CURRENT_TIME":
+ rhs_name = "timenow()"
+ return f"{name} {op_sym} {rhs_name}"
+ rhs_text = self.RhsVarCombo.currentText().strip()
+ if rhs_text:
+ return f"{name} {op_sym} {rhs_text}"
return ""
- w = self._literalWidgets.get(vartype)
+ w = self.literal_widgets.get(vartype)
if w:
- rawVal = getValueFromWidget(w)
- encoded = encodeValueStr(rawVal, vartype)
- return f"{name} {opSym} {encoded}"
+ raw_val = getValueFromWidget(w)
+ encoded = encodeValueStr(raw_val, vartype)
+ return f"{name} {op_sym} {encoded}"
return ""
def refreshVarCombos(
@@ -220,19 +220,19 @@ class ConditionRowFrame(QFrame):
idx
):
- self._rawRhsExpr = ""
+ self._raw_rhs_expr = ""
if idx < 0:
return
data = self.LeftVarCombo.itemData(idx)
if not data:
return
name, vartype = data
- isBool = name in ("true", "false")
- self._isBoolMode = isBool
- self.OpCombo.setVisible(not isBool)
- self._CompTypeCombo.setVisible(not isBool)
- self.RhsStack.setVisible(not isBool)
- if not isBool:
+ is_bool = name in ("true", "false")
+ self._is_bool_mode = is_bool
+ self.OpCombo.setVisible(not is_bool)
+ self.CompTypeCombo.setVisible(not is_bool)
+ self.RhsStack.setVisible(not is_bool)
+ if not is_bool:
self.updateRHSLiteralWidget(vartype)
@Slot(int)
@@ -241,8 +241,8 @@ class ConditionRowFrame(QFrame):
idx
):
- self._rawRhsExpr = ""
- isVar = (self._CompTypeCombo.currentData() == "variable")
+ self._raw_rhs_expr = ""
+ isVar = (self.CompTypeCombo.currentData() == "variable")
self.RhsStack.setCurrentIndex(1 if isVar else 0)
if isVar:
self.populateRHSVarCombo()
@@ -252,15 +252,15 @@ class ActionStepFrame(QFrame):
def __init__(
self,
- varMgr,
- parentBlockIndex: int = 0,
+ var_mgr,
+ parent_block_index: int = 0,
parent = None
):
super().__init__(parent)
- self._varMgr = varMgr
- self._blockIndex = parentBlockIndex
- self._currentTargetType = "String"
+ self._var_mgr = var_mgr
+ self._block_index = parent_block_index
+ self._current_target_type = "String"
self.setupUi()
self.connectSignals()
@@ -312,7 +312,7 @@ class ActionStepFrame(QFrame):
for p in getPresetVars():
if p["name"] in ("CURRENT_TIME", "CURRENT_DATE"):
continue
- info = self._varMgr.getInfoByName(p["name"])
+ info = self._var_mgr.getInfoByName(p["name"])
if info:
self.TargetCombo.addItem(
info["display"],
@@ -324,18 +324,18 @@ class ActionStepFrame(QFrame):
self
):
- self._literalWidgets = {}
- self._offsetWidgets = {}
+ self.literal_widgets = {}
+ self.offset_widgets = {}
for vt in getTypeOrder():
- self._literalWidgets[vt] = makeValueWidget(vt, self.ValueStack)
- self.ValueStack.addWidget(self._literalWidgets[vt])
+ self.literal_widgets[vt] = makeValueWidget(vt, self.ValueStack)
+ self.ValueStack.addWidget(self.literal_widgets[vt])
if getArithType(vt):
- self._offsetWidgets[vt] = makeOffsetWidget(vt, self.ValueStack)
- self.ValueStack.addWidget(self._offsetWidgets[vt])
+ self.offset_widgets[vt] = makeOffsetWidget(vt, self.ValueStack)
+ self.ValueStack.addWidget(self.offset_widgets[vt])
else:
Lbl = QLabel("(不支持该操作)", self.ValueStack)
Lbl.setFixedHeight(25)
- self._offsetWidgets[vt] = Lbl
+ self.offset_widgets[vt] = Lbl
self.ValueStack.addWidget(Lbl)
def connectSignals(
@@ -358,14 +358,14 @@ class ActionStepFrame(QFrame):
):
op = self.OpTypeCombo.currentData()
- isArith = (op in ("add", "sub"))
- actualType = self._currentTargetType
- if isArith and actualType in self._offsetWidgets:
- self.ValueStack.setCurrentWidget(self._offsetWidgets[actualType])
- elif actualType in self._literalWidgets:
- self.ValueStack.setCurrentWidget(self._literalWidgets[actualType])
+ is_arith = (op in ("add", "sub"))
+ actual_type = self._current_target_type
+ if is_arith and actual_type in self.offset_widgets:
+ self.ValueStack.setCurrentWidget(self.offset_widgets[actual_type])
+ elif actual_type in self.literal_widgets:
+ self.ValueStack.setCurrentWidget(self.literal_widgets[actual_type])
else:
- self.ValueStack.setCurrentWidget(self._literalWidgets.get("String"))
+ self.ValueStack.setCurrentWidget(self.literal_widgets.get("String"))
def toScript(
self
@@ -380,10 +380,10 @@ class ActionStepFrame(QFrame):
return " -- pass"
if not target:
return ""
- rawVal = self.getValueRaw()
- vartype = self._currentTargetType
+ raw_val = self.getValueRaw()
+ vartype = self._current_target_type
if op == "set":
- encoded = encodeValueStr(rawVal, vartype)
+ encoded = encodeValueStr(raw_val, vartype)
return f" {target} = {encoded}"
elif op == "add":
if vartype == "Date" and hasattr(self.ValueStack.currentWidget(), "getOffsetDays"):
@@ -392,7 +392,7 @@ class ActionStepFrame(QFrame):
if vartype == "Time" and hasattr(self.ValueStack.currentWidget(), "getOffsetHours"):
hours = self.ValueStack.currentWidget().getOffsetHours()
return f" {target} = timeadd({target}, {hours})"
- return f" {target} = {target} + {rawVal}"
+ return f" {target} = {target} + {raw_val}"
elif op == "sub":
if vartype == "Date" and hasattr(self.ValueStack.currentWidget(), "getOffsetDays"):
days = self.ValueStack.currentWidget().getOffsetDays()
@@ -400,7 +400,7 @@ class ActionStepFrame(QFrame):
if vartype == "Time" and hasattr(self.ValueStack.currentWidget(), "getOffsetHours"):
hours = self.ValueStack.currentWidget().getOffsetHours()
return f" {target} = timeadd({target}, -{hours})"
- return f" {target} = {target} - {rawVal}"
+ return f" {target} = {target} - {raw_val}"
return ""
def getValueRaw(
@@ -419,15 +419,15 @@ class ActionStepFrame(QFrame):
self
):
- currentData = self.TargetCombo.currentData()
+ current_data = self.TargetCombo.currentData()
self.populateTargetCombo()
- if currentData:
+ if current_data:
for i in range(self.TargetCombo.count()):
d = self.TargetCombo.itemData(i)
- if d and d[0] == currentData[0]:
+ if d and d[0] == current_data[0]:
self.TargetCombo.setCurrentIndex(i)
break
- self._varMgr.populateCombo(self.ExistingVarCombo)
+ self._var_mgr.populateCombo(self.ExistingVarCombo)
@Slot(int)
def onTargetChanged(
@@ -441,7 +441,7 @@ class ActionStepFrame(QFrame):
if not data:
return
_, vartype = data
- self._currentTargetType = vartype
+ self._current_target_type = vartype
self.updateValueWidget()
self.onValueSrcChanged(self.ValueSrcCombo.currentIndex())
@@ -459,10 +459,10 @@ class ActionStepFrame(QFrame):
idx
):
- isVar = (self.ValueSrcCombo.currentData() == "variable")
- self.ValueStack.setVisible(not isVar)
- self.ExistingVarCombo.setVisible(isVar)
- if isVar:
- self._varMgr.populateCombo(self.ExistingVarCombo)
+ is_var = (self.ValueSrcCombo.currentData() == "variable")
+ self.ValueStack.setVisible(not is_var)
+ self.ExistingVarCombo.setVisible(is_var)
+ if is_var:
+ self._var_mgr.populateCombo(self.ExistingVarCombo)
else:
self.updateValueWidget()
diff --git a/src/gui/ALBulletinDialog.py b/src/gui/ALBulletinDialog.py
new file mode 100644
index 0000000..ef66430
--- /dev/null
+++ b/src/gui/ALBulletinDialog.py
@@ -0,0 +1,394 @@
+# -*- coding: utf-8 -*-
+"""
+Copyright (c) 2026 KenanZhu.
+All rights reserved.
+
+This software is provided "as is", without any warranty of any kind.
+You may use, modify, and distribute this file under the terms of the MIT License.
+See the LICENSE file for details.
+"""
+import requests
+
+from datetime import datetime, timedelta
+from typing import Any
+
+from PySide6.QtCore import (
+ Qt,
+ QThread,
+ Signal,
+ Slot,
+ QTimer
+)
+from PySide6.QtGui import (
+ QFont
+)
+from PySide6.QtWidgets import (
+ QDialog,
+ QHBoxLayout,
+ QLabel,
+ QListWidgetItem,
+ QMessageBox,
+ QVBoxLayout,
+ QWidget
+)
+
+from gui.ALStatusLabel import ALStatusLabel
+from gui.resources.ui.Ui_ALBulletinDialog import Ui_ALBulletinDialog
+from managers.bulletin.BulletinManager import instance as bulletinInstance
+
+
+class ALBulletinFetchWorker(QThread):
+ """
+ Worker thread for fetching bulletins from the server.
+ """
+
+ fetchWorkerIsFinished = Signal(dict)
+ fetchWorkerFinishedWithError = Signal(str)
+
+ def __init__(
+ self,
+ parent=None,
+ request_url: str = "",
+ params: dict = None
+ ):
+
+ super().__init__(parent)
+ self.__request_url = request_url.rstrip("/") if request_url else ""
+ self.__params = params or {}
+
+ def run(
+ self
+ ):
+
+ try:
+ response = requests.get(
+ self.__request_url, params=self.__params, timeout=5
+ )
+ response.raise_for_status()
+ r = response.json()
+ if r.get("code") == 200:
+ data = r.get("data", {})
+ self.fetchWorkerIsFinished.emit(data)
+ else:
+ raise Exception(
+ f"服务器返回错误: [{r.get('code', '未知代码')}] {r.get('msg', '未知错误')}"
+ )
+ except requests.RequestException as e:
+ self.fetchWorkerFinishedWithError.emit(f"获取公告数据时发生网络错误: \n{e}")
+ except Exception as e:
+ self.fetchWorkerFinishedWithError.emit(f"获取公告数据时发生未知错误: \n{e}")
+
+
+class ALBulletinItemWidget(QWidget):
+ """
+ Single bulletin item widget for the bulletin list.
+ """
+
+ def __init__(
+ self,
+ parent=None,
+ bulletin: dict[str, Any] = None
+ ):
+
+ super().__init__(parent)
+ self.__bulletin = bulletin.copy() if bulletin else {}
+ self.modifyUi()
+
+ def modifyUi(
+ self
+ ):
+
+ self.ItemWidgetLayout = QVBoxLayout()
+ self.ItemWidgetLayout.setSpacing(10)
+ self.ItemWidgetLayout.setContentsMargins(10, 5, 10, 5)
+ self.BulletinTitleLayout = QHBoxLayout()
+ self.BulletinTitleLabel = QLabel(self.__bulletin.get("title", "无标题"))
+ 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
+ ):
+
+ 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):
+ bulletin["isNew"] = False
+ widget = self.BulletinListWidget.itemWidget(item)
+ if widget:
+ widget.markAsRead()
+ item.setData(Qt.UserRole, bulletin)
+ self.__bulletin_mgr.markBulletinAsRead(bulletin["id"])
+ self.showBulletin(bulletin)
diff --git a/src/gui/ALBulletinPoller.py b/src/gui/ALBulletinPoller.py
new file mode 100644
index 0000000..5a328c3
--- /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.ALBulletinDialog import ALBulletinFetchWorker
+from managers.bulletin.BulletinManager import instance as bulletinInstance
+
+
+class ALBulletinPoller(QObject):
+ """
+ Background bulletin poller.
+
+ Owns the periodic poll timer and the ALBulletinFetchWorker
+ lifecycle. Emits signals when new bulletins are detected so
+ the owner can show tray notifications without knowing the
+ fetch / merge details.
+ """
+
+ newBulletinsDetected = Signal(int)
+
+ def __init__(
+ self,
+ parent=None
+ ):
+
+ super().__init__(parent)
+ self.__timer = QTimer(self)
+ self.__timer.timeout.connect(self.__poll)
+ self.__worker = None
+ self.__dialog_open = False
+ self.__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(2000)
+ 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(2000)
+ 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:
+ 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(2000)
+ worker.deleteLater()
+ self.__worker = None
+ if not self.__stopped:
+ interval_ms = self.__mgr.syncInterval()*60*1000
+ self.__timer.start(interval_ms)
diff --git a/src/gui/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..6de44dc 100644
--- a/src/gui/ALMainWindow.py
+++ b/src/gui/ALMainWindow.py
@@ -32,6 +32,8 @@ from PySide6.QtWidgets import (
from base.MsgBase import MsgBase
from gui.ALAboutDialog import ALAboutDialog
+from gui.ALBulletinDialog import ALBulletinDialog
+from gui.ALBulletinPoller import ALBulletinPoller
from gui.ALConfigWidget import ALConfigWidget
from gui.ALSettingsWidget import ALSettingsWidget
from gui.ALMainWorkers import (
@@ -41,6 +43,7 @@ from gui.ALMainWorkers import (
from gui.ALTimerTaskManageWidget import ALTimerTaskManageWidget
from gui.resources import ALResource
from gui.resources.ui.Ui_ALMainWindow import Ui_ALMainWindow
+from managers.bulletin.BulletinManager import instance as bulletinInstance
from managers.config.ConfigUtils import ConfigUtils
@@ -59,9 +62,11 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
QMainWindow.__init__(self)
self.__timer_task_queue = queue.Queue()
self.__config_paths = ConfigUtils.getAutomationConfigPaths()
- self.__alTimerTaskManageWidget = None
- self.__alConfigWidget = None
- self.__alSettingsWidget = None
+ self.__ALTimerTaskManageWidget = None
+ self.__ALConfigWidget = None
+ self.__ALSettingsWidget = None
+ self.__ALBulletinDialog = None
+ self.__bulletin_poller = ALBulletinPoller(self)
self.__auto_lib_thread = None
self.__current_timer_task_thread = None
self.__is_running_timer_task = False
@@ -72,6 +77,11 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.connectSignals()
self.startMsgPolling()
self.startTimerTaskPolling()
+
+ self.__bulletin_poller.start()
+ if bulletinInstance().autoFetch():
+ QTimer.singleShot(1000, self.__bulletin_poller.fetchNow)
+
self._showLog("主窗口初始化完成")
def modifyUi(
@@ -84,26 +94,28 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.ManualAction.triggered.connect(self.onManualActionTriggered)
self.AboutAction.triggered.connect(self.onAboutActionTriggered)
self.SettingsAction.triggered.connect(self.onSettingsActionTriggered)
+ if hasattr(self, 'BulletinAction'):
+ self.BulletinAction.triggered.connect(self.onBulletinActionTriggered)
# initialize timer task widget, but not show it
try:
- self.__alTimerTaskManageWidget = ALTimerTaskManageWidget(self)
+ self.__ALTimerTaskManageWidget = ALTimerTaskManageWidget(self)
except Exception as e:
QMessageBox.critical(
self,
"错误 - AutoLibrary",
f"初始化定时任务功能失败: \n{e}"
)
- self.__alTimerTaskManageWidget = None
+ self.__ALTimerTaskManageWidget = None
self.TimerTaskManageWidgetButton.setEnabled(False)
self.TimerTaskManageWidgetButton.setToolTip("定时任务功能初始化失败, 请检查配置文件。")
return
- self.timerTaskIsRunning.connect(self.__alTimerTaskManageWidget.onTimerTaskIsRunning)
- self.timerTaskIsExecuted.connect(self.__alTimerTaskManageWidget.onTimerTaskIsExecuted)
- self.timerTaskIsError.connect(self.__alTimerTaskManageWidget.onTimerTaskIsError)
- self.__alTimerTaskManageWidget.timerTaskIsReady.connect(self.onTimerTaskIsReady)
- self.__alTimerTaskManageWidget.timerTaskManageWidgetIsClosed.connect(self.onTimerTaskManageWidgetClosed)
- self.__alTimerTaskManageWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint)
+ self.timerTaskIsRunning.connect(self.__ALTimerTaskManageWidget.onTimerTaskIsRunning)
+ self.timerTaskIsExecuted.connect(self.__ALTimerTaskManageWidget.onTimerTaskIsExecuted)
+ self.timerTaskIsError.connect(self.__ALTimerTaskManageWidget.onTimerTaskIsError)
+ self.__ALTimerTaskManageWidget.timerTaskIsReady.connect(self.onTimerTaskIsReady)
+ self.__ALTimerTaskManageWidget.timerTaskManageWidgetIsClosed.connect(self.onTimerTaskManageWidgetClosed)
+ self.__ALTimerTaskManageWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint)
def onAboutActionTriggered(
self
@@ -116,8 +128,8 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self
):
- Url = QUrl("https://www.autolibrary.kenanzhu.com/manuals")
- QDesktopServices.openUrl(Url)
+ url = QUrl("https://www.autolibrary.kenanzhu.com/manuals")
+ QDesktopServices.openUrl(url)
def setupTray(
self
@@ -131,12 +143,13 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.TrayMenu = QMenu()
self.TrayMenu.addAction("显示主窗口", self.showNormal)
self.TrayMenu.addAction("显示定时窗口", self.onTimerTaskManageWidgetButtonClicked)
+ self.TrayMenu.addAction("公告栏", self.onBulletinActionTriggered)
self.TrayMenu.addAction("最小化到托盘", self.hideToTray)
self.TrayMenu.addSeparator()
self.TrayMenu.addAction("退出", self.close)
self.TrayIcon.setContextMenu(self.TrayMenu)
-
self.TrayIcon.activated.connect(self.onTrayIconActivated)
+ self.TrayIcon.messageClicked.connect(self.onBulletinActionTriggered)
self.TrayIcon.show()
def hideToTray(
@@ -169,6 +182,9 @@ 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,
@@ -186,15 +202,20 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
if self.__is_running_timer_task:
self.__current_timer_task_thread.wait(2000)
self.__current_timer_task_thread.deleteLater()
- if self.__alTimerTaskManageWidget:
- self.__alTimerTaskManageWidget.close()
- self.__alTimerTaskManageWidget.deleteLater()
- if self.__alConfigWidget:
- self.__alConfigWidget.close()
+ if self.__ALTimerTaskManageWidget:
+ self.__ALTimerTaskManageWidget.close()
+ self.__ALTimerTaskManageWidget.deleteLater()
+ if self.__ALConfigWidget:
+ self.__ALConfigWidget.close()
# the config widget is already deleted in the 'self.onConfigWidgetClosed'
- if self.__alSettingsWidget:
- self.__alSettingsWidget.close()
+ if self.__ALSettingsWidget:
+ self.__ALSettingsWidget.close()
# the settings widget is already deleted in the 'self.onSettingsWidgetClosed'
+ if self.__ALBulletinDialog:
+ self.__ALBulletinDialog.close()
+ # the bulletin dialog is already deleted in the 'self.onBulletinDialogClosed'
+ if self.__bulletin_poller:
+ self.__bulletin_poller.stop()
self._showLog("主窗口关闭")
QMainWindow.closeEvent(self, event)
@@ -299,10 +320,10 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self
):
- if self.__alConfigWidget:
- self.__alConfigWidget.configWidgetIsClosed.disconnect(self.onConfigWidgetClosed)
- self.__alConfigWidget.deleteLater()
- self.__alConfigWidget = None
+ if self.__ALConfigWidget:
+ self.__ALConfigWidget.configWidgetIsClosed.disconnect(self.onConfigWidgetClosed)
+ self.__ALConfigWidget.deleteLater()
+ self.__ALConfigWidget = None
self.__config_paths = ConfigUtils.getAutomationConfigPaths()
self.setControlButtons(True, None, None)
self._showLog("配置窗口已关闭,配置文件路径已更新")
@@ -312,23 +333,63 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self
):
- if self.__alSettingsWidget:
- self.__alSettingsWidget.settingsWidgetIsClosed.disconnect(self.onSettingsWidgetClosed)
- self.__alSettingsWidget.deleteLater()
- self.__alSettingsWidget = None
+ if self.__ALSettingsWidget:
+ self.__ALSettingsWidget.settingsWidgetIsClosed.disconnect(self.onSettingsWidgetClosed)
+ self.__ALSettingsWidget.deleteLater()
+ self.__ALSettingsWidget = None
self.SettingsAction.setEnabled(True)
+ @Slot()
+ def onBulletinActionTriggered(
+ self
+ ):
+
+ self.__bulletin_poller.setDialogOpen(True)
+ if self.__ALBulletinDialog is None:
+ self.__ALBulletinDialog = ALBulletinDialog(self)
+ self.__ALBulletinDialog.finished.connect(self.onBulletinDialogClosed)
+ self.__ALBulletinDialog.show()
+ self.__ALBulletinDialog.raise_()
+ self.__ALBulletinDialog.activateWindow()
+ self._showLog("打开公告栏窗口")
+
+ @Slot()
+ def onBulletinDialogClosed(
+ self
+ ):
+
+ if self.__ALBulletinDialog:
+ self.__ALBulletinDialog.finished.disconnect(self.onBulletinDialogClosed)
+ self.__ALBulletinDialog.deleteLater()
+ self.__ALBulletinDialog = None
+ self.__bulletin_poller.setDialogOpen(False)
+
+ @Slot(int)
+ def _onBulletinPollerNewBulletins(
+ self,
+ count: int
+ ):
+
+ if not hasattr(self, 'TrayIcon'):
+ return
+ self.TrayIcon.showMessage(
+ "公告栏 - AutoLibrary",
+ f"有 {count} 条新公告,点击查看详情。",
+ QSystemTrayIcon.MessageIcon.Information,
+ 3000
+ )
+
@Slot()
def onSettingsActionTriggered(
self
):
- if self.__alSettingsWidget is None:
- self.__alSettingsWidget = ALSettingsWidget(self)
- self.__alSettingsWidget.settingsWidgetIsClosed.connect(self.onSettingsWidgetClosed)
- self.__alSettingsWidget.show()
- self.__alSettingsWidget.raise_()
- self.__alSettingsWidget.activateWindow()
+ if self.__ALSettingsWidget is None:
+ self.__ALSettingsWidget = ALSettingsWidget(self)
+ self.__ALSettingsWidget.settingsWidgetIsClosed.connect(self.onSettingsWidgetClosed)
+ self.__ALSettingsWidget.show()
+ self.__ALSettingsWidget.raise_()
+ self.__ALSettingsWidget.activateWindow()
self.SettingsAction.setEnabled(False)
self._showLog("打开全局设置窗口")
@@ -374,9 +435,9 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self
):
- self.__alTimerTaskManageWidget.show()
- self.__alTimerTaskManageWidget.raise_()
- self.__alTimerTaskManageWidget.activateWindow()
+ self.__ALTimerTaskManageWidget.show()
+ self.__ALTimerTaskManageWidget.raise_()
+ self.__ALTimerTaskManageWidget.activateWindow()
self.TimerTaskManageWidgetButton.setEnabled(False)
self._showLog("打开定时任务管理窗口")
@@ -385,12 +446,12 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self
):
- if self.__alConfigWidget is None:
- self.__alConfigWidget = ALConfigWidget(self)
- self.__alConfigWidget.configWidgetIsClosed.connect(self.onConfigWidgetClosed)
- self.__alConfigWidget.show()
- self.__alConfigWidget.raise_()
- self.__alConfigWidget.activateWindow()
+ if self.__ALConfigWidget is None:
+ self.__ALConfigWidget = ALConfigWidget(self)
+ self.__ALConfigWidget.configWidgetIsClosed.connect(self.onConfigWidgetClosed)
+ self.__ALConfigWidget.show()
+ self.__ALConfigWidget.raise_()
+ self.__ALConfigWidget.activateWindow()
self.ConfigButton.setEnabled(False)
self._showLog("打开配置窗口")
diff --git a/src/gui/ALSeatMapSelectDialog.py b/src/gui/ALSeatMapSelectDialog.py
index 67c812b..342d60e 100644
--- a/src/gui/ALSeatMapSelectDialog.py
+++ b/src/gui/ALSeatMapSelectDialog.py
@@ -55,7 +55,6 @@ class ALSeatMapSelectDialog(CenterOnParentMixin, QDialog):
self.setMinimumSize(800, 600)
self.resize(800, 600)
self.setWindowTitle(f"选择楼层座位 - AutoLibrary")
-
self.SeatMapWidgetMainLayout = QVBoxLayout(self)
self.SeatMapWidgetMainLayout.setContentsMargins(5, 5, 5, 5)
self.SeatMapWidgetMainLayout.setSpacing(5)
@@ -63,10 +62,8 @@ class ALSeatMapSelectDialog(CenterOnParentMixin, QDialog):
self.TitleLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.TitleLabel.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
self.SeatMapWidgetMainLayout.addWidget(self.TitleLabel)
-
self.SeatMapGraphicsView = ALSeatMapView(None, self.__seats_data)
self.SeatMapWidgetMainLayout.addWidget(self.SeatMapGraphicsView)
-
self.TipsLabel = QLabel(
" 点击座位进行选择/取消选择, 最多选择1个座位 \n"
" [操作方法: Ctrl+鼠标滚轮缩放 | 滚轮/拖拽/方向键 移动]"
@@ -74,7 +71,6 @@ class ALSeatMapSelectDialog(CenterOnParentMixin, QDialog):
self.TipsLabel.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.TipsLabel.setStyleSheet("color: #666; margin: 5px;")
self.SeatMapWidgetMainLayout.addWidget(self.TipsLabel)
-
self.ConfirmButton = QPushButton("确认")
self.ConfirmButton.setFixedSize(80, 25)
self.ConfirmButton.setAutoDefault(True)
diff --git a/src/gui/ALSeatMapView.py b/src/gui/ALSeatMapView.py
index 0379bb0..56b6878 100644
--- a/src/gui/ALSeatMapView.py
+++ b/src/gui/ALSeatMapView.py
@@ -85,11 +85,9 @@ class ALSeatMapView(QGraphicsView):
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.viewport().installEventFilter(self)
-
self.SeatsContainerWidget = QWidget()
self.SeatsContainerLayout = QGridLayout(self.SeatsContainerWidget)
self.setupSeatMap()
-
self.ContainerProxy = self.SeatMapGraphicsScene.addWidget(self.SeatsContainerWidget)
self.ContainerProxy.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, False)
diff --git a/src/gui/ALSettingsWidget.py b/src/gui/ALSettingsWidget.py
index 7dab1f1..36eeaa3 100644
--- a/src/gui/ALSettingsWidget.py
+++ b/src/gui/ALSettingsWidget.py
@@ -9,12 +9,15 @@ See the LICENSE file for details.
"""
import os
import sys
+import time
+import requests
import qtawesome as qta
from PySide6.QtCore import (
QProcess,
Qt,
+ QTimer,
Signal,
Slot
)
@@ -30,6 +33,7 @@ from PySide6.QtWidgets import (
)
import managers.config.ConfigManager as ConfigManager
+from managers.bulletin.BulletinManager import instance as bulletinInstance
from managers.log.LogManager import instance as logInstance
from managers.theme.ThemeManager import(
getActiveStyle,
@@ -96,6 +100,9 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self.__original_theme: str = ""
self.__original_custom_theme: str = ""
self.__original_style: str = ""
+ self.__original_bulletin_url: str = ""
+ self.__original_bulletin_auto_fetch: bool = False
+ self.__original_bulletin_sync_interval: int = 10
self.setupUi(self)
self.modifyUi()
@@ -130,6 +137,7 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
"padding: 5px;"
)
self.NavigationList.setCurrentRow(0)
+ self.NavigationList.currentRowChanged.connect(self.PageStack.setCurrentIndex)
self.populateStyles()
self.populateCustomThemes()
@@ -139,9 +147,12 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
app : QApplication | None = QApplication.instance()
color = app.palette().color(app.palette().ColorRole.WindowText).name()
- item = self.NavigationList.item(0)
- if item:
- item.setIcon(qta.icon("fa6s.palette", color=color))
+ item0 = self.NavigationList.item(0)
+ if item0:
+ item0.setIcon(qta.icon("fa6s.palette", color=color))
+ item1 = self.NavigationList.item(1)
+ if item1:
+ item1.setIcon(qta.icon("fa6s.bullhorn", color=color))
def populateStyles(
self
@@ -176,6 +187,7 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self.RemoveCustomThemeButton.clicked.connect(self.onRemoveCustomThemeButtonClicked)
self.CustomThemeComboBox.currentIndexChanged.connect(self.onCustomThemeComboBoxChanged)
self.ResetCustomThemeButton.clicked.connect(self.onResetCustomThemeButtonClicked)
+ self.BulletinTestButton.clicked.connect(self.onBulletinTestButtonClicked)
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
self.ApplyButton.clicked.connect(self.onApplyButtonClicked)
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
@@ -207,6 +219,14 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self.updateCustomThemeInfo()
self.updateCustomThemeStatus()
+ bulletin_mgr = bulletinInstance()
+ self.__original_bulletin_url = bulletin_mgr.serverUrl()
+ self.__original_bulletin_auto_fetch = bulletin_mgr.autoFetch()
+ self.__original_bulletin_sync_interval = bulletin_mgr.syncInterval()
+ self.BulletinServerUrlEdit.setText(self.__original_bulletin_url)
+ self.BulletinAutoFetchCheck.setChecked(self.__original_bulletin_auto_fetch)
+ self.BulletinSyncIntervalSpin.setValue(self.__original_bulletin_sync_interval)
+
def updateCustomThemeInfo(
self
):
@@ -270,6 +290,29 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
custom_theme = ""
return theme, style, custom_theme
+ def clearBulletinTestStatus(
+ self
+ ):
+
+ self.BulletinTestStatusLabel.setText("")
+ self.BulletinTestStatusLabel.setStyleSheet("")
+
+ def saveBulletinSettings(
+ self
+ ):
+
+ url = self.BulletinServerUrlEdit.text().strip()
+ auto_fetch = self.BulletinAutoFetchCheck.isChecked()
+ sync_interval = self.BulletinSyncIntervalSpin.value()
+ if sync_interval < 1:
+ sync_interval = 5
+ self.__cfg_mgr.set(CfgKey.GLOBAL.BULLETIN.SERVER_URL, url)
+ self.__cfg_mgr.set(CfgKey.GLOBAL.BULLETIN.AUTO_FETCH, auto_fetch)
+ self.__cfg_mgr.set(CfgKey.GLOBAL.BULLETIN.SYNC_INTERVAL, sync_interval)
+ self.__original_bulletin_url = url
+ self.__original_bulletin_auto_fetch = auto_fetch
+ self.__original_bulletin_sync_interval = sync_interval
+
def saveAndApply(
self
):
@@ -281,8 +324,6 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
if not _applyCustomTheme(custom_theme, theme):
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "")
self.syncRadioFromNeedTheme(custom_theme)
- # Re-read theme after syncRadioFromNeedTheme — the radio may have
- # changed to match the custom theme's need_theme
theme, _, _ = self.collectSettings()
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.THEME, theme)
self.setNavigationIcons()
@@ -292,6 +333,8 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self.__original_custom_theme = custom_theme if custom_theme else ""
self.__original_style = getActiveStyle()
+ self.saveBulletinSettings()
+
def maybeRestart(
self
) -> bool:
@@ -380,7 +423,6 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
):
self.updateCustomThemeInfo()
- # no status update, because custom theme is not applied yet.
@Slot()
def onResetCustomThemeButtonClicked(
@@ -406,6 +448,75 @@ 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()
+ from gui.ALBulletinDialog import ALBulletinFetchWorker
+ api_url = url.rstrip("/") + "/bulletins"
+ self.__bulletin_test_worker = ALBulletinFetchWorker(
+ self, api_url, {"date": "", "time": "", "range_hour": "1"}
+ )
+ self.__bulletin_test_worker.fetchWorkerIsFinished.connect(
+ self.__onBulletinTestFetched
+ )
+ self.__bulletin_test_worker.fetchWorkerFinishedWithError.connect(
+ self.__onBulletinTestError
+ )
+ self.__bulletin_test_worker.start()
+
+ @Slot(dict)
+ def __onBulletinTestFetched(
+ self,
+ data: dict
+ ):
+
+ self.__bulletin_test_worker.fetchWorkerIsFinished.disconnect(
+ self.__onBulletinTestFetched
+ )
+ self.__bulletin_test_worker.fetchWorkerFinishedWithError.disconnect(
+ self.__onBulletinTestError
+ )
+ self.__bulletin_test_worker.deleteLater()
+ self.__bulletin_test_worker = None
+ elapsed_ms = (time.monotonic() - self.__bulletin_test_t0) * 1000
+ self.BulletinTestStatusLabel.setText(
+ f"连接成功!响应延迟 {elapsed_ms:.0f} ms"
+ )
+ self.BulletinTestStatusLabel.setStyleSheet("color: green;")
+ self.BulletinTestButton.setEnabled(True)
+ QTimer.singleShot(3000, self, self.clearBulletinTestStatus)
+
+ @Slot(str)
+ def __onBulletinTestError(
+ self,
+ error_message: str
+ ):
+
+ self.__bulletin_test_worker.fetchWorkerIsFinished.disconnect(
+ self.__onBulletinTestFetched
+ )
+ self.__bulletin_test_worker.fetchWorkerFinishedWithError.disconnect(
+ self.__onBulletinTestError
+ )
+ self.__bulletin_test_worker.deleteLater()
+ self.__bulletin_test_worker = None
+ self.BulletinTestStatusLabel.setText(f"连接失败:{error_message}")
+ self.BulletinTestStatusLabel.setStyleSheet("color: red;")
+ self.BulletinTestButton.setEnabled(True)
+
@Slot()
def onCancelButtonClicked(
self
@@ -429,5 +540,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/ALTimerTaskHistoryDialog.py b/src/gui/ALTimerTaskHistoryDialog.py
index 40eec21..2267bf2 100644
--- a/src/gui/ALTimerTaskHistoryDialog.py
+++ b/src/gui/ALTimerTaskHistoryDialog.py
@@ -108,24 +108,24 @@ class ALTimerTaskHistoryDialog(QDialog):
execute_time = record.get("execute_time", "")
result = record.get("result", ALTimerTaskStatus.UNKNOWN)
duration = record.get("duration", 0)
- ExecuteTimeItem = QTableWidgetItem(execute_time)
- ExecuteTimeItem.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
- self.HistoryTableWidget.setItem(row, 0, ExecuteTimeItem)
- ResultItem = QTableWidgetItem(result.value)
- ResultItem.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
+ execute_time_item = QTableWidgetItem(execute_time)
+ execute_time_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
+ self.HistoryTableWidget.setItem(row, 0, execute_time_item)
+ result_item = QTableWidgetItem(result.value)
+ result_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
match result:
case ALTimerTaskStatus.EXECUTED:
- ResultItem.setForeground(Qt.GlobalColor.green)
+ result_item.setForeground(Qt.GlobalColor.green)
case ALTimerTaskStatus.ERROR:
- ResultItem.setForeground(Qt.GlobalColor.red)
+ result_item.setForeground(Qt.GlobalColor.red)
case ALTimerTaskStatus.OUTDATED:
- ResultItem.setForeground(Qt.GlobalColor.red)
+ result_item.setForeground(Qt.GlobalColor.red)
case _:
- ResultItem.setForeground(Qt.GlobalColor.black)
- self.HistoryTableWidget.setItem(row, 1, ResultItem)
- DurationItem = QTableWidgetItem(f"{duration:.2f}")
- DurationItem.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
- self.HistoryTableWidget.setItem(row, 2, DurationItem)
+ result_item.setForeground(Qt.GlobalColor.black)
+ self.HistoryTableWidget.setItem(row, 1, result_item)
+ duration_item = QTableWidgetItem(f"{duration:.2f}")
+ duration_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
+ self.HistoryTableWidget.setItem(row, 2, duration_item)
self.HistoryTableWidget.setRowHeight(row, 25)
@Slot()
diff --git a/src/gui/ALTimerTaskManageWidget.py b/src/gui/ALTimerTaskManageWidget.py
index 8b9ac30..d97ef6b 100644
--- a/src/gui/ALTimerTaskManageWidget.py
+++ b/src/gui/ALTimerTaskManageWidget.py
@@ -81,12 +81,12 @@ class ALTimerTaskItemWidget(QWidget):
self.TaskInfoLayout = QVBoxLayout()
self.TaskInfoLayout.setSpacing(5)
TaskNameLabel = QLabel(self.__timer_task["name"])
- TaskNameLabelFont = TaskNameLabel.font()
- TaskNameLabelFont.setBold(True)
- TaskNameLabel.setFont(TaskNameLabelFont)
+ task_name_label_font = TaskNameLabel.font()
+ task_name_label_font.setBold(True)
+ TaskNameLabel.setFont(task_name_label_font)
TaskNameLabel.setFixedHeight(25)
self.TaskInfoLayout.addWidget(TaskNameLabel)
- ExecuteTimeStr = self.__timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S")
+ execute_time_str = self.__timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S")
if self.__timer_task.get("repeat", False):
repeat_days = self.__timer_task.get("repeat_days", [])
repeat_hour = self.__timer_task.get("repeat_hour", 0)
@@ -94,14 +94,14 @@ class ALTimerTaskItemWidget(QWidget):
repeat_second = self.__timer_task.get("repeat_second", 0)
if len(repeat_days) == 7:
time_str = f"{repeat_hour:02d}:{repeat_minute:02d}:{repeat_second:02d}"
- ExecuteTimeLabel = QLabel(f"下次执行时间: {ExecuteTimeStr} (每日 {time_str})")
+ ExecuteTimeLabel = QLabel(f"下次执行时间: {execute_time_str} (每日 {time_str})")
else:
day_names = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
selected_days = [day_names[d] for d in repeat_days]
time_str = f"{repeat_hour:02d}:{repeat_minute:02d}:{repeat_second:02d}"
- ExecuteTimeLabel = QLabel(f"下次执行时间: {ExecuteTimeStr} (每{','.join(selected_days)} {time_str})")
+ ExecuteTimeLabel = QLabel(f"下次执行时间: {execute_time_str} (每{','.join(selected_days)} {time_str})")
else:
- ExecuteTimeLabel = QLabel(f"执行时间: {ExecuteTimeStr}")
+ ExecuteTimeLabel = QLabel(f"执行时间: {execute_time_str}")
ExecuteTimeLabel.setStyleSheet("color: #969696;")
ExecuteTimeLabel.setFixedHeight(20)
self.TaskInfoLayout.addWidget(ExecuteTimeLabel)
diff --git a/src/gui/ALUserTreeWidget.py b/src/gui/ALUserTreeWidget.py
index 17c9ae4..53d2392 100644
--- a/src/gui/ALUserTreeWidget.py
+++ b/src/gui/ALUserTreeWidget.py
@@ -109,27 +109,27 @@ class ALUserTreeWidget(QTreeWidget):
super().dragMoveEvent(event)
- SourceItem = self.currentItem()
- TargetItem = self.itemAt(event.position().toPoint())
- if SourceItem is None:
+ source_item = self.currentItem()
+ target_item = self.itemAt(event.position().toPoint())
+ if source_item is None:
event.ignore()
return
- if SourceItem.type() == ALUserTreeItemType.GROUP.value:
- if TargetItem is not None:
+ if source_item.type() == ALUserTreeItemType.GROUP.value:
+ if target_item is not None:
event.ignore()
return
- elif SourceItem.type() == ALUserTreeItemType.USER.value:
- if TargetItem is None:
+ elif source_item.type() == ALUserTreeItemType.USER.value:
+ if target_item is None:
event.ignore()
return
- if TargetItem.type() != ALUserTreeItemType.GROUP.value:
+ if target_item.type() != ALUserTreeItemType.GROUP.value:
event.ignore()
return
- if TargetItem.checkState(1) == Qt.CheckState.Unchecked:
+ if target_item.checkState(1) == Qt.CheckState.Unchecked:
event.ignore()
return
if not self.isDragPositionValid(
- self.visualItemRect(TargetItem),
+ self.visualItemRect(target_item),
event.position().toPoint()
):
event.ignore()
diff --git a/src/gui/resources/ui/ALBulletinDialog.ui b/src/gui/resources/ui/ALBulletinDialog.ui
new file mode 100644
index 0000000..40fdba5
--- /dev/null
+++ b/src/gui/resources/ui/ALBulletinDialog.ui
@@ -0,0 +1,385 @@
+
+
+ ALBulletinDialog
+
+
+ Qt::WindowModality::NonModal
+
+
+
+ 0
+ 0
+ 640
+ 450
+
+
+
+
+ 400
+ 450
+
+
+
+
+ 800
+ 450
+
+
+
+ 公告栏 - AutoLibrary
+
+
+
+ 5
+
+
+ 5
+
+
+ 5
+
+
+ 5
+
+
+ 5
+
+ -
+
+
+ 10
+
+
-
+
+
+ true
+
+
+
+ 80
+ 25
+
+
+
+
+ 80
+ 25
+
+
+
+ Qt::FocusPolicy::NoFocus
+
+
+ 同步
+
+
+ false
+
+
+
+ -
+
+
+
+ 30
+ 30
+
+
+
+
+ 30
+ 30
+
+
+
+
+
+
+
+ -
+
+
+
+ 80
+ 25
+
+
+
+
+ 200
+ 25
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 800
+ 25
+
+
+
+ QFrame::Shape::NoFrame
+
+
+ QFrame::Shadow::Plain
+
+
+ 0
+
+
+
+ -
+
+
+
+ 50
+ 25
+
+
+
+
+ 50
+ 25
+
+
+
+ 上次同步:
+
+
+
+ -
+
+
+
+ 130
+ 25
+
+
+
+
+ 130
+ 25
+
+
+
+ false
+
+
+ true
+
+
+ QAbstractSpinBox::ButtonSymbols::NoButtons
+
+
+ false
+
+
+ yyyy/MM/dd HH:mm:ss
+
+
+ Qt::TimeSpec::LocalTime
+
+
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ QFrame::Shape::NoFrame
+
+
+ QFrame::Shadow::Plain
+
+
+
+ 5
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ QFrame::Shape::NoFrame
+
+
+ QAbstractItemView::EditTrigger::NoEditTriggers
+
+
+ QAbstractItemView::SelectionMode::SingleSelection
+
+
+
+
+
+
+
+ QFrame::Shape::NoFrame
+
+
+ QFrame::Shadow::Plain
+
+
+
+ 5
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+
+
+
+
+ -
+
+
+ 5
+
+
-
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+ (已编辑)
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+ true
+
+
+ true
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+ Qt::Orientation::Horizontal
+
+
+ QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok
+
+
+
+
+
+
+
+
+ BulletinDialogButtonBox
+ accepted()
+ ALBulletinDialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ BulletinDialogButtonBox
+ rejected()
+ ALBulletinDialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/src/gui/resources/ui/ALSettingsWidget.ui b/src/gui/resources/ui/ALSettingsWidget.ui
index 45ed0e8..03ac1f4 100644
--- a/src/gui/resources/ui/ALSettingsWidget.ui
+++ b/src/gui/resources/ui/ALSettingsWidget.ui
@@ -101,340 +101,555 @@
+ -
+
+ 公告
+
+
+
+
+
-
-
-
- QFrame::Shape::NoFrame
-
-
- true
-
-
-
-
- 0
- -51
- 397
- 434
-
+
+
+
+ QFrame::Shape::NoFrame
-
-
- 5
+
+ true
+
+
+
+
+ 0
+ 0
+ 397
+ 434
+
-
- 3
+
+
+ 5
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
-
+
+
+ 主题模式
+
+
+
+ 5
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
-
+
+
+ 浅色
+
+
+
+ -
+
+
+ 深色
+
+
+
+ -
+
+
+ 跟随系统
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+ 界面风格
+
+
+
+ 5
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
-
+
+
+ 5
+
+
-
+
+
+
+ 100
+ 25
+
+
+
+
+ 100
+ 25
+
+
+
+ 应用程序样式:
+
+
+
+ -
+
+
+
+ 160
+ 25
+
+
+
+
+
+
+ -
+
+
+ 更改样式将在下次启动应用程序时生效。
+
+
+
+
+
+
+ -
+
+
+ 自定义外观
+
+
+
+ 5
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
-
+
+
+ 选择一个主题,或导入新的主题文件:
+
+
+ true
+
+
+
+ -
+
+
+ 5
+
+
-
+
+
+
+ 160
+ 25
+
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+ false
+
+
+ 选择或输入 QSS 样式表文件路径...
+
+
+
+ -
+
+
+
+ 25
+ 25
+
+
+
+
+ 25
+ 25
+
+
+
+ +
+
+
+
+ -
+
+
+
+ 25
+ 25
+
+
+
+
+ 25
+ 25
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+ 0
+ 60
+
+
+
+
+
+
+ Qt::TextFormat::RichText
+
+
+ Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop
+
+
+ true
+
+
+
+ -
+
+
+ 5
+
+
-
+
+
+
+ 80
+ 25
+
+
+
+ 重置主题
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+ 当前使用程序 默认 外观。
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+ Qt::Orientation::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
+ QFrame::Shape::NoFrame
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 397
+ 434
+
-
- 3
-
-
- 3
-
-
- 3
-
- -
-
-
- 主题模式
-
-
-
- 5
+
+
+ 5
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
-
+
+
+ 公告设置
-
- 3
+
+
+ 5
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
-
+
+
+ 5
+
+
-
+
+
+
+ 100
+ 25
+
+
+
+
+ 100
+ 25
+
+
+
+ 服务器地址:
+
+
+
+ -
+
+
+
+ 200
+ 25
+
+
+
+ https://api.autolibrary.kenanzhu.com
+
+
+
+
+
+ -
+
+
+ 软件启动时自动获取公告
+
+
+
+ -
+
+
+ 5
+
+
-
+
+
+
+ 100
+ 25
+
+
+
+
+ 100
+ 25
+
+
+
+ 自动同步间隔:
+
+
+
+ -
+
+
+
+ 80
+ 25
+
+
+
+ 1
+
+
+ 1440
+
+
+ 分钟
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+
+ 100
+ 25
+
+
+
+
+ 100
+ 25
+
+
+
+ 测试连接
+
+
+
+ -
+
+
+
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+ Qt::Orientation::Vertical
-
- 3
+
+
+ 20
+ 40
+
-
- 3
-
-
- 3
-
-
-
-
-
- 浅色
-
-
-
- -
-
-
- 深色
-
-
-
- -
-
-
- 跟随系统
-
-
- true
-
-
-
-
-
-
- -
-
-
- 界面风格
-
-
-
- 5
-
-
- 3
-
-
- 3
-
-
- 3
-
-
- 3
-
-
-
-
-
- 5
-
-
-
-
-
-
- 100
- 25
-
-
-
-
- 100
- 25
-
-
-
- 应用程序样式:
-
-
-
- -
-
-
-
- 160
- 25
-
-
-
-
-
-
- -
-
-
- 更改样式将在下次启动应用程序时生效。
-
-
-
-
-
-
- -
-
-
- 自定义外观
-
-
-
- 5
-
-
- 3
-
-
- 3
-
-
- 3
-
-
- 3
-
-
-
-
-
- 选择一个主题,或导入新的主题文件:
-
-
- true
-
-
-
- -
-
-
- 5
-
-
-
-
-
-
- 160
- 25
-
-
-
-
- -
-
-
-
- 0
- 25
-
-
-
- false
-
-
- 选择或输入 QSS 样式表文件路径...
-
-
-
- -
-
-
-
- 25
- 25
-
-
-
-
- 25
- 25
-
-
-
- +
-
-
-
- -
-
-
-
- 25
- 25
-
-
-
-
- 25
- 25
-
-
-
- -
-
-
-
-
-
- -
-
-
-
- 0
- 60
-
-
-
-
-
-
- Qt::TextFormat::RichText
-
-
- Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop
-
-
- true
-
-
-
- -
-
-
- 5
-
-
-
-
-
-
- 80
- 25
-
-
-
- 重置主题
-
-
-
- -
-
-
- Qt::Orientation::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
-
- -
-
-
- 当前使用程序 默认 外观。
-
-
- true
-
-
-
-
-
-
- -
-
-
- Qt::Orientation::Vertical
-
-
-
- 20
- 40
-
-
-
-
-
+
+
+
+
diff --git a/src/interfaces/ConfigProvider.py b/src/interfaces/ConfigProvider.py
index d4f180a..ef9c889 100644
--- a/src/interfaces/ConfigProvider.py
+++ b/src/interfaces/ConfigProvider.py
@@ -72,6 +72,12 @@ class CfgKey:
STYLE = ConfigPath(ConfigType.GLOBAL, "appearance.style")
CUSTOM_THEME = ConfigPath(ConfigType.GLOBAL, "appearance.custom_theme")
+ class BULLETIN:
+ ROOT = ConfigPath(ConfigType.GLOBAL, "bulletin")
+ AUTO_FETCH = ConfigPath(ConfigType.GLOBAL, "bulletin.auto_fetch")
+ SERVER_URL = ConfigPath(ConfigType.GLOBAL, "bulletin.server_url")
+ SYNC_INTERVAL = ConfigPath(ConfigType.GLOBAL, "bulletin.sync_interval")
+
class TIMERTASK:
ROOT = ConfigPath(ConfigType.TIMERTASK, "")
TIMER_TASKS = ConfigPath(ConfigType.TIMERTASK, "timer_tasks")
diff --git a/src/managers/bulletin/BulletinManager.py b/src/managers/bulletin/BulletinManager.py
new file mode 100644
index 0000000..49a75c2
--- /dev/null
+++ b/src/managers/bulletin/BulletinManager.py
@@ -0,0 +1,342 @@
+# -*- 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..c26b7bf 100644
--- a/src/managers/config/ConfigManager.py
+++ b/src/managers/config/ConfigManager.py
@@ -59,6 +59,11 @@ class ConfigTemplate:
"theme": "system",
"style": "Fusion",
"custom_theme": ""
+ },
+ "bulletin": {
+ "auto_fetch": False,
+ "server_url": "https://api.autolibrary.kenanzhu.com",
+ "sync_interval": 10
}
}
case ConfigType.BULLETIN:
@@ -105,7 +110,7 @@ class ConfigManager:
config_data = JSONReader(config_path).data()
self.__config_data[config_type.value] = config_data
return
- except:
+ except Exception:
pass
self.__config_data[config_type.value] = ConfigTemplate(config_type).template()
JSONWriter(config_path, self.__config_data[config_type.value])