From c250fa4a6e3e2645eb39df2a87c4df99f2991976 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Fri, 19 Jun 2026 11:21:50 +0800 Subject: [PATCH] =?UTF-8?q?refactor(theme):=20=E5=B0=86=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E7=9A=84=E4=B8=BB=E9=A2=98=E9=80=BB=E8=BE=91=E4=B8=8B=E6=B2=89?= =?UTF-8?q?=E8=87=B3=20ThemeUtils=EF=BC=8C=E6=B6=88=E9=99=A4=20validateThe?= =?UTF-8?q?me=20=E8=81=8C=E8=B4=A3=E8=BF=87=E9=87=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/managers/theme/ThemeManager.py | 76 ++++++++-------- src/utils/ThemeUtils.py | 135 ++++++++++++++--------------- 2 files changed, 103 insertions(+), 108 deletions(-) diff --git a/src/managers/theme/ThemeManager.py b/src/managers/theme/ThemeManager.py index 12b0e81..755c67b 100644 --- a/src/managers/theme/ThemeManager.py +++ b/src/managers/theme/ThemeManager.py @@ -21,6 +21,7 @@ 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 ( + readThemeInfo, readThemeQss, validateTheme, wrapQssToAtheme @@ -79,17 +80,21 @@ class ThemeManager: else: return Qt.ColorScheme.Unknown - def themesDir( - self - ) -> str: + def _deleteThemeFile( + self, + name: str + ): """ - Get the themes directory path. + Delete a theme file in the themes storage directory. - Returns: - str: The absolute path to the themes storage directory. + The caller must hold self.__lock before invoking this method. + + **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( self, @@ -121,7 +126,7 @@ class ThemeManager: existing_info = validateTheme(default_path) existing_author = existing_info.get("author", "") except Exception: - self._removeThemeFile(theme_name) # caller holds the lock + self._deleteThemeFile(theme_name) # caller holds the lock raise ValueError( f"主题 '{theme_name}' 已存在但无法通过验证, 已清理该主题文件" ) @@ -139,6 +144,18 @@ class ThemeManager: ) 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( self, source_path: str @@ -164,16 +181,16 @@ class ThemeManager: if not os.path.isfile(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: if ext == ".qss": - name = os.path.splitext(os.path.basename(source_path))[0] - dest_path = self._resolveDestPath(name, "未知作者") + dest_path = self._resolveDestPath(base_name, "未知作者") wrapQssToAtheme(source_path, dest_path, "both") return os.path.splitext(os.path.basename(dest_path))[0] elif ext == ".altheme": 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) new_author = info.get("author", "") dest_path = self._resolveDestPath(safe_name, new_author) @@ -203,7 +220,7 @@ class ThemeManager: if filename.endswith(".altheme"): filepath = os.path.join(self.__themes_dir, filename) try: - info = validateTheme(filepath, check_qss=False) # skip QSS read for list scan + info = validateTheme(filepath) name = info.get("name", "") author = info.get("author", "") key = (name, author) @@ -225,26 +242,6 @@ 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 @@ -253,14 +250,21 @@ class ThemeManager: Remove a theme by name. 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: name (str): The theme name to remove. """ 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( self, @@ -284,7 +288,7 @@ class ThemeManager: if not os.path.isfile(filepath): raise FileNotFoundError(filepath) with self.__lock: - info = validateTheme(filepath) + info = readThemeInfo(filepath) qss = readThemeQss(filepath) app = QApplication.instance() if app: diff --git a/src/utils/ThemeUtils.py b/src/utils/ThemeUtils.py index 305f201..94ed0d0 100644 --- a/src/utils/ThemeUtils.py +++ b/src/utils/ThemeUtils.py @@ -68,17 +68,20 @@ def readThemeInfo( altheme_path: str ) -> 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: altheme_path (str): Path to the .altheme file. Returns: - dict: The theme metadata dictionary. + dict: The validated theme metadata dictionary. Raises: 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): @@ -87,76 +90,14 @@ def readThemeInfo( if "info.json" not in zf.namelist(): raise ValueError("无效的 .altheme: 缺少 info.json") 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: - info = json.loads(info_bytes.decode("utf-8")) - except (json.JSONDecodeError, UnicodeDecodeError) as e: - raise ValueError(f"无效的 .altheme: info.json 解析失败 — {e}") + try: + info = json.loads(fh.read().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' 字段") - # 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) or not info["author"].strip()): raise ValueError("无效的 .altheme: info.json 缺少有效的 'author' 字段") @@ -168,8 +109,58 @@ def validateTheme( ) if "brief" not in info or not isinstance(info.get("brief"), str): 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 为空") + 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 def wrapQssToAtheme(