1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-06-18 07:23:03 +08:00

fix(theme): 主题系统交叉审查缺陷修复

启动恢复:
- _initializeAppearance 自定义主题加载失败时调用 clearTheme 回退配色方案

列表校验:
- listThemes 同时校验 info.json 和 theme.qss 完整性
- 损坏的主题文件记录 LogManager 警告并跳过
- 按 (名称, 作者) 去重,同一作者同名主题仅保留一个

导入保护:
- importTheme 新增 (名称, 作者) 冲突检查
- applyTheme 缺少 theme.qss 时抛出明确 ValueError

状态一致性:
- saveAndApply 在 syncRadioFromNeedTheme 后重新采集 THEME 再保存
- __original_theme / __original_custom_theme 随每次 Apply 同步更新
- Reset 按钮恢复组合框到原始位置并刷新状态标签

代码质量:
- 提取 _colorSchemeFor 静态方法消除 applyTheme/clearTheme 中的重复映射
- 移除未使用的 _applyTheme 死代码
- _active_style_name 默认值从 '' 改为 'Fusion'
- 日志调用统一使用 LogManager
- _applyCustomTheme 异常时通过 LogManager 记录详细错误

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 00:46:05 +08:00
parent 62f8ec3d91
commit 44dbde3355
3 changed files with 134 additions and 141 deletions
+9 -8
View File
@@ -12,15 +12,14 @@ import os
from PySide6.QtCore import QStandardPaths, QDir from PySide6.QtCore import QStandardPaths, QDir
from PySide6.QtWidgets import QApplication from PySide6.QtWidgets import QApplication
from gui.ALSettingsWidget import (
_setActiveStyleName,
_applyTheme,
)
from interfaces.ConfigProvider import CfgKey from interfaces.ConfigProvider import CfgKey
from managers.config.ConfigManager import instance as configInstance from managers.config.ConfigManager import instance as configInstance
from managers.driver.WebDriverManager import instance as webdriverInstance from managers.driver.WebDriverManager import instance as webdriverInstance
from managers.log.LogManager import instance as logInstance from managers.log.LogManager import instance as logInstance
from managers.theme.ThemeManager import instance as themeInstance from managers.theme.ThemeManager import(
setActiveStyle,
instance as themeInstance
)
def _initializeLogManager( def _initializeLogManager(
@@ -82,14 +81,16 @@ def _initializeAppearance(
saved_theme = cfg.get(CfgKey.GLOBAL.APPEARANCE.THEME, "system") saved_theme = cfg.get(CfgKey.GLOBAL.APPEARANCE.THEME, "system")
saved_custom_theme = cfg.get(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "") saved_custom_theme = cfg.get(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "")
app.setStyle(saved_style) app.setStyle(saved_style)
_setActiveStyleName(saved_style) setActiveStyle(saved_style)
logger = logInstance().getLogger("AppInitializer") logger = logInstance().getLogger("AppInitializer")
if saved_custom_theme: if saved_custom_theme:
try: try:
themeInstance().applyTheme(saved_custom_theme) themeInstance().applyTheme(saved_custom_theme)
except Exception: except Exception:
logger.warning("无法应用自定义主题 '%s',回退到默认外观", saved_custom_theme) logger.warning("无法应用自定义主题 '%s'回退到默认外观", saved_custom_theme)
_applyTheme(saved_theme) themeInstance().clearTheme(saved_theme)
return
themeInstance().clearTheme(saved_theme)
def initializeApp( def initializeApp(
) -> bool: ) -> bool:
+46 -36
View File
@@ -31,8 +31,9 @@ from PySide6.QtWidgets import (
) )
import managers.config.ConfigManager as ConfigManager import managers.config.ConfigManager as ConfigManager
from managers.theme.ThemeManager import ( from managers.log.LogManager import instance as logInstance
ThemeManager, from managers.theme.ThemeManager import(
getActiveStyle,
instance as themeInstance instance as themeInstance
) )
@@ -43,31 +44,34 @@ from interfaces.ConfigProvider import (
) )
_active_style_name = "" def _applyCustomTheme(
name: str,
fallback_theme: str = "system"
def _setActiveStyleName(
name: str
): ):
global _active_style_name if not name:
_active_style_name = name themeInstance().clearTheme(fallback_theme)
def _applyTheme(
theme: str
):
global _active_style_name
app : QApplication | None = QApplication.instance()
if not app:
return return
if theme == "dark": try:
app.styleHints().setColorScheme(Qt.ColorScheme.Dark) themeInstance().applyTheme(name)
elif theme == "light": except Exception as e:
app.styleHints().setColorScheme(Qt.ColorScheme.Light) logInstance().getLogger("ALSettingsWidget").warning(
f"无法应用自定义主题 '{name}',回退到 {fallback_theme} 外观: {e}"
)
themeInstance().clearTheme(fallback_theme)
def _themeToReadable(
need_theme: str
) -> str:
if need_theme == "dark":
return "深色"
elif need_theme == "light":
return "浅色"
elif need_theme == "both":
return "所有"
else: else:
app.styleHints().setColorScheme(Qt.ColorScheme.Unknown) return "未知"
app.setStyle(QStyleFactory.create(_active_style_name))
def _restartApp( def _restartApp(
): ):
@@ -75,6 +79,7 @@ def _restartApp(
QApplication.instance().quit() QApplication.instance().quit()
QProcess.startDetached(sys.executable, sys.argv) QProcess.startDetached(sys.executable, sys.argv)
class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
settingsWidgetIsClosed = Signal() settingsWidgetIsClosed = Signal()
@@ -126,12 +131,6 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
self.StyleComboBox.clear() self.StyleComboBox.clear()
self.StyleComboBox.addItems(QStyleFactory.keys()) self.StyleComboBox.addItems(QStyleFactory.keys())
def currentStyleKey(
self
) -> str:
return _active_style_name
def connectSignals( def connectSignals(
self self
): ):
@@ -181,7 +180,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
custom_theme = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "") custom_theme = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "")
self.__original_theme = theme self.__original_theme = theme
self.__original_custom_theme = custom_theme self.__original_custom_theme = custom_theme
self.__original_style = self.currentStyleKey() self.__original_style = getActiveStyle()
if theme == "light": if theme == "light":
self.LightThemeRadio.setChecked(True) self.LightThemeRadio.setChecked(True)
elif theme == "dark": elif theme == "dark":
@@ -224,7 +223,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
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.ThemeInfoLabel.setText(
f"<b>{name}</b> - 适用于 <i>{ThemeManager.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}"
) )
@@ -267,15 +266,18 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
theme, style, custom_theme = self.collectSettings() theme, style, custom_theme = self.collectSettings()
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.STYLE, style) self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.STYLE, style)
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, custom_theme) self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, custom_theme)
themeInstance().applyThemeOrClear(custom_theme, theme) _applyCustomTheme(custom_theme, 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)
_applyTheme(theme)
self.setNavigationIcons() self.setNavigationIcons()
self.updateThemeStatus() self.updateThemeStatus()
self.updateThemeInfo() self.updateThemeInfo()
self.__original_style = self.currentStyleKey() self.__original_theme = theme
self.__original_custom_theme = custom_theme if custom_theme else ""
self.__original_style = getActiveStyle()
def maybeRestart( def maybeRestart(
self self
@@ -351,7 +353,14 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
): ):
self.ThemeComboBox.blockSignals(True) self.ThemeComboBox.blockSignals(True)
self.ThemeComboBox.setCurrentIndex(0) if self.__original_custom_theme:
idx = self.ThemeComboBox.findText(self.__original_custom_theme)
if idx >= 0:
self.ThemeComboBox.setCurrentIndex(idx)
else:
self.ThemeComboBox.setCurrentIndex(0)
else:
self.ThemeComboBox.setCurrentIndex(0)
self.ThemeComboBox.blockSignals(False) self.ThemeComboBox.blockSignals(False)
if self.__original_theme == "light": if self.__original_theme == "light":
self.LightThemeRadio.setChecked(True) self.LightThemeRadio.setChecked(True)
@@ -359,7 +368,8 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
self.DarkThemeRadio.setChecked(True) self.DarkThemeRadio.setChecked(True)
else: else:
self.SystemThemeRadio.setChecked(True) self.SystemThemeRadio.setChecked(True)
themeInstance().clearTheme(self.__original_theme) _applyCustomTheme(self.__original_custom_theme, self.__original_theme)
self.updateThemeStatus()
self.updateThemeInfo() self.updateThemeInfo()
@Slot() @Slot()
+79 -97
View File
@@ -14,9 +14,13 @@ import threading
import zipfile import zipfile
from PySide6.QtCore import Qt from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication from PySide6.QtWidgets import (
QApplication,
QStyleFactory
)
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 utils.ThemeUtils import ( from utils.ThemeUtils import (
packTheme, packTheme,
readThemeInfo, readThemeInfo,
@@ -25,6 +29,22 @@ from utils.ThemeUtils import (
) )
_active_style_name = "Fusion"
def setActiveStyle(
style_name: str
):
global _active_style_name
_active_style_name = style_name
def getActiveStyle(
) -> str:
return _active_style_name
class ThemeManager: class ThemeManager:
""" """
Theme manager class. Theme manager class.
@@ -46,6 +66,21 @@ class ThemeManager:
self.__current_theme_name = "" self.__current_theme_name = ""
os.makedirs(self.__themes_dir, exist_ok=True) os.makedirs(self.__themes_dir, exist_ok=True)
@staticmethod
def _colorSchemeFor(
theme: str
) -> Qt.ColorScheme:
"""
Map a theme identifier to the corresponding Qt color scheme.
"""
if theme == "dark":
return Qt.ColorScheme.Dark
elif theme == "light":
return Qt.ColorScheme.Light
else:
return Qt.ColorScheme.Unknown
def themesDir( def themesDir(
self self
) -> str: ) -> str:
@@ -101,6 +136,14 @@ class ThemeManager:
dest_path = os.path.join(self.__themes_dir, safe_name + ".altheme") dest_path = os.path.join(self.__themes_dir, safe_name + ".altheme")
if os.path.exists(dest_path): if os.path.exists(dest_path):
raise ValueError(f"主题 '{safe_name}' 已存在") raise ValueError(f"主题 '{safe_name}' 已存在")
# Check for name collision with existing themes by the same author
new_author = info.get("author", "")
for existing in self.listThemes():
if (existing.get("name", "") == safe_name
and existing.get("author", "") == new_author):
raise ValueError(
f"主题名称 '{safe_name}' (作者 '{new_author}') 已存在"
)
shutil.copy2(source_path, dest_path) shutil.copy2(source_path, dest_path)
return safe_name return safe_name
else: else:
@@ -120,6 +163,7 @@ class ThemeManager:
""" """
themes = [] themes = []
seen_keys = set()
if not os.path.isdir(self.__themes_dir): if not os.path.isdir(self.__themes_dir):
return themes return themes
for filename in sorted(os.listdir(self.__themes_dir)): for filename in sorted(os.listdir(self.__themes_dir)):
@@ -127,9 +171,27 @@ class ThemeManager:
filepath = os.path.join(self.__themes_dir, filename) filepath = os.path.join(self.__themes_dir, filename)
try: try:
info = readThemeInfo(filepath) info = readThemeInfo(filepath)
with zipfile.ZipFile(filepath, "r") as zf:
if "theme.qss" not in zf.namelist():
raise ValueError("缺少 theme.qss")
name = info.get("name", "")
author = info.get("author", "")
key = (name, author)
if key in seen_keys:
logInstance().getLogger("ThemeManager").warning(
f"主题名称 '{name}' (作者 '{author}') 重复 (文件 '{filename}') 已跳过"
)
continue
seen_keys.add(key)
themes.append(info) themes.append(info)
except Exception: except Exception as e:
pass logInstance().getLogger("ThemeManager").warning(
f"无法读取主题文件 '{filename}',已跳过: {e}"
)
else:
logInstance().getLogger("ThemeManager").warning(
f"未知文件类型 '{filename}',已跳过"
)
return themes return themes
def removeTheme( def removeTheme(
@@ -152,7 +214,7 @@ class ThemeManager:
os.remove(filepath) os.remove(filepath)
if self.__current_theme_name == name: if self.__current_theme_name == name:
self.__current_theme_name = "" self.__current_theme_name = ""
self._clearQss() self.clearTheme("system")
def applyTheme( def applyTheme(
self, self,
@@ -186,13 +248,17 @@ class ThemeManager:
app = QApplication.instance() app = QApplication.instance()
if app: if app:
app.setStyleSheet(qss) app.setStyleSheet(qss)
else:
raise ValueError(
f"主题 '{name}' 的 .altheme 文件中缺少 theme.qss"
)
app = QApplication.instance() app = QApplication.instance()
if app: if app:
need_theme = info.get("need_theme", "both") need_theme = info.get("need_theme", "both")
if need_theme == "dark": app.styleHints().setColorScheme(
app.styleHints().setColorScheme(Qt.ColorScheme.Dark) ThemeManager._colorSchemeFor(need_theme)
elif need_theme == "light": )
app.styleHints().setColorScheme(Qt.ColorScheme.Light) app.setStyle(QStyleFactory.create(_active_style_name))
self.__current_theme_name = name self.__current_theme_name = name
def clearTheme( def clearTheme(
@@ -207,99 +273,15 @@ class ThemeManager:
("light", "dark", or "system"). ("light", "dark", or "system").
""" """
app = QApplication.instance()
if app:
app.setStyleSheet("")
self._applyColorScheme(theme)
def applyThemeOrClear(
self,
name: str,
fallback_theme: str = "system"
):
"""
Apply a custom theme by name, or clear to fallback if empty.
Args:
name (str): The theme name to apply, or empty to clear.
fallback_theme (str): Color scheme to use if name is empty
or if the theme fails to apply.
"""
if not name:
self.clearTheme(fallback_theme)
return
try:
self.applyTheme(name)
except Exception:
self.clearTheme(fallback_theme)
def _applyColorScheme(
self,
theme: str
):
"""
Set the Qt application color scheme.
Args:
theme (str): "dark", "light", or any other value for system default.
"""
app = QApplication.instance() app = QApplication.instance()
if not app: if not app:
return return
if theme == "dark": app.setStyleSheet("")
app.styleHints().setColorScheme(Qt.ColorScheme.Dark) app.styleHints().setColorScheme(
elif theme == "light": ThemeManager._colorSchemeFor(theme)
app.styleHints().setColorScheme(Qt.ColorScheme.Light) )
else: app.setStyle(QStyleFactory.create(_active_style_name))
app.styleHints().setColorScheme(Qt.ColorScheme.Unknown)
@staticmethod
def themeToReadable(
need_theme: str
) -> str:
"""
Convert a need_theme code to human-readable Chinese text.
Args:
need_theme (str): "dark", "light", "both", or other.
Returns:
str: Readable Chinese label.
"""
if need_theme == "dark":
return "深色"
elif need_theme == "light":
return "浅色"
elif need_theme == "both":
return "所有"
else:
return "未知"
def currentThemeName(
self
) -> str:
"""
Get the name of the currently active theme.
Returns:
str: Current theme name, or empty string if none is active.
"""
return self.__current_theme_name
def _clearQss(
self
):
"""
Clear the current QSS stylesheet from the application.
"""
app = QApplication.instance()
if app:
app.setStyleSheet("")
# ThemeManager singleton instance. # ThemeManager singleton instance.
_theme_manager_instance = None _theme_manager_instance = None