From 57f1cfb3f2b40cf3c81f0476b128fcdcfe3b2645 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Tue, 16 Jun 2026 19:37:09 +0800 Subject: [PATCH] =?UTF-8?q?fix(theme):=20=E4=BF=AE=E5=A4=8D=E6=AD=BB?= =?UTF-8?q?=E9=94=81=E3=80=81=E5=86=97=E4=BD=99=E8=AF=BB=E5=8F=96=E3=80=81?= =?UTF-8?q?=E7=A9=BA=E4=BD=9C=E8=80=85=E5=AD=97=E7=AC=A6=E4=B8=B2=E7=AD=89?= =?UTF-8?q?=E4=BA=A4=E5=8F=89=E5=AE=A1=E6=9F=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ThemeManager 拆分 _removeThemeFile 无锁版本, 消除 importTheme 持锁 时调用 removeTheme 导致的死锁 - validateTheme 增加 check_qss 参数, listThemes 跳过 QSS 读取 - validateTheme 拒绝空/空白作者字符串, 避免 info.json 与文件名不一致 - 统一默认作者为 "未知作者" - ALSettingsWidget.ui 增加删除按钮 [-], 浏览按钮改为 [+] - ALSettingsWidget 实现 onRemoveThemeButtonClicked 删除逻辑 --- src/gui/ALSettingsWidget.py | 46 ++++++++++++++++++++---- src/gui/resources/ui/ALSettingsWidget.ui | 21 ++++++++++- src/managers/theme/ThemeManager.py | 38 +++++++++++++------- src/utils/ThemeUtils.py | 15 +++++--- 4 files changed, 95 insertions(+), 25 deletions(-) diff --git a/src/gui/ALSettingsWidget.py b/src/gui/ALSettingsWidget.py index f7f6666..b32aca5 100644 --- a/src/gui/ALSettingsWidget.py +++ b/src/gui/ALSettingsWidget.py @@ -139,6 +139,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): ): self.BrowseQssButton.clicked.connect(self.onImportThemeButtonClicked) + self.RemoveThemeButton.clicked.connect(self.onRemoveThemeButtonClicked) self.ThemeComboBox.currentIndexChanged.connect(self.onThemeComboBoxChanged) self.ResetThemeButton.clicked.connect(self.onResetThemeButtonClicked) self.CancelButton.clicked.connect(self.onCancelButtonClicked) @@ -225,7 +226,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): t = self.__theme_cache.get(file) if t: name = t.get("name", "未知") - author = t.get("author", "未知") + author = t.get("author", "未知作者") need_theme = t.get("need_theme", "both") brief = t.get("brief", "没有相关简介") self.ThemeInfoLabel.setText( @@ -318,13 +319,46 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): author = t.get("author", "") if name: self.__theme_cache[file] = t - if author and author != "未知": - display = f"{name} ({author})" - else: - display = name - self.ThemeComboBox.addItem(display, file) + self.ThemeComboBox.addItem(name, file) self.ThemeComboBox.blockSignals(False) + @Slot() + def onRemoveThemeButtonClicked( + self + ): + + file = self.ThemeComboBox.currentData() + if not file: + QMessageBox.information( + self, + "提示 - AutoLibrary", + "请先选择一个主题。" + ) + return + t = self.__theme_cache.get(file) + name = t.get("name", file) if t else file + reply = QMessageBox.question( + self, + "删除主题 - AutoLibrary", + f"确定要删除主题 \"{name}\" 吗?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + if reply != QMessageBox.Yes: + return + try: + themeInstance().removeTheme(file) + self.populateThemeList() + self.ThemeComboBox.setCurrentIndex(0) + self.updateThemeStatus() + self.updateThemeInfo() + except Exception as e: + QMessageBox.warning( + self, + "删除失败 - AutoLibrary", + f"无法删除主题:{e}" + ) + @Slot() def onImportThemeButtonClicked( self diff --git a/src/gui/resources/ui/ALSettingsWidget.ui b/src/gui/resources/ui/ALSettingsWidget.ui index a661738..413f8f6 100644 --- a/src/gui/resources/ui/ALSettingsWidget.ui +++ b/src/gui/resources/ui/ALSettingsWidget.ui @@ -328,7 +328,26 @@ - ... + + + + + + + + + + 25 + 25 + + + + + 25 + 25 + + + + - diff --git a/src/managers/theme/ThemeManager.py b/src/managers/theme/ThemeManager.py index 12d55f0..12b0e81 100644 --- a/src/managers/theme/ThemeManager.py +++ b/src/managers/theme/ThemeManager.py @@ -121,7 +121,7 @@ class ThemeManager: existing_info = validateTheme(default_path) existing_author = existing_info.get("author", "") except Exception: - self.removeTheme(theme_name) + self._removeThemeFile(theme_name) # caller holds the lock raise ValueError( f"主题 '{theme_name}' 已存在但无法通过验证, 已清理该主题文件" ) @@ -129,7 +129,7 @@ class ThemeManager: raise ValueError( f"主题名称 '{theme_name}' (作者 '{author}') 已存在" ) - safe_author = os.path.basename(author) if author else "未知" + safe_author = os.path.basename(author) if author else "未知作者" alt_path = os.path.join( self.__themes_dir, f"{theme_name}_{safe_author}.altheme" ) @@ -168,7 +168,7 @@ class ThemeManager: with self.__lock: if ext == ".qss": name = os.path.splitext(os.path.basename(source_path))[0] - dest_path = self._resolveDestPath(name, "未知") + dest_path = self._resolveDestPath(name, "未知作者") wrapQssToAtheme(source_path, dest_path, "both") return os.path.splitext(os.path.basename(dest_path))[0] elif ext == ".altheme": @@ -203,7 +203,7 @@ class ThemeManager: if filename.endswith(".altheme"): filepath = os.path.join(self.__themes_dir, filename) try: - info = validateTheme(filepath) + info = validateTheme(filepath, check_qss=False) # skip QSS read for list scan name = info.get("name", "") author = info.get("author", "") key = (name, author) @@ -225,6 +225,26 @@ class ThemeManager: ) 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( self, name: str @@ -239,16 +259,8 @@ class ThemeManager: name (str): The theme name to remove. """ - filepath = os.path.join(self.__themes_dir, name + ".altheme") with self.__lock: - 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) + self._removeThemeFile(name) def applyTheme( self, diff --git a/src/utils/ThemeUtils.py b/src/utils/ThemeUtils.py index c45c5dc..305f201 100644 --- a/src/utils/ThemeUtils.py +++ b/src/utils/ThemeUtils.py @@ -117,7 +117,8 @@ def readThemeQss( return zf.read("theme.qss").decode("utf-8") def validateTheme( - altheme_path: str + altheme_path: str, + check_qss: bool = True ) -> dict: """ Validate a .altheme file and return its metadata. @@ -128,6 +129,8 @@ def validateTheme( 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. @@ -146,14 +149,16 @@ def validateTheme( if "theme.qss" not in names: raise ValueError("无效的 .altheme: 缺少 theme.qss") info_bytes = zf.read("info.json") - qss_bytes = zf.read("theme.qss") + qss_bytes = zf.read("theme.qss") if check_qss else None # skip QSS read when only listing 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): + # reject blank author so info.json does not drift from the "未知作者" filename fallback + if ("author" not in info or not isinstance(info.get("author"), str) + or not info["author"].strip()): raise ValueError("无效的 .altheme: info.json 缺少有效的 'author' 字段") need_theme = info.get("need_theme", "both") if need_theme not in ("light", "dark", "both"): @@ -163,7 +168,7 @@ def validateTheme( ) if "brief" not in info or not isinstance(info.get("brief"), str): raise ValueError("无效的 .altheme: info.json 缺少有效的 'brief' 字段") - if not qss_bytes.strip(): + if check_qss and not qss_bytes.strip(): raise ValueError("无效的 .altheme: theme.qss 为空") return info @@ -191,7 +196,7 @@ def wrapQssToAtheme( filename = os.path.splitext(os.path.basename(qss_path))[0] info = { "name": filename, - "author": "未知", + "author": "未知作者", "need_theme": current_theme, "brief": "没有相关简介" }