mirror of
https://github.com/KenanZhu/AutoLibrary.git
synced 2026-06-17 23:13:03 +08:00
fix(theme): 修复死锁、冗余读取、空作者字符串等交叉审查问题
- ThemeManager 拆分 _removeThemeFile 无锁版本, 消除 importTheme 持锁 时调用 removeTheme 导致的死锁 - validateTheme 增加 check_qss 参数, listThemes 跳过 QSS 读取 - validateTheme 拒绝空/空白作者字符串, 避免 info.json 与文件名不一致 - 统一默认作者为 "未知作者" - ALSettingsWidget.ui 增加删除按钮 [-], 浏览按钮改为 [+] - ALSettingsWidget 实现 onRemoveThemeButtonClicked 删除逻辑
This commit is contained in:
@@ -139,6 +139,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
|
|||||||
):
|
):
|
||||||
|
|
||||||
self.BrowseQssButton.clicked.connect(self.onImportThemeButtonClicked)
|
self.BrowseQssButton.clicked.connect(self.onImportThemeButtonClicked)
|
||||||
|
self.RemoveThemeButton.clicked.connect(self.onRemoveThemeButtonClicked)
|
||||||
self.ThemeComboBox.currentIndexChanged.connect(self.onThemeComboBoxChanged)
|
self.ThemeComboBox.currentIndexChanged.connect(self.onThemeComboBoxChanged)
|
||||||
self.ResetThemeButton.clicked.connect(self.onResetThemeButtonClicked)
|
self.ResetThemeButton.clicked.connect(self.onResetThemeButtonClicked)
|
||||||
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
|
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
|
||||||
@@ -225,7 +226,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
|
|||||||
t = self.__theme_cache.get(file)
|
t = self.__theme_cache.get(file)
|
||||||
if t:
|
if t:
|
||||||
name = t.get("name", "未知")
|
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", "没有相关简介")
|
||||||
self.ThemeInfoLabel.setText(
|
self.ThemeInfoLabel.setText(
|
||||||
@@ -318,13 +319,46 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
|
|||||||
author = t.get("author", "")
|
author = t.get("author", "")
|
||||||
if name:
|
if name:
|
||||||
self.__theme_cache[file] = t
|
self.__theme_cache[file] = t
|
||||||
if author and author != "未知":
|
self.ThemeComboBox.addItem(name, file)
|
||||||
display = f"{name} ({author})"
|
|
||||||
else:
|
|
||||||
display = name
|
|
||||||
self.ThemeComboBox.addItem(display, file)
|
|
||||||
self.ThemeComboBox.blockSignals(False)
|
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()
|
@Slot()
|
||||||
def onImportThemeButtonClicked(
|
def onImportThemeButtonClicked(
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -328,7 +328,26 @@
|
|||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>...</string>
|
<string>+</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="RemoveThemeButton">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>25</width>
|
||||||
|
<height>25</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>25</width>
|
||||||
|
<height>25</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>-</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|||||||
@@ -121,7 +121,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.removeTheme(theme_name)
|
self._removeThemeFile(theme_name) # caller holds the lock
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"主题 '{theme_name}' 已存在但无法通过验证, 已清理该主题文件"
|
f"主题 '{theme_name}' 已存在但无法通过验证, 已清理该主题文件"
|
||||||
)
|
)
|
||||||
@@ -129,7 +129,7 @@ class ThemeManager:
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"主题名称 '{theme_name}' (作者 '{author}') 已存在"
|
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(
|
alt_path = os.path.join(
|
||||||
self.__themes_dir, f"{theme_name}_{safe_author}.altheme"
|
self.__themes_dir, f"{theme_name}_{safe_author}.altheme"
|
||||||
)
|
)
|
||||||
@@ -168,7 +168,7 @@ 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 = self._resolveDestPath(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":
|
||||||
@@ -203,7 +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 = validateTheme(filepath)
|
info = validateTheme(filepath, check_qss=False) # skip QSS read for list scan
|
||||||
name = info.get("name", "")
|
name = info.get("name", "")
|
||||||
author = info.get("author", "")
|
author = info.get("author", "")
|
||||||
key = (name, author)
|
key = (name, author)
|
||||||
@@ -225,6 +225,26 @@ 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
|
||||||
@@ -239,16 +259,8 @@ class ThemeManager:
|
|||||||
name (str): The theme name to remove.
|
name (str): The theme name to remove.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
filepath = os.path.join(self.__themes_dir, name + ".altheme")
|
|
||||||
with self.__lock:
|
with self.__lock:
|
||||||
if os.path.isfile(filepath):
|
self._removeThemeFile(name)
|
||||||
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 applyTheme(
|
def applyTheme(
|
||||||
self,
|
self,
|
||||||
|
|||||||
+10
-5
@@ -117,7 +117,8 @@ def readThemeQss(
|
|||||||
return zf.read("theme.qss").decode("utf-8")
|
return zf.read("theme.qss").decode("utf-8")
|
||||||
|
|
||||||
def validateTheme(
|
def validateTheme(
|
||||||
altheme_path: str
|
altheme_path: str,
|
||||||
|
check_qss: bool = True
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Validate a .altheme file and return its metadata.
|
Validate a .altheme file and return its metadata.
|
||||||
@@ -128,6 +129,8 @@ def validateTheme(
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
altheme_path (str): Path to the .altheme file.
|
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:
|
Returns:
|
||||||
dict: The validated theme metadata dictionary.
|
dict: The validated theme metadata dictionary.
|
||||||
@@ -146,14 +149,16 @@ def validateTheme(
|
|||||||
if "theme.qss" not in names:
|
if "theme.qss" not in names:
|
||||||
raise ValueError("无效的 .altheme: 缺少 theme.qss")
|
raise ValueError("无效的 .altheme: 缺少 theme.qss")
|
||||||
info_bytes = zf.read("info.json")
|
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:
|
try:
|
||||||
info = json.loads(info_bytes.decode("utf-8"))
|
info = json.loads(info_bytes.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' 字段")
|
||||||
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' 字段")
|
raise ValueError("无效的 .altheme: info.json 缺少有效的 'author' 字段")
|
||||||
need_theme = info.get("need_theme", "both")
|
need_theme = info.get("need_theme", "both")
|
||||||
if need_theme not in ("light", "dark", "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):
|
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 not qss_bytes.strip():
|
if check_qss and not qss_bytes.strip():
|
||||||
raise ValueError("无效的 .altheme: theme.qss 为空")
|
raise ValueError("无效的 .altheme: theme.qss 为空")
|
||||||
return info
|
return info
|
||||||
|
|
||||||
@@ -191,7 +196,7 @@ def wrapQssToAtheme(
|
|||||||
filename = os.path.splitext(os.path.basename(qss_path))[0]
|
filename = os.path.splitext(os.path.basename(qss_path))[0]
|
||||||
info = {
|
info = {
|
||||||
"name": filename,
|
"name": filename,
|
||||||
"author": "未知",
|
"author": "未知作者",
|
||||||
"need_theme": current_theme,
|
"need_theme": current_theme,
|
||||||
"brief": "没有相关简介"
|
"brief": "没有相关简介"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user