1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-08-02 06:09:36 +08:00

chore: 合并 feature/bulletin-system 到 develop

This commit is contained in:
2026-06-25 13:26:12 +08:00
23 changed files with 2636 additions and 929 deletions
+3 -5
View File
@@ -55,9 +55,9 @@ class ALAboutDialog(QDialog, Ui_ALAboutDialog):
AboutBrowser.setOpenExternalLinks(True) AboutBrowser.setOpenExternalLinks(True)
AboutBrowser.setLineWrapMode(QTextBrowser.LineWrapMode.NoWrap) AboutBrowser.setLineWrapMode(QTextBrowser.LineWrapMode.NoWrap)
AboutBrowser.setTextInteractionFlags(Qt.TextBrowserInteraction) AboutBrowser.setTextInteractionFlags(Qt.TextBrowserInteraction)
BrowserFont = AboutBrowser.font() browser_font = AboutBrowser.font()
BrowserFont.setFamilies(["Courier New", "Consolas", "Menlo", "DejaVu Sans Mono", "monospace"]) browser_font.setFamilies(["Courier New", "Consolas", "Menlo", "DejaVu Sans Mono", "monospace"])
AboutBrowser.setFont(BrowserFont) AboutBrowser.setFont(browser_font)
self.TabWidget.addTab(AboutBrowser, "关于") self.TabWidget.addTab(AboutBrowser, "关于")
LicenseBrowser = QTextBrowser() LicenseBrowser = QTextBrowser()
LicenseBrowser.setHtml(self.generateLicenseText()) LicenseBrowser.setHtml(self.generateLicenseText())
@@ -135,7 +135,6 @@ THE SOFTWARE.</p>"""
system = platform.system() system = platform.system()
version = platform.version() version = platform.version()
architecture = platform.architecture()[0] architecture = platform.architecture()[0]
if system == "Windows": if system == "Windows":
try: try:
version = platform.win32_ver()[1] version = platform.win32_ver()[1]
@@ -152,7 +151,6 @@ THE SOFTWARE.</p>"""
version = f"{distro.name()} {distro.version()}" version = f"{distro.name()} {distro.version()}"
except ImportError: except ImportError:
pass pass
return { return {
'system': system, 'system': system,
'version': version, 'version': version,
+109 -109
View File
@@ -75,54 +75,54 @@ class ALScriptHighlighter(QSyntaxHighlighter):
super().__init__(parent) super().__init__(parent)
self._rules = [] self._rules = []
KeywordFmt = QTextCharFormat() keyword_fmt = QTextCharFormat()
KeywordFmt.setForeground(QColor("#569CD6")) keyword_fmt.setForeground(QColor("#569CD6"))
KeywordFmt.setFontWeight(QFont.Weight.Bold) keyword_fmt.setFontWeight(QFont.Weight.Bold)
for kw in [ for kw in [
"if", "elseif", "else", "end", "then", "if", "elseif", "else", "end", "then",
"and", "or", "not", "and", "or", "not",
"local", "function", "return", "nil", "local", "function", "return", "nil",
]: ]:
self._rules.append((r"\b" + kw + r"\b", KeywordFmt)) self._rules.append((r"\b" + kw + r"\b", keyword_fmt))
BoolFmt = QTextCharFormat() bool_fmt = QTextCharFormat()
BoolFmt.setForeground(QColor("#4FC1FF")) bool_fmt.setForeground(QColor("#4FC1FF"))
BoolFmt.setFontWeight(QFont.Weight.Bold) bool_fmt.setFontWeight(QFont.Weight.Bold)
self._rules.append((r"\btrue\b", BoolFmt)) self._rules.append((r"\btrue\b", bool_fmt))
self._rules.append((r"\bfalse\b", BoolFmt)) self._rules.append((r"\bfalse\b", bool_fmt))
CmpFmt = QTextCharFormat() cmp_fmt = QTextCharFormat()
CmpFmt.setForeground(QColor("#C586C0")) cmp_fmt.setForeground(QColor("#C586C0"))
CmpFmt.setFontWeight(QFont.Weight.Normal) cmp_fmt.setFontWeight(QFont.Weight.Normal)
for op in [r"==", r"~=", r">=", r"<=", r">", r"<"]: for op in [r"==", r"~=", r">=", r"<=", r">", r"<"]:
self._rules.append((op, CmpFmt)) self._rules.append((op, cmp_fmt))
ArithFmt = QTextCharFormat() arith_fmt = QTextCharFormat()
ArithFmt.setForeground(QColor("#C586C0")) arith_fmt.setForeground(QColor("#C586C0"))
ArithFmt.setFontWeight(QFont.Weight.Normal) arith_fmt.setFontWeight(QFont.Weight.Normal)
for op in [r"\+", r"-", r"\*", r"/", r"\.\."]: for op in [r"\+", r"-", r"\*", r"/", r"\.\."]:
self._rules.append((op, ArithFmt)) self._rules.append((op, arith_fmt))
FuncFmt = QTextCharFormat() func_fmt = QTextCharFormat()
FuncFmt.setForeground(QColor("#DCDCAA")) func_fmt.setForeground(QColor("#DCDCAA"))
FuncFmt.setFontWeight(QFont.Weight.Normal) func_fmt.setFontWeight(QFont.Weight.Normal)
for fn in [ "time", "date", "datenow", "timenow", "dateadd", "timeadd"]: for fn in [ "time", "date", "datenow", "timenow", "dateadd", "timeadd"]:
self._rules.append((r"\b" + fn + r"\b", FuncFmt)) self._rules.append((r"\b" + fn + r"\b", func_fmt))
VarFmt = QTextCharFormat() var_fmt = QTextCharFormat()
VarFmt.setForeground(QColor("#9CDCFE")) var_fmt.setForeground(QColor("#9CDCFE"))
VarFmt.setFontWeight(QFont.Weight.Normal) var_fmt.setFontWeight(QFont.Weight.Normal)
var_names = [name for _, (name, _) in createAllVariablesTable().items()] var_names = [name for _, (name, _) in createAllVariablesTable().items()]
for var in var_names: for var in var_names:
self._rules.append((r"\b" + var + r"\b", VarFmt)) self._rules.append((r"\b" + var + r"\b", var_fmt))
StrFmt = QTextCharFormat() st_fmt = QTextCharFormat()
StrFmt.setForeground(QColor("#CE9178")) st_fmt.setForeground(QColor("#CE9178"))
StrFmt.setFontWeight(QFont.Weight.Normal) st_fmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((r'"[^"]*"', StrFmt)) self._rules.append((r'"[^"]*"', st_fmt))
self._rules.append((r"'[^']*'", StrFmt)) self._rules.append((r"'[^']*'", st_fmt))
NumFmt = QTextCharFormat() num_fmt = QTextCharFormat()
NumFmt.setForeground(QColor("#B5CEA8")) num_fmt.setForeground(QColor("#B5CEA8"))
NumFmt.setFontWeight(QFont.Weight.Normal) num_fmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((r"\b\d+(?:\.\d+)?\b", NumFmt)) self._rules.append((r"\b\d+(?:\.\d+)?\b", num_fmt))
CommentFmt = QTextCharFormat() comment_fmt = QTextCharFormat()
CommentFmt.setForeground(QColor("#6A9955")) comment_fmt.setForeground(QColor("#6A9955"))
CommentFmt.setFontItalic(True) comment_fmt.setFontItalic(True)
self._rules.append((r"--[^\n]*", CommentFmt)) self._rules.append((r"--[^\n]*", comment_fmt))
def highlightBlock( def highlightBlock(
self, self,
@@ -189,13 +189,13 @@ class ALAutoScriptEditDialog(QDialog):
): ):
super().__init__(parent) super().__init__(parent)
self._fontSize = 21 self._font_size = 21
self._mockWidgets = {} self._mock_widgets = {}
self.setupUi() self.setupUi()
self.connectSignals() self.connectSignals()
self.TextEdit.setPlainText(script) self.TextEdit.setPlainText(script)
self._Highlighter = ALScriptHighlighter( self._high_lighter = ALScriptHighlighter(
self.TextEdit.document() self.TextEdit.document()
) )
if mockData: if mockData:
@@ -224,7 +224,7 @@ class ALAutoScriptEditDialog(QDialog):
self.ZoomResetBtn.setIconSize(QSize(14, 14)) self.ZoomResetBtn.setIconSize(QSize(14, 14))
self.ZoomResetBtn.setFixedSize(25, 25) self.ZoomResetBtn.setFixedSize(25, 25)
self.ZoomResetBtn.setToolTip("重置缩放") self.ZoomResetBtn.setToolTip("重置缩放")
self.ZoomLabel = QLabel(f"{self._fontSize}px") self.ZoomLabel = QLabel(f"{self._font_size}px")
self.ZoomLabel.setFixedHeight(25) self.ZoomLabel.setFixedHeight(25)
self.OrchBtn = QPushButton("编排") self.OrchBtn = QPushButton("编排")
self.OrchBtn.setFixedSize(80, 25) self.OrchBtn.setFixedSize(80, 25)
@@ -259,7 +259,7 @@ class ALAutoScriptEditDialog(QDialog):
self.TextEdit.setStyleSheet( self.TextEdit.setStyleSheet(
"QPlainTextEdit {" "QPlainTextEdit {"
" font-family: 'Courier New', 'Consolas', monospace;" " font-family: 'Courier New', 'Consolas', monospace;"
f" font-size: {self._fontSize}px;" f" font-size: {self._font_size}px;"
"}" "}"
) )
Layout.addWidget(self.TextEdit) Layout.addWidget(self.TextEdit)
@@ -287,30 +287,30 @@ class ALAutoScriptEditDialog(QDialog):
BasicLayout.setSpacing(4) BasicLayout.setSpacing(4)
BasicLayout.setContentsMargins(4, 4, 4, 4) BasicLayout.setContentsMargins(4, 4, 4, 4)
BasicLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) BasicLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
controlButtons = [ control_buttons = [
("如果 (if...)", "if then\n \nend"), ("如果 (if...)", "if then\n \nend"),
("再如果 (elseif...)", "elseif then\n "), ("再如果 (elseif...)", "elseif then\n "),
("否则 (else)", "else"), ("否则 (else)", "else"),
("结束 (end)", "end"), ("结束 (end)", "end"),
("跳过 (pass)", "-- pass"), ("跳过 (pass)", "-- pass"),
] ]
self.addButtonsToGrid(BasicLayout, controlButtons, 0, 0, 3) self.addButtonsToGrid(BasicLayout, control_buttons, 0, 0, 3)
assignButtons = [ assign_buttons = [
("赋值 (=)", " = "), ("赋值 (=)", " = "),
] ]
self.addButtonsToGrid(BasicLayout, assignButtons, 1, 2, 3) self.addButtonsToGrid(BasicLayout, assign_buttons, 1, 2, 3)
TabWidget.addTab(BasicWidget, "基本语法") TabWidget.addTab(BasicWidget, "基本语法")
OperatorWidget = QWidget() OperatorWidget = QWidget()
OperatorLayout = QGridLayout(OperatorWidget) OperatorLayout = QGridLayout(OperatorWidget)
OperatorLayout.setSpacing(4) OperatorLayout.setSpacing(4)
OperatorLayout.setContentsMargins(4, 4, 4, 4) OperatorLayout.setContentsMargins(4, 4, 4, 4)
OperatorLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) OperatorLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
arithmeticButtons = [ arithmetic_buttons = [
("加 (+)", " + "), ("加 (+)", " + "),
("减 (-)", " - "), ("减 (-)", " - "),
] ]
self.addButtonsToGrid(OperatorLayout, arithmeticButtons, 0, 0, 3) self.addButtonsToGrid(OperatorLayout, arithmetic_buttons, 0, 0, 3)
compareButtons = [ 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 = [ logic_buttons = [
("且 (and)", " and "), ("且 (and)", " and "),
("或 (or)", " or "), ("或 (or)", " or "),
@@ -335,40 +335,40 @@ class ALAutoScriptEditDialog(QDialog):
("假 (false)", "false"), ("假 (false)", "false"),
] ]
self.addButtonsToGrid(LiteralLayout, bool_buttons, 0, 0, 3) self.addButtonsToGrid(LiteralLayout, bool_buttons, 0, 0, 3)
dateTimeButtons = [ date_time_buttons = [
("日期", 'date(2026, 1, 1)'), ("日期", 'date(2026, 1, 1)'),
("时间", 'time(0, 0)'), ("时间", 'time(0, 0)'),
] ]
self.addButtonsToGrid(LiteralLayout, dateTimeButtons, 1, 0, 3) self.addButtonsToGrid(LiteralLayout, date_time_buttons, 1, 0, 3)
hintButtons = [ hint_buttons = [
("字符串", '"请输入文本"'), ("字符串", '"请输入文本"'),
("数字", "123"), ("数字", "123"),
("注释", "-- 请输入注释"), ("注释", "-- 请输入注释"),
] ]
self.addButtonsToGrid(LiteralLayout, hintButtons, 2, 0, 3) self.addButtonsToGrid(LiteralLayout, hint_buttons, 2, 0, 3)
TabWidget.addTab(LiteralWidget, "字面量") TabWidget.addTab(LiteralWidget, "字面量")
VarWidget = QWidget() VarWidget = QWidget()
VarLayout = QGridLayout(VarWidget) VarLayout = QGridLayout(VarWidget)
VarLayout.setSpacing(4) VarLayout.setSpacing(4)
VarLayout.setContentsMargins(4, 4, 4, 4) VarLayout.setContentsMargins(4, 4, 4, 4)
VarLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) VarLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
varButtons = [ var_buttons = [
(display_name, name) for display_name, (name, _) in createAllVariablesTable().items() (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, "变量") TabWidget.addTab(VarWidget, "变量")
FuncWidget = QWidget() FuncWidget = QWidget()
FuncLayout = QGridLayout(FuncWidget) FuncLayout = QGridLayout(FuncWidget)
FuncLayout.setSpacing(4) FuncLayout.setSpacing(4)
FuncLayout.setContentsMargins(4, 4, 4, 4) FuncLayout.setContentsMargins(4, 4, 4, 4)
FuncLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) FuncLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
funcButtons = [ func_buttons = [
("datenow()", "datenow()", "返回当前日期的 Unix 时间戳"), ("datenow()", "datenow()", "返回当前日期的 Unix 时间戳"),
("timenow()", "timenow()", "返回当前时间在一天中的分钟数"), ("timenow()", "timenow()", "返回当前时间在一天中的分钟数"),
("dateadd(day, n)", "dateadd(, )", "日期偏移: dateadd(日期时间戳, 天数)"), ("dateadd(day, n)", "dateadd(, )", "日期偏移: dateadd(日期时间戳, 天数)"),
("timeadd(time, n)", "timeadd(, )", "时间偏移: timeadd(分钟数, 分钟数)"), ("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 = QPushButton(text)
Btn.setProperty("template", template) Btn.setProperty("template", template)
Btn.clicked.connect(self.insertTemplate) Btn.clicked.connect(self.insertTemplate)
@@ -419,17 +419,17 @@ class ALAutoScriptEditDialog(QDialog):
Form = QFormLayout(Group) Form = QFormLayout(Group)
Form.setSpacing(4) Form.setSpacing(4)
Form.setContentsMargins(5, 10, 5, 5) Form.setContentsMargins(5, 10, 5, 5)
self._mockWidgets = {} self._mock_widgets = {}
mockData = createMockTargetData() mock_data = createMockTargetData()
for name, var_type, key_path, display_name in createTargetVarDefs(): for name, var_type, key_path, display_name in createTargetVarDefs():
d = mockData d = mock_data
for key in key_path: for key in key_path:
d = d[key] d = d[key]
default = d default = d
Widget = self.makeMockInput(var_type, default) Widget = self.makeMockInput(var_type, default)
Label = QLabel(f"{display_name}: {name}({var_type})") Label = QLabel(f"{display_name}: {name}({var_type})")
Form.addRow(Label, Widget) Form.addRow(Label, Widget)
self._mockWidgets[name] = (Widget, var_type, key_path) self._mock_widgets[name] = (Widget, var_type, key_path)
return Group return Group
def makeMockInput( def makeMockInput(
@@ -439,49 +439,49 @@ class ALAutoScriptEditDialog(QDialog):
) -> QWidget: ) -> QWidget:
if var_type == "String": if var_type == "String":
W = QLineEdit() widget = QLineEdit()
W.setText(str(default)) widget.setText(str(default))
return W return widget
if var_type == "Boolean": if var_type == "Boolean":
W = QComboBox() widget = QComboBox()
W.addItems(["", ""]) widget.addItems(["", ""])
W.setCurrentIndex(0 if default else 1) widget.setCurrentIndex(0 if default else 1)
return W return widget
if var_type == "Date": if var_type == "Date":
W = QDateEdit() widget = QDateEdit()
W.setCalendarPopup(True) widget.setCalendarPopup(True)
W.setDisplayFormat("yyyy-MM-dd") widget.setDisplayFormat("yyyy-MM-dd")
W.setDate(QDate.fromString(str(default), "yyyy-MM-dd")) widget.setDate(QDate.fromString(str(default), "yyyy-MM-dd"))
return W return widget
if var_type == "Time": if var_type == "Time":
W = QTimeEdit() widget = QTimeEdit()
W.setDisplayFormat("HH:mm") widget.setDisplayFormat("HH:mm")
W.setTime(QTime.fromString(str(default), "HH:mm")) widget.setTime(QTime.fromString(str(default), "HH:mm"))
return W return widget
if var_type == "Int": if var_type == "Int":
W = QSpinBox() widget = QSpinBox()
W.setMinimum(-999999) widget.setMinimum(-999999)
W.setMaximum(999999) widget.setMaximum(999999)
W.setValue(int(default) if default else 0) widget.setValue(int(default) if default else 0)
return W return widget
if var_type == "Float": if var_type == "Float":
W = QDoubleSpinBox() widget = QDoubleSpinBox()
W.setMinimum(-999999.0) widget.setMinimum(-999999.0)
W.setMaximum(999999.0) widget.setMaximum(999999.0)
W.setDecimals(2) widget.setDecimals(2)
W.setValue(float(default) if default else 0.0) widget.setValue(float(default) if default else 0.0)
return W return widget
W = QLineEdit() widget = QLineEdit()
W.setText(str(default)) widget.setText(str(default))
return W return widget
def getMockData( def getMockData(
self self
) -> dict: ) -> dict:
data = {} data = {}
for name, var_type, key_path, display_name in createTargetVarDefs(): for name, var_type, key_path, _ in createTargetVarDefs():
widget, _, _ = self._mockWidgets[name] widget, _, _ = self._mock_widgets[name]
value = self.getMockValue(widget, var_type) value = self.getMockValue(widget, var_type)
d = data d = data
for key in key_path[:-1]: for key in key_path[:-1]:
@@ -496,14 +496,14 @@ class ALAutoScriptEditDialog(QDialog):
if not data: if not data:
return return
for name, var_type, key_path, display_name in createTargetVarDefs(): for name, var_type, key_path, _ in createTargetVarDefs():
d = data d = data
try: try:
for key in key_path: for key in key_path:
d = d[key] d = d[key]
except (KeyError, TypeError): except (KeyError, TypeError):
continue continue
widget, _, _ = self._mockWidgets[name] widget, _, _ = self._mock_widgets[name]
self.setMockValue(widget, var_type, d) self.setMockValue(widget, var_type, d)
def getMockValue( def getMockValue(
@@ -578,10 +578,10 @@ class ALAutoScriptEditDialog(QDialog):
self.TextEdit.setStyleSheet( self.TextEdit.setStyleSheet(
"QPlainTextEdit {" "QPlainTextEdit {"
" font-family: 'Courier New', 'Consolas', monospace;" " 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() @Slot()
def insertTemplate( def insertTemplate(
@@ -602,7 +602,7 @@ class ALAutoScriptEditDialog(QDialog):
self self
): ):
self._fontSize = min(self._fontSize + 2, 40) self._font_size = min(self._font_size + 2, 40)
self.updateFontSize() self.updateFontSize()
@Slot() @Slot()
@@ -610,7 +610,7 @@ class ALAutoScriptEditDialog(QDialog):
self self
): ):
self._fontSize = max(self._fontSize - 2, 8) self._font_size = max(self._font_size - 2, 8)
self.updateFontSize() self.updateFontSize()
@Slot() @Slot()
@@ -618,7 +618,7 @@ class ALAutoScriptEditDialog(QDialog):
self self
): ):
self._fontSize = 21 self._font_size = 21
self.updateFontSize() self.updateFontSize()
@Slot() @Slot()
@@ -639,13 +639,13 @@ class ALAutoScriptEditDialog(QDialog):
): ):
from gui.ALAutoScriptOrchDialog import ALAutoScriptOrchDialog from gui.ALAutoScriptOrchDialog import ALAutoScriptOrchDialog
Dlg = ALAutoScriptOrchDialog(self) Dialpg = ALAutoScriptOrchDialog(self)
if Dlg.exec() == QDialog.DialogCode.Accepted: if Dialpg.exec() == QDialog.DialogCode.Accepted:
script = Dlg.getScript() script = Dialpg.getScript()
if script: if script:
cursor = self.TextEdit.textCursor() cursor = self.TextEdit.textCursor()
cursor.insertText(script) cursor.insertText(script)
Dlg.deleteLater() Dialpg.deleteLater()
@Slot() @Slot()
def onDebugRun( def onDebugRun(
@@ -679,6 +679,6 @@ class ALAutoScriptEditDialog(QDialog):
if not changes: if not changes:
QMessageBox.information(self, "调试运行", "目标变量未发生变化。") QMessageBox.information(self, "调试运行", "目标变量未发生变化。")
return return
Dlg = _DebugResultDialog(changes, self) Dialpg = _DebugResultDialog(changes, self)
Dlg.exec() Dialpg.exec()
Dlg.deleteLater() Dialpg.deleteLater()
+53 -49
View File
@@ -32,16 +32,16 @@ class ConditionalBlock(QGroupBox):
def __init__( def __init__(
self, self,
blockIndex: int, block_index: int,
varMgr = None, var_mgr = None,
parent = None parent = None
): ):
super().__init__(parent) super().__init__(parent)
self.blockIndex = blockIndex self._block_index = block_index
self._varMgr = varMgr self._var_mgr = var_mgr
self._actionWidgets = [] self._action_widgets = []
self._conditionRows = [] self._condition_rows = []
self.setupUi() self.setupUi()
self.connectSignals() self.connectSignals()
@@ -53,8 +53,12 @@ class ConditionalBlock(QGroupBox):
self.setUpdatesEnabled(False) self.setUpdatesEnabled(False)
self.setStyleSheet( self.setStyleSheet(
"QGroupBox { font-weight: bold; border: 1px solid #ccc; " "QGroupBox { "\
"margin-top: 5px; padding-top: 5px; }" "font-weight: bold;"\
"border: 1px solid #ccc;"\
"margin-top: 5px;"\
"padding-top: 5px; "\
"}"
) )
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
MainLayout = QVBoxLayout(self) MainLayout = QVBoxLayout(self)
@@ -67,7 +71,7 @@ class ConditionalBlock(QGroupBox):
self.TypeCombo.addItem("ELSE IF", "ELSE IF") self.TypeCombo.addItem("ELSE IF", "ELSE IF")
self.TypeCombo.addItem("ELSE", "ELSE") self.TypeCombo.addItem("ELSE", "ELSE")
self.TypeCombo.setFixedHeight(25) self.TypeCombo.setFixedHeight(25)
if self.blockIndex == 0: if self._block_index == 0:
self.TypeCombo.setEnabled(False) self.TypeCombo.setEnabled(False)
HeaderLayout.addWidget(QLabel("类型:", self)) HeaderLayout.addWidget(QLabel("类型:", self))
HeaderLayout.addWidget(self.TypeCombo) HeaderLayout.addWidget(self.TypeCombo)
@@ -115,10 +119,10 @@ class ConditionalBlock(QGroupBox):
): ):
Row = ConditionRowFrame( Row = ConditionRowFrame(
self._varMgr, self.blockIndex, self._var_mgr, self._block_index,
isFirst=True, parent=self is_first=True, parent=self
) )
self._conditionRows.append(Row) self._condition_rows.append(Row)
self.CondRowsLayout.addWidget(Row) self.CondRowsLayout.addWidget(Row)
def addConditionRow( def addConditionRow(
@@ -126,11 +130,11 @@ class ConditionalBlock(QGroupBox):
): ):
Row = ConditionRowFrame( Row = ConditionRowFrame(
self._varMgr, self.blockIndex, self._var_mgr, self._block_index,
isFirst=False, parent=self is_first=False, parent=self
) )
Row.DeleteBtn.clicked.connect(lambda: self.removeConditionRow(Row)) Row.DeleteBtn.clicked.connect(lambda: self.removeConditionRow(Row))
self._conditionRows.append(Row) self._condition_rows.append(Row)
self.CondRowsLayout.addWidget(Row) self.CondRowsLayout.addWidget(Row)
def removeConditionRow( def removeConditionRow(
@@ -138,8 +142,8 @@ class ConditionalBlock(QGroupBox):
row: ConditionRowFrame row: ConditionRowFrame
): ):
if row in self._conditionRows and len(self._conditionRows) > 1: if row in self._condition_rows and len(self._condition_rows) > 1:
self._conditionRows.remove(row) self._condition_rows.remove(row)
self.CondRowsLayout.removeWidget(row) self.CondRowsLayout.removeWidget(row)
row.hide() row.hide()
row.deleteLater() row.deleteLater()
@@ -148,9 +152,9 @@ class ConditionalBlock(QGroupBox):
self 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)) Step.DeleteBtn.clicked.connect(lambda: self.removeActionStep(Step))
self._actionWidgets.append(Step) self._action_widgets.append(Step)
self.ActionsLayout.addWidget(Step) self.ActionsLayout.addWidget(Step)
def removeActionStep( def removeActionStep(
@@ -158,8 +162,8 @@ class ConditionalBlock(QGroupBox):
step: ActionStepFrame step: ActionStepFrame
): ):
if step in self._actionWidgets: if step in self._action_widgets:
self._actionWidgets.remove(step) self._action_widgets.remove(step)
self.ActionsLayout.removeWidget(step) self.ActionsLayout.removeWidget(step)
step.hide() step.hide()
step.deleteLater() step.deleteLater()
@@ -174,19 +178,19 @@ class ConditionalBlock(QGroupBox):
self self
): ):
return list(self._conditionRows) return list(self._condition_rows)
def getActionSteps( def getActionSteps(
self self
): ):
return list(self._actionWidgets) return list(self._action_widgets)
def countActionSteps( def countActionSteps(
self self
) -> int: ) -> int:
return len(self._actionWidgets) return len(self._action_widgets)
def toScript( def toScript(
self self
@@ -195,48 +199,48 @@ class ConditionalBlock(QGroupBox):
Generate Lua script lines for this conditional block. Generate Lua script lines for this conditional block.
""" """
blockType = self.getBlockType() block_type = self.getBlockType()
lines = [] lines = []
if blockType in ("IF", "ELSE IF"): if block_type in ("IF", "ELSE IF"):
condTexts = [ cond_texts = [
r.toScript() for r in self._conditionRows if r.toScript() r.toScript() for r in self._condition_rows if r.toScript()
] ]
if not condTexts: if not cond_texts:
condTexts = ["true"] cond_texts = ["true"]
if len(condTexts) == 1: if len(cond_texts) == 1:
combined = condTexts[0] combined = cond_texts[0]
else: else:
parts = [] parts = []
for i, ct in enumerate(condTexts): for i, ct in enumerate(cond_texts):
if i > 0: 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" {logic} ")
parts.append(f"({ct})") parts.append(f"({ct})")
combined = "".join(parts) combined = "".join(parts)
if blockType == "IF": if block_type == "IF":
lines.append(f"if {combined} then") lines.append(f"if {combined} then")
else: else:
lines.append(f"elseif {combined} then") lines.append(f"elseif {combined} then")
else: else:
lines.append("else") lines.append("else")
for step in self._actionWidgets: for step in self._action_widgets:
scriptLine = step.toScript() script_line = step.toScript()
if scriptLine: if script_line:
lines.append(scriptLine) lines.append(script_line)
return lines return lines
def refreshVarCombos( def refreshVarCombos(
self self
): ):
for row in self._conditionRows: for row in self._condition_rows:
row.refreshVarCombos() row.refreshVarCombos()
for step in self._actionWidgets: for step in self._action_widgets:
step.refreshVarCombos() step.refreshVarCombos()
def setPrevBlockType( def setPrevBlockType(
self, self,
prevType: str | None prev_type: str | None
): ):
model = self.TypeCombo.model() model = self.TypeCombo.model()
@@ -247,19 +251,19 @@ class ConditionalBlock(QGroupBox):
if idx < 0: if idx < 0:
continue continue
item = model.item(idx) item = model.item(idx)
shouldEnable = prevType != "ELSE" should_enable = prev_type != "ELSE"
item.setEnabled(shouldEnable) item.setEnabled(should_enable)
if prevType == "ELSE" and self.TypeCombo.currentData() in ("ELSE IF", "ELSE"): if prev_type == "ELSE" and self.TypeCombo.currentData() in ("ELSE IF", "ELSE"):
self.TypeCombo.setCurrentIndex(0) self.TypeCombo.setCurrentIndex(0)
@Slot(int) @Slot(int)
def onTypeChanged( def onTypeChanged(
self, self,
_idx idx: int
): ):
isCond = self.TypeCombo.currentData() in ("IF", "ELSE IF") is_cond = self.TypeCombo.currentData() in ("IF", "ELSE IF")
self.ConditionWidget.setVisible(isCond) self.ConditionWidget.setVisible(is_cond)
self.ActionLabel.setText( self.ActionLabel.setText(
"执行步骤:" if isCond else "ELSE 执行步骤:" "执行步骤:" if is_cond else "ELSE 执行步骤:"
) )
+2 -2
View File
@@ -35,7 +35,7 @@ class ALAutoScriptOrchDialog(QDialog):
super().__init__(parent) super().__init__(parent)
self._blocks = [] self._blocks = []
self._varMgr = VariableManager(self) self._var_mgr = VariableManager(self)
self.setupUi() self.setupUi()
self.connectSignals() self.connectSignals()
@@ -91,7 +91,7 @@ class ALAutoScriptOrchDialog(QDialog):
): ):
Block = ConditionalBlock( 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.DeleteBlockBtn.clicked.connect(lambda: self.removeBlock(Block))
Block.TypeCombo.currentIndexChanged.connect(self.updateBlockTypeRestrictions) Block.TypeCombo.currentIndexChanged.connect(self.updateBlockTypeRestrictions)
+120 -120
View File
@@ -46,11 +46,11 @@ def getTypeOrder(
return [t for t, _ in VARTYPE_INFOS] return [t for t, _ in VARTYPE_INFOS]
def getArithType( def getArithType(
varType: str var_type: str
) -> bool: ) -> bool:
for t, a in VARTYPE_INFOS: for t, a in VARTYPE_INFOS:
if t == varType: if t == var_type:
return a return a
def getPresetVars( def getPresetVars(
@@ -113,36 +113,36 @@ class _DateInputContainer(QWidget):
Layout = QHBoxLayout(self) Layout = QHBoxLayout(self)
Layout.setContentsMargins(0, 0, 0, 0) Layout.setContentsMargins(0, 0, 0, 0)
Layout.setSpacing(4) Layout.setSpacing(4)
self._ModeCombo = QComboBox(self) self.ModeCombo = QComboBox(self)
self._ModeCombo.addItem("相对日期", "relative") self.ModeCombo.addItem("相对日期", "relative")
self._ModeCombo.addItem("绝对日期", "absolute") self.ModeCombo.addItem("绝对日期", "absolute")
self._ModeCombo.setFixedHeight(25) self.ModeCombo.setFixedHeight(25)
self._Stack = QStackedWidget(self) self.Stack = QStackedWidget(self)
self._RelCombo = QComboBox(self) self.RelCombo = QComboBox(self)
for display, data in DATE_OPTIONS: for display, data in DATE_OPTIONS:
self._RelCombo.addItem(display, data) self.RelCombo.addItem(display, data)
self._RelCombo.setFixedHeight(25) self.RelCombo.setFixedHeight(25)
self._Stack.addWidget(self._RelCombo) self.Stack.addWidget(self.RelCombo)
self._DateEdit = QDateEdit(self) self.DateEdit = QDateEdit(self)
self._DateEdit.setDisplayFormat("yyyy-MM-dd") self.DateEdit.setDisplayFormat("yyyy-MM-dd")
self._DateEdit.setCalendarPopup(True) self.DateEdit.setCalendarPopup(True)
self._DateEdit.setFixedHeight(25) self.DateEdit.setFixedHeight(25)
self._Stack.addWidget(self._DateEdit) self.Stack.addWidget(self.DateEdit)
self._ModeCombo.currentIndexChanged.connect( self.ModeCombo.currentIndexChanged.connect(
lambda i: self._Stack.setCurrentIndex(i) lambda i: self.Stack.setCurrentIndex(i)
) )
Layout.addWidget(self._ModeCombo) Layout.addWidget(self.ModeCombo)
Layout.addWidget(self._Stack) Layout.addWidget(self.Stack)
Layout.addStretch() Layout.addStretch()
def getValue( def getValue(
self self
) -> str: ) -> str:
mode = self._ModeCombo.currentData() mode = self.ModeCombo.currentData()
if mode == "relative": if mode == "relative":
return self._RelCombo.currentText() return self.RelCombo.currentText()
return self._DateEdit.date().toString("yyyy-MM-dd") return self.DateEdit.date().toString("yyyy-MM-dd")
class _TimeInputContainer(QWidget): class _TimeInputContainer(QWidget):
@@ -153,19 +153,19 @@ class _TimeInputContainer(QWidget):
): ):
super().__init__(parent) super().__init__(parent)
self._TimeEdit = QTimeEdit(self) self.TimeEdit = QTimeEdit(self)
self._TimeEdit.setDisplayFormat("HH:mm") self.TimeEdit.setDisplayFormat("HH:mm")
self._TimeEdit.setFixedHeight(25) self.TimeEdit.setFixedHeight(25)
Layout = QHBoxLayout(self) Layout = QHBoxLayout(self)
Layout.setContentsMargins(0, 0, 0, 0) Layout.setContentsMargins(0, 0, 0, 0)
Layout.addWidget(self._TimeEdit) Layout.addWidget(self.TimeEdit)
def getValue( def getValue(
self self
) -> str: ) -> str:
return self._TimeEdit.time().toString("HH:mm") return self.TimeEdit.time().toString("HH:mm")
class _DateOffsetContainer(QWidget): class _DateOffsetContainer(QWidget):
@@ -176,19 +176,19 @@ class _DateOffsetContainer(QWidget):
): ):
super().__init__(parent) super().__init__(parent)
self._SpinBox = QSpinBox(self) self.SpinBox = QSpinBox(self)
self._SpinBox.setRange(0, 99999) self.SpinBox.setRange(0, 99999)
self._SpinBox.setFixedHeight(25) self.SpinBox.setFixedHeight(25)
self._UnitCombo = QComboBox(self) self.UnitCombo = QComboBox(self)
for display, data in DATE_OFFSET_OPTIONS: for display, data in DATE_OFFSET_OPTIONS:
self._UnitCombo.addItem(display, data) self.UnitCombo.addItem(display, data)
self._UnitCombo.setFixedHeight(25) self.UnitCombo.setFixedHeight(25)
Layout = QHBoxLayout(self) Layout = QHBoxLayout(self)
Layout.setContentsMargins(0, 0, 0, 0) Layout.setContentsMargins(0, 0, 0, 0)
Layout.setSpacing(4) Layout.setSpacing(4)
Layout.addWidget(self._SpinBox) Layout.addWidget(self.SpinBox)
Layout.addWidget(self._UnitCombo) Layout.addWidget(self.UnitCombo)
Layout.addStretch() Layout.addStretch()
def getValue( def getValue(
@@ -201,8 +201,8 @@ class _DateOffsetContainer(QWidget):
self self
) -> int: ) -> int:
val = self._SpinBox.value() val = self.SpinBox.value()
unit = self._UnitCombo.currentData() unit = self.UnitCombo.currentData()
if unit == "weeks": if unit == "weeks":
return val*7 return val*7
if unit == "months": if unit == "months":
@@ -220,14 +220,14 @@ class _TimeOffsetContainer(QWidget):
): ):
super().__init__(parent) super().__init__(parent)
self._SpinBox = QSpinBox(self) self.SpinBox = QSpinBox(self)
self._SpinBox.setRange(0, 99999) self.SpinBox.setRange(0, 99999)
self._SpinBox.setSuffix(" 小时") self.SpinBox.setSuffix(" 小时")
self._SpinBox.setFixedHeight(25) self.SpinBox.setFixedHeight(25)
Layout = QHBoxLayout(self) Layout = QHBoxLayout(self)
Layout.setContentsMargins(0, 0, 0, 0) Layout.setContentsMargins(0, 0, 0, 0)
Layout.addWidget(self._SpinBox) Layout.addWidget(self.SpinBox)
def getValue( def getValue(
self self
@@ -239,7 +239,7 @@ class _TimeOffsetContainer(QWidget):
self self
) -> int: ) -> int:
return self._SpinBox.value() return self.SpinBox.value()
class VariableManager(QObject): class VariableManager(QObject):
@@ -251,7 +251,7 @@ class VariableManager(QObject):
super().__init__(parent) super().__init__(parent)
self._vars = [] self._vars = []
self._nameMap = {} self._name_map = {}
self.initPresetVars() self.initPresetVars()
@@ -262,21 +262,21 @@ class VariableManager(QObject):
for p in getPresetVars(): for p in getPresetVars():
entry = {"name": p["name"], "type": p["type"], "display": p["display"]} entry = {"name": p["name"], "type": p["type"], "display": p["display"]}
self._vars.append(entry) self._vars.append(entry)
self._nameMap[p["name"]] = entry self._name_map[p["name"]] = entry
def getInfoByName( def getInfoByName(
self, self,
name: str name: str
): ):
return self._nameMap.get(name.upper().strip()) return self._name_map.get(name.upper().strip())
def populateCombo( def populateCombo(
self, self,
combo: QComboBox combo: QComboBox
): ):
currentData = combo.currentData() current_data = combo.currentData()
combo.blockSignals(True) combo.blockSignals(True)
combo.clear() combo.clear()
for entry in self._vars: for entry in self._vars:
@@ -284,10 +284,10 @@ class VariableManager(QObject):
entry["display"], entry["display"],
(entry["name"], entry["type"]) (entry["name"], entry["type"])
) )
if currentData: if current_data:
for i in range(combo.count()): for i in range(combo.count()):
d = combo.itemData(i) d = combo.itemData(i)
if d and d[0] == currentData[0]: if d and d[0] == current_data[0]:
combo.setCurrentIndex(i) combo.setCurrentIndex(i)
break break
combo.blockSignals(False) combo.blockSignals(False)
@@ -299,40 +299,40 @@ def makeValueWidget(
) -> QWidget: ) -> QWidget:
if var_type == "Int": if var_type == "Int":
w = QSpinBox(parent) Widget = QSpinBox(parent)
w.setRange(-999999, 999999) Widget.setRange(-999999, 999999)
w.setFixedHeight(25) Widget.setFixedHeight(25)
w.setMinimumWidth(100) Widget.setMinimumWidth(100)
return w return Widget
if var_type == "Float": if var_type == "Float":
w = QDoubleSpinBox(parent) Widget = QDoubleSpinBox(parent)
w.setRange(-999999.0, 999999.0) Widget.setRange(-999999.0, 999999.0)
w.setDecimals(2) Widget.setDecimals(2)
w.setFixedHeight(25) Widget.setFixedHeight(25)
w.setMinimumWidth(100) Widget.setMinimumWidth(100)
return w return Widget
if var_type == "String": if var_type == "String":
w = QLineEdit(parent) Widget = QLineEdit(parent)
w.setPlaceholderText("输入值") Widget.setPlaceholderText("输入值")
w.setFixedHeight(25) Widget.setFixedHeight(25)
w.setMinimumWidth(120) Widget.setMinimumWidth(120)
return w return Widget
if var_type == "Boolean": if var_type == "Boolean":
w = QComboBox(parent) Widget = QComboBox(parent)
w.addItem("是 (true)", "true") Widget.addItem("是 (true)", "true")
w.addItem("否 (false)", "false") Widget.addItem("否 (false)", "false")
w.setFixedHeight(25) Widget.setFixedHeight(25)
w.setMinimumWidth(100) Widget.setMinimumWidth(100)
return w return Widget
if var_type == "Date": if var_type == "Date":
return _DateInputContainer(parent) return _DateInputContainer(parent)
if var_type == "Time": if var_type == "Time":
return _TimeInputContainer(parent) return _TimeInputContainer(parent)
w = QLineEdit(parent) Widget = QLineEdit(parent)
w.setPlaceholderText("输入值") Widget.setPlaceholderText("输入值")
w.setFixedHeight(25) Widget.setFixedHeight(25)
w.setMinimumWidth(120) Widget.setMinimumWidth(120)
return w return Widget
def makeOffsetWidget( def makeOffsetWidget(
var_type: str, var_type: str,
@@ -340,35 +340,35 @@ def makeOffsetWidget(
) -> QWidget: ) -> QWidget:
if var_type == "Int": if var_type == "Int":
w = QSpinBox(parent) Widget = QSpinBox(parent)
w.setRange(-999999, 999999) Widget.setRange(-999999, 999999)
w.setFixedHeight(25) Widget.setFixedHeight(25)
w.setMinimumWidth(100) Widget.setMinimumWidth(100)
return w return Widget
if var_type == "Float": if var_type == "Float":
w = QDoubleSpinBox(parent) Widget = QDoubleSpinBox(parent)
w.setRange(-999999.0, 999999.0) Widget.setRange(-999999.0, 999999.0)
w.setDecimals(2) Widget.setDecimals(2)
w.setFixedHeight(25) Widget.setFixedHeight(25)
w.setMinimumWidth(100) Widget.setMinimumWidth(100)
return w return Widget
if var_type == "Date": if var_type == "Date":
return _DateOffsetContainer(parent) return _DateOffsetContainer(parent)
if var_type == "Time": if var_type == "Time":
return _TimeOffsetContainer(parent) return _TimeOffsetContainer(parent)
w = QLabel("(不支持该操作)", parent) Widget = QLabel("(不支持该操作)", parent)
w.setFixedHeight(25) Widget.setFixedHeight(25)
return w return Widget
def makeVarRefCombo( def makeVarRefCombo(
parent: QWidget = None parent: QWidget = None
) -> QComboBox: ) -> QComboBox:
Cb = QComboBox(parent) ComboBox = QComboBox(parent)
Cb.setFixedHeight(25) ComboBox.setFixedHeight(25)
Cb.setMinimumWidth(120) ComboBox.setMinimumWidth(120)
Cb.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) ComboBox.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
return Cb return ComboBox
def makeComboWidget( def makeComboWidget(
items, items,
@@ -376,12 +376,12 @@ def makeComboWidget(
parent: QWidget = None parent: QWidget = None
) -> QComboBox: ) -> QComboBox:
Cb = QComboBox(parent) ComboBox = QComboBox(parent)
for display, data in items: for display, data in items:
Cb.addItem(display, data) ComboBox.addItem(display, data)
Cb.setFixedHeight(25) ComboBox.setFixedHeight(25)
Cb.setMinimumWidth(min_width) ComboBox.setMinimumWidth(min_width)
return Cb return ComboBox
def makeLabel( def makeLabel(
text: str, text: str,
@@ -389,30 +389,30 @@ def makeLabel(
width: int = None width: int = None
) -> QLabel: ) -> QLabel:
Lbl = QLabel(text, parent) Label = QLabel(text, parent)
Lbl.setFixedHeight(25) Label.setFixedHeight(25)
if width: if width:
Lbl.setFixedWidth(width) Label.setFixedWidth(width)
return Lbl return Label
def getValueFromWidget( def getValueFromWidget(
w: QWidget widget: QWidget
) -> str: ) -> str:
if hasattr(w, "getValue"): if hasattr(widget, "getValue"):
return w.getValue() return widget.getValue()
if isinstance(w, QTimeEdit): if isinstance(widget, QTimeEdit):
return w.time().toString("HH:mm") return widget.time().toString("HH:mm")
if isinstance(w, QDateEdit): if isinstance(widget, QDateEdit):
return w.date().toString("yyyy-MM-dd") return widget.date().toString("yyyy-MM-dd")
if isinstance(w, QComboBox): if isinstance(widget, QComboBox):
return w.currentData() or w.currentText() return widget.currentData() or widget.currentText()
if isinstance(w, QSpinBox): if isinstance(widget, QSpinBox):
return str(w.value()) return str(widget.value())
if isinstance(w, QDoubleSpinBox): if isinstance(widget, QDoubleSpinBox):
return str(w.value()) return str(widget.value())
if isinstance(w, QLineEdit): if isinstance(widget, QLineEdit):
return w.text() return widget.text()
return "" return ""
def encodeValueStr( def encodeValueStr(
+89 -89
View File
@@ -42,18 +42,18 @@ class ConditionRowFrame(QFrame):
def __init__( def __init__(
self, self,
varMgr, var_mgr,
parentBlockIndex: int = 0, parent_block_index: int = 0,
isFirst: bool = False, is_first: bool = False,
parent = None parent = None
): ):
super().__init__(parent) super().__init__(parent)
self._varMgr = varMgr self._var_mgr = var_mgr
self._blockIndex = parentBlockIndex self._block_index = parent_block_index
self._isFirst = isFirst self._is_first = is_first
self._isBoolMode = False self._is_bool_mode = False
self._rawRhsExpr = "" self._raw_rhs_expr = ""
self.setupUi() self.setupUi()
self.connectSignals() self.connectSignals()
@@ -69,7 +69,7 @@ class ConditionRowFrame(QFrame):
Layout = QHBoxLayout(self) Layout = QHBoxLayout(self)
Layout.setContentsMargins(2, 2, 2, 2) Layout.setContentsMargins(2, 2, 2, 2)
Layout.setSpacing(4) Layout.setSpacing(4)
if self._isFirst: if self._is_first:
self.LogicCombo = None self.LogicCombo = None
else: else:
self.LogicCombo = makeComboWidget(LOGIC_OPTIONS, min_width=110, parent=self) self.LogicCombo = makeComboWidget(LOGIC_OPTIONS, min_width=110, parent=self)
@@ -82,11 +82,11 @@ class ConditionRowFrame(QFrame):
Layout.addWidget(self.LeftVarCombo) Layout.addWidget(self.LeftVarCombo)
self.OpCombo = makeComboWidget(COMPARE_OPTIONS, min_width=80, parent=self) self.OpCombo = makeComboWidget(COMPARE_OPTIONS, min_width=80, parent=self)
Layout.addWidget(self.OpCombo) Layout.addWidget(self.OpCombo)
self._CompTypeCombo = makeComboWidget([ self.CompTypeCombo = makeComboWidget([
("特定值", "literal"), ("特定值", "literal"),
("变量", "variable"), ("变量", "variable"),
], min_width=70, parent=self) ], min_width=70, parent=self)
Layout.addWidget(self._CompTypeCombo) Layout.addWidget(self.CompTypeCombo)
self.RhsStack = QStackedWidget(self) self.RhsStack = QStackedWidget(self)
self.RhsStack.setFixedHeight(25) self.RhsStack.setFixedHeight(25)
self.initLiteralStack() self.initLiteralStack()
@@ -94,7 +94,7 @@ class ConditionRowFrame(QFrame):
self.RhsStack.addWidget(self.RhsVarCombo) self.RhsStack.addWidget(self.RhsVarCombo)
self.RhsStack.setCurrentIndex(0) self.RhsStack.setCurrentIndex(0)
Layout.addWidget(self.RhsStack) Layout.addWidget(self.RhsStack)
if not self._isFirst: if not self._is_first:
self.DeleteBtn = QPushButton("×", self) self.DeleteBtn = QPushButton("×", self)
self.DeleteBtn.setFixedSize(25, 25) self.DeleteBtn.setFixedSize(25, 25)
self.DeleteBtn.setStyleSheet("color: red; font-weight: bold;") self.DeleteBtn.setStyleSheet("color: red; font-weight: bold;")
@@ -108,21 +108,21 @@ class ConditionRowFrame(QFrame):
self self
): ):
wasBool = self._isBoolMode was_bool = self._is_bool_mode
boolName = None bool_name = None
if wasBool: if was_bool:
data = self.LeftVarCombo.currentData() data = self.LeftVarCombo.currentData()
if data: if data:
boolName = data[0] bool_name = data[0]
self._varMgr.populateCombo(self.LeftVarCombo) self._var_mgr.populateCombo(self.LeftVarCombo)
# Append boolean literal sentinels at the end # Append boolean literal sentinels at the end
self.LeftVarCombo.insertSeparator(self.LeftVarCombo.count()) self.LeftVarCombo.insertSeparator(self.LeftVarCombo.count())
self.LeftVarCombo.addItem("true", ("true", "Boolean")) self.LeftVarCombo.addItem("true", ("true", "Boolean"))
self.LeftVarCombo.addItem("false", ("false", "Boolean")) self.LeftVarCombo.addItem("false", ("false", "Boolean"))
if wasBool and boolName: if was_bool and bool_name:
for ci in range(self.LeftVarCombo.count()): for ci in range(self.LeftVarCombo.count()):
d = self.LeftVarCombo.itemData(ci) d = self.LeftVarCombo.itemData(ci)
if d and d[0] == boolName: if d and d[0] == bool_name:
self.LeftVarCombo.setCurrentIndex(ci) self.LeftVarCombo.setCurrentIndex(ci)
break break
@@ -130,7 +130,7 @@ class ConditionRowFrame(QFrame):
self self
): ):
self._varMgr.populateCombo(self.RhsVarCombo) self._var_mgr.populateCombo(self.RhsVarCombo)
def initLiteralStack( def initLiteralStack(
self self
@@ -138,12 +138,12 @@ class ConditionRowFrame(QFrame):
self.LiteralStack = QStackedWidget(self) self.LiteralStack = QStackedWidget(self)
self.LiteralStack.setFixedHeight(25) self.LiteralStack.setFixedHeight(25)
self._literalWidgets = {} self.literal_widgets = {}
for vt in getTypeOrder(): for vt in getTypeOrder():
W = makeValueWidget(vt, self.LiteralStack) W = makeValueWidget(vt, self.LiteralStack)
self._literalWidgets[vt] = W self.literal_widgets[vt] = W
self.LiteralStack.addWidget(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) self.RhsStack.addWidget(self.LiteralStack)
def connectSignals( def connectSignals(
@@ -151,7 +151,7 @@ class ConditionRowFrame(QFrame):
): ):
self.LeftVarCombo.currentIndexChanged.connect(self.onLeftVarChanged) self.LeftVarCombo.currentIndexChanged.connect(self.onLeftVarChanged)
self._CompTypeCombo.currentIndexChanged.connect(self.onCompTypeChanged) self.CompTypeCombo.currentIndexChanged.connect(self.onCompTypeChanged)
def getLogic( def getLogic(
self self
@@ -164,16 +164,16 @@ class ConditionRowFrame(QFrame):
vartype: str vartype: str
): ):
if vartype not in self._literalWidgets: if vartype not in self.literal_widgets:
vartype = "String" vartype = "String"
self.LiteralStack.setCurrentWidget(self._literalWidgets[vartype]) self.LiteralStack.setCurrentWidget(self.literal_widgets[vartype])
def toScript( def toScript(
self self
) -> str: ) -> str:
data = self.LeftVarCombo.currentData() data = self.LeftVarCombo.currentData()
if self._isBoolMode and data: if self._is_bool_mode and data:
return data[0] return data[0]
if not data: if not data:
return "" return ""
@@ -183,28 +183,28 @@ class ConditionRowFrame(QFrame):
name = "datenow()" name = "datenow()"
elif name == "CURRENT_TIME": elif name == "CURRENT_TIME":
name = "timenow()" name = "timenow()"
opSym = self.OpCombo.currentData() op_sym = self.OpCombo.currentData()
if self._rawRhsExpr: if self._raw_rhs_expr:
return f"{name} {opSym} {self._rawRhsExpr}" return f"{name} {op_sym} {self._raw_rhs_expr}"
isVarRef = (self._CompTypeCombo.currentData() == "variable") is_var_ref = (self.CompTypeCombo.currentData() == "variable")
if isVarRef: if is_var_ref:
rd = self.RhsVarCombo.currentData() rd = self.RhsVarCombo.currentData()
if rd: if rd:
rhsName = rd[0] rhs_name = rd[0]
if rhsName == "CURRENT_DATE": if rhs_name == "CURRENT_DATE":
rhsName = "datenow()" rhs_name = "datenow()"
elif rhsName == "CURRENT_TIME": elif rhs_name == "CURRENT_TIME":
rhsName = "timenow()" rhs_name = "timenow()"
return f"{name} {opSym} {rhsName}" return f"{name} {op_sym} {rhs_name}"
rhsText = self.RhsVarCombo.currentText().strip() rhs_text = self.RhsVarCombo.currentText().strip()
if rhsText: if rhs_text:
return f"{name} {opSym} {rhsText}" return f"{name} {op_sym} {rhs_text}"
return "" return ""
w = self._literalWidgets.get(vartype) w = self.literal_widgets.get(vartype)
if w: if w:
rawVal = getValueFromWidget(w) raw_val = getValueFromWidget(w)
encoded = encodeValueStr(rawVal, vartype) encoded = encodeValueStr(raw_val, vartype)
return f"{name} {opSym} {encoded}" return f"{name} {op_sym} {encoded}"
return "" return ""
def refreshVarCombos( def refreshVarCombos(
@@ -220,19 +220,19 @@ class ConditionRowFrame(QFrame):
idx idx
): ):
self._rawRhsExpr = "" self._raw_rhs_expr = ""
if idx < 0: if idx < 0:
return return
data = self.LeftVarCombo.itemData(idx) data = self.LeftVarCombo.itemData(idx)
if not data: if not data:
return return
name, vartype = data name, vartype = data
isBool = name in ("true", "false") is_bool = name in ("true", "false")
self._isBoolMode = isBool self._is_bool_mode = is_bool
self.OpCombo.setVisible(not isBool) self.OpCombo.setVisible(not is_bool)
self._CompTypeCombo.setVisible(not isBool) self.CompTypeCombo.setVisible(not is_bool)
self.RhsStack.setVisible(not isBool) self.RhsStack.setVisible(not is_bool)
if not isBool: if not is_bool:
self.updateRHSLiteralWidget(vartype) self.updateRHSLiteralWidget(vartype)
@Slot(int) @Slot(int)
@@ -241,8 +241,8 @@ class ConditionRowFrame(QFrame):
idx idx
): ):
self._rawRhsExpr = "" self._raw_rhs_expr = ""
isVar = (self._CompTypeCombo.currentData() == "variable") isVar = (self.CompTypeCombo.currentData() == "variable")
self.RhsStack.setCurrentIndex(1 if isVar else 0) self.RhsStack.setCurrentIndex(1 if isVar else 0)
if isVar: if isVar:
self.populateRHSVarCombo() self.populateRHSVarCombo()
@@ -252,15 +252,15 @@ class ActionStepFrame(QFrame):
def __init__( def __init__(
self, self,
varMgr, var_mgr,
parentBlockIndex: int = 0, parent_block_index: int = 0,
parent = None parent = None
): ):
super().__init__(parent) super().__init__(parent)
self._varMgr = varMgr self._var_mgr = var_mgr
self._blockIndex = parentBlockIndex self._block_index = parent_block_index
self._currentTargetType = "String" self._current_target_type = "String"
self.setupUi() self.setupUi()
self.connectSignals() self.connectSignals()
@@ -312,7 +312,7 @@ class ActionStepFrame(QFrame):
for p in getPresetVars(): for p in getPresetVars():
if p["name"] in ("CURRENT_TIME", "CURRENT_DATE"): if p["name"] in ("CURRENT_TIME", "CURRENT_DATE"):
continue continue
info = self._varMgr.getInfoByName(p["name"]) info = self._var_mgr.getInfoByName(p["name"])
if info: if info:
self.TargetCombo.addItem( self.TargetCombo.addItem(
info["display"], info["display"],
@@ -324,18 +324,18 @@ class ActionStepFrame(QFrame):
self self
): ):
self._literalWidgets = {} self.literal_widgets = {}
self._offsetWidgets = {} self.offset_widgets = {}
for vt in getTypeOrder(): for vt in getTypeOrder():
self._literalWidgets[vt] = makeValueWidget(vt, self.ValueStack) self.literal_widgets[vt] = makeValueWidget(vt, self.ValueStack)
self.ValueStack.addWidget(self._literalWidgets[vt]) self.ValueStack.addWidget(self.literal_widgets[vt])
if getArithType(vt): if getArithType(vt):
self._offsetWidgets[vt] = makeOffsetWidget(vt, self.ValueStack) self.offset_widgets[vt] = makeOffsetWidget(vt, self.ValueStack)
self.ValueStack.addWidget(self._offsetWidgets[vt]) self.ValueStack.addWidget(self.offset_widgets[vt])
else: else:
Lbl = QLabel("(不支持该操作)", self.ValueStack) Lbl = QLabel("(不支持该操作)", self.ValueStack)
Lbl.setFixedHeight(25) Lbl.setFixedHeight(25)
self._offsetWidgets[vt] = Lbl self.offset_widgets[vt] = Lbl
self.ValueStack.addWidget(Lbl) self.ValueStack.addWidget(Lbl)
def connectSignals( def connectSignals(
@@ -358,14 +358,14 @@ class ActionStepFrame(QFrame):
): ):
op = self.OpTypeCombo.currentData() op = self.OpTypeCombo.currentData()
isArith = (op in ("add", "sub")) is_arith = (op in ("add", "sub"))
actualType = self._currentTargetType actual_type = self._current_target_type
if isArith and actualType in self._offsetWidgets: if is_arith and actual_type in self.offset_widgets:
self.ValueStack.setCurrentWidget(self._offsetWidgets[actualType]) self.ValueStack.setCurrentWidget(self.offset_widgets[actual_type])
elif actualType in self._literalWidgets: elif actual_type in self.literal_widgets:
self.ValueStack.setCurrentWidget(self._literalWidgets[actualType]) self.ValueStack.setCurrentWidget(self.literal_widgets[actual_type])
else: else:
self.ValueStack.setCurrentWidget(self._literalWidgets.get("String")) self.ValueStack.setCurrentWidget(self.literal_widgets.get("String"))
def toScript( def toScript(
self self
@@ -380,10 +380,10 @@ class ActionStepFrame(QFrame):
return " -- pass" return " -- pass"
if not target: if not target:
return "" return ""
rawVal = self.getValueRaw() raw_val = self.getValueRaw()
vartype = self._currentTargetType vartype = self._current_target_type
if op == "set": if op == "set":
encoded = encodeValueStr(rawVal, vartype) encoded = encodeValueStr(raw_val, vartype)
return f" {target} = {encoded}" return f" {target} = {encoded}"
elif op == "add": elif op == "add":
if vartype == "Date" and hasattr(self.ValueStack.currentWidget(), "getOffsetDays"): 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"): if vartype == "Time" and hasattr(self.ValueStack.currentWidget(), "getOffsetHours"):
hours = self.ValueStack.currentWidget().getOffsetHours() hours = self.ValueStack.currentWidget().getOffsetHours()
return f" {target} = timeadd({target}, {hours})" return f" {target} = timeadd({target}, {hours})"
return f" {target} = {target} + {rawVal}" return f" {target} = {target} + {raw_val}"
elif op == "sub": elif op == "sub":
if vartype == "Date" and hasattr(self.ValueStack.currentWidget(), "getOffsetDays"): if vartype == "Date" and hasattr(self.ValueStack.currentWidget(), "getOffsetDays"):
days = 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"): if vartype == "Time" and hasattr(self.ValueStack.currentWidget(), "getOffsetHours"):
hours = self.ValueStack.currentWidget().getOffsetHours() hours = self.ValueStack.currentWidget().getOffsetHours()
return f" {target} = timeadd({target}, -{hours})" return f" {target} = timeadd({target}, -{hours})"
return f" {target} = {target} - {rawVal}" return f" {target} = {target} - {raw_val}"
return "" return ""
def getValueRaw( def getValueRaw(
@@ -419,15 +419,15 @@ class ActionStepFrame(QFrame):
self self
): ):
currentData = self.TargetCombo.currentData() current_data = self.TargetCombo.currentData()
self.populateTargetCombo() self.populateTargetCombo()
if currentData: if current_data:
for i in range(self.TargetCombo.count()): for i in range(self.TargetCombo.count()):
d = self.TargetCombo.itemData(i) d = self.TargetCombo.itemData(i)
if d and d[0] == currentData[0]: if d and d[0] == current_data[0]:
self.TargetCombo.setCurrentIndex(i) self.TargetCombo.setCurrentIndex(i)
break break
self._varMgr.populateCombo(self.ExistingVarCombo) self._var_mgr.populateCombo(self.ExistingVarCombo)
@Slot(int) @Slot(int)
def onTargetChanged( def onTargetChanged(
@@ -441,7 +441,7 @@ class ActionStepFrame(QFrame):
if not data: if not data:
return return
_, vartype = data _, vartype = data
self._currentTargetType = vartype self._current_target_type = vartype
self.updateValueWidget() self.updateValueWidget()
self.onValueSrcChanged(self.ValueSrcCombo.currentIndex()) self.onValueSrcChanged(self.ValueSrcCombo.currentIndex())
@@ -459,10 +459,10 @@ class ActionStepFrame(QFrame):
idx idx
): ):
isVar = (self.ValueSrcCombo.currentData() == "variable") is_var = (self.ValueSrcCombo.currentData() == "variable")
self.ValueStack.setVisible(not isVar) self.ValueStack.setVisible(not is_var)
self.ExistingVarCombo.setVisible(isVar) self.ExistingVarCombo.setVisible(is_var)
if isVar: if is_var:
self._varMgr.populateCombo(self.ExistingVarCombo) self._var_mgr.populateCombo(self.ExistingVarCombo)
else: else:
self.updateValueWidget() self.updateValueWidget()
+394
View File
@@ -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(
"<div style='font-size: 14px;'>{content}</div>".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)
+188
View File
@@ -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)
+74 -75
View File
@@ -364,18 +364,18 @@ class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget):
user_config = self.defaultUserConfig() user_config = self.defaultUserConfig()
for i in range(self.UserTreeWidget.topLevelItemCount()): for i in range(self.UserTreeWidget.topLevelItemCount()):
GroupItem = self.UserTreeWidget.topLevelItem(i) group_item = self.UserTreeWidget.topLevelItem(i)
group_config = { group_config = {
"name": GroupItem.text(0), "name": group_item.text(0),
"enabled": GroupItem.checkState(1) == Qt.CheckState.Checked, "enabled": group_item.checkState(1) == Qt.CheckState.Checked,
"users": [] "users": []
} }
for j in range(GroupItem.childCount()): for j in range(group_item.childCount()):
UserItem = GroupItem.child(j) user_item = group_item.child(j)
user = UserItem.data(0, Qt.UserRole) user = user_item.data(0, Qt.UserRole)
if not user: if not user:
continue continue
user["enabled"] = UserItem.checkState(1) == Qt.CheckState.Checked user["enabled"] = user_item.checkState(1) == Qt.CheckState.Checked
group_config["users"].append(user) group_config["users"].append(user)
user_config["groups"].append(group_config) user_config["groups"].append(group_config)
return user_config return user_config
@@ -431,18 +431,18 @@ class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget):
try: try:
if "groups" in users: if "groups" in users:
for group_config in users["groups"]: for group_config in users["groups"]:
GroupItem = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value) group_item = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value)
GroupItem.setText(0, group_config["name"]) group_item.setText(0, group_config["name"])
GroupItem.setFlags(GroupItem.flags() | Qt.ItemIsEditable) group_item.setFlags(group_item.flags() | Qt.ItemIsEditable)
GroupItem.setCheckState(1, Qt.Checked if group_config.get("enabled", True) else Qt.Unchecked) group_item.setCheckState(1, Qt.Checked if group_config.get("enabled", True) else Qt.Unchecked)
for user_config in group_config["users"]: for user_config in group_config["users"]:
UserItem = QTreeWidgetItem(GroupItem, ALUserTreeItemType.USER.value) user_item = QTreeWidgetItem(group_item, ALUserTreeItemType.USER.value)
UserItem.setText(0, user_config["username"]) user_item.setText(0, user_config["username"])
UserItem.setText(1, "" if user_config.get("enabled", True) else "跳过") user_item.setText(1, "" if user_config.get("enabled", True) else "跳过")
UserItem.setData(0, Qt.UserRole, user_config) user_item.setData(0, Qt.UserRole, user_config)
UserItem.setCheckState(1, Qt.Checked if user_config.get("enabled", True) else Qt.Unchecked) user_item.setCheckState(1, Qt.Checked if user_config.get("enabled", True) else Qt.Unchecked)
UserItem.setDisabled(not group_config.get("enabled", True)) user_item.setDisabled(not group_config.get("enabled", True))
GroupItem.setExpanded(True) group_item.setExpanded(True)
except KeyError as e: except KeyError as e:
QMessageBox.warning( QMessageBox.warning(
self, self,
@@ -616,43 +616,43 @@ class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget):
) -> QTreeWidgetItem: ) -> QTreeWidgetItem:
self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged) 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: if not group_name:
group_name = f"新分组-{self.UserTreeWidget.topLevelItemCount()}" group_name = f"新分组-{self.UserTreeWidget.topLevelItemCount()}"
GroupItem.setText(0, group_name) group_item.setText(0, group_name)
GroupItem.setFlags(GroupItem.flags() | Qt.ItemIsEditable) group_item.setFlags(group_item.flags() | Qt.ItemIsEditable)
GroupItem.setCheckState(1, Qt.Checked) group_item.setCheckState(1, Qt.Checked)
self.UserTreeWidget.setCurrentItem(GroupItem) self.UserTreeWidget.setCurrentItem(group_item)
self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged) self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged)
return GroupItem return group_item
def delGroup( def delGroup(
self, self,
GroupItem: QTreeWidgetItem = None group_item: QTreeWidgetItem = None
): ):
if GroupItem is None: if group_item is None:
return return
if GroupItem.type() != ALUserTreeItemType.GROUP.value: if group_item.type() != ALUserTreeItemType.GROUP.value:
return return
index = self.UserTreeWidget.indexOfTopLevelItem(GroupItem) index = self.UserTreeWidget.indexOfTopLevelItem(group_item)
self.UserTreeWidget.takeTopLevelItem(index) self.UserTreeWidget.takeTopLevelItem(index)
def addUser( def addUser(
self, self,
GroupItem: QTreeWidgetItem = None group_item: QTreeWidgetItem = None
) -> QTreeWidgetItem: ) -> QTreeWidgetItem:
if GroupItem is None: if group_item is None:
CurrentItem = self.UserTreeWidget.currentItem() current_item = self.UserTreeWidget.currentItem()
if CurrentItem is None: if current_item is None:
GroupItem = self.addGroup() group_item = self.addGroup()
if GroupItem.type() == ALUserTreeItemType.USER.value: if group_item.type() == ALUserTreeItemType.USER.value:
GroupItem = GroupItem.parent() group_item = group_item.parent()
if GroupItem.checkState(1) == Qt.CheckState.Unchecked: if group_item.checkState(1) == Qt.CheckState.Unchecked:
return None return None
new_user = { new_user = {
"username": f"新用户-{GroupItem.childCount()}", "username": f"新用户-{group_item.childCount()}",
"password": "000000", "password": "000000",
"enabled": True, "enabled": True,
"reserve_info": { "reserve_info": {
@@ -681,30 +681,30 @@ class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget):
} }
} }
self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged) self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged)
UserItem = QTreeWidgetItem(GroupItem, ALUserTreeItemType.USER.value) user_item = QTreeWidgetItem(group_item, ALUserTreeItemType.USER.value)
UserItem.setText(0, new_user["username"]) user_item.setText(0, new_user["username"])
UserItem.setText(1, "") user_item.setText(1, "")
UserItem.setData(0, Qt.UserRole, new_user) user_item.setData(0, Qt.UserRole, new_user)
UserItem.setCheckState(1, Qt.CheckState.Checked) user_item.setCheckState(1, Qt.CheckState.Checked)
GroupItem.setExpanded(True) group_item.setExpanded(True)
self.UserTreeWidget.setCurrentItem(UserItem) self.UserTreeWidget.setCurrentItem(user_item)
self.setUserToWidget(new_user) self.setUserToWidget(new_user)
self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged) self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged)
return UserItem return user_item
def delUser( def delUser(
self, self,
UserItem: QTreeWidgetItem = None user_item: QTreeWidgetItem = None
): ):
if UserItem is None: if user_item is None:
return return
if UserItem.type() != ALUserTreeItemType.USER.value: if user_item.type() != ALUserTreeItemType.USER.value:
return return
ParentItem = UserItem.parent() parent_item = user_item.parent()
index = ParentItem.indexOfChild(UserItem) index = parent_item.indexOfChild(user_item)
ParentItem.takeChild(index) parent_item.takeChild(index)
if ParentItem.childCount() == 0: if parent_item.childCount() == 0:
self.UserTreeWidget.setCurrentItem(None) self.UserTreeWidget.setCurrentItem(None)
def renameItem( def renameItem(
@@ -822,10 +822,10 @@ class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget):
if item.type() == ALUserTreeItemType.GROUP.value: if item.type() == ALUserTreeItemType.GROUP.value:
is_checked = item.checkState(1) == Qt.CheckState.Checked is_checked = item.checkState(1) == Qt.CheckState.Checked
for i in range(item.childCount()): for i in range(item.childCount()):
Child = item.child(i) child = item.child(i)
if self.UserTreeWidget.currentItem() == Child: if self.UserTreeWidget.currentItem() == child:
self.UserTreeWidget.setCurrentItem(item) self.UserTreeWidget.setCurrentItem(item)
Child.setDisabled(not is_checked) child.setDisabled(not is_checked)
else: else:
is_checked = item.checkState(1) == Qt.CheckState.Checked is_checked = item.checkState(1) == Qt.CheckState.Checked
item.setText(1, "" if is_checked else "跳过") item.setText(1, "" if is_checked else "跳过")
@@ -842,32 +842,32 @@ class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget):
def showGroupMenu( def showGroupMenu(
self, self,
menu: QMenu, menu: QMenu,
GroupItem: QTreeWidgetItem = None group_item: QTreeWidgetItem = None
): ):
AddUserAction = QAction("添加用户", menu) AddUserAction = QAction("添加用户", menu)
RenameGroupAction = QAction("重命名分组", menu) RenameGroupAction = QAction("重命名分组", menu)
DelGroupAction = QAction("删除分组", menu) DelGroupAction = QAction("删除分组", menu)
AddUserAction.triggered.connect(lambda: self.addUser(GroupItem)) AddUserAction.triggered.connect(lambda: self.addUser(group_item))
RenameGroupAction.triggered.connect(lambda: self.renameItem(GroupItem)) RenameGroupAction.triggered.connect(lambda: self.renameItem(group_item))
DelGroupAction.triggered.connect(lambda: self.delGroup(GroupItem)) DelGroupAction.triggered.connect(lambda: self.delGroup(group_item))
menu.addAction(AddUserAction) menu.addAction(AddUserAction)
menu.addSeparator() menu.addSeparator()
menu.addAction(RenameGroupAction) menu.addAction(RenameGroupAction)
menu.addAction(DelGroupAction) menu.addAction(DelGroupAction)
if GroupItem.checkState(1) == Qt.CheckState.Unchecked: if group_item.checkState(1) == Qt.CheckState.Unchecked:
AddUserAction.setEnabled(False) AddUserAction.setEnabled(False)
def showUserMenu( def showUserMenu(
self, self,
menu: QMenu, menu: QMenu,
UserItem: QTreeWidgetItem = None user_item: QTreeWidgetItem = None
): ):
RenameUserAction = QAction("重命名用户", menu) RenameUserAction = QAction("重命名用户", menu)
DelUserAction = QAction("删除用户", menu) DelUserAction = QAction("删除用户", menu)
RenameUserAction.triggered.connect(lambda: self.renameItem(UserItem)) RenameUserAction.triggered.connect(lambda: self.renameItem(user_item))
DelUserAction.triggered.connect(lambda: self.delUser(UserItem)) DelUserAction.triggered.connect(lambda: self.delUser(user_item))
menu.addAction(RenameUserAction) menu.addAction(RenameUserAction)
menu.addAction(DelUserAction) menu.addAction(DelUserAction)
@@ -877,14 +877,14 @@ class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget):
pos pos
): ):
CurrentItem = self.UserTreeWidget.itemAt(pos) current_item = self.UserTreeWidget.itemAt(pos)
Menu = QMenu(self.UserTreeWidget) Menu = QMenu(self.UserTreeWidget)
if CurrentItem is None: if current_item is None:
self.showTreeMenu(Menu) self.showTreeMenu(Menu)
elif CurrentItem.type() == ALUserTreeItemType.GROUP.value: elif current_item.type() == ALUserTreeItemType.GROUP.value:
self.showGroupMenu(Menu, CurrentItem) self.showGroupMenu(Menu, current_item)
else: else:
self.showUserMenu(Menu, CurrentItem) self.showUserMenu(Menu, current_item)
Menu.exec_(self.UserTreeWidget.mapToGlobal(pos)) Menu.exec_(self.UserTreeWidget.mapToGlobal(pos))
@Slot() @Slot()
@@ -892,16 +892,16 @@ class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget):
self self
): ):
CurrentItem = self.UserTreeWidget.currentItem() current_item = self.UserTreeWidget.currentItem()
self.addUser(CurrentItem) self.addUser(current_item)
@Slot() @Slot()
def onDelUserButtonClicked( def onDelUserButtonClicked(
self self
): ):
CurrentItem = self.UserTreeWidget.currentItem() current_item = self.UserTreeWidget.currentItem()
self.delUser(CurrentItem) self.delUser(current_item)
@Slot() @Slot()
def onBrowseBrowserDriverButtonClicked( def onBrowseBrowserDriverButtonClicked(
@@ -1034,7 +1034,6 @@ class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget):
): ):
msg = "" msg = ""
run_config_path = self.ExportRunConfigEdit.text() run_config_path = self.ExportRunConfigEdit.text()
user_config_path = self.ExportUserConfigEdit.text() user_config_path = self.ExportUserConfigEdit.text()
if run_config_path: if run_config_path:
@@ -1111,8 +1110,8 @@ class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget):
self self
): ):
CurrentItem = self.UserTreeWidget.currentItem() current_item = self.UserTreeWidget.currentItem()
if CurrentItem and CurrentItem.type() == ALUserTreeItemType.USER.value: if current_item and current_item.type() == ALUserTreeItemType.USER.value:
self.UserTreeWidget.setCurrentItem(None) self.UserTreeWidget.setCurrentItem(None)
if self.saveConfigs( if self.saveConfigs(
self.__config_paths["run"], self.__config_paths["run"],
+105 -44
View File
@@ -32,6 +32,8 @@ from PySide6.QtWidgets import (
from base.MsgBase import MsgBase from base.MsgBase import MsgBase
from gui.ALAboutDialog import ALAboutDialog from gui.ALAboutDialog import ALAboutDialog
from gui.ALBulletinDialog import ALBulletinDialog
from gui.ALBulletinPoller import ALBulletinPoller
from gui.ALConfigWidget import ALConfigWidget from gui.ALConfigWidget import ALConfigWidget
from gui.ALSettingsWidget import ALSettingsWidget from gui.ALSettingsWidget import ALSettingsWidget
from gui.ALMainWorkers import ( from gui.ALMainWorkers import (
@@ -41,6 +43,7 @@ from gui.ALMainWorkers import (
from gui.ALTimerTaskManageWidget import ALTimerTaskManageWidget from gui.ALTimerTaskManageWidget import ALTimerTaskManageWidget
from gui.resources import ALResource from gui.resources import ALResource
from gui.resources.ui.Ui_ALMainWindow import Ui_ALMainWindow from gui.resources.ui.Ui_ALMainWindow import Ui_ALMainWindow
from managers.bulletin.BulletinManager import instance as bulletinInstance
from managers.config.ConfigUtils import ConfigUtils from managers.config.ConfigUtils import ConfigUtils
@@ -59,9 +62,11 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
QMainWindow.__init__(self) QMainWindow.__init__(self)
self.__timer_task_queue = queue.Queue() self.__timer_task_queue = queue.Queue()
self.__config_paths = ConfigUtils.getAutomationConfigPaths() self.__config_paths = ConfigUtils.getAutomationConfigPaths()
self.__alTimerTaskManageWidget = None self.__ALTimerTaskManageWidget = None
self.__alConfigWidget = None self.__ALConfigWidget = None
self.__alSettingsWidget = None self.__ALSettingsWidget = None
self.__ALBulletinDialog = None
self.__bulletin_poller = ALBulletinPoller(self)
self.__auto_lib_thread = None self.__auto_lib_thread = None
self.__current_timer_task_thread = None self.__current_timer_task_thread = None
self.__is_running_timer_task = False self.__is_running_timer_task = False
@@ -72,6 +77,11 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.connectSignals() self.connectSignals()
self.startMsgPolling() self.startMsgPolling()
self.startTimerTaskPolling() self.startTimerTaskPolling()
self.__bulletin_poller.start()
if bulletinInstance().autoFetch():
QTimer.singleShot(1000, self.__bulletin_poller.fetchNow)
self._showLog("主窗口初始化完成") self._showLog("主窗口初始化完成")
def modifyUi( def modifyUi(
@@ -84,26 +94,28 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.ManualAction.triggered.connect(self.onManualActionTriggered) self.ManualAction.triggered.connect(self.onManualActionTriggered)
self.AboutAction.triggered.connect(self.onAboutActionTriggered) self.AboutAction.triggered.connect(self.onAboutActionTriggered)
self.SettingsAction.triggered.connect(self.onSettingsActionTriggered) self.SettingsAction.triggered.connect(self.onSettingsActionTriggered)
if hasattr(self, 'BulletinAction'):
self.BulletinAction.triggered.connect(self.onBulletinActionTriggered)
# initialize timer task widget, but not show it # initialize timer task widget, but not show it
try: try:
self.__alTimerTaskManageWidget = ALTimerTaskManageWidget(self) self.__ALTimerTaskManageWidget = ALTimerTaskManageWidget(self)
except Exception as e: except Exception as e:
QMessageBox.critical( QMessageBox.critical(
self, self,
"错误 - AutoLibrary", "错误 - AutoLibrary",
f"初始化定时任务功能失败: \n{e}" f"初始化定时任务功能失败: \n{e}"
) )
self.__alTimerTaskManageWidget = None self.__ALTimerTaskManageWidget = None
self.TimerTaskManageWidgetButton.setEnabled(False) self.TimerTaskManageWidgetButton.setEnabled(False)
self.TimerTaskManageWidgetButton.setToolTip("定时任务功能初始化失败, 请检查配置文件。") self.TimerTaskManageWidgetButton.setToolTip("定时任务功能初始化失败, 请检查配置文件。")
return return
self.timerTaskIsRunning.connect(self.__alTimerTaskManageWidget.onTimerTaskIsRunning) self.timerTaskIsRunning.connect(self.__ALTimerTaskManageWidget.onTimerTaskIsRunning)
self.timerTaskIsExecuted.connect(self.__alTimerTaskManageWidget.onTimerTaskIsExecuted) self.timerTaskIsExecuted.connect(self.__ALTimerTaskManageWidget.onTimerTaskIsExecuted)
self.timerTaskIsError.connect(self.__alTimerTaskManageWidget.onTimerTaskIsError) self.timerTaskIsError.connect(self.__ALTimerTaskManageWidget.onTimerTaskIsError)
self.__alTimerTaskManageWidget.timerTaskIsReady.connect(self.onTimerTaskIsReady) self.__ALTimerTaskManageWidget.timerTaskIsReady.connect(self.onTimerTaskIsReady)
self.__alTimerTaskManageWidget.timerTaskManageWidgetIsClosed.connect(self.onTimerTaskManageWidgetClosed) self.__ALTimerTaskManageWidget.timerTaskManageWidgetIsClosed.connect(self.onTimerTaskManageWidgetClosed)
self.__alTimerTaskManageWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint) self.__ALTimerTaskManageWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint)
def onAboutActionTriggered( def onAboutActionTriggered(
self self
@@ -116,8 +128,8 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self self
): ):
Url = QUrl("https://www.autolibrary.kenanzhu.com/manuals") url = QUrl("https://www.autolibrary.kenanzhu.com/manuals")
QDesktopServices.openUrl(Url) QDesktopServices.openUrl(url)
def setupTray( def setupTray(
self self
@@ -131,12 +143,13 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.TrayMenu = QMenu() self.TrayMenu = QMenu()
self.TrayMenu.addAction("显示主窗口", self.showNormal) self.TrayMenu.addAction("显示主窗口", self.showNormal)
self.TrayMenu.addAction("显示定时窗口", self.onTimerTaskManageWidgetButtonClicked) self.TrayMenu.addAction("显示定时窗口", self.onTimerTaskManageWidgetButtonClicked)
self.TrayMenu.addAction("公告栏", self.onBulletinActionTriggered)
self.TrayMenu.addAction("最小化到托盘", self.hideToTray) self.TrayMenu.addAction("最小化到托盘", self.hideToTray)
self.TrayMenu.addSeparator() self.TrayMenu.addSeparator()
self.TrayMenu.addAction("退出", self.close) self.TrayMenu.addAction("退出", self.close)
self.TrayIcon.setContextMenu(self.TrayMenu) self.TrayIcon.setContextMenu(self.TrayMenu)
self.TrayIcon.activated.connect(self.onTrayIconActivated) self.TrayIcon.activated.connect(self.onTrayIconActivated)
self.TrayIcon.messageClicked.connect(self.onBulletinActionTriggered)
self.TrayIcon.show() self.TrayIcon.show()
def hideToTray( def hideToTray(
@@ -169,6 +182,9 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.StopButton.clicked.connect(self.onStopButtonClicked) self.StopButton.clicked.connect(self.onStopButtonClicked)
self.SendButton.clicked.connect(self.onSendButtonClicked) self.SendButton.clicked.connect(self.onSendButtonClicked)
self.MessageEdit.returnPressed.connect(self.onSendButtonClicked) self.MessageEdit.returnPressed.connect(self.onSendButtonClicked)
self.__bulletin_poller.newBulletinsDetected.connect(
self._onBulletinPollerNewBulletins
)
def closeEvent( def closeEvent(
self, self,
@@ -186,15 +202,20 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
if self.__is_running_timer_task: if self.__is_running_timer_task:
self.__current_timer_task_thread.wait(2000) self.__current_timer_task_thread.wait(2000)
self.__current_timer_task_thread.deleteLater() self.__current_timer_task_thread.deleteLater()
if self.__alTimerTaskManageWidget: if self.__ALTimerTaskManageWidget:
self.__alTimerTaskManageWidget.close() self.__ALTimerTaskManageWidget.close()
self.__alTimerTaskManageWidget.deleteLater() self.__ALTimerTaskManageWidget.deleteLater()
if self.__alConfigWidget: if self.__ALConfigWidget:
self.__alConfigWidget.close() self.__ALConfigWidget.close()
# the config widget is already deleted in the 'self.onConfigWidgetClosed' # the config widget is already deleted in the 'self.onConfigWidgetClosed'
if self.__alSettingsWidget: if self.__ALSettingsWidget:
self.__alSettingsWidget.close() self.__ALSettingsWidget.close()
# the settings widget is already deleted in the 'self.onSettingsWidgetClosed' # 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("主窗口关闭") self._showLog("主窗口关闭")
QMainWindow.closeEvent(self, event) QMainWindow.closeEvent(self, event)
@@ -299,10 +320,10 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self self
): ):
if self.__alConfigWidget: if self.__ALConfigWidget:
self.__alConfigWidget.configWidgetIsClosed.disconnect(self.onConfigWidgetClosed) self.__ALConfigWidget.configWidgetIsClosed.disconnect(self.onConfigWidgetClosed)
self.__alConfigWidget.deleteLater() self.__ALConfigWidget.deleteLater()
self.__alConfigWidget = None self.__ALConfigWidget = None
self.__config_paths = ConfigUtils.getAutomationConfigPaths() self.__config_paths = ConfigUtils.getAutomationConfigPaths()
self.setControlButtons(True, None, None) self.setControlButtons(True, None, None)
self._showLog("配置窗口已关闭,配置文件路径已更新") self._showLog("配置窗口已关闭,配置文件路径已更新")
@@ -312,23 +333,63 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self self
): ):
if self.__alSettingsWidget: if self.__ALSettingsWidget:
self.__alSettingsWidget.settingsWidgetIsClosed.disconnect(self.onSettingsWidgetClosed) self.__ALSettingsWidget.settingsWidgetIsClosed.disconnect(self.onSettingsWidgetClosed)
self.__alSettingsWidget.deleteLater() self.__ALSettingsWidget.deleteLater()
self.__alSettingsWidget = None self.__ALSettingsWidget = None
self.SettingsAction.setEnabled(True) 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() @Slot()
def onSettingsActionTriggered( def onSettingsActionTriggered(
self self
): ):
if self.__alSettingsWidget is None: if self.__ALSettingsWidget is None:
self.__alSettingsWidget = ALSettingsWidget(self) self.__ALSettingsWidget = ALSettingsWidget(self)
self.__alSettingsWidget.settingsWidgetIsClosed.connect(self.onSettingsWidgetClosed) self.__ALSettingsWidget.settingsWidgetIsClosed.connect(self.onSettingsWidgetClosed)
self.__alSettingsWidget.show() self.__ALSettingsWidget.show()
self.__alSettingsWidget.raise_() self.__ALSettingsWidget.raise_()
self.__alSettingsWidget.activateWindow() self.__ALSettingsWidget.activateWindow()
self.SettingsAction.setEnabled(False) self.SettingsAction.setEnabled(False)
self._showLog("打开全局设置窗口") self._showLog("打开全局设置窗口")
@@ -374,9 +435,9 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self self
): ):
self.__alTimerTaskManageWidget.show() self.__ALTimerTaskManageWidget.show()
self.__alTimerTaskManageWidget.raise_() self.__ALTimerTaskManageWidget.raise_()
self.__alTimerTaskManageWidget.activateWindow() self.__ALTimerTaskManageWidget.activateWindow()
self.TimerTaskManageWidgetButton.setEnabled(False) self.TimerTaskManageWidgetButton.setEnabled(False)
self._showLog("打开定时任务管理窗口") self._showLog("打开定时任务管理窗口")
@@ -385,12 +446,12 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self self
): ):
if self.__alConfigWidget is None: if self.__ALConfigWidget is None:
self.__alConfigWidget = ALConfigWidget(self) self.__ALConfigWidget = ALConfigWidget(self)
self.__alConfigWidget.configWidgetIsClosed.connect(self.onConfigWidgetClosed) self.__ALConfigWidget.configWidgetIsClosed.connect(self.onConfigWidgetClosed)
self.__alConfigWidget.show() self.__ALConfigWidget.show()
self.__alConfigWidget.raise_() self.__ALConfigWidget.raise_()
self.__alConfigWidget.activateWindow() self.__ALConfigWidget.activateWindow()
self.ConfigButton.setEnabled(False) self.ConfigButton.setEnabled(False)
self._showLog("打开配置窗口") self._showLog("打开配置窗口")
-4
View File
@@ -55,7 +55,6 @@ class ALSeatMapSelectDialog(CenterOnParentMixin, QDialog):
self.setMinimumSize(800, 600) self.setMinimumSize(800, 600)
self.resize(800, 600) self.resize(800, 600)
self.setWindowTitle(f"选择楼层座位 - AutoLibrary") self.setWindowTitle(f"选择楼层座位 - AutoLibrary")
self.SeatMapWidgetMainLayout = QVBoxLayout(self) self.SeatMapWidgetMainLayout = QVBoxLayout(self)
self.SeatMapWidgetMainLayout.setContentsMargins(5, 5, 5, 5) self.SeatMapWidgetMainLayout.setContentsMargins(5, 5, 5, 5)
self.SeatMapWidgetMainLayout.setSpacing(5) self.SeatMapWidgetMainLayout.setSpacing(5)
@@ -63,10 +62,8 @@ class ALSeatMapSelectDialog(CenterOnParentMixin, QDialog):
self.TitleLabel.setAlignment(Qt.AlignmentFlag.AlignCenter) self.TitleLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.TitleLabel.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;") self.TitleLabel.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
self.SeatMapWidgetMainLayout.addWidget(self.TitleLabel) self.SeatMapWidgetMainLayout.addWidget(self.TitleLabel)
self.SeatMapGraphicsView = ALSeatMapView(None, self.__seats_data) self.SeatMapGraphicsView = ALSeatMapView(None, self.__seats_data)
self.SeatMapWidgetMainLayout.addWidget(self.SeatMapGraphicsView) self.SeatMapWidgetMainLayout.addWidget(self.SeatMapGraphicsView)
self.TipsLabel = QLabel( self.TipsLabel = QLabel(
" 点击座位进行选择/取消选择, 最多选择1个座位 \n" " 点击座位进行选择/取消选择, 最多选择1个座位 \n"
" [操作方法: Ctrl+鼠标滚轮缩放 | 滚轮/拖拽/方向键 移动]" " [操作方法: Ctrl+鼠标滚轮缩放 | 滚轮/拖拽/方向键 移动]"
@@ -74,7 +71,6 @@ class ALSeatMapSelectDialog(CenterOnParentMixin, QDialog):
self.TipsLabel.setAlignment(Qt.AlignmentFlag.AlignLeft) self.TipsLabel.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.TipsLabel.setStyleSheet("color: #666; margin: 5px;") self.TipsLabel.setStyleSheet("color: #666; margin: 5px;")
self.SeatMapWidgetMainLayout.addWidget(self.TipsLabel) self.SeatMapWidgetMainLayout.addWidget(self.TipsLabel)
self.ConfirmButton = QPushButton("确认") self.ConfirmButton = QPushButton("确认")
self.ConfirmButton.setFixedSize(80, 25) self.ConfirmButton.setFixedSize(80, 25)
self.ConfirmButton.setAutoDefault(True) self.ConfirmButton.setAutoDefault(True)
-2
View File
@@ -85,11 +85,9 @@ class ALSeatMapView(QGraphicsView):
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.viewport().installEventFilter(self) self.viewport().installEventFilter(self)
self.SeatsContainerWidget = QWidget() self.SeatsContainerWidget = QWidget()
self.SeatsContainerLayout = QGridLayout(self.SeatsContainerWidget) self.SeatsContainerLayout = QGridLayout(self.SeatsContainerWidget)
self.setupSeatMap() self.setupSeatMap()
self.ContainerProxy = self.SeatMapGraphicsScene.addWidget(self.SeatsContainerWidget) self.ContainerProxy = self.SeatMapGraphicsScene.addWidget(self.SeatsContainerWidget)
self.ContainerProxy.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, False) self.ContainerProxy.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, False)
+118 -7
View File
@@ -9,12 +9,15 @@ See the LICENSE file for details.
""" """
import os import os
import sys import sys
import time
import requests
import qtawesome as qta import qtawesome as qta
from PySide6.QtCore import ( from PySide6.QtCore import (
QProcess, QProcess,
Qt, Qt,
QTimer,
Signal, Signal,
Slot Slot
) )
@@ -30,6 +33,7 @@ from PySide6.QtWidgets import (
) )
import managers.config.ConfigManager as ConfigManager import managers.config.ConfigManager as ConfigManager
from managers.bulletin.BulletinManager import instance as bulletinInstance
from managers.log.LogManager import instance as logInstance from managers.log.LogManager import instance as logInstance
from managers.theme.ThemeManager import( from managers.theme.ThemeManager import(
getActiveStyle, getActiveStyle,
@@ -96,6 +100,9 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self.__original_theme: str = "" self.__original_theme: str = ""
self.__original_custom_theme: str = "" self.__original_custom_theme: str = ""
self.__original_style: 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.setupUi(self)
self.modifyUi() self.modifyUi()
@@ -130,6 +137,7 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
"padding: 5px;" "padding: 5px;"
) )
self.NavigationList.setCurrentRow(0) self.NavigationList.setCurrentRow(0)
self.NavigationList.currentRowChanged.connect(self.PageStack.setCurrentIndex)
self.populateStyles() self.populateStyles()
self.populateCustomThemes() self.populateCustomThemes()
@@ -139,9 +147,12 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
app : QApplication | None = QApplication.instance() app : QApplication | None = QApplication.instance()
color = app.palette().color(app.palette().ColorRole.WindowText).name() color = app.palette().color(app.palette().ColorRole.WindowText).name()
item = self.NavigationList.item(0) item0 = self.NavigationList.item(0)
if item: if item0:
item.setIcon(qta.icon("fa6s.palette", color=color)) 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( def populateStyles(
self self
@@ -176,6 +187,7 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self.RemoveCustomThemeButton.clicked.connect(self.onRemoveCustomThemeButtonClicked) self.RemoveCustomThemeButton.clicked.connect(self.onRemoveCustomThemeButtonClicked)
self.CustomThemeComboBox.currentIndexChanged.connect(self.onCustomThemeComboBoxChanged) self.CustomThemeComboBox.currentIndexChanged.connect(self.onCustomThemeComboBoxChanged)
self.ResetCustomThemeButton.clicked.connect(self.onResetCustomThemeButtonClicked) self.ResetCustomThemeButton.clicked.connect(self.onResetCustomThemeButtonClicked)
self.BulletinTestButton.clicked.connect(self.onBulletinTestButtonClicked)
self.CancelButton.clicked.connect(self.onCancelButtonClicked) self.CancelButton.clicked.connect(self.onCancelButtonClicked)
self.ApplyButton.clicked.connect(self.onApplyButtonClicked) self.ApplyButton.clicked.connect(self.onApplyButtonClicked)
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked) self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
@@ -207,6 +219,14 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self.updateCustomThemeInfo() self.updateCustomThemeInfo()
self.updateCustomThemeStatus() 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( def updateCustomThemeInfo(
self self
): ):
@@ -270,6 +290,29 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
custom_theme = "" custom_theme = ""
return theme, style, 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( def saveAndApply(
self self
): ):
@@ -281,8 +324,6 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
if not _applyCustomTheme(custom_theme, theme): if not _applyCustomTheme(custom_theme, theme):
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "") self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "")
self.syncRadioFromNeedTheme(custom_theme) self.syncRadioFromNeedTheme(custom_theme)
# Re-read theme after syncRadioFromNeedTheme — the radio may have
# changed to match the custom theme's need_theme
theme, _, _ = self.collectSettings() theme, _, _ = self.collectSettings()
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.THEME, theme) self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.THEME, theme)
self.setNavigationIcons() self.setNavigationIcons()
@@ -292,6 +333,8 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self.__original_custom_theme = custom_theme if custom_theme else "" self.__original_custom_theme = custom_theme if custom_theme else ""
self.__original_style = getActiveStyle() self.__original_style = getActiveStyle()
self.saveBulletinSettings()
def maybeRestart( def maybeRestart(
self self
) -> bool: ) -> bool:
@@ -380,7 +423,6 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
): ):
self.updateCustomThemeInfo() self.updateCustomThemeInfo()
# no status update, because custom theme is not applied yet.
@Slot() @Slot()
def onResetCustomThemeButtonClicked( def onResetCustomThemeButtonClicked(
@@ -406,6 +448,75 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self.updateCustomThemeInfo() self.updateCustomThemeInfo()
self.updateCustomThemeStatus() 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() @Slot()
def onCancelButtonClicked( def onCancelButtonClicked(
self self
@@ -429,5 +540,5 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self self
): ):
self.onApplyButtonClicked() # virtually call apply button clicked self.onApplyButtonClicked()
self.close() self.close()
+63 -67
View File
@@ -118,35 +118,34 @@ class ALStatusLabel(QLabel):
event event
): ):
Painter = QPainter(self) painter = QPainter(self)
Painter.setRenderHint(QPainter.RenderHint.Antialiasing) painter.setRenderHint(QPainter.RenderHint.Antialiasing)
center_x = self.width()/2 center_x = self.width()/2
center_y = self.height()/2 center_y = self.height()/2
radius = min(center_x, center_y) - 3 radius = min(center_x, center_y) - 3
match self.__status: match self.__status:
case self.Status.WAITING: case self.Status.WAITING:
Pen = Painter.pen() pen = painter.pen()
Pen.setWidth(2) pen.setWidth(2)
Pen.setBrush(Qt.BrushStyle.NoBrush) pen.setCapStyle(Qt.PenCapStyle.RoundCap)
Pen.setCapStyle(Qt.PenCapStyle.RoundCap) pen.setColor(QColor("#969696")) # grey
Pen.setColor(QColor("#969696")) # grey painter.setPen(pen)
Painter.setPen(Pen) painter.drawEllipse(
Painter.drawEllipse(
int(center_x - radius), int(center_x - radius),
int(center_y - radius), int(center_y - radius),
int(radius*2), int(radius*2),
int(radius*2) int(radius*2)
) )
case self.Status.RUNNING: case self.Status.RUNNING:
Gradient = QConicalGradient(center_x, center_y, self.__icon_angle) gradient = QConicalGradient(center_x, center_y, self.__icon_angle)
Gradient.setColorAt(0.0, QColor("#2294FF" if self.isDarkMode() else "#0094FF")) gradient.setColorAt(0.0, QColor("#2294FF" if self.isDarkMode() else "#0094FF"))
Gradient.setColorAt(1.0, QColor("#2294FF00")) gradient.setColorAt(1.0, QColor("#2294FF00"))
Pen = Painter.pen() pen = painter.pen()
Pen.setWidth(3) pen.setWidth(3)
Pen.setBrush(Gradient) pen.setBrush(gradient)
Pen.setCapStyle(Qt.PenCapStyle.RoundCap) pen.setCapStyle(Qt.PenCapStyle.RoundCap)
Painter.setPen(Pen) painter.setPen(pen)
Painter.drawEllipse( painter.drawEllipse(
int(center_x - radius), int(center_x - radius),
int(center_y - radius), int(center_y - radius),
int(radius*2), int(radius*2),
@@ -154,102 +153,99 @@ class ALStatusLabel(QLabel):
) )
case self.Status.SUCCESS: case self.Status.SUCCESS:
# draw the success green circle # draw the success green circle
Pen = Painter.pen() pen = painter.pen()
Pen.setWidth(2) pen.setWidth(2)
Pen.setBrush(Qt.BrushStyle.NoBrush) pen.setCapStyle(Qt.PenCapStyle.RoundCap)
Pen.setCapStyle(Qt.PenCapStyle.RoundCap) pen.setColor(QColor("#4CAF50" if self.isDarkMode() else "#00AF50")) # green
Pen.setColor(QColor("#4CAF50" if self.isDarkMode() else "#00AF50")) # green painter.setPen(pen)
Painter.setPen(Pen) painter.drawEllipse(
Painter.drawEllipse(
int(center_x - radius), int(center_x - radius),
int(center_y - radius), int(center_y - radius),
int(radius*2), int(radius*2),
int(radius*2) int(radius*2)
) )
# draw the success check mark '✓' # draw the success check mark '✓'
Painter.setPen(Qt.PenStyle.SolidLine) painter.setPen(Qt.PenStyle.SolidLine)
Pen = Painter.pen() pen = painter.pen()
Pen.setWidth(3) pen.setWidth(3)
Pen.setBrush(Qt.BrushStyle.NoBrush) pen.setBrush(Qt.BrushStyle.NoBrush)
Pen.setCapStyle(Qt.PenCapStyle.RoundCap) pen.setCapStyle(Qt.PenCapStyle.RoundCap)
# white when dark mode, black when light mode # white when dark mode, black when light mode
Pen.setColor(self.getMarkColor()) pen.setColor(self.getMarkColor())
Painter.setPen(Pen) painter.setPen(pen)
mark_size = radius/2 mark_size = radius/2
mark_path = [ mark_path = [
(center_x - mark_size, center_y), (center_x - mark_size, center_y),
(center_x - mark_size/3, center_y + mark_size/2), (center_x - mark_size/3, center_y + mark_size/2),
(center_x + mark_size, 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[0][0]),int(mark_path[0][1]),
int(mark_path[1][0]),int(mark_path[1][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[1][0]),int(mark_path[1][1]),
int(mark_path[2][0]),int(mark_path[2][1]) int(mark_path[2][0]),int(mark_path[2][1])
) )
case self.Status.WARNING: case self.Status.WARNING:
# draw the warning orange circle # draw the warning orange circle
Pen = Painter.pen() pen = painter.pen()
Pen.setWidth(2) pen.setWidth(2)
Pen.setBrush(Qt.BrushStyle.NoBrush) pen.setCapStyle(Qt.PenCapStyle.RoundCap)
Pen.setCapStyle(Qt.PenCapStyle.RoundCap) pen.setColor(QColor("#FF9800")) # orange
Pen.setColor(QColor("#FF9800")) # orange painter.setPen(pen)
Painter.setPen(Pen) painter.drawEllipse(
Painter.drawEllipse(
int(center_x - radius), int(center_x - radius),
int(center_y - radius), int(center_y - radius),
int(radius*2), int(radius*2),
int(radius*2) int(radius*2)
) )
# draw the warning exclamation mark '!' # draw the warning exclamation mark '!'
Painter.setPen(Qt.PenStyle.SolidLine) painter.setPen(Qt.PenStyle.SolidLine)
Pen = Painter.pen() pen = painter.pen()
Pen.setWidth(3) pen.setWidth(3)
Pen.setBrush(Qt.BrushStyle.NoBrush) pen.setBrush(Qt.BrushStyle.NoBrush)
Pen.setCapStyle(Qt.PenCapStyle.RoundCap) pen.setCapStyle(Qt.PenCapStyle.RoundCap)
# white when dark mode, black when light mode # white when dark mode, black when light mode
Pen.setColor(self.getMarkColor()) pen.setColor(self.getMarkColor())
Painter.setPen(Pen) painter.setPen(pen)
Painter.drawLine( painter.drawLine(
int(center_x), int(center_y - radius/2), int(center_x), int(center_y - radius/2),
int(center_x), int(center_y + radius/6) int(center_x), int(center_y + radius/6)
) )
Painter.drawPoint( painter.drawPoint(
int(center_x), int(center_y + radius/2) int(center_x), int(center_y + radius/2)
) )
case self.Status.FAILURE: case self.Status.FAILURE:
# draw the failure red circle # draw the failure red circle
Pen = Painter.pen() pen = painter.pen()
Pen.setWidth(2) pen.setWidth(2)
Pen.setBrush(Qt.BrushStyle.NoBrush) pen.setCapStyle(Qt.PenCapStyle.RoundCap)
Pen.setCapStyle(Qt.PenCapStyle.RoundCap) pen.setColor(QColor("#DC0000")) # red
Pen.setColor(QColor("#DC0000")) # red painter.setPen(pen)
Painter.setPen(Pen) painter.drawEllipse(
Painter.drawEllipse(
int(center_x - radius), int(center_x - radius),
int(center_y - radius), int(center_y - radius),
int(radius*2), int(radius*2),
int(radius*2) int(radius*2)
) )
# draw the failure cross mark '✗' # draw the failure cross mark '✗'
Painter.setPen(Qt.PenStyle.SolidLine) painter.setPen(Qt.PenStyle.SolidLine)
Pen = Painter.pen() pen = painter.pen()
Pen.setWidth(3) pen.setWidth(3)
Pen.setBrush(Qt.BrushStyle.NoBrush) pen.setBrush(Qt.BrushStyle.NoBrush)
Pen.setCapStyle(Qt.PenCapStyle.RoundCap) pen.setCapStyle(Qt.PenCapStyle.RoundCap)
# white when dark mode, black when light mode # white when dark mode, black when light mode
Pen.setColor(self.getMarkColor()) pen.setColor(self.getMarkColor())
Painter.setPen(Pen) painter.setPen(pen)
mark_size = radius/3 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),
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),
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) super().paintEvent(event)
+13 -13
View File
@@ -108,24 +108,24 @@ class ALTimerTaskHistoryDialog(QDialog):
execute_time = record.get("execute_time", "") execute_time = record.get("execute_time", "")
result = record.get("result", ALTimerTaskStatus.UNKNOWN) result = record.get("result", ALTimerTaskStatus.UNKNOWN)
duration = record.get("duration", 0) duration = record.get("duration", 0)
ExecuteTimeItem = QTableWidgetItem(execute_time) execute_time_item = QTableWidgetItem(execute_time)
ExecuteTimeItem.setTextAlignment(Qt.AlignmentFlag.AlignCenter) execute_time_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.HistoryTableWidget.setItem(row, 0, ExecuteTimeItem) self.HistoryTableWidget.setItem(row, 0, execute_time_item)
ResultItem = QTableWidgetItem(result.value) result_item = QTableWidgetItem(result.value)
ResultItem.setTextAlignment(Qt.AlignmentFlag.AlignCenter) result_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
match result: match result:
case ALTimerTaskStatus.EXECUTED: case ALTimerTaskStatus.EXECUTED:
ResultItem.setForeground(Qt.GlobalColor.green) result_item.setForeground(Qt.GlobalColor.green)
case ALTimerTaskStatus.ERROR: case ALTimerTaskStatus.ERROR:
ResultItem.setForeground(Qt.GlobalColor.red) result_item.setForeground(Qt.GlobalColor.red)
case ALTimerTaskStatus.OUTDATED: case ALTimerTaskStatus.OUTDATED:
ResultItem.setForeground(Qt.GlobalColor.red) result_item.setForeground(Qt.GlobalColor.red)
case _: case _:
ResultItem.setForeground(Qt.GlobalColor.black) result_item.setForeground(Qt.GlobalColor.black)
self.HistoryTableWidget.setItem(row, 1, ResultItem) self.HistoryTableWidget.setItem(row, 1, result_item)
DurationItem = QTableWidgetItem(f"{duration:.2f}") duration_item = QTableWidgetItem(f"{duration:.2f}")
DurationItem.setTextAlignment(Qt.AlignmentFlag.AlignCenter) duration_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.HistoryTableWidget.setItem(row, 2, DurationItem) self.HistoryTableWidget.setItem(row, 2, duration_item)
self.HistoryTableWidget.setRowHeight(row, 25) self.HistoryTableWidget.setRowHeight(row, 25)
@Slot() @Slot()
+7 -7
View File
@@ -81,12 +81,12 @@ class ALTimerTaskItemWidget(QWidget):
self.TaskInfoLayout = QVBoxLayout() self.TaskInfoLayout = QVBoxLayout()
self.TaskInfoLayout.setSpacing(5) self.TaskInfoLayout.setSpacing(5)
TaskNameLabel = QLabel(self.__timer_task["name"]) TaskNameLabel = QLabel(self.__timer_task["name"])
TaskNameLabelFont = TaskNameLabel.font() task_name_label_font = TaskNameLabel.font()
TaskNameLabelFont.setBold(True) task_name_label_font.setBold(True)
TaskNameLabel.setFont(TaskNameLabelFont) TaskNameLabel.setFont(task_name_label_font)
TaskNameLabel.setFixedHeight(25) TaskNameLabel.setFixedHeight(25)
self.TaskInfoLayout.addWidget(TaskNameLabel) 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): if self.__timer_task.get("repeat", False):
repeat_days = self.__timer_task.get("repeat_days", []) repeat_days = self.__timer_task.get("repeat_days", [])
repeat_hour = self.__timer_task.get("repeat_hour", 0) 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) repeat_second = self.__timer_task.get("repeat_second", 0)
if len(repeat_days) == 7: if len(repeat_days) == 7:
time_str = f"{repeat_hour:02d}:{repeat_minute:02d}:{repeat_second:02d}" 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: else:
day_names = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"] day_names = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
selected_days = [day_names[d] for d in repeat_days] selected_days = [day_names[d] for d in repeat_days]
time_str = f"{repeat_hour:02d}:{repeat_minute:02d}:{repeat_second:02d}" 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: else:
ExecuteTimeLabel = QLabel(f"执行时间: {ExecuteTimeStr}") ExecuteTimeLabel = QLabel(f"执行时间: {execute_time_str}")
ExecuteTimeLabel.setStyleSheet("color: #969696;") ExecuteTimeLabel.setStyleSheet("color: #969696;")
ExecuteTimeLabel.setFixedHeight(20) ExecuteTimeLabel.setFixedHeight(20)
self.TaskInfoLayout.addWidget(ExecuteTimeLabel) self.TaskInfoLayout.addWidget(ExecuteTimeLabel)
+10 -10
View File
@@ -109,27 +109,27 @@ class ALUserTreeWidget(QTreeWidget):
super().dragMoveEvent(event) super().dragMoveEvent(event)
SourceItem = self.currentItem() source_item = self.currentItem()
TargetItem = self.itemAt(event.position().toPoint()) target_item = self.itemAt(event.position().toPoint())
if SourceItem is None: if source_item is None:
event.ignore() event.ignore()
return return
if SourceItem.type() == ALUserTreeItemType.GROUP.value: if source_item.type() == ALUserTreeItemType.GROUP.value:
if TargetItem is not None: if target_item is not None:
event.ignore() event.ignore()
return return
elif SourceItem.type() == ALUserTreeItemType.USER.value: elif source_item.type() == ALUserTreeItemType.USER.value:
if TargetItem is None: if target_item is None:
event.ignore() event.ignore()
return return
if TargetItem.type() != ALUserTreeItemType.GROUP.value: if target_item.type() != ALUserTreeItemType.GROUP.value:
event.ignore() event.ignore()
return return
if TargetItem.checkState(1) == Qt.CheckState.Unchecked: if target_item.checkState(1) == Qt.CheckState.Unchecked:
event.ignore() event.ignore()
return return
if not self.isDragPositionValid( if not self.isDragPositionValid(
self.visualItemRect(TargetItem), self.visualItemRect(target_item),
event.position().toPoint() event.position().toPoint()
): ):
event.ignore() event.ignore()
+385
View File
@@ -0,0 +1,385 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ALBulletinDialog</class>
<widget class="QDialog" name="ALBulletinDialog">
<property name="windowModality">
<enum>Qt::WindowModality::NonModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>640</width>
<height>450</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>450</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>800</width>
<height>450</height>
</size>
</property>
<property name="windowTitle">
<string>公告栏 - AutoLibrary</string>
</property>
<layout class="QVBoxLayout" name="ALBulletinDialogLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<layout class="QHBoxLayout" name="SyncLayout">
<property name="spacing">
<number>10</number>
</property>
<item>
<widget class="QPushButton" name="SyncButton">
<property name="enabled">
<bool>true</bool>
</property>
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="focusPolicy">
<enum>Qt::FocusPolicy::NoFocus</enum>
</property>
<property name="text">
<string>同步</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="SyncStatusPlaceholder">
<property name="minimumSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="SyncStatusDetailLabel">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>25</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="SyncSpaceFrame">
<property name="minimumSize">
<size>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>800</width>
<height>25</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="LastSyncTimeLabel">
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>上次同步:</string>
</property>
</widget>
</item>
<item>
<widget class="QDateTimeEdit" name="LastSyncDateTimeEdit">
<property name="minimumSize">
<size>
<width>130</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>130</width>
<height>25</height>
</size>
</property>
<property name="frame">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="keyboardTracking">
<bool>false</bool>
</property>
<property name="displayFormat">
<string>yyyy/MM/dd HH:mm:ss</string>
</property>
<property name="timeSpec">
<enum>Qt::TimeSpec::LocalTime</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QSplitter" name="BulletinSplitter">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<widget class="QFrame" name="LeftFrame">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<layout class="QVBoxLayout" name="LeftFrameLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QListWidget" name="BulletinListWidget">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SelectionMode::SingleSelection</enum>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QFrame" name="RightFrame">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<layout class="QVBoxLayout" name="RightFrameLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="BulletinTitleLabel">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="BulletinInfoLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="BulletinDateLabel">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="BulletinAuthorLabel">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="BulletinIsEditedLabel">
<property name="text">
<string>(已编辑)</string>
</property>
</widget>
</item>
<item>
<spacer name="BulletinInfoSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QTextBrowser" name="BulletinContentTextBrowser">
<property name="readOnly">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="BulletinDialogButtonBox">
<property name="minimumSize">
<size>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>BulletinDialogButtonBox</sender>
<signal>accepted()</signal>
<receiver>ALBulletinDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>BulletinDialogButtonBox</sender>
<signal>rejected()</signal>
<receiver>ALBulletinDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
+540 -325
View File
@@ -101,340 +101,555 @@
<iconset theme="preferences-desktop-color"/> <iconset theme="preferences-desktop-color"/>
</property> </property>
</item> </item>
<item>
<property name="text">
<string>公告</string>
</property>
<property name="icon">
<iconset theme="preferences-desktop-color"/>
</property>
</item>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QScrollArea" name="AppearanceScrollArea"> <widget class="QStackedWidget" name="PageStack">
<property name="frameShape"> <widget class="QScrollArea" name="AppearanceScrollArea">
<enum>QFrame::Shape::NoFrame</enum> <property name="frameShape">
</property> <enum>QFrame::Shape::NoFrame</enum>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="AppearancePageContent">
<property name="geometry">
<rect>
<x>0</x>
<y>-51</y>
<width>397</width>
<height>434</height>
</rect>
</property> </property>
<layout class="QVBoxLayout" name="AppearancePageLayout"> <property name="widgetResizable">
<property name="spacing"> <bool>true</bool>
<number>5</number> </property>
<widget class="QWidget" name="AppearancePageContent">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>397</width>
<height>434</height>
</rect>
</property> </property>
<property name="leftMargin"> <layout class="QVBoxLayout" name="AppearancePageLayout">
<number>3</number> <property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<widget class="QGroupBox" name="AppearanceGroupBox">
<property name="title">
<string>主题模式</string>
</property>
<layout class="QVBoxLayout" name="AppearanceGroupBoxLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<widget class="QRadioButton" name="LightThemeRadio">
<property name="text">
<string>浅色</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="DarkThemeRadio">
<property name="text">
<string>深色</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="SystemThemeRadio">
<property name="text">
<string>跟随系统</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="InterfaceGroupBox">
<property name="title">
<string>界面风格</string>
</property>
<layout class="QVBoxLayout" name="InterfaceGroupBoxLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<layout class="QHBoxLayout" name="StyleSelectLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="StyleSelectLabel">
<property name="minimumSize">
<size>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>应用程序样式:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="StyleComboBox">
<property name="minimumSize">
<size>
<width>160</width>
<height>25</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="StyleHintLabel">
<property name="text">
<string>更改样式将在下次启动应用程序时生效。</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="CustomThemeGroupBox">
<property name="title">
<string>自定义外观</string>
</property>
<layout class="QVBoxLayout" name="CustomQssGroupBoxLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<widget class="QLabel" name="CustomThemeHintLabel">
<property name="text">
<string>选择一个主题,或导入新的主题文件:</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="CustomThemePathLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QComboBox" name="CustomThemeComboBox">
<property name="minimumSize">
<size>
<width>160</width>
<height>25</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="CustomThemePathEdit">
<property name="minimumSize">
<size>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="visible">
<bool>false</bool>
</property>
<property name="placeholderText">
<string>选择或输入 QSS 样式表文件路径...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ImportCustomThemeButton">
<property name="minimumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>+</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="RemoveCustomThemeButton">
<property name="minimumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>-</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="CustomThemeInfoLabel">
<property name="minimumSize">
<size>
<width>0</width>
<height>60</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="textFormat">
<enum>Qt::TextFormat::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="CustomThemeActionLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QPushButton" name="ResetCustomThemeButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>重置主题</string>
</property>
</widget>
</item>
<item>
<spacer name="CustomThemeActionSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="CustomThemeStatusLabel">
<property name="text">
<string>当前使用程序 默认 外观。</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="AppearancePageSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
<widget class="QScrollArea" name="BulletinScrollArea">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="BulletinPageContent">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>397</width>
<height>434</height>
</rect>
</property> </property>
<property name="topMargin"> <layout class="QVBoxLayout" name="BulletinPageLayout">
<number>3</number> <property name="spacing">
</property> <number>5</number>
<property name="rightMargin"> </property>
<number>3</number> <property name="leftMargin">
</property> <number>3</number>
<property name="bottomMargin"> </property>
<number>3</number> <property name="topMargin">
</property> <number>3</number>
<item> </property>
<widget class="QGroupBox" name="AppearanceGroupBox"> <property name="rightMargin">
<property name="title"> <number>3</number>
<string>主题模式</string> </property>
</property> <property name="bottomMargin">
<layout class="QVBoxLayout" name="AppearanceGroupBoxLayout"> <number>3</number>
<property name="spacing"> </property>
<number>5</number> <item>
<widget class="QGroupBox" name="BulletinGroupBox">
<property name="title">
<string>公告设置</string>
</property> </property>
<property name="leftMargin"> <layout class="QVBoxLayout" name="BulletinGroupBoxLayout">
<number>3</number> <property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<layout class="QHBoxLayout" name="BulletinServerUrlLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="BulletinServerUrlLabel">
<property name="minimumSize">
<size>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>服务器地址:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="BulletinServerUrlEdit">
<property name="minimumSize">
<size>
<width>200</width>
<height>25</height>
</size>
</property>
<property name="placeholderText">
<string>https://api.autolibrary.kenanzhu.com</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="BulletinAutoFetchCheck">
<property name="text">
<string>软件启动时自动获取公告</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="BulletinSyncIntervalLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="BulletinSyncIntervalLabel">
<property name="minimumSize">
<size>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>自动同步间隔:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="BulletinSyncIntervalSpin">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>1440</number>
</property>
<property name="suffix">
<string> 分钟</string>
</property>
</widget>
</item>
<item>
<spacer name="BulletinSyncIntervalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="BulletinTestButton">
<property name="minimumSize">
<size>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>测试连接</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="BulletinTestStatusLabel">
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="BulletinPageSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property> </property>
<property name="topMargin"> <property name="sizeHint" stdset="0">
<number>3</number> <size>
<width>20</width>
<height>40</height>
</size>
</property> </property>
<property name="rightMargin"> </spacer>
<number>3</number> </item>
</property> </layout>
<property name="bottomMargin"> </widget>
<number>3</number>
</property>
<item>
<widget class="QRadioButton" name="LightThemeRadio">
<property name="text">
<string>浅色</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="DarkThemeRadio">
<property name="text">
<string>深色</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="SystemThemeRadio">
<property name="text">
<string>跟随系统</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="InterfaceGroupBox">
<property name="title">
<string>界面风格</string>
</property>
<layout class="QVBoxLayout" name="InterfaceGroupBoxLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<layout class="QHBoxLayout" name="StyleSelectLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="StyleSelectLabel">
<property name="minimumSize">
<size>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>应用程序样式:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="StyleComboBox">
<property name="minimumSize">
<size>
<width>160</width>
<height>25</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="StyleHintLabel">
<property name="text">
<string>更改样式将在下次启动应用程序时生效。</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="CustomThemeGroupBox">
<property name="title">
<string>自定义外观</string>
</property>
<layout class="QVBoxLayout" name="CustomQssGroupBoxLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<widget class="QLabel" name="CustomThemeHintLabel">
<property name="text">
<string>选择一个主题,或导入新的主题文件:</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="CustomThemePathLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QComboBox" name="CustomThemeComboBox">
<property name="minimumSize">
<size>
<width>160</width>
<height>25</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="CustomThemePathEdit">
<property name="minimumSize">
<size>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="visible">
<bool>false</bool>
</property>
<property name="placeholderText">
<string>选择或输入 QSS 样式表文件路径...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ImportCustomThemeButton">
<property name="minimumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>+</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="RemoveCustomThemeButton">
<property name="minimumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>-</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="CustomThemeInfoLabel">
<property name="minimumSize">
<size>
<width>0</width>
<height>60</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="textFormat">
<enum>Qt::TextFormat::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="CustomThemeActionLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QPushButton" name="ResetCustomThemeButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>重置主题</string>
</property>
</widget>
</item>
<item>
<spacer name="CustomThemeActionSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="CustomThemeStatusLabel">
<property name="text">
<string>当前使用程序 默认 外观。</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="AppearancePageSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget> </widget>
</widget> </widget>
</item> </item>
+6
View File
@@ -72,6 +72,12 @@ class CfgKey:
STYLE = ConfigPath(ConfigType.GLOBAL, "appearance.style") STYLE = ConfigPath(ConfigType.GLOBAL, "appearance.style")
CUSTOM_THEME = ConfigPath(ConfigType.GLOBAL, "appearance.custom_theme") 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: class TIMERTASK:
ROOT = ConfigPath(ConfigType.TIMERTASK, "") ROOT = ConfigPath(ConfigType.TIMERTASK, "")
TIMER_TASKS = ConfigPath(ConfigType.TIMERTASK, "timer_tasks") TIMER_TASKS = ConfigPath(ConfigType.TIMERTASK, "timer_tasks")
+342
View File
@@ -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
+9
View File
@@ -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.
"""
+6 -1
View File
@@ -59,6 +59,11 @@ class ConfigTemplate:
"theme": "system", "theme": "system",
"style": "Fusion", "style": "Fusion",
"custom_theme": "" "custom_theme": ""
},
"bulletin": {
"auto_fetch": False,
"server_url": "https://api.autolibrary.kenanzhu.com",
"sync_interval": 10
} }
} }
case ConfigType.BULLETIN: case ConfigType.BULLETIN:
@@ -105,7 +110,7 @@ class ConfigManager:
config_data = JSONReader(config_path).data() config_data = JSONReader(config_path).data()
self.__config_data[config_type.value] = config_data self.__config_data[config_type.value] = config_data
return return
except: except Exception:
pass pass
self.__config_data[config_type.value] = ConfigTemplate(config_type).template() self.__config_data[config_type.value] = ConfigTemplate(config_type).template()
JSONWriter(config_path, self.__config_data[config_type.value]) JSONWriter(config_path, self.__config_data[config_type.value])