From 007b4dc2efd5b23cd50a9cc246070072eed8847a Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Tue, 16 Jun 2026 18:37:47 +0800 Subject: [PATCH] =?UTF-8?q?fix(theme):=20=E4=BF=AE=E5=A4=8D=E5=90=8C?= =?UTF-8?q?=E5=90=8D=E4=B8=BB=E9=A2=98=E6=97=A0=E6=B3=95=E5=8C=BA=E5=88=86?= =?UTF-8?q?=E4=BD=9C=E8=80=85=E5=8F=8A=E5=AF=BC=E5=85=A5=E9=93=BE=E8=B7=AF?= =?UTF-8?q?=E8=BE=B9=E7=95=8C=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ThemeUtils.validateTheme 和 readThemeQss 集中校验与读取逻辑 - ThemeManager.importTheme 通过 _resolveDestPath 处理同名主题: 不同作者自动命名为 {主题名}_{作者名}.altheme, 首次导入保持原名 - ThemeManager.listThemes 返回 file 字段以便 UI 层定位文件 - ALSettingsWidget 全线改用 file 标识符, 组合框按作者消歧义显示 - 移除 applyTheme 中的临时目录解压, 改用 readThemeQss 直接读取 --- src/gui/ALSettingsWidget.py | 37 +++++++---- src/managers/theme/ThemeManager.py | 102 +++++++++++++++++------------ src/utils/ThemeUtils.py | 76 ++++++++++++++++++++- 3 files changed, 157 insertions(+), 58 deletions(-) diff --git a/src/gui/ALSettingsWidget.py b/src/gui/ALSettingsWidget.py index 4450218..f7f6666 100644 --- a/src/gui/ALSettingsWidget.py +++ b/src/gui/ALSettingsWidget.py @@ -196,7 +196,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): self.StyleComboBox.setCurrentIndex(index) self.populateThemeList() if custom_theme: - idx = self.ThemeComboBox.findText(custom_theme) + idx = self.ThemeComboBox.findData(custom_theme) if idx >= 0: self.ThemeComboBox.setCurrentIndex(idx) self.updateThemeStatus() @@ -206,8 +206,10 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): self ): - name = self.ThemeComboBox.currentText() - if name and name != "默认": + file = self.ThemeComboBox.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("当前使用 默认 主题。") @@ -216,12 +218,13 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): self ): - name = self.ThemeComboBox.currentText() - if not name or name == "默认": + file = self.ThemeComboBox.currentData() + if not file: self.ThemeInfoLabel.setText("") return - t = self.__theme_cache.get(name) + t = self.__theme_cache.get(file) if t: + name = t.get("name", "未知") author = t.get("author", "未知") need_theme = t.get("need_theme", "both") brief = t.get("brief", "没有相关简介") @@ -257,8 +260,8 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): else: theme = "system" style = self.StyleComboBox.currentText() - custom_theme = self.ThemeComboBox.currentText() - if custom_theme == "默认": + custom_theme = self.ThemeComboBox.currentData() or "" + if not custom_theme: custom_theme = "" return theme, style, custom_theme @@ -306,14 +309,20 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): self.ThemeComboBox.blockSignals(True) self.ThemeComboBox.clear() - self.ThemeComboBox.addItem("默认") + 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[name] = t - self.ThemeComboBox.addItem(name) + self.__theme_cache[file] = t + if author and author != "未知": + display = f"{name} ({author})" + else: + display = name + self.ThemeComboBox.addItem(display, file) self.ThemeComboBox.blockSignals(False) @Slot() @@ -330,9 +339,9 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): if not file_path: return try: - name = themeInstance().importTheme(file_path) + file_id = themeInstance().importTheme(file_path) self.populateThemeList() - idx = self.ThemeComboBox.findText(name) + idx = self.ThemeComboBox.findData(file_id) if idx >= 0: self.ThemeComboBox.setCurrentIndex(idx) self.updateThemeStatus() @@ -359,7 +368,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): self.ThemeComboBox.blockSignals(True) if self.__original_custom_theme: - idx = self.ThemeComboBox.findText(self.__original_custom_theme) + idx = self.ThemeComboBox.findData(self.__original_custom_theme) if idx >= 0: self.ThemeComboBox.setCurrentIndex(idx) else: diff --git a/src/managers/theme/ThemeManager.py b/src/managers/theme/ThemeManager.py index 3e77346..12d55f0 100644 --- a/src/managers/theme/ThemeManager.py +++ b/src/managers/theme/ThemeManager.py @@ -9,9 +9,7 @@ See the LICENSE file for details. """ import os import shutil -import tempfile import threading -import zipfile from PySide6.QtCore import Qt from PySide6.QtWidgets import ( @@ -23,9 +21,8 @@ from interfaces.ConfigProvider import CfgKey from managers.config.ConfigManager import instance as configInstance from managers.log.LogManager import instance as logInstance from utils.ThemeUtils import ( - packTheme, - readThemeInfo, - unpackTheme, + readThemeQss, + validateTheme, wrapQssToAtheme ) @@ -94,6 +91,54 @@ class ThemeManager: return self.__themes_dir + def _resolveDestPath( + self, + theme_name: str, + author: str + ) -> str: + """ + Resolve the destination path for an imported theme. + + If the default {name}.altheme path does not exist, use it directly. + If it exists and has a different author, use {name}_{author}.altheme. + If it exists and has the same author, raise ValueError. + + Args: + theme_name (str): Sanitised theme name. + author (str): Theme author string. + + Returns: + str: The resolved destination file path. + + Raises: + ValueError: If a theme with the same name and author already exists. + """ + + default_path = os.path.join(self.__themes_dir, theme_name + ".altheme") + if not os.path.exists(default_path): + return default_path + try: + existing_info = validateTheme(default_path) + existing_author = existing_info.get("author", "") + except Exception: + self.removeTheme(theme_name) + raise ValueError( + f"主题 '{theme_name}' 已存在但无法通过验证, 已清理该主题文件" + ) + if existing_author == author: + raise ValueError( + f"主题名称 '{theme_name}' (作者 '{author}') 已存在" + ) + safe_author = os.path.basename(author) if author else "未知" + alt_path = os.path.join( + self.__themes_dir, f"{theme_name}_{safe_author}.altheme" + ) + if os.path.exists(alt_path): + raise ValueError( + f"主题名称 '{theme_name}' (作者 '{author}') 已存在" + ) + return alt_path + def importTheme( self, source_path: str @@ -123,31 +168,17 @@ class ThemeManager: with self.__lock: if ext == ".qss": name = os.path.splitext(os.path.basename(source_path))[0] - dest_path = os.path.join(self.__themes_dir, name + ".altheme") - if os.path.exists(dest_path): - raise ValueError(f"主题 '{name}' 已存在") + dest_path = self._resolveDestPath(name, "未知") wrapQssToAtheme(source_path, dest_path, "both") - return name + return os.path.splitext(os.path.basename(dest_path))[0] elif ext == ".altheme": - with zipfile.ZipFile(source_path, "r") as zf: - if "theme.qss" not in zf.namelist(): - raise ValueError("无效的 .altheme: 缺少 theme.qss") - info = readThemeInfo(source_path) + info = validateTheme(source_path) name = info.get("name", os.path.splitext(os.path.basename(source_path))[0]) safe_name = os.path.basename(name) - dest_path = os.path.join(self.__themes_dir, safe_name + ".altheme") - if os.path.exists(dest_path): - 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}') 已存在" - ) + dest_path = self._resolveDestPath(safe_name, new_author) shutil.copy2(source_path, dest_path) - return safe_name + return os.path.splitext(os.path.basename(dest_path))[0] else: raise ValueError(f"不支持的文件类型: {ext}") @@ -172,10 +203,7 @@ class ThemeManager: if filename.endswith(".altheme"): filepath = os.path.join(self.__themes_dir, filename) try: - info = readThemeInfo(filepath) - with zipfile.ZipFile(filepath, "r") as zf: - if "theme.qss" not in zf.namelist(): - raise ValueError("缺少 theme.qss") + info = validateTheme(filepath) name = info.get("name", "") author = info.get("author", "") key = (name, author) @@ -185,6 +213,7 @@ class ThemeManager: ) continue seen_keys.add(key) + info["file"] = os.path.splitext(filename)[0] themes.append(info) except Exception as e: logInstance().getLogger("ThemeManager").warning( @@ -243,22 +272,11 @@ class ThemeManager: if not os.path.isfile(filepath): raise FileNotFoundError(filepath) with self.__lock: - info = readThemeInfo(filepath) - with tempfile.TemporaryDirectory() as tmpdir: - unpackTheme(filepath, tmpdir) - qss_path = os.path.join(tmpdir, "theme.qss") - if os.path.isfile(qss_path): - with open(qss_path, "r", encoding="utf-8") as fh: - qss = fh.read() - app = QApplication.instance() - if app: - app.setStyleSheet(qss) - else: - raise ValueError( - f"主题 '{name}' 的 .altheme 文件中缺少 theme.qss" - ) + info = validateTheme(filepath) + qss = readThemeQss(filepath) app = QApplication.instance() if app: + app.setStyleSheet(qss) need_theme = info.get("need_theme", "both") app.styleHints().setColorScheme( ThemeManager._colorSchemeFor(need_theme) diff --git a/src/utils/ThemeUtils.py b/src/utils/ThemeUtils.py index 5e40598..c45c5dc 100644 --- a/src/utils/ThemeUtils.py +++ b/src/utils/ThemeUtils.py @@ -37,7 +37,6 @@ def packTheme( zf.writestr("info.json", json.dumps(info, ensure_ascii=False, indent=4)) zf.write(qss_path, "theme.qss") - def unpackTheme( altheme_path: str, output_dir: str @@ -65,7 +64,6 @@ def unpackTheme( raise ValueError(f"不安全的 .altheme 入口: {name}") zf.extractall(output_dir) - def readThemeInfo( altheme_path: str ) -> dict: @@ -94,6 +92,80 @@ def readThemeInfo( 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 +) -> 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. + + 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") + try: + info = json.loads(info_bytes.decode("utf-8")) + except (json.JSONDecodeError, UnicodeDecodeError) as 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(): + raise ValueError("无效的 .altheme: info.json 缺少有效的 'name' 字段") + if "author" not in info or not isinstance(info.get("author"), str): + raise ValueError("无效的 .altheme: info.json 缺少有效的 'author' 字段") + need_theme = info.get("need_theme", "both") + if need_theme not in ("light", "dark", "both"): + raise ValueError( + f"无效的 .altheme: need_theme 值 '{need_theme}' 无效, " + f"应为 'light'、'dark' 或 'both'" + ) + if "brief" not in info or not isinstance(info.get("brief"), str): + raise ValueError("无效的 .altheme: info.json 缺少有效的 'brief' 字段") + if not qss_bytes.strip(): + raise ValueError("无效的 .altheme: theme.qss 为空") + return info def wrapQssToAtheme( qss_path: str,