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

fix(theme): 修复主题管理系统逻辑缺陷

- removeTheme() 删除当前活动主题后从 ConfigManager 读取已保存的主题偏好作为回退方案,不再硬编码 'system'
- saveAndApply() 在调用 _applyCustomTheme 前先 setActiveStyle(style),确保主题应用时使用最新选择的内置样式
- _applyCustomTheme() 返回 bool 表示成败,失败时调用方清除配置中的 custom_theme 避免下次启动循环失败
- importTheme() 增加 self.__lock 保护,消除 TOCTOU 竞态条件
- ThemeManager 新增 CfgKey 导入以支持 removeTheme 读取配置

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 18:58:07 +08:00
parent b56d2c203e
commit 79e5b43498
2 changed files with 43 additions and 33 deletions
+8 -3
View File
@@ -34,6 +34,7 @@ import managers.config.ConfigManager as ConfigManager
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,
setActiveStyle,
instance as themeInstance instance as themeInstance
) )
@@ -47,18 +48,20 @@ from interfaces.ConfigProvider import (
def _applyCustomTheme( def _applyCustomTheme(
name: str, name: str,
fallback_theme: str = "system" fallback_theme: str = "system"
): ) -> bool:
if not name: if not name:
themeInstance().clearTheme(fallback_theme) themeInstance().clearTheme(fallback_theme)
return return True
try: try:
themeInstance().applyTheme(name) themeInstance().applyTheme(name)
return True
except Exception as e: except Exception as e:
logInstance().getLogger("ALSettingsWidget").warning( logInstance().getLogger("ALSettingsWidget").warning(
f"无法应用自定义主题 '{name}',回退到 {fallback_theme} 外观: {e}" f"无法应用自定义主题 '{name}',回退到 {fallback_theme} 外观: {e}"
) )
themeInstance().clearTheme(fallback_theme) themeInstance().clearTheme(fallback_theme)
return False
def _themeToReadable( def _themeToReadable(
need_theme: str need_theme: str
@@ -266,7 +269,9 @@ 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)
_applyCustomTheme(custom_theme, theme) setActiveStyle(style)
if not _applyCustomTheme(custom_theme, 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 # Re-read theme after syncRadioFromNeedTheme — the radio may have
# changed to match the custom theme's need_theme # changed to match the custom theme's need_theme
+35 -30
View File
@@ -19,6 +19,7 @@ from PySide6.QtWidgets import (
QStyleFactory QStyleFactory
) )
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 (
@@ -119,35 +120,36 @@ 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() ext = os.path.splitext(source_path)[1].lower()
if ext == ".qss": with self.__lock:
name = os.path.splitext(os.path.basename(source_path))[0] if ext == ".qss":
dest_path = os.path.join(self.__themes_dir, name + ".altheme") name = os.path.splitext(os.path.basename(source_path))[0]
if os.path.exists(dest_path): dest_path = os.path.join(self.__themes_dir, name + ".altheme")
raise ValueError(f"主题 '{name}' 已存在") if os.path.exists(dest_path):
wrapQssToAtheme(source_path, dest_path, "both") raise ValueError(f"主题 '{name}' 已存在")
return name wrapQssToAtheme(source_path, dest_path, "both")
elif ext == ".altheme": return name
with zipfile.ZipFile(source_path, "r") as zf: elif ext == ".altheme":
if "theme.qss" not in zf.namelist(): with zipfile.ZipFile(source_path, "r") as zf:
raise ValueError("无效的 .altheme: 缺少 theme.qss") if "theme.qss" not in zf.namelist():
info = readThemeInfo(source_path) raise ValueError("无效的 .altheme: 缺少 theme.qss")
name = info.get("name", os.path.splitext(os.path.basename(source_path))[0]) info = readThemeInfo(source_path)
safe_name = os.path.basename(name) name = info.get("name", os.path.splitext(os.path.basename(source_path))[0])
dest_path = os.path.join(self.__themes_dir, safe_name + ".altheme") safe_name = os.path.basename(name)
if os.path.exists(dest_path): dest_path = os.path.join(self.__themes_dir, safe_name + ".altheme")
raise ValueError(f"主题 '{safe_name}' 已存在") if os.path.exists(dest_path):
# Check for name collision with existing themes by the same author raise ValueError(f"主题 '{safe_name}' 已存在")
new_author = info.get("author", "") # Check for name collision with existing themes by the same author
for existing in self.listThemes(): new_author = info.get("author", "")
if (existing.get("name", "") == safe_name for existing in self.listThemes():
and existing.get("author", "") == new_author): if (existing.get("name", "") == safe_name
raise ValueError( and existing.get("author", "") == new_author):
f"主题名称 '{safe_name}' (作者 '{new_author}') 已存在" raise ValueError(
) f"主题名称 '{safe_name}' (作者 '{new_author}') 已存在"
shutil.copy2(source_path, dest_path) )
return safe_name shutil.copy2(source_path, dest_path)
else: return safe_name
raise ValueError(f"不支持的文件类型: {ext}") else:
raise ValueError(f"不支持的文件类型: {ext}")
def listThemes( def listThemes(
self self
@@ -214,7 +216,10 @@ 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.clearTheme("system") saved_theme = configInstance().get(
CfgKey.GLOBAL.APPEARANCE.THEME, "system"
)
self.clearTheme(saved_theme)
def applyTheme( def applyTheme(
self, self,