mirror of
https://github.com/KenanZhu/AutoLibrary.git
synced 2026-06-17 23:13:03 +08:00
fix(theme): 修复同名主题无法区分作者及导入链路边界问题
- 新增 ThemeUtils.validateTheme 和 readThemeQss 集中校验与读取逻辑
- ThemeManager.importTheme 通过 _resolveDestPath 处理同名主题:
不同作者自动命名为 {主题名}_{作者名}.altheme, 首次导入保持原名
- ThemeManager.listThemes 返回 file 字段以便 UI 层定位文件
- ALSettingsWidget 全线改用 file 标识符, 组合框按作者消歧义显示
- 移除 applyTheme 中的临时目录解压, 改用 readThemeQss 直接读取
This commit is contained in:
+23
-14
@@ -196,7 +196,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
|
|||||||
self.StyleComboBox.setCurrentIndex(index)
|
self.StyleComboBox.setCurrentIndex(index)
|
||||||
self.populateThemeList()
|
self.populateThemeList()
|
||||||
if custom_theme:
|
if custom_theme:
|
||||||
idx = self.ThemeComboBox.findText(custom_theme)
|
idx = self.ThemeComboBox.findData(custom_theme)
|
||||||
if idx >= 0:
|
if idx >= 0:
|
||||||
self.ThemeComboBox.setCurrentIndex(idx)
|
self.ThemeComboBox.setCurrentIndex(idx)
|
||||||
self.updateThemeStatus()
|
self.updateThemeStatus()
|
||||||
@@ -206,8 +206,10 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
|
|||||||
self
|
self
|
||||||
):
|
):
|
||||||
|
|
||||||
name = self.ThemeComboBox.currentText()
|
file = self.ThemeComboBox.currentData()
|
||||||
if name and name != "默认":
|
t = self.__theme_cache.get(file) if file else None
|
||||||
|
name = t.get("name", "") if t else ""
|
||||||
|
if name:
|
||||||
self.QssStatusLabel.setText(f"当前使用 {name} 主题。")
|
self.QssStatusLabel.setText(f"当前使用 {name} 主题。")
|
||||||
else:
|
else:
|
||||||
self.QssStatusLabel.setText("当前使用 默认 主题。")
|
self.QssStatusLabel.setText("当前使用 默认 主题。")
|
||||||
@@ -216,12 +218,13 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
|
|||||||
self
|
self
|
||||||
):
|
):
|
||||||
|
|
||||||
name = self.ThemeComboBox.currentText()
|
file = self.ThemeComboBox.currentData()
|
||||||
if not name or name == "默认":
|
if not file:
|
||||||
self.ThemeInfoLabel.setText("")
|
self.ThemeInfoLabel.setText("")
|
||||||
return
|
return
|
||||||
t = self.__theme_cache.get(name)
|
t = self.__theme_cache.get(file)
|
||||||
if t:
|
if t:
|
||||||
|
name = t.get("name", "未知")
|
||||||
author = t.get("author", "未知")
|
author = t.get("author", "未知")
|
||||||
need_theme = t.get("need_theme", "both")
|
need_theme = t.get("need_theme", "both")
|
||||||
brief = t.get("brief", "没有相关简介")
|
brief = t.get("brief", "没有相关简介")
|
||||||
@@ -257,8 +260,8 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
|
|||||||
else:
|
else:
|
||||||
theme = "system"
|
theme = "system"
|
||||||
style = self.StyleComboBox.currentText()
|
style = self.StyleComboBox.currentText()
|
||||||
custom_theme = self.ThemeComboBox.currentText()
|
custom_theme = self.ThemeComboBox.currentData() or ""
|
||||||
if custom_theme == "默认":
|
if not custom_theme:
|
||||||
custom_theme = ""
|
custom_theme = ""
|
||||||
return theme, style, custom_theme
|
return theme, style, custom_theme
|
||||||
|
|
||||||
@@ -306,14 +309,20 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
|
|||||||
|
|
||||||
self.ThemeComboBox.blockSignals(True)
|
self.ThemeComboBox.blockSignals(True)
|
||||||
self.ThemeComboBox.clear()
|
self.ThemeComboBox.clear()
|
||||||
self.ThemeComboBox.addItem("默认")
|
self.ThemeComboBox.addItem("默认", "")
|
||||||
self.__theme_cache = {}
|
self.__theme_cache = {}
|
||||||
themes = themeInstance().listThemes()
|
themes = themeInstance().listThemes()
|
||||||
for t in themes:
|
for t in themes:
|
||||||
name = t.get("name", "")
|
name = t.get("name", "")
|
||||||
|
file = t.get("file", name)
|
||||||
|
author = t.get("author", "")
|
||||||
if name:
|
if name:
|
||||||
self.__theme_cache[name] = t
|
self.__theme_cache[file] = t
|
||||||
self.ThemeComboBox.addItem(name)
|
if author and author != "未知":
|
||||||
|
display = f"{name} ({author})"
|
||||||
|
else:
|
||||||
|
display = name
|
||||||
|
self.ThemeComboBox.addItem(display, file)
|
||||||
self.ThemeComboBox.blockSignals(False)
|
self.ThemeComboBox.blockSignals(False)
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
@@ -330,9 +339,9 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
|
|||||||
if not file_path:
|
if not file_path:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
name = themeInstance().importTheme(file_path)
|
file_id = themeInstance().importTheme(file_path)
|
||||||
self.populateThemeList()
|
self.populateThemeList()
|
||||||
idx = self.ThemeComboBox.findText(name)
|
idx = self.ThemeComboBox.findData(file_id)
|
||||||
if idx >= 0:
|
if idx >= 0:
|
||||||
self.ThemeComboBox.setCurrentIndex(idx)
|
self.ThemeComboBox.setCurrentIndex(idx)
|
||||||
self.updateThemeStatus()
|
self.updateThemeStatus()
|
||||||
@@ -359,7 +368,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
|
|||||||
|
|
||||||
self.ThemeComboBox.blockSignals(True)
|
self.ThemeComboBox.blockSignals(True)
|
||||||
if self.__original_custom_theme:
|
if self.__original_custom_theme:
|
||||||
idx = self.ThemeComboBox.findText(self.__original_custom_theme)
|
idx = self.ThemeComboBox.findData(self.__original_custom_theme)
|
||||||
if idx >= 0:
|
if idx >= 0:
|
||||||
self.ThemeComboBox.setCurrentIndex(idx)
|
self.ThemeComboBox.setCurrentIndex(idx)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ See the LICENSE file for details.
|
|||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
|
||||||
import threading
|
import threading
|
||||||
import zipfile
|
|
||||||
|
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
@@ -23,9 +21,8 @@ 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 (
|
||||||
packTheme,
|
readThemeQss,
|
||||||
readThemeInfo,
|
validateTheme,
|
||||||
unpackTheme,
|
|
||||||
wrapQssToAtheme
|
wrapQssToAtheme
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -94,6 +91,54 @@ class ThemeManager:
|
|||||||
|
|
||||||
return self.__themes_dir
|
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(
|
def importTheme(
|
||||||
self,
|
self,
|
||||||
source_path: str
|
source_path: str
|
||||||
@@ -123,31 +168,17 @@ class ThemeManager:
|
|||||||
with self.__lock:
|
with self.__lock:
|
||||||
if ext == ".qss":
|
if ext == ".qss":
|
||||||
name = os.path.splitext(os.path.basename(source_path))[0]
|
name = os.path.splitext(os.path.basename(source_path))[0]
|
||||||
dest_path = os.path.join(self.__themes_dir, name + ".altheme")
|
dest_path = self._resolveDestPath(name, "未知")
|
||||||
if os.path.exists(dest_path):
|
|
||||||
raise ValueError(f"主题 '{name}' 已存在")
|
|
||||||
wrapQssToAtheme(source_path, dest_path, "both")
|
wrapQssToAtheme(source_path, dest_path, "both")
|
||||||
return name
|
return os.path.splitext(os.path.basename(dest_path))[0]
|
||||||
elif ext == ".altheme":
|
elif ext == ".altheme":
|
||||||
with zipfile.ZipFile(source_path, "r") as zf:
|
info = validateTheme(source_path)
|
||||||
if "theme.qss" not in zf.namelist():
|
|
||||||
raise ValueError("无效的 .altheme: 缺少 theme.qss")
|
|
||||||
info = readThemeInfo(source_path)
|
|
||||||
name = info.get("name", os.path.splitext(os.path.basename(source_path))[0])
|
name = info.get("name", os.path.splitext(os.path.basename(source_path))[0])
|
||||||
safe_name = os.path.basename(name)
|
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", "")
|
new_author = info.get("author", "")
|
||||||
for existing in self.listThemes():
|
dest_path = self._resolveDestPath(safe_name, new_author)
|
||||||
if (existing.get("name", "") == safe_name
|
|
||||||
and existing.get("author", "") == new_author):
|
|
||||||
raise ValueError(
|
|
||||||
f"主题名称 '{safe_name}' (作者 '{new_author}') 已存在"
|
|
||||||
)
|
|
||||||
shutil.copy2(source_path, dest_path)
|
shutil.copy2(source_path, dest_path)
|
||||||
return safe_name
|
return os.path.splitext(os.path.basename(dest_path))[0]
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"不支持的文件类型: {ext}")
|
raise ValueError(f"不支持的文件类型: {ext}")
|
||||||
|
|
||||||
@@ -172,10 +203,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 = readThemeInfo(filepath)
|
info = validateTheme(filepath)
|
||||||
with zipfile.ZipFile(filepath, "r") as zf:
|
|
||||||
if "theme.qss" not in zf.namelist():
|
|
||||||
raise ValueError("缺少 theme.qss")
|
|
||||||
name = info.get("name", "")
|
name = info.get("name", "")
|
||||||
author = info.get("author", "")
|
author = info.get("author", "")
|
||||||
key = (name, author)
|
key = (name, author)
|
||||||
@@ -185,6 +213,7 @@ class ThemeManager:
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
seen_keys.add(key)
|
seen_keys.add(key)
|
||||||
|
info["file"] = os.path.splitext(filename)[0]
|
||||||
themes.append(info)
|
themes.append(info)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logInstance().getLogger("ThemeManager").warning(
|
logInstance().getLogger("ThemeManager").warning(
|
||||||
@@ -243,22 +272,11 @@ 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 = readThemeInfo(filepath)
|
info = validateTheme(filepath)
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
qss = readThemeQss(filepath)
|
||||||
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"
|
|
||||||
)
|
|
||||||
app = QApplication.instance()
|
app = QApplication.instance()
|
||||||
if app:
|
if app:
|
||||||
|
app.setStyleSheet(qss)
|
||||||
need_theme = info.get("need_theme", "both")
|
need_theme = info.get("need_theme", "both")
|
||||||
app.styleHints().setColorScheme(
|
app.styleHints().setColorScheme(
|
||||||
ThemeManager._colorSchemeFor(need_theme)
|
ThemeManager._colorSchemeFor(need_theme)
|
||||||
|
|||||||
+74
-2
@@ -37,7 +37,6 @@ def packTheme(
|
|||||||
zf.writestr("info.json", json.dumps(info, ensure_ascii=False, indent=4))
|
zf.writestr("info.json", json.dumps(info, ensure_ascii=False, indent=4))
|
||||||
zf.write(qss_path, "theme.qss")
|
zf.write(qss_path, "theme.qss")
|
||||||
|
|
||||||
|
|
||||||
def unpackTheme(
|
def unpackTheme(
|
||||||
altheme_path: str,
|
altheme_path: str,
|
||||||
output_dir: str
|
output_dir: str
|
||||||
@@ -65,7 +64,6 @@ def unpackTheme(
|
|||||||
raise ValueError(f"不安全的 .altheme 入口: {name}")
|
raise ValueError(f"不安全的 .altheme 入口: {name}")
|
||||||
zf.extractall(output_dir)
|
zf.extractall(output_dir)
|
||||||
|
|
||||||
|
|
||||||
def readThemeInfo(
|
def readThemeInfo(
|
||||||
altheme_path: str
|
altheme_path: str
|
||||||
) -> dict:
|
) -> dict:
|
||||||
@@ -94,6 +92,80 @@ def readThemeInfo(
|
|||||||
raise ValueError("无效的 .altheme: info.json 缺少 'name' 字段")
|
raise ValueError("无效的 .altheme: info.json 缺少 'name' 字段")
|
||||||
return info
|
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(
|
def wrapQssToAtheme(
|
||||||
qss_path: str,
|
qss_path: str,
|
||||||
|
|||||||
Reference in New Issue
Block a user