1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-06-22 17:33:03 +08:00

refactor(theme): 将重复的主题逻辑下沉至 ThemeUtils,消除 validateTheme 职责过重

This commit is contained in:
2026-06-19 11:21:50 +08:00
parent 8f8e3e4ba7
commit c250fa4a6e
2 changed files with 103 additions and 108 deletions
+40 -36
View File
@@ -21,6 +21,7 @@ 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 (
readThemeInfo,
readThemeQss, readThemeQss,
validateTheme, validateTheme,
wrapQssToAtheme wrapQssToAtheme
@@ -79,17 +80,21 @@ class ThemeManager:
else: else:
return Qt.ColorScheme.Unknown return Qt.ColorScheme.Unknown
def themesDir( def _deleteThemeFile(
self self,
) -> str: name: str
):
""" """
Get the themes directory path. Delete a theme file in the themes storage directory.
Returns: The caller must hold self.__lock before invoking this method.
str: The absolute path to the themes storage directory.
**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( def _resolveDestPath(
self, self,
@@ -121,7 +126,7 @@ class ThemeManager:
existing_info = validateTheme(default_path) existing_info = validateTheme(default_path)
existing_author = existing_info.get("author", "") existing_author = existing_info.get("author", "")
except Exception: except Exception:
self._removeThemeFile(theme_name) # caller holds the lock self._deleteThemeFile(theme_name) # caller holds the lock
raise ValueError( raise ValueError(
f"主题 '{theme_name}' 已存在但无法通过验证, 已清理该主题文件" f"主题 '{theme_name}' 已存在但无法通过验证, 已清理该主题文件"
) )
@@ -139,6 +144,18 @@ class ThemeManager:
) )
return alt_path 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( def importTheme(
self, self,
source_path: str source_path: str
@@ -164,16 +181,16 @@ 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() base_name, ext = os.path.splitext(os.path.basename(source_path))
ext = ext.lower()
with self.__lock: with self.__lock:
if ext == ".qss": if ext == ".qss":
name = os.path.splitext(os.path.basename(source_path))[0] dest_path = self._resolveDestPath(base_name, "未知作者")
dest_path = self._resolveDestPath(name, "未知作者")
wrapQssToAtheme(source_path, dest_path, "both") wrapQssToAtheme(source_path, dest_path, "both")
return os.path.splitext(os.path.basename(dest_path))[0] return os.path.splitext(os.path.basename(dest_path))[0]
elif ext == ".altheme": elif ext == ".altheme":
info = validateTheme(source_path) 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) safe_name = os.path.basename(name)
new_author = info.get("author", "") new_author = info.get("author", "")
dest_path = self._resolveDestPath(safe_name, new_author) dest_path = self._resolveDestPath(safe_name, new_author)
@@ -203,7 +220,7 @@ class ThemeManager:
if filename.endswith(".altheme"): if filename.endswith(".altheme"):
filepath = os.path.join(self.__themes_dir, filename) filepath = os.path.join(self.__themes_dir, filename)
try: try:
info = validateTheme(filepath, check_qss=False) # skip QSS read for list scan info = validateTheme(filepath)
name = info.get("name", "") name = info.get("name", "")
author = info.get("author", "") author = info.get("author", "")
key = (name, author) key = (name, author)
@@ -225,26 +242,6 @@ class ThemeManager:
) )
return themes 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( def removeTheme(
self, self,
name: str name: str
@@ -253,14 +250,21 @@ class ThemeManager:
Remove a theme by name. Remove a theme by name.
If the removed theme is currently active, clears the QSS 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: Args:
name (str): The theme name to remove. name (str): The theme name to remove.
""" """
with self.__lock: 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( def applyTheme(
self, self,
@@ -284,7 +288,7 @@ class ThemeManager:
if not os.path.isfile(filepath): if not os.path.isfile(filepath):
raise FileNotFoundError(filepath) raise FileNotFoundError(filepath)
with self.__lock: with self.__lock:
info = validateTheme(filepath) info = readThemeInfo(filepath)
qss = readThemeQss(filepath) qss = readThemeQss(filepath)
app = QApplication.instance() app = QApplication.instance()
if app: if app:
+63 -72
View File
@@ -68,17 +68,20 @@ def readThemeInfo(
altheme_path: str altheme_path: str
) -> dict: ) -> 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: Args:
altheme_path (str): Path to the .altheme file. altheme_path (str): Path to the .altheme file.
Returns: Returns:
dict: The theme metadata dictionary. dict: The validated theme metadata dictionary.
Raises: Raises:
FileNotFoundError: If altheme_path does not exist. 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): if not os.path.isfile(altheme_path):
@@ -87,76 +90,14 @@ def readThemeInfo(
if "info.json" not in zf.namelist(): if "info.json" not in zf.namelist():
raise ValueError("无效的 .altheme: 缺少 info.json") raise ValueError("无效的 .altheme: 缺少 info.json")
with zf.open("info.json") as fh: with zf.open("info.json") as fh:
info = json.loads(fh.read().decode("utf-8")) try:
if "name" not in info: info = json.loads(fh.read().decode("utf-8"))
raise ValueError("无效的 .altheme: info.json 缺少 'name' 字段") except (json.JSONDecodeError, UnicodeDecodeError) as e:
return info raise ValueError(f"无效的 .altheme: info.json 解析失败 — {e}")
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}")
if "name" not in info or not isinstance(info.get("name"), str) or not info["name"].strip(): if "name" not in info or not isinstance(info.get("name"), str) or not info["name"].strip():
raise ValueError("无效的 .altheme: info.json 缺少有效的 'name' 字段") 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) if ("author" not in info or not isinstance(info.get("author"), str)
or not info["author"].strip()): or not info["author"].strip()):
raise ValueError("无效的 .altheme: info.json 缺少有效的 'author' 字段") raise ValueError("无效的 .altheme: info.json 缺少有效的 'author' 字段")
@@ -168,8 +109,58 @@ def validateTheme(
) )
if "brief" not in info or not isinstance(info.get("brief"), str): if "brief" not in info or not isinstance(info.get("brief"), str):
raise ValueError("无效的 .altheme: info.json 缺少有效的 'brief' 字段") 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 为空") 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 return info
def wrapQssToAtheme( def wrapQssToAtheme(