mirror of
https://github.com/KenanZhu/AutoLibrary.git
synced 2026-08-02 22:19:37 +08:00
feat(bulletin): 公告栏系统完整实现
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,382 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (c) 2026 KenanZhu.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This software is provided "as is", without any warranty of any kind.
|
||||||
|
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||||
|
See the LICENSE file for details.
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from PySide6.QtCore import (
|
||||||
|
Qt,
|
||||||
|
QThread,
|
||||||
|
Signal,
|
||||||
|
Slot,
|
||||||
|
QTimer
|
||||||
|
)
|
||||||
|
from PySide6.QtGui import (
|
||||||
|
QFont
|
||||||
|
)
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QDialog,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QListWidgetItem,
|
||||||
|
QMessageBox,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget
|
||||||
|
)
|
||||||
|
|
||||||
|
from gui.ALStatusLabel import ALStatusLabel
|
||||||
|
from gui.resources.ui.Ui_ALBulletinDialog import Ui_ALBulletinDialog
|
||||||
|
from managers.bulletin.BulletinManager import instance as bulletinInstance
|
||||||
|
|
||||||
|
|
||||||
|
class ALBulletinFetchWorker(QThread):
|
||||||
|
"""
|
||||||
|
Worker thread for fetching bulletins from the server.
|
||||||
|
"""
|
||||||
|
|
||||||
|
fetchWorkerIsFinished = Signal(dict)
|
||||||
|
fetchWorkerFinishedWithError = Signal(str)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent=None,
|
||||||
|
request_url: str = "",
|
||||||
|
params: dict = None
|
||||||
|
):
|
||||||
|
|
||||||
|
super().__init__(parent)
|
||||||
|
self.__request_url = request_url.rstrip("/") if request_url else ""
|
||||||
|
self.__params = params or {}
|
||||||
|
|
||||||
|
def run(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
self.__request_url, params=self.__params, timeout=5
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
r = response.json()
|
||||||
|
if r.get("code") == 200:
|
||||||
|
data = r.get("data", {})
|
||||||
|
self.fetchWorkerIsFinished.emit(data)
|
||||||
|
else:
|
||||||
|
raise Exception(
|
||||||
|
f"服务器返回错误: [{r.get('code', '未知代码')}] {r.get('msg', '未知错误')}"
|
||||||
|
)
|
||||||
|
except requests.RequestException as e:
|
||||||
|
self.fetchWorkerFinishedWithError.emit(f"获取公告数据时发生网络错误: \n{e}")
|
||||||
|
except Exception as e:
|
||||||
|
self.fetchWorkerFinishedWithError.emit(f"获取公告数据时发生未知错误: \n{e}")
|
||||||
|
|
||||||
|
|
||||||
|
class ALBulletinItemWidget(QWidget):
|
||||||
|
"""
|
||||||
|
Single bulletin item widget for the bulletin list.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent=None,
|
||||||
|
bulletin: dict[str, Any] = None
|
||||||
|
):
|
||||||
|
|
||||||
|
super().__init__(parent)
|
||||||
|
self.__bulletin = bulletin.copy() if bulletin else {}
|
||||||
|
self.modifyUi()
|
||||||
|
|
||||||
|
def modifyUi(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
self.ItemWidgetLayout = QVBoxLayout()
|
||||||
|
self.ItemWidgetLayout.setSpacing(10)
|
||||||
|
self.ItemWidgetLayout.setContentsMargins(10, 5, 10, 5)
|
||||||
|
self.BulletinTitleLayout = QHBoxLayout()
|
||||||
|
self.BulletinTitleLabel = QLabel(self.__bulletin.get("title", "无标题"))
|
||||||
|
titleFont = QFont()
|
||||||
|
titleFont.setBold(True)
|
||||||
|
self.BulletinTitleLabel.setFont(titleFont)
|
||||||
|
self.BulletinTitleLayout.addWidget(self.BulletinTitleLabel)
|
||||||
|
if self.__bulletin.get("isNew", False):
|
||||||
|
self.NewIndicatorLabel = QLabel("新 !")
|
||||||
|
self.NewIndicatorLabel.setStyleSheet(
|
||||||
|
"color: #DC0000;"\
|
||||||
|
"font-size: 10px;"\
|
||||||
|
"font-weight: bold;"\
|
||||||
|
"font-style: italic;"
|
||||||
|
)
|
||||||
|
self.NewIndicatorLabel.setFixedSize(25, 25)
|
||||||
|
self.BulletinTitleLayout.addWidget(self.NewIndicatorLabel)
|
||||||
|
else:
|
||||||
|
self.NewIndicatorLabel = None
|
||||||
|
if self.__bulletin.get("isEdited", False):
|
||||||
|
self.BulletinIsEditedLabel = QLabel("(已编辑)")
|
||||||
|
self.BulletinIsEditedLabel.setStyleSheet(
|
||||||
|
"color: #FF9800;"\
|
||||||
|
"font-size: 10px;"
|
||||||
|
)
|
||||||
|
self.BulletinTitleLayout.addWidget(self.BulletinIsEditedLabel)
|
||||||
|
self.BulletinTitleLayout.addStretch()
|
||||||
|
self.ItemWidgetLayout.addLayout(self.BulletinTitleLayout)
|
||||||
|
self.BulletinInfoLayout = QHBoxLayout()
|
||||||
|
self.BulletinDateLabel = QLabel()
|
||||||
|
date_time = datetime.fromisoformat(self.__bulletin.get("dateTime", ""))
|
||||||
|
self.BulletinDateLabel.setText(date_time.strftime("%Y-%m-%d %H:%M:%S"))
|
||||||
|
self.BulletinDateLabel.setStyleSheet("color: #969696; font-size: 11px;")
|
||||||
|
self.BulletinInfoLayout.addWidget(self.BulletinDateLabel)
|
||||||
|
self.BulletinAuthorLabel = QLabel(self.__bulletin.get("author", "未知"))
|
||||||
|
self.BulletinAuthorLabel.setStyleSheet("color: #969696; font-size: 11px;")
|
||||||
|
self.BulletinInfoLayout.addWidget(self.BulletinAuthorLabel)
|
||||||
|
self.BulletinInfoLayout.addStretch()
|
||||||
|
self.ItemWidgetLayout.addLayout(self.BulletinInfoLayout)
|
||||||
|
self.setLayout(self.ItemWidgetLayout)
|
||||||
|
self.__bulletin = None
|
||||||
|
|
||||||
|
def markAsRead(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
if self.NewIndicatorLabel:
|
||||||
|
self.NewIndicatorLabel.hide()
|
||||||
|
|
||||||
|
|
||||||
|
class ALBulletinDialog(QDialog, Ui_ALBulletinDialog):
|
||||||
|
"""
|
||||||
|
Bulletin viewer dialog.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent=None
|
||||||
|
):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.__sync_timer: QTimer | None = None
|
||||||
|
self.__fetch_worker: ALBulletinFetchWorker | None = None
|
||||||
|
self.__bulletin_mgr = bulletinInstance()
|
||||||
|
|
||||||
|
self.setupUi(self)
|
||||||
|
self.modifyUi()
|
||||||
|
self.connectSignals()
|
||||||
|
self.setupTimer()
|
||||||
|
if self.shouldAutoSync():
|
||||||
|
self.syncBulletin()
|
||||||
|
|
||||||
|
def modifyUi(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
self.ALSyncStatusLabel = ALStatusLabel(self)
|
||||||
|
self.ALSyncStatusLabel.setFixedSize(30, 30)
|
||||||
|
self.SyncLayout.replaceWidget(
|
||||||
|
self.SyncStatusPlaceholder, self.ALSyncStatusLabel
|
||||||
|
)
|
||||||
|
|
||||||
|
titleFont = QFont()
|
||||||
|
titleFont.setBold(True)
|
||||||
|
titleFont.setPointSize(15)
|
||||||
|
self.BulletinTitleLabel.setFont(titleFont)
|
||||||
|
self.BulletinDateLabel.setStyleSheet("color: #969696;")
|
||||||
|
self.BulletinAuthorLabel.setStyleSheet("color: #969696;")
|
||||||
|
self.BulletinIsEditedLabel.setStyleSheet("color: #FF9800;")
|
||||||
|
self.BulletinIsEditedLabel.hide()
|
||||||
|
last_time = self.__bulletin_mgr.lastSyncTime()
|
||||||
|
if last_time:
|
||||||
|
self.setLastSyncTime(last_time)
|
||||||
|
self.updateBulletinList(self.__bulletin_mgr.bulletins())
|
||||||
|
|
||||||
|
def shouldAutoSync(
|
||||||
|
self
|
||||||
|
) -> bool:
|
||||||
|
|
||||||
|
last = self.__bulletin_mgr.lastSyncTime()
|
||||||
|
if last is None:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
last_dt = datetime.fromisoformat(last)
|
||||||
|
interval_min = max(self.__bulletin_mgr.syncInterval()//2, 1)
|
||||||
|
threshold = timedelta(minutes=interval_min)
|
||||||
|
return (datetime.now().astimezone() - last_dt) > threshold
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def setLastSyncTime(
|
||||||
|
self,
|
||||||
|
iso_str: str
|
||||||
|
):
|
||||||
|
|
||||||
|
try:
|
||||||
|
dt = datetime.fromisoformat(iso_str)
|
||||||
|
self.LastSyncDateTimeEdit.setDateTime(dt)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def connectSignals(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
self.BulletinListWidget.itemClicked.connect(self.onBulletinListWidgetItemClicked)
|
||||||
|
self.SyncButton.clicked.connect(self.syncBulletin)
|
||||||
|
|
||||||
|
def setupTimer(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
self.__sync_timer = QTimer(self)
|
||||||
|
self.__sync_timer.timeout.connect(self.syncBulletin)
|
||||||
|
self.__sync_timer.start(self.__bulletin_mgr.syncInterval()*60*1000)
|
||||||
|
|
||||||
|
def updateBulletinList(
|
||||||
|
self,
|
||||||
|
bulletins: list[dict]
|
||||||
|
):
|
||||||
|
|
||||||
|
self.BulletinListWidget.clear()
|
||||||
|
sorted_list = sorted(
|
||||||
|
bulletins, key=lambda x: x.get("dateTime", ""), reverse=True
|
||||||
|
)
|
||||||
|
for bulletin in sorted_list:
|
||||||
|
item = QListWidgetItem()
|
||||||
|
item.setData(Qt.UserRole, bulletin)
|
||||||
|
widget = ALBulletinItemWidget(self, bulletin)
|
||||||
|
item.setSizeHint(widget.sizeHint())
|
||||||
|
self.BulletinListWidget.addItem(item)
|
||||||
|
self.BulletinListWidget.setItemWidget(item, widget)
|
||||||
|
|
||||||
|
def showBulletin(
|
||||||
|
self,
|
||||||
|
bulletin: dict
|
||||||
|
):
|
||||||
|
|
||||||
|
self.BulletinTitleLabel.setText(bulletin.get("title", "无标题"))
|
||||||
|
date_time = datetime.fromisoformat(bulletin.get("dateTime", ""))
|
||||||
|
self.BulletinDateLabel.setText(date_time.strftime("%Y-%m-%d %H:%M:%S"))
|
||||||
|
self.BulletinAuthorLabel.setText(bulletin.get("author", "未知"))
|
||||||
|
if bulletin.get("isEdited", False):
|
||||||
|
self.BulletinIsEditedLabel.show()
|
||||||
|
else:
|
||||||
|
self.BulletinIsEditedLabel.hide()
|
||||||
|
self.BulletinContentTextBrowser.setHtml(
|
||||||
|
"<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
|
||||||
|
):
|
||||||
|
|
||||||
|
if self.__fetch_worker:
|
||||||
|
self.__fetch_worker.wait(2000)
|
||||||
|
self.__fetch_worker.fetchWorkerIsFinished.disconnect(self.onBulletinsFetched)
|
||||||
|
self.__fetch_worker.fetchWorkerFinishedWithError.disconnect(self.onBulletinsFetchError)
|
||||||
|
self.__fetch_worker.deleteLater()
|
||||||
|
self.__fetch_worker = None
|
||||||
|
bulletins = data.get("bulletins", [])
|
||||||
|
delete_ids = data.get("delete_ids", [])
|
||||||
|
merged = self.__bulletin_mgr.updateAndMergeBulletins(bulletins, delete_ids)
|
||||||
|
self.__bulletin_mgr.setLastSyncTime(datetime.now().astimezone().isoformat())
|
||||||
|
self.SyncButton.setEnabled(True)
|
||||||
|
self.SyncButton.setText("同步")
|
||||||
|
self.ALSyncStatusLabel.status = ALStatusLabel.Status.SUCCESS
|
||||||
|
self.SyncStatusDetailLabel.setText("同步成功")
|
||||||
|
self.SyncStatusDetailLabel.setStyleSheet("color: green;")
|
||||||
|
QTimer.singleShot(3000, self.clearSyncStatus)
|
||||||
|
|
||||||
|
self.setLastSyncTime(self.__bulletin_mgr.lastSyncTime())
|
||||||
|
self.updateBulletinList(merged)
|
||||||
|
self.clearBulletin()
|
||||||
|
interval_ms = self.__bulletin_mgr.syncInterval()*60*1000
|
||||||
|
if self.__sync_timer:
|
||||||
|
self.__sync_timer.start(interval_ms)
|
||||||
|
|
||||||
|
@Slot(str)
|
||||||
|
def onBulletinsFetchError(
|
||||||
|
self,
|
||||||
|
error_message: str
|
||||||
|
):
|
||||||
|
|
||||||
|
if self.__fetch_worker:
|
||||||
|
self.__fetch_worker.wait(2000)
|
||||||
|
self.__fetch_worker.fetchWorkerIsFinished.disconnect(self.onBulletinsFetched)
|
||||||
|
self.__fetch_worker.fetchWorkerFinishedWithError.disconnect(self.onBulletinsFetchError)
|
||||||
|
self.__fetch_worker.deleteLater()
|
||||||
|
self.__fetch_worker = None
|
||||||
|
self.SyncButton.setEnabled(True)
|
||||||
|
self.SyncButton.setText("重试")
|
||||||
|
self.ALSyncStatusLabel.status = ALStatusLabel.Status.FAILURE
|
||||||
|
self.SyncStatusDetailLabel.setText("同步失败,请检查网络连接")
|
||||||
|
self.SyncStatusDetailLabel.setStyleSheet("color: red;")
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"警告 - AutoLibrary",
|
||||||
|
f"同步失败:{error_message}"
|
||||||
|
)
|
||||||
|
interval_ms = self.__bulletin_mgr.syncInterval()*60*1000
|
||||||
|
if self.__sync_timer:
|
||||||
|
self.__sync_timer.start(interval_ms)
|
||||||
|
|
||||||
|
def clearSyncStatus(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
self.ALSyncStatusLabel.status = ALStatusLabel.Status.WAITING
|
||||||
|
self.SyncStatusDetailLabel.setText("")
|
||||||
|
self.SyncStatusDetailLabel.setStyleSheet("")
|
||||||
|
|
||||||
|
@Slot(QListWidgetItem)
|
||||||
|
def onBulletinListWidgetItemClicked(
|
||||||
|
self,
|
||||||
|
item: QListWidgetItem
|
||||||
|
):
|
||||||
|
|
||||||
|
if item is None or item.data(Qt.UserRole) is None:
|
||||||
|
return
|
||||||
|
bulletin = item.data(Qt.UserRole)
|
||||||
|
if bulletin.get("isNew", False):
|
||||||
|
bulletin["isNew"] = False
|
||||||
|
widget = self.BulletinListWidget.itemWidget(item)
|
||||||
|
widget.markAsRead()
|
||||||
|
item.setData(Qt.UserRole, bulletin)
|
||||||
|
self.__bulletin_mgr.markBulletinAsRead(bulletin["id"])
|
||||||
|
self.showBulletin(bulletin)
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (c) 2026 KenanZhu.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This software is provided "as is", without any warranty of any kind.
|
||||||
|
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||||
|
See the LICENSE file for details.
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from PySide6.QtCore import (
|
||||||
|
QObject,
|
||||||
|
QTimer,
|
||||||
|
Signal,
|
||||||
|
Slot
|
||||||
|
)
|
||||||
|
|
||||||
|
from gui.ALBulletinDialog import ALBulletinFetchWorker
|
||||||
|
from managers.bulletin.BulletinManager import instance as bulletinInstance
|
||||||
|
|
||||||
|
|
||||||
|
class ALBulletinPoller(QObject):
|
||||||
|
"""
|
||||||
|
Background bulletin poller.
|
||||||
|
|
||||||
|
Owns the periodic poll timer and the ALBulletinFetchWorker
|
||||||
|
lifecycle. Emits signals when new bulletins are detected so
|
||||||
|
the owner can show tray notifications without knowing the
|
||||||
|
fetch / merge details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
newBulletinsDetected = Signal(int)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent=None
|
||||||
|
):
|
||||||
|
|
||||||
|
super().__init__(parent)
|
||||||
|
self.__timer = QTimer(self)
|
||||||
|
self.__timer.timeout.connect(self.__poll)
|
||||||
|
self.__worker = None
|
||||||
|
self.__dialog_open = False
|
||||||
|
self.__mgr = bulletinInstance()
|
||||||
|
|
||||||
|
def start(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
interval_ms = self.__mgr.syncInterval()*60*1000
|
||||||
|
self.__timer.start(interval_ms)
|
||||||
|
|
||||||
|
def stop(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
self.__timer.stop()
|
||||||
|
self.__cleanupWorker()
|
||||||
|
|
||||||
|
def restart(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
self.stop()
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def fetchNow(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
if self.__worker is not None:
|
||||||
|
return
|
||||||
|
if self.__dialog_open:
|
||||||
|
return
|
||||||
|
self.__doFetch()
|
||||||
|
|
||||||
|
def isPolling(
|
||||||
|
self
|
||||||
|
) -> bool:
|
||||||
|
|
||||||
|
return self.__timer.isActive()
|
||||||
|
|
||||||
|
def setDialogOpen(
|
||||||
|
self,
|
||||||
|
open: bool
|
||||||
|
):
|
||||||
|
|
||||||
|
self.__dialog_open = open
|
||||||
|
|
||||||
|
def __doFetch(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
params = self.__mgr.getSyncDateTimeAndRange()
|
||||||
|
self.__worker = ALBulletinFetchWorker(
|
||||||
|
self, self.__mgr.apiUrl(), params
|
||||||
|
)
|
||||||
|
self.__worker.fetchWorkerIsFinished.connect(self.__onFetched)
|
||||||
|
self.__worker.fetchWorkerFinishedWithError.connect(self.__onError)
|
||||||
|
self.__worker.start()
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def __poll(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
if self.__worker is not None:
|
||||||
|
return
|
||||||
|
if self.__dialog_open:
|
||||||
|
return
|
||||||
|
self.__doFetch()
|
||||||
|
|
||||||
|
@Slot(dict)
|
||||||
|
def __onFetched(
|
||||||
|
self,
|
||||||
|
data: dict
|
||||||
|
):
|
||||||
|
|
||||||
|
if self.__worker is None:
|
||||||
|
return
|
||||||
|
self.__worker.fetchWorkerIsFinished.disconnect(self.__onFetched)
|
||||||
|
self.__worker.fetchWorkerFinishedWithError.disconnect(self.__onError)
|
||||||
|
self.__worker.wait(2000)
|
||||||
|
self.__worker.deleteLater()
|
||||||
|
self.__worker = None
|
||||||
|
old_ids = {b["id"] for b in self.__mgr.bulletins()}
|
||||||
|
bulletins = data.get("bulletins", [])
|
||||||
|
delete_ids = data.get("delete_ids", [])
|
||||||
|
self.__mgr.updateAndMergeBulletins(bulletins, delete_ids)
|
||||||
|
self.__mgr.setLastSyncTime(datetime.now().astimezone().isoformat())
|
||||||
|
new_ids = {b["id"] for b in bulletins if b["id"] not in old_ids}
|
||||||
|
if new_ids:
|
||||||
|
self.newBulletinsDetected.emit(len(new_ids))
|
||||||
|
interval_ms = self.__mgr.syncInterval()*60*1000
|
||||||
|
self.__timer.start(interval_ms)
|
||||||
|
|
||||||
|
@Slot(str)
|
||||||
|
def __onError(
|
||||||
|
self,
|
||||||
|
error_message: str
|
||||||
|
):
|
||||||
|
|
||||||
|
if self.__worker is None:
|
||||||
|
return
|
||||||
|
self.__worker.fetchWorkerIsFinished.disconnect(self.__onFetched)
|
||||||
|
self.__worker.fetchWorkerFinishedWithError.disconnect(self.__onError)
|
||||||
|
self.__worker.wait(2000)
|
||||||
|
self.__worker.deleteLater()
|
||||||
|
self.__worker = None
|
||||||
|
interval_ms = self.__mgr.syncInterval()*60*1000
|
||||||
|
self.__timer.start(interval_ms)
|
||||||
|
|
||||||
|
def __cleanupWorker(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
if self.__worker is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.__worker.fetchWorkerIsFinished.disconnect()
|
||||||
|
except (TypeError, RuntimeError):
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.__worker.fetchWorkerFinishedWithError.disconnect()
|
||||||
|
except (TypeError, RuntimeError):
|
||||||
|
pass
|
||||||
|
self.__worker.wait(2000)
|
||||||
|
self.__worker.deleteLater()
|
||||||
|
self.__worker = None
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
@@ -62,6 +65,8 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
|||||||
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,6 +94,8 @@ 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:
|
||||||
@@ -131,12 +143,14 @@ 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(
|
||||||
@@ -170,6 +184,10 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
|||||||
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,
|
||||||
event: QCloseEvent
|
event: QCloseEvent
|
||||||
@@ -195,6 +213,11 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
|||||||
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)
|
||||||
|
|
||||||
@@ -318,6 +341,46 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
|||||||
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
|
||||||
|
|||||||
@@ -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,43 @@ 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
|
||||||
|
self.BulletinTestButton.setEnabled(False)
|
||||||
|
self.BulletinTestStatusLabel.setText("正在测试连接...")
|
||||||
|
self.BulletinTestStatusLabel.setStyleSheet("")
|
||||||
|
try:
|
||||||
|
api_url = url.rstrip("/") + "/bulletins"
|
||||||
|
t0 = time.monotonic()
|
||||||
|
response = requests.get(api_url, timeout=5)
|
||||||
|
elapsed_ms = (time.monotonic() - t0) * 1000
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
if data.get("code") == 200:
|
||||||
|
self.BulletinTestStatusLabel.setText(
|
||||||
|
f"连接成功!响应延迟 {elapsed_ms:.0f} ms"
|
||||||
|
)
|
||||||
|
self.BulletinTestStatusLabel.setStyleSheet("color: green;")
|
||||||
|
else:
|
||||||
|
self.BulletinTestStatusLabel.setText(
|
||||||
|
f"服务器返回异常: [{data.get('code', '?')}] {data.get('msg', '未知错误')}"
|
||||||
|
)
|
||||||
|
self.BulletinTestStatusLabel.setStyleSheet("color: red;")
|
||||||
|
except Exception as e:
|
||||||
|
self.BulletinTestStatusLabel.setText(f"连接失败:{e}")
|
||||||
|
self.BulletinTestStatusLabel.setStyleSheet("color: red;")
|
||||||
|
finally:
|
||||||
|
self.BulletinTestButton.setEnabled(True)
|
||||||
|
QTimer.singleShot(3000, self.clearBulletinTestStatus)
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def onCancelButtonClicked(
|
def onCancelButtonClicked(
|
||||||
self
|
self
|
||||||
@@ -429,5 +508,5 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
|
|||||||
self
|
self
|
||||||
):
|
):
|
||||||
|
|
||||||
self.onApplyButtonClicked() # virtually call apply button clicked
|
self.onApplyButtonClicked()
|
||||||
self.close()
|
self.close()
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -0,0 +1,292 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (c) 2026 KenanZhu.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This software is provided "as is", without any warranty of any kind.
|
||||||
|
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||||
|
See the LICENSE file for details.
|
||||||
|
"""
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from interfaces.ConfigProvider import (
|
||||||
|
CfgKey,
|
||||||
|
ConfigProvider
|
||||||
|
)
|
||||||
|
from managers.config.ConfigManager import instance as configInstance
|
||||||
|
|
||||||
|
|
||||||
|
class BulletinManager:
|
||||||
|
"""
|
||||||
|
Bulletin Manager Singleton Class
|
||||||
|
|
||||||
|
Manages bulletin data persistence via ConfigManager and provides
|
||||||
|
merge, read-status and sync-parameter logic. The HTTP fetch and
|
||||||
|
UI concerns are handled by the GUI layer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
self.__lock = threading.Lock()
|
||||||
|
self.__cfg: ConfigProvider = configInstance()
|
||||||
|
|
||||||
|
def bulletins(
|
||||||
|
self
|
||||||
|
) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Get cached bulletins.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[dict]: Cached bulletin list.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.__cfg.get(CfgKey.BULLETIN.BULLETIN, [])
|
||||||
|
|
||||||
|
def setBulletins(
|
||||||
|
self,
|
||||||
|
bulletins: list[dict]
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Replace the bulletin cache.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bulletins (list[dict]): Bulletin list to store.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.__cfg.set(CfgKey.BULLETIN.BULLETIN, bulletins)
|
||||||
|
|
||||||
|
def lastSyncTime(
|
||||||
|
self
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Get last sync time string.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[str]: ISO-format last sync time, or None.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.__cfg.get(CfgKey.BULLETIN.LAST_SYNC_TIME, None)
|
||||||
|
|
||||||
|
def setLastSyncTime(
|
||||||
|
self,
|
||||||
|
value: Optional[str]
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Set last sync time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (Optional[str]): ISO-format datetime string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.__cfg.set(CfgKey.BULLETIN.LAST_SYNC_TIME, value)
|
||||||
|
|
||||||
|
def autoFetch(
|
||||||
|
self
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Get auto-fetch-on-startup setting.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: Whether to fetch bulletins on application startup.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.__cfg.get(CfgKey.GLOBAL.BULLETIN.AUTO_FETCH, False)
|
||||||
|
|
||||||
|
def serverUrl(
|
||||||
|
self
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Get bulletin server URL.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Server base URL.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.__cfg.get(
|
||||||
|
CfgKey.GLOBAL.BULLETIN.SERVER_URL,
|
||||||
|
"https://api.autolibrary.kenanzhu.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
def syncInterval(
|
||||||
|
self
|
||||||
|
) -> int:
|
||||||
|
"""
|
||||||
|
Get auto-sync interval in minutes.
|
||||||
|
|
||||||
|
Values below 1 are clamped to 5 minutes.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Sync interval (minutes), minimum 1.
|
||||||
|
"""
|
||||||
|
|
||||||
|
interval = self.__cfg.get(CfgKey.GLOBAL.BULLETIN.SYNC_INTERVAL, 10)
|
||||||
|
if interval < 1:
|
||||||
|
return 5
|
||||||
|
return interval
|
||||||
|
|
||||||
|
def apiUrl(
|
||||||
|
self
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Build the full bulletin API endpoint URL.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Full API URL (base + /bulletins).
|
||||||
|
"""
|
||||||
|
|
||||||
|
base = self.serverUrl().rstrip("/")
|
||||||
|
return f"{base}/bulletins"
|
||||||
|
|
||||||
|
def isFirstSync(
|
||||||
|
self
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Check whether this is the first sync.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if no cached bulletins or no last sync time.
|
||||||
|
"""
|
||||||
|
|
||||||
|
last = self.lastSyncTime()
|
||||||
|
bulletins = self.bulletins()
|
||||||
|
return last is None or not bulletins
|
||||||
|
|
||||||
|
def shouldFullSync(
|
||||||
|
self
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Check whether a full sync is needed.
|
||||||
|
|
||||||
|
A full sync is triggered when the last sync time is more than
|
||||||
|
one hour ago.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if a full sync should be performed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
last = self.lastSyncTime()
|
||||||
|
if last is None:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
last_dt = datetime.fromisoformat(last)
|
||||||
|
return (datetime.now().astimezone() - last_dt) > timedelta(hours=1)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def getSyncDateTimeAndRange(
|
||||||
|
self
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Calculate the date / time / range_hour query parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Keys "date", "time", "range_hour" for the API request.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.isFirstSync():
|
||||||
|
start_date = datetime.now() - timedelta(days=7)
|
||||||
|
range_hour = str(24 * (7 + 1))
|
||||||
|
elif self.shouldFullSync():
|
||||||
|
bulletins = self.bulletins()
|
||||||
|
earliest = min(bulletins, key=lambda x: x.get("dateTime", ""))
|
||||||
|
start_date = datetime.fromisoformat(earliest["dateTime"])
|
||||||
|
diff = datetime.now().astimezone() - start_date
|
||||||
|
range_hour = str(int(diff.total_seconds() / 3600) + 1)
|
||||||
|
else:
|
||||||
|
bulletins = self.bulletins()
|
||||||
|
latest = max(bulletins, key=lambda x: x.get("dateTime", ""))
|
||||||
|
start_date = datetime.fromisoformat(latest["dateTime"])
|
||||||
|
diff = datetime.now().astimezone() - start_date
|
||||||
|
range_hour = str(int(diff.total_seconds() / 3600) + 1)
|
||||||
|
return {
|
||||||
|
"date": start_date.strftime("%Y-%m-%d"),
|
||||||
|
"time": start_date.strftime("%H:%M:%S"),
|
||||||
|
"range_hour": range_hour,
|
||||||
|
}
|
||||||
|
|
||||||
|
def updateAndMergeBulletins(
|
||||||
|
self,
|
||||||
|
new_bulletins: list[dict],
|
||||||
|
delete_ids: list[str]
|
||||||
|
) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Merge incoming bulletins into the cache.
|
||||||
|
|
||||||
|
New bulletins are added with isNew=True. Existing bulletins
|
||||||
|
keep their current isNew state. Entries listed in delete_ids
|
||||||
|
are removed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
new_bulletins (list[dict]): Incoming bulletin list.
|
||||||
|
delete_ids (list[str]): IDs to remove.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[dict]: Merged bulletin list sorted by id.
|
||||||
|
"""
|
||||||
|
|
||||||
|
with self.__lock:
|
||||||
|
delete_set = set(delete_ids)
|
||||||
|
bulletins_dict = {b["id"]: b for b in self.bulletins()}
|
||||||
|
for bulletin in new_bulletins:
|
||||||
|
bid = bulletin["id"]
|
||||||
|
if bid in delete_set:
|
||||||
|
bulletins_dict.pop(bid, None)
|
||||||
|
continue
|
||||||
|
if bid not in bulletins_dict:
|
||||||
|
bulletin["isNew"] = True
|
||||||
|
else:
|
||||||
|
bulletin["isNew"] = bulletins_dict[bid].get("isNew", True)
|
||||||
|
bulletins_dict[bid] = bulletin
|
||||||
|
for bid in delete_set:
|
||||||
|
bulletins_dict.pop(bid, None)
|
||||||
|
result = list(bulletins_dict.values())
|
||||||
|
result.sort(key=lambda x: int(x["id"]))
|
||||||
|
self.setBulletins(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def markBulletinAsRead(
|
||||||
|
self,
|
||||||
|
bulletin_id: str
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Mark a bulletin as read by its id.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bulletin_id (str): The bulletin id.
|
||||||
|
"""
|
||||||
|
|
||||||
|
with self.__lock:
|
||||||
|
bulletins = self.bulletins()
|
||||||
|
for b in bulletins:
|
||||||
|
if b["id"] == bulletin_id:
|
||||||
|
b["isNew"] = False
|
||||||
|
self.setBulletins(bulletins)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
# BulletinManager singleton instance.
|
||||||
|
_bulletin_manager_instance = None
|
||||||
|
|
||||||
|
# Singleton instance lock.
|
||||||
|
_instance_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def instance(
|
||||||
|
) -> BulletinManager:
|
||||||
|
"""
|
||||||
|
Get the BulletinManager singleton instance.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BulletinManager: The singleton BulletinManager instance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
global _bulletin_manager_instance
|
||||||
|
with _instance_lock:
|
||||||
|
if _bulletin_manager_instance is None:
|
||||||
|
_bulletin_manager_instance = BulletinManager()
|
||||||
|
return _bulletin_manager_instance
|
||||||
@@ -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.
|
||||||
|
"""
|
||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user