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:
@@ -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:
|
||||||
|
|||||||
+60
-69
@@ -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"))
|
|
||||||
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:
|
try:
|
||||||
info = json.loads(info_bytes.decode("utf-8"))
|
info = json.loads(fh.read().decode("utf-8"))
|
||||||
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
||||||
raise ValueError(f"无效的 .altheme: info.json 解析失败 — {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(
|
||||||
|
|||||||
Reference in New Issue
Block a user