1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-06-20 16:33:03 +08:00

Compare commits

..

6 Commits

Author SHA1 Message Date
KenanZhu c250fa4a6e refactor(theme): 将重复的主题逻辑下沉至 ThemeUtils,消除 validateTheme 职责过重 2026-06-19 11:21:50 +08:00
KenanZhu 8f8e3e4ba7 refactor(gui): 自定义主题控件及函数重命名,统一 CustomTheme 前缀 2026-06-19 10:22:36 +08:00
KenanZhu 88a74a7a47 refactor(gui): 提取窗口居中逻辑至 CenterOnParentMixin,消除5处重复 showEvent 2026-06-19 10:20:35 +08:00
KenanZhu 5552af1345 refactor(gui): 消除确认按钮重复逻辑,重置按钮不再提前应用主题 2026-06-19 09:36:18 +08:00
KenanZhu f9175371dc feat(gui): +/- 按钮文本替换为 QtAwesome 图标,fa5s 统一升级为 fa6s
- ALSettingsWidget: BrowseQssButton/RemoveThemeButton 的 + / - 文本改为 fa6s.plus/fa6s.minus 图标
- ALAutoScriptEditDialog: ZoomInBtn/ZoomOutBtn 的全角 +/- 改为 fa6s.plus/fa6s.minus 图标
- 其余图标同步从 fa5s 升级至 fa6s (Font Awesome 6)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-17 08:15:03 +08:00
KenanZhu 8e1b28f3fe fix: requirements.txt 编码从 UTF-16 LE 转为 UTF-8,移除 8 个多余依赖包
移除的包: altgraph, mpmath, pefile, pyinstaller-hooks-contrib, pywin32-ctypes, setuptools, sympy, websocket-client
(这些均为传递依赖,pip 会根据直接依赖自动解析安装)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 22:19:05 +08:00
11 changed files with 528 additions and 602 deletions
BIN
View File
Binary file not shown.
+8 -4
View File
@@ -211,12 +211,16 @@ class ALAutoScriptEditDialog(QDialog):
Layout.setSpacing(3) Layout.setSpacing(3)
Layout.setContentsMargins(3, 3, 3, 3) Layout.setContentsMargins(3, 3, 3, 3)
ToolbarLayout = QHBoxLayout() ToolbarLayout = QHBoxLayout()
self.ZoomInBtn = QPushButton("") self.ZoomInBtn = QPushButton("")
self.ZoomInBtn.setIcon(qta.icon("fa6s.plus", color=self._iconColor()))
self.ZoomInBtn.setIconSize(QSize(14, 14))
self.ZoomInBtn.setFixedSize(25, 25) self.ZoomInBtn.setFixedSize(25, 25)
self.ZoomOutBtn = QPushButton("") self.ZoomOutBtn = QPushButton("")
self.ZoomOutBtn.setIcon(qta.icon("fa6s.minus", color=self._iconColor()))
self.ZoomOutBtn.setIconSize(QSize(14, 14))
self.ZoomOutBtn.setFixedSize(25, 25) self.ZoomOutBtn.setFixedSize(25, 25)
self.ZoomResetBtn = QPushButton("") self.ZoomResetBtn = QPushButton("")
self.ZoomResetBtn.setIcon(qta.icon("fa5s.undo", color=self._iconColor())) self.ZoomResetBtn.setIcon(qta.icon("fa6s.rotate-left", color=self._iconColor()))
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("重置缩放")
@@ -241,7 +245,7 @@ class ALAutoScriptEditDialog(QDialog):
ToolbarLayout.addWidget(self.ZoomLabel) ToolbarLayout.addWidget(self.ZoomLabel)
ToolbarLayout.addStretch() ToolbarLayout.addStretch()
self.CopyBtn = QPushButton("") self.CopyBtn = QPushButton("")
self.CopyBtn.setIcon(qta.icon("fa5s.copy", color=self._iconColor())) self.CopyBtn.setIcon(qta.icon("fa6s.copy", color=self._iconColor()))
self.CopyBtn.setIconSize(QSize(14, 14)) self.CopyBtn.setIconSize(QSize(14, 14))
self.CopyBtn.setFixedSize(25, 25) self.CopyBtn.setFixedSize(25, 25)
self.CopyBtn.setToolTip("复制脚本") self.CopyBtn.setToolTip("复制脚本")
+2 -24
View File
@@ -42,6 +42,7 @@ from gui.ALUserTreeWidget import (
ALUserTreeWidget ALUserTreeWidget
) )
from gui.ALWebDriverDownloadDialog import ALWebDriverDownloadDialog from gui.ALWebDriverDownloadDialog import ALWebDriverDownloadDialog
from gui.ALWidgetMixin import CenterOnParentMixin
from gui.resources.ui.Ui_ALConfigWidget import Ui_ALConfigWidget from gui.resources.ui.Ui_ALConfigWidget import Ui_ALConfigWidget
from interfaces.ConfigProvider import ( from interfaces.ConfigProvider import (
CfgKey, CfgKey,
@@ -52,7 +53,7 @@ from utils.JSONReader import JSONReader
from utils.JSONWriter import JSONWriter from utils.JSONWriter import JSONWriter
class ALConfigWidget(QWidget, Ui_ALConfigWidget): class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget):
configWidgetIsClosed = Signal() configWidgetIsClosed = Signal()
@@ -110,29 +111,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked) self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
self.CancelButton.clicked.connect(self.onCancelButtonClicked) self.CancelButton.clicked.connect(self.onCancelButtonClicked)
def showEvent(
self,
event
):
result = super().showEvent(event)
screen_rect = self.screen().geometry()
target_pos = self.parent().geometry().center()
target_pos.setX(target_pos.x() - self.width()//2)
target_pos.setY(target_pos.y() - self.height()//2)
if target_pos.x() < 0:
target_pos.setX(0)
if target_pos.x() + self.width() > screen_rect.width():
target_pos.setX(screen_rect.width() - self.width())
if target_pos.y() < 0:
target_pos.setY(0)
if target_pos.y() + self.height() > screen_rect.height():
target_pos.setY(screen_rect.height() - self.height())
self.move(target_pos)
return result
def closeEvent( def closeEvent(
self, self,
event: QCloseEvent event: QCloseEvent
+2 -25
View File
@@ -24,9 +24,9 @@ from PySide6.QtWidgets import (
) )
from gui.ALSeatMapView import ALSeatMapView from gui.ALSeatMapView import ALSeatMapView
from gui.ALWidgetMixin import CenterOnParentMixin
class ALSeatMapSelectDialog(CenterOnParentMixin, QDialog):
class ALSeatMapSelectDialog(QDialog):
seatMapSelectDialogIsClosed = Signal(list) seatMapSelectDialogIsClosed = Signal(list)
@@ -96,29 +96,6 @@ class ALSeatMapSelectDialog(QDialog):
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked) self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
self.CancelButton.clicked.connect(self.onCancelButtonClicked) self.CancelButton.clicked.connect(self.onCancelButtonClicked)
def showEvent(
self,
event
):
result = super().showEvent(event)
screen_rect = self.screen().geometry()
target_pos = self.parent().geometry().center()
target_pos.setX(target_pos.x() - self.width()//2)
target_pos.setY(target_pos.y() - self.height()//2)
if target_pos.x() < 0:
target_pos.setX(0)
if target_pos.x() + self.width() > screen_rect.width():
target_pos.setX(screen_rect.width() - self.width())
if target_pos.y() < 0:
target_pos.setY(0)
if target_pos.y() + self.height() > screen_rect.height():
target_pos.setY(screen_rect.height() - self.height())
self.move(target_pos)
return result
def closeEvent( def closeEvent(
self, self,
event: QCloseEvent event: QCloseEvent
+95 -113
View File
@@ -19,8 +19,7 @@ from PySide6.QtCore import (
Slot Slot
) )
from PySide6.QtGui import ( from PySide6.QtGui import (
QCloseEvent, QCloseEvent
QShowEvent
) )
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QApplication, QApplication,
@@ -38,6 +37,7 @@ from managers.theme.ThemeManager import(
instance as themeInstance instance as themeInstance
) )
from gui.ALWidgetMixin import CenterOnParentMixin
from gui.resources.ui.Ui_ALSettingsWidget import Ui_ALSettingsWidget from gui.resources.ui.Ui_ALSettingsWidget import Ui_ALSettingsWidget
from interfaces.ConfigProvider import ( from interfaces.ConfigProvider import (
CfgKey, CfgKey,
@@ -83,7 +83,7 @@ def _restartApp(
QProcess.startDetached(sys.executable, sys.argv) QProcess.startDetached(sys.executable, sys.argv)
class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
settingsWidgetIsClosed = Signal() settingsWidgetIsClosed = Signal()
@@ -102,20 +102,36 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
self.connectSignals() self.connectSignals()
self.loadSettings() self.loadSettings()
def closeEvent(
self,
event: QCloseEvent
):
self.settingsWidgetIsClosed.emit()
super().closeEvent(event)
def modifyUi( def modifyUi(
self self
): ):
self.setWindowFlags(Qt.WindowType.Window) self.setWindowFlags(Qt.WindowType.Window)
self.NavigationList.setCurrentRow(0)
self.populateStyles()
self.setNavigationIcons() self.setNavigationIcons()
self.ThemeInfoLabel.setTextFormat(Qt.TextFormat.RichText) color = QApplication.instance().palette().color(
self.ThemeInfoLabel.setStyleSheet( QApplication.instance().palette().ColorRole.WindowText
).name()
self.ImportCustomThemeButton.setIcon(qta.icon("fa6s.plus", color=color))
self.ImportCustomThemeButton.setText("")
self.RemoveCustomThemeButton.setIcon(qta.icon("fa6s.minus", color=color))
self.RemoveCustomThemeButton.setText("")
self.CustomThemeInfoLabel.setTextFormat(Qt.TextFormat.RichText)
self.CustomThemeInfoLabel.setStyleSheet(
"border: 1px solid palette(mid);"\ "border: 1px solid palette(mid);"\
"border-radius: 2px;"\ "border-radius: 2px;"\
"padding: 5px;" "padding: 5px;"
) )
self.NavigationList.setCurrentRow(0)
self.populateStyles()
self.populateCustomThemes()
def setNavigationIcons( def setNavigationIcons(
self self
@@ -125,7 +141,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
color = app.palette().color(app.palette().ColorRole.WindowText).name() color = app.palette().color(app.palette().ColorRole.WindowText).name()
item = self.NavigationList.item(0) item = self.NavigationList.item(0)
if item: if item:
item.setIcon(qta.icon("fa5s.palette", color=color)) item.setIcon(qta.icon("fa6s.palette", color=color))
def populateStyles( def populateStyles(
self self
@@ -134,47 +150,36 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
self.StyleComboBox.clear() self.StyleComboBox.clear()
self.StyleComboBox.addItems(QStyleFactory.keys()) self.StyleComboBox.addItems(QStyleFactory.keys())
def populateCustomThemes(
self
):
self.CustomThemeComboBox.blockSignals(True)
self.CustomThemeComboBox.clear()
self.CustomThemeComboBox.addItem("默认", "")
self.__theme_cache = {}
themes = themeInstance().listThemes()
for t in themes:
name = t.get("name", "")
file = t.get("file", name)
author = t.get("author", "")
if name:
self.__theme_cache[file] = t
self.CustomThemeComboBox.addItem(name, file)
self.CustomThemeComboBox.blockSignals(False)
def connectSignals( def connectSignals(
self self
): ):
self.BrowseQssButton.clicked.connect(self.onImportThemeButtonClicked) self.ImportCustomThemeButton.clicked.connect(self.onImportCustomThemeButtonClicked)
self.RemoveThemeButton.clicked.connect(self.onRemoveThemeButtonClicked) self.RemoveCustomThemeButton.clicked.connect(self.onRemoveCustomThemeButtonClicked)
self.ThemeComboBox.currentIndexChanged.connect(self.onThemeComboBoxChanged) self.CustomThemeComboBox.currentIndexChanged.connect(self.onCustomThemeComboBoxChanged)
self.ResetThemeButton.clicked.connect(self.onResetThemeButtonClicked) self.ResetCustomThemeButton.clicked.connect(self.onResetCustomThemeButtonClicked)
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)
def showEvent(
self,
event: QShowEvent
):
result = super().showEvent(event)
screen_rect = self.screen().geometry()
target_pos = self.parent().geometry().center()
target_pos.setX(target_pos.x() - self.width()//2)
target_pos.setY(target_pos.y() - self.height()//2)
if target_pos.x() < 0:
target_pos.setX(0)
if target_pos.x() + self.width() > screen_rect.width():
target_pos.setX(screen_rect.width() - self.width())
if target_pos.y() < 0:
target_pos.setY(0)
if target_pos.y() + self.height() > screen_rect.height():
target_pos.setY(screen_rect.height() - self.height())
self.move(target_pos)
return result
def closeEvent(
self,
event: QCloseEvent
):
self.settingsWidgetIsClosed.emit()
super().closeEvent(event)
def loadSettings( def loadSettings(
self self
): ):
@@ -195,33 +200,20 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
if index < 0: if index < 0:
index = 0 index = 0
self.StyleComboBox.setCurrentIndex(index) self.StyleComboBox.setCurrentIndex(index)
self.populateThemeList()
if custom_theme: if custom_theme:
idx = self.ThemeComboBox.findData(custom_theme) idx = self.CustomThemeComboBox.findData(custom_theme)
if idx >= 0: if idx >= 0:
self.ThemeComboBox.setCurrentIndex(idx) self.CustomThemeComboBox.setCurrentIndex(idx)
self.updateThemeStatus() self.updateCustomThemeInfo()
self.updateThemeInfo() self.updateCustomThemeStatus()
def updateThemeStatus( def updateCustomThemeInfo(
self self
): ):
file = self.ThemeComboBox.currentData() file = self.CustomThemeComboBox.currentData()
t = self.__theme_cache.get(file) if file else None
name = t.get("name", "") if t else ""
if name:
self.QssStatusLabel.setText(f"当前使用 {name} 主题。")
else:
self.QssStatusLabel.setText("当前使用 默认 主题。")
def updateThemeInfo(
self
):
file = self.ThemeComboBox.currentData()
if not file: if not file:
self.ThemeInfoLabel.setText("") self.CustomThemeInfoLabel.setText("")
return return
t = self.__theme_cache.get(file) t = self.__theme_cache.get(file)
if t: if t:
@@ -229,13 +221,25 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
author = t.get("author", "未知作者") author = t.get("author", "未知作者")
need_theme = t.get("need_theme", "both") need_theme = t.get("need_theme", "both")
brief = t.get("brief", "没有相关简介") brief = t.get("brief", "没有相关简介")
self.ThemeInfoLabel.setText( self.CustomThemeInfoLabel.setText(
f"<b>{name}</b> - 适用于 <i>{_themeToReadable(need_theme)}</i> 主题<br>" f"<b>{name}</b> - 适用于 <i>{_themeToReadable(need_theme)}</i> 主题<br>"
f"作者:{author}<br><br>" f"作者:{author}<br><br>"
f"{brief}" f"{brief}"
) )
else: else:
self.ThemeInfoLabel.setText("") self.CustomThemeInfoLabel.setText("")
def updateCustomThemeStatus(
self
):
file = self.CustomThemeComboBox.currentData()
t = self.__theme_cache.get(file) if file else None
name = t.get("name", "") if t else ""
if name:
self.CustomThemeStatusLabel.setText(f"当前使用 {name} 主题。")
else:
self.CustomThemeStatusLabel.setText("当前使用 默认 主题。")
def syncRadioFromNeedTheme( def syncRadioFromNeedTheme(
self, self,
@@ -261,7 +265,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
else: else:
theme = "system" theme = "system"
style = self.StyleComboBox.currentText() style = self.StyleComboBox.currentText()
custom_theme = self.ThemeComboBox.currentData() or "" custom_theme = self.CustomThemeComboBox.currentData() or ""
if not custom_theme: if not custom_theme:
custom_theme = "" custom_theme = ""
return theme, style, custom_theme return theme, style, custom_theme
@@ -282,8 +286,8 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
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()
self.updateThemeStatus() self.updateCustomThemeStatus()
self.updateThemeInfo() self.updateCustomThemeInfo()
self.__original_theme = theme self.__original_theme = theme
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()
@@ -304,30 +308,12 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
return True return True
return False return False
def populateThemeList(
self
):
self.ThemeComboBox.blockSignals(True)
self.ThemeComboBox.clear()
self.ThemeComboBox.addItem("默认", "")
self.__theme_cache = {}
themes = themeInstance().listThemes()
for t in themes:
name = t.get("name", "")
file = t.get("file", name)
author = t.get("author", "")
if name:
self.__theme_cache[file] = t
self.ThemeComboBox.addItem(name, file)
self.ThemeComboBox.blockSignals(False)
@Slot() @Slot()
def onRemoveThemeButtonClicked( def onRemoveCustomThemeButtonClicked(
self self
): ):
file = self.ThemeComboBox.currentData() file = self.CustomThemeComboBox.currentData()
if not file: if not file:
QMessageBox.information( QMessageBox.information(
self, self,
@@ -348,10 +334,10 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
return return
try: try:
themeInstance().removeTheme(file) themeInstance().removeTheme(file)
self.populateThemeList() self.populateCustomThemes()
self.ThemeComboBox.setCurrentIndex(0) self.CustomThemeComboBox.setCurrentIndex(0)
self.updateThemeStatus() self.updateCustomThemeStatus()
self.updateThemeInfo() self.updateCustomThemeInfo()
except Exception as e: except Exception as e:
QMessageBox.warning( QMessageBox.warning(
self, self,
@@ -360,7 +346,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
) )
@Slot() @Slot()
def onImportThemeButtonClicked( def onImportCustomThemeButtonClicked(
self self
): ):
@@ -374,12 +360,12 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
return return
try: try:
file_id = themeInstance().importTheme(file_path) file_id = themeInstance().importTheme(file_path)
self.populateThemeList() self.populateCustomThemes()
idx = self.ThemeComboBox.findData(file_id) idx = self.CustomThemeComboBox.findData(file_id)
if idx >= 0: if idx >= 0:
self.ThemeComboBox.setCurrentIndex(idx) self.CustomThemeComboBox.setCurrentIndex(idx)
self.updateThemeStatus() self.updateCustomThemeStatus()
self.updateThemeInfo() self.updateCustomThemeInfo()
except Exception as e: except Exception as e:
QMessageBox.warning( QMessageBox.warning(
self, self,
@@ -388,37 +374,37 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
) )
@Slot() @Slot()
def onThemeComboBoxChanged( def onCustomThemeComboBoxChanged(
self, self,
index: int index: int
): ):
self.updateThemeInfo() self.updateCustomThemeInfo()
# no status update, because custom theme is not applied yet.
@Slot() @Slot()
def onResetThemeButtonClicked( def onResetCustomThemeButtonClicked(
self self
): ):
self.ThemeComboBox.blockSignals(True) self.CustomThemeComboBox.blockSignals(True)
if self.__original_custom_theme: if self.__original_custom_theme:
idx = self.ThemeComboBox.findData(self.__original_custom_theme) idx = self.CustomThemeComboBox.findData(self.__original_custom_theme)
if idx >= 0: if idx >= 0:
self.ThemeComboBox.setCurrentIndex(idx) self.CustomThemeComboBox.setCurrentIndex(idx)
else: else:
self.ThemeComboBox.setCurrentIndex(0) self.CustomThemeComboBox.setCurrentIndex(0)
else: else:
self.ThemeComboBox.setCurrentIndex(0) self.CustomThemeComboBox.setCurrentIndex(0)
self.ThemeComboBox.blockSignals(False) self.CustomThemeComboBox.blockSignals(False)
if self.__original_theme == "light": if self.__original_theme == "light":
self.LightThemeRadio.setChecked(True) self.LightThemeRadio.setChecked(True)
elif self.__original_theme == "dark": elif self.__original_theme == "dark":
self.DarkThemeRadio.setChecked(True) self.DarkThemeRadio.setChecked(True)
else: else:
self.SystemThemeRadio.setChecked(True) self.SystemThemeRadio.setChecked(True)
_applyCustomTheme(self.__original_custom_theme, self.__original_theme) self.updateCustomThemeInfo()
self.updateThemeStatus() self.updateCustomThemeStatus()
self.updateThemeInfo()
@Slot() @Slot()
def onCancelButtonClicked( def onCancelButtonClicked(
@@ -443,9 +429,5 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
self self
): ):
_, style, _ = self.collectSettings() self.onApplyButtonClicked() # virtually call apply button clicked
style_changed = self.__original_style != style
self.saveAndApply()
if style_changed:
self.maybeRestart()
self.close() self.close()
+2 -24
View File
@@ -43,6 +43,7 @@ from gui.ALTimerTaskAddDialog import (
ALTimerTaskStatus ALTimerTaskStatus
) )
from gui.ALTimerTaskHistoryDialog import ALTimerTaskHistoryDialog from gui.ALTimerTaskHistoryDialog import ALTimerTaskHistoryDialog
from gui.ALWidgetMixin import CenterOnParentMixin
from gui.resources.ui.Ui_ALTimerTaskManageWidget import Ui_ALTimerTaskManageWidget from gui.resources.ui.Ui_ALTimerTaskManageWidget import Ui_ALTimerTaskManageWidget
from interfaces.ConfigProvider import ( from interfaces.ConfigProvider import (
CfgKey, CfgKey,
@@ -189,7 +190,7 @@ class ALTimerTaskItemWidget(QWidget):
Menu.exec(self.mapToGlobal(pos)) Menu.exec(self.mapToGlobal(pos))
class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): class ALTimerTaskManageWidget(CenterOnParentMixin, QWidget, Ui_ALTimerTaskManageWidget):
class SortPolicy(Enum): class SortPolicy(Enum):
@@ -299,29 +300,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
) )
return False return False
def showEvent(
self,
event
):
result = super().showEvent(event)
screen_rect = self.screen().geometry()
target_pos = self.parent().geometry().center()
target_pos.setX(target_pos.x() - self.width()//2)
target_pos.setY(target_pos.y() - self.height()//2)
if target_pos.x() < 0:
target_pos.setX(0)
if target_pos.x() + self.width() > screen_rect.width():
target_pos.setX(screen_rect.width() - self.width())
if target_pos.y() < 0:
target_pos.setY(0)
if target_pos.y() + self.height() > screen_rect.height():
target_pos.setY(screen_rect.height() - self.height())
self.move(target_pos)
return result
def closeEvent( def closeEvent(
self, self,
event: QCloseEvent event: QCloseEvent
+2 -23
View File
@@ -38,6 +38,7 @@ from managers.driver.WebDriverManager import (
WebDriverStatus WebDriverStatus
) )
from gui.ALStatusLabel import ALStatusLabel from gui.ALStatusLabel import ALStatusLabel
from gui.ALWidgetMixin import CenterOnParentMixin
class DownloadWorker(QThread): class DownloadWorker(QThread):
@@ -123,7 +124,7 @@ class DownloadWorker(QThread):
self.wait() self.wait()
class ALWebDriverDownloadDialog(QDialog): class ALWebDriverDownloadDialog(CenterOnParentMixin, QDialog):
def __init__( def __init__(
self, self,
@@ -152,28 +153,6 @@ class ALWebDriverDownloadDialog(QDialog):
self.initializeDriverManager() self.initializeDriverManager()
self.refreshDriverList() self.refreshDriverList()
def showEvent(
self,
event
):
result = super().showEvent(event)
if self.parent():
screen_rect = self.screen().geometry()
target_pos = self.parent().geometry().center()
target_pos.setX(target_pos.x() - self.width()//2)
target_pos.setY(target_pos.y() - self.height()//2)
if target_pos.x() < 0:
target_pos.setX(0)
if target_pos.x() + self.width() > screen_rect.width():
target_pos.setX(screen_rect.width() - self.width())
if target_pos.y() < 0:
target_pos.setY(0)
if target_pos.y() + self.height() > screen_rect.height():
target_pos.setY(screen_rect.height() - self.height())
self.move(target_pos)
return result
def setupUi( def setupUi(
self self
): ):
+49
View File
@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from PySide6.QtGui import QShowEvent
class CenterOnParentMixin:
"""
Mixin that centres the widget relative to its parent on first show,
clamping the position to the screen bounds.
Usage::
class MyWidget(CenterOnParentMixin, QWidget, Ui_MyWidget):
pass
class MyDialog(CenterOnParentMixin, QDialog):
pass
The mixin must appear **before** QWidget / QDialog in the base list
so that ``super().showEvent(event)`` resolves up the MRO correctly.
"""
def showEvent(
self,
event: QShowEvent
):
super().showEvent(event)
if self.parent():
screen_rect = self.screen().geometry()
target_pos = self.parent().geometry().center()
target_pos.setX(target_pos.x() - self.width() // 2)
target_pos.setY(target_pos.y() - self.height() // 2)
if target_pos.x() < 0:
target_pos.setX(0)
if target_pos.x() + self.width() > screen_rect.width():
target_pos.setX(screen_rect.width() - self.width())
if target_pos.y() < 0:
target_pos.setY(0)
if target_pos.y() + self.height() > screen_rect.height():
target_pos.setY(screen_rect.height() - self.height())
self.move(target_pos)
+16 -32
View File
@@ -115,9 +115,9 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>-51</y>
<width>450</width> <width>397</width>
<height>380</height> <height>434</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="AppearancePageLayout"> <layout class="QVBoxLayout" name="AppearancePageLayout">
@@ -252,7 +252,7 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QGroupBox" name="CustomQssGroupBox"> <widget class="QGroupBox" name="CustomThemeGroupBox">
<property name="title"> <property name="title">
<string>自定义外观</string> <string>自定义外观</string>
</property> </property>
@@ -273,7 +273,7 @@
<number>3</number> <number>3</number>
</property> </property>
<item> <item>
<widget class="QLabel" name="CustomQssHintLabel"> <widget class="QLabel" name="CustomThemeHintLabel">
<property name="text"> <property name="text">
<string>选择一个主题,或导入新的主题文件:</string> <string>选择一个主题,或导入新的主题文件:</string>
</property> </property>
@@ -283,12 +283,12 @@
</widget> </widget>
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="QssPathLayout"> <layout class="QHBoxLayout" name="CustomThemePathLayout">
<property name="spacing"> <property name="spacing">
<number>5</number> <number>5</number>
</property> </property>
<item> <item>
<widget class="QComboBox" name="ThemeComboBox"> <widget class="QComboBox" name="CustomThemeComboBox">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>160</width> <width>160</width>
@@ -298,7 +298,7 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QLineEdit" name="QssPathEdit"> <widget class="QLineEdit" name="CustomThemePathEdit">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>0</width> <width>0</width>
@@ -314,7 +314,7 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QPushButton" name="BrowseQssButton"> <widget class="QPushButton" name="ImportCustomThemeButton">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>25</width> <width>25</width>
@@ -333,7 +333,7 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QPushButton" name="RemoveThemeButton"> <widget class="QPushButton" name="RemoveCustomThemeButton">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>25</width> <width>25</width>
@@ -354,7 +354,7 @@
</layout> </layout>
</item> </item>
<item> <item>
<widget class="QLabel" name="ThemeInfoLabel"> <widget class="QLabel" name="CustomThemeInfoLabel">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>0</width> <width>0</width>
@@ -368,7 +368,7 @@
<enum>Qt::TextFormat::RichText</enum> <enum>Qt::TextFormat::RichText</enum>
</property> </property>
<property name="alignment"> <property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignTop</set> <set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property> </property>
<property name="wordWrap"> <property name="wordWrap">
<bool>true</bool> <bool>true</bool>
@@ -376,28 +376,12 @@
</widget> </widget>
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="QssActionLayout"> <layout class="QHBoxLayout" name="CustomThemeActionLayout">
<property name="spacing"> <property name="spacing">
<number>5</number> <number>5</number>
</property> </property>
<item> <item>
<widget class="QPushButton" name="ApplyQssButton"> <widget class="QPushButton" name="ResetCustomThemeButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="visible">
<bool>false</bool>
</property>
<property name="text">
<string>应用样式</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ResetThemeButton">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>80</width> <width>80</width>
@@ -410,7 +394,7 @@
</widget> </widget>
</item> </item>
<item> <item>
<spacer name="QssActionSpacer"> <spacer name="CustomThemeActionSpacer">
<property name="orientation"> <property name="orientation">
<enum>Qt::Orientation::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
@@ -425,7 +409,7 @@
</layout> </layout>
</item> </item>
<item> <item>
<widget class="QLabel" name="QssStatusLabel"> <widget class="QLabel" name="CustomThemeStatusLabel">
<property name="text"> <property name="text">
<string>当前使用程序 默认 外观。</string> <string>当前使用程序 默认 外观。</string>
</property> </property>
+40 -36
View File
@@ -21,6 +21,7 @@ from interfaces.ConfigProvider import CfgKey
from managers.config.ConfigManager import instance as configInstance from managers.config.ConfigManager import instance as configInstance
from managers.log.LogManager import instance as logInstance from managers.log.LogManager import instance as logInstance
from utils.ThemeUtils import ( from utils.ThemeUtils import (
readThemeInfo,
readThemeQss, readThemeQss,
validateTheme, validateTheme,
wrapQssToAtheme wrapQssToAtheme
@@ -79,17 +80,21 @@ class ThemeManager:
else: else:
return Qt.ColorScheme.Unknown return Qt.ColorScheme.Unknown
def themesDir( def _deleteThemeFile(
self self,
) -> str: name: str
):
""" """
Get the themes directory path. Delete a theme file in the themes storage directory.
Returns: The caller must hold self.__lock before invoking this method.
str: The absolute path to the themes storage directory.
**This method ONLY deletes the file**.
""" """
return self.__themes_dir filepath = os.path.join(self.__themes_dir, name + ".altheme")
if os.path.isfile(filepath):
os.remove(filepath)
def _resolveDestPath( def _resolveDestPath(
self, self,
@@ -121,7 +126,7 @@ class ThemeManager:
existing_info = validateTheme(default_path) existing_info = validateTheme(default_path)
existing_author = existing_info.get("author", "") existing_author = existing_info.get("author", "")
except Exception: except Exception:
self._removeThemeFile(theme_name) # caller holds the lock self._deleteThemeFile(theme_name) # caller holds the lock
raise ValueError( raise ValueError(
f"主题 '{theme_name}' 已存在但无法通过验证, 已清理该主题文件" f"主题 '{theme_name}' 已存在但无法通过验证, 已清理该主题文件"
) )
@@ -139,6 +144,18 @@ class ThemeManager:
) )
return alt_path return alt_path
def themesDir(
self
) -> str:
"""
Get the themes directory path.
Returns:
str: The absolute path to the themes storage directory.
"""
return self.__themes_dir
def importTheme( def importTheme(
self, self,
source_path: str source_path: str
@@ -164,16 +181,16 @@ class ThemeManager:
if not os.path.isfile(source_path): if not os.path.isfile(source_path):
raise FileNotFoundError(source_path) raise FileNotFoundError(source_path)
ext = os.path.splitext(source_path)[1].lower() base_name, ext = os.path.splitext(os.path.basename(source_path))
ext = ext.lower()
with self.__lock: with self.__lock:
if ext == ".qss": if ext == ".qss":
name = os.path.splitext(os.path.basename(source_path))[0] dest_path = self._resolveDestPath(base_name, "未知作者")
dest_path = self._resolveDestPath(name, "未知作者")
wrapQssToAtheme(source_path, dest_path, "both") wrapQssToAtheme(source_path, dest_path, "both")
return os.path.splitext(os.path.basename(dest_path))[0] return os.path.splitext(os.path.basename(dest_path))[0]
elif ext == ".altheme": elif ext == ".altheme":
info = validateTheme(source_path) info = validateTheme(source_path)
name = info.get("name", os.path.splitext(os.path.basename(source_path))[0]) name = info.get("name", base_name)
safe_name = os.path.basename(name) safe_name = os.path.basename(name)
new_author = info.get("author", "") new_author = info.get("author", "")
dest_path = self._resolveDestPath(safe_name, new_author) dest_path = self._resolveDestPath(safe_name, new_author)
@@ -203,7 +220,7 @@ class ThemeManager:
if filename.endswith(".altheme"): if filename.endswith(".altheme"):
filepath = os.path.join(self.__themes_dir, filename) filepath = os.path.join(self.__themes_dir, filename)
try: try:
info = validateTheme(filepath, check_qss=False) # skip QSS read for list scan info = validateTheme(filepath)
name = info.get("name", "") name = info.get("name", "")
author = info.get("author", "") author = info.get("author", "")
key = (name, author) key = (name, author)
@@ -225,26 +242,6 @@ class ThemeManager:
) )
return themes return themes
def _removeThemeFile(
self,
name: str
):
"""
Remove a theme file without locking.
The caller must hold self.__lock before invoking this method.
"""
filepath = os.path.join(self.__themes_dir, name + ".altheme")
if os.path.isfile(filepath):
os.remove(filepath)
if self.__current_theme_name == name:
self.__current_theme_name = ""
saved_theme = configInstance().get(
CfgKey.GLOBAL.APPEARANCE.THEME, "system"
)
self.clearTheme(saved_theme)
def removeTheme( def removeTheme(
self, self,
name: str name: str
@@ -253,14 +250,21 @@ class ThemeManager:
Remove a theme by name. Remove a theme by name.
If the removed theme is currently active, clears the QSS If the removed theme is currently active, clears the QSS
stylesheet from the application. stylesheet from the application and reverts to the saved
colour scheme.
Args: Args:
name (str): The theme name to remove. name (str): The theme name to remove.
""" """
with self.__lock: with self.__lock:
self._removeThemeFile(name) self._deleteThemeFile(name)
if self.__current_theme_name == name:
self.__current_theme_name = ""
saved_theme = configInstance().get(
CfgKey.GLOBAL.APPEARANCE.THEME, "system"
)
self.clearTheme(saved_theme)
def applyTheme( def applyTheme(
self, self,
@@ -284,7 +288,7 @@ class ThemeManager:
if not os.path.isfile(filepath): if not os.path.isfile(filepath):
raise FileNotFoundError(filepath) raise FileNotFoundError(filepath)
with self.__lock: with self.__lock:
info = validateTheme(filepath) info = readThemeInfo(filepath)
qss = readThemeQss(filepath) qss = readThemeQss(filepath)
app = QApplication.instance() app = QApplication.instance()
if app: if app:
+60 -69
View File
@@ -68,17 +68,20 @@ def readThemeInfo(
altheme_path: str altheme_path: str
) -> dict: ) -> dict:
""" """
Read only the info.json metadata from a .altheme file. Read and validate the info.json metadata from a .altheme file.
Verifies that all required fields (name, author, need_theme, brief)
are present with valid values.
Args: Args:
altheme_path (str): Path to the .altheme file. altheme_path (str): Path to the .altheme file.
Returns: Returns:
dict: The theme metadata dictionary. dict: The validated theme metadata dictionary.
Raises: Raises:
FileNotFoundError: If altheme_path does not exist. FileNotFoundError: If altheme_path does not exist.
ValueError: If the .altheme does not contain info.json. ValueError: If info.json is missing or any field is invalid.
""" """
if not os.path.isfile(altheme_path): if not os.path.isfile(altheme_path):
@@ -87,76 +90,14 @@ def readThemeInfo(
if "info.json" not in zf.namelist(): if "info.json" not in zf.namelist():
raise ValueError("无效的 .altheme: 缺少 info.json") raise ValueError("无效的 .altheme: 缺少 info.json")
with zf.open("info.json") as fh: with zf.open("info.json") as fh:
info = json.loads(fh.read().decode("utf-8"))
if "name" not in info:
raise ValueError("无效的 .altheme: info.json 缺少 'name' 字段")
return info
def readThemeQss(
altheme_path: str
) -> str:
"""
Read the theme.qss content directly from a .altheme archive.
Args:
altheme_path (str): Path to the .altheme file.
Returns:
str: The QSS stylesheet content.
Raises:
FileNotFoundError: If altheme_path does not exist.
ValueError: If theme.qss is missing from the archive.
"""
if not os.path.isfile(altheme_path):
raise FileNotFoundError(altheme_path)
with zipfile.ZipFile(altheme_path, "r") as zf:
if "theme.qss" not in zf.namelist():
raise ValueError("无效的 .altheme: 缺少 theme.qss")
return zf.read("theme.qss").decode("utf-8")
def validateTheme(
altheme_path: str,
check_qss: bool = True
) -> dict:
"""
Validate a .altheme file and return its metadata.
Checks that info.json and theme.qss both exist, info.json
contains all required fields with valid values, and theme.qss
is a non-empty entry in the archive.
Args:
altheme_path (str): Path to the .altheme file.
check_qss (bool): If False, skip theme.qss existence and
content checks (for list-only operations).
Returns:
dict: The validated theme metadata dictionary.
Raises:
FileNotFoundError: If altheme_path does not exist.
ValueError: If validation fails for any reason.
"""
if not os.path.isfile(altheme_path):
raise FileNotFoundError(altheme_path)
with zipfile.ZipFile(altheme_path, "r") as zf:
names = zf.namelist()
if "info.json" not in names:
raise ValueError("无效的 .altheme: 缺少 info.json")
if "theme.qss" not in names:
raise ValueError("无效的 .altheme: 缺少 theme.qss")
info_bytes = zf.read("info.json")
qss_bytes = zf.read("theme.qss") if check_qss else None # skip QSS read when only listing
try: try:
info = json.loads(info_bytes.decode("utf-8")) info = json.loads(fh.read().decode("utf-8"))
except (json.JSONDecodeError, UnicodeDecodeError) as e: except (json.JSONDecodeError, UnicodeDecodeError) as e:
raise ValueError(f"无效的 .altheme: info.json 解析失败 — {e}") raise ValueError(f"无效的 .altheme: info.json 解析失败 — {e}")
if "name" not in info or not isinstance(info.get("name"), str) or not info["name"].strip(): if "name" not in info or not isinstance(info.get("name"), str) or not info["name"].strip():
raise ValueError("无效的 .altheme: info.json 缺少有效的 'name' 字段") raise ValueError("无效的 .altheme: info.json 缺少有效的 'name' 字段")
# reject blank author so info.json does not drift from the "未知作者" filename fallback # reject blank author so that info.json does not drift from the
# "未知作者" filename fallback used by wrapQssToAtheme
if ("author" not in info or not isinstance(info.get("author"), str) if ("author" not in info or not isinstance(info.get("author"), str)
or not info["author"].strip()): or not info["author"].strip()):
raise ValueError("无效的 .altheme: info.json 缺少有效的 'author' 字段") raise ValueError("无效的 .altheme: info.json 缺少有效的 'author' 字段")
@@ -168,8 +109,58 @@ def validateTheme(
) )
if "brief" not in info or not isinstance(info.get("brief"), str): if "brief" not in info or not isinstance(info.get("brief"), str):
raise ValueError("无效的 .altheme: info.json 缺少有效的 'brief' 字段") raise ValueError("无效的 .altheme: info.json 缺少有效的 'brief' 字段")
if check_qss and not qss_bytes.strip(): return info
def readThemeQss(
altheme_path: str
) -> str:
"""
Read the theme.qss content from a .altheme archive.
Args:
altheme_path (str): Path to the .altheme file.
Returns:
str: The non-empty QSS stylesheet content.
Raises:
FileNotFoundError: If altheme_path does not exist.
ValueError: If theme.qss is missing or empty.
"""
if not os.path.isfile(altheme_path):
raise FileNotFoundError(altheme_path)
with zipfile.ZipFile(altheme_path, "r") as zf:
if "theme.qss" not in zf.namelist():
raise ValueError("无效的 .altheme: 缺少 theme.qss")
qss = zf.read("theme.qss").decode("utf-8")
if not qss.strip():
raise ValueError("无效的 .altheme: theme.qss 为空") raise ValueError("无效的 .altheme: theme.qss 为空")
return qss
def validateTheme(
altheme_path: str
) -> dict:
"""
Fully validate a .altheme file and return its metadata.
Delegates info validation to readThemeInfo and QSS validation
to readThemeQss, then additionally checks that theme.qss is
non-empty.
Args:
altheme_path (str): Path to the .altheme file.
Returns:
dict: The validated theme metadata dictionary.
Raises:
FileNotFoundError: If altheme_path does not exist.
ValueError: If validation fails for any reason.
"""
info = readThemeInfo(altheme_path)
readThemeQss(altheme_path) # validates existence and non-empty
return info return info
def wrapQssToAtheme( def wrapQssToAtheme(