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

Compare commits

...

4 Commits

Author SHA1 Message Date
KenanZhu f9175371dc feat(gui): +/- 按钮文本替换为 QtAwesome 图标,fa5s 统一升级为 fa6s
- ALSettingsWidget: BrowseQssButton/RemoveThemeButton 的 + / - 文本改为 fa6s.plus/fa6s.minus 图标
- ALAutoScriptEditDialog: ZoomInBtn/ZoomOutBtn 的全角 +/- 改为 fa6s.plus/fa6s.minus 图标
- 其余图标同步从 fa5s 升级至 fa6s (Font Awesome 6)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-17 08:15:03 +08:00
KenanZhu 8e1b28f3fe fix: requirements.txt 编码从 UTF-16 LE 转为 UTF-8,移除 8 个多余依赖包
移除的包: altgraph, mpmath, pefile, pyinstaller-hooks-contrib, pywin32-ctypes, setuptools, sympy, websocket-client
(这些均为传递依赖,pip 会根据直接依赖自动解析安装)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 22:19:05 +08:00
KenanZhu 57f1cfb3f2 fix(theme): 修复死锁、冗余读取、空作者字符串等交叉审查问题
- ThemeManager 拆分 _removeThemeFile 无锁版本, 消除 importTheme 持锁
  时调用 removeTheme 导致的死锁
- validateTheme 增加 check_qss 参数, listThemes 跳过 QSS 读取
- validateTheme 拒绝空/空白作者字符串, 避免 info.json 与文件名不一致
- 统一默认作者为 "未知作者"
- ALSettingsWidget.ui 增加删除按钮 [-], 浏览按钮改为 [+]
- ALSettingsWidget 实现 onRemoveThemeButtonClicked 删除逻辑
2026-06-16 19:37:09 +08:00
KenanZhu 007b4dc2ef fix(theme): 修复同名主题无法区分作者及导入链路边界问题
- 新增 ThemeUtils.validateTheme 和 readThemeQss 集中校验与读取逻辑
- ThemeManager.importTheme 通过 _resolveDestPath 处理同名主题:
  不同作者自动命名为 {主题名}_{作者名}.altheme, 首次导入保持原名
- ThemeManager.listThemes 返回 file 字段以便 UI 层定位文件
- ALSettingsWidget 全线改用 file 标识符, 组合框按作者消歧义显示
- 移除 applyTheme 中的临时目录解压, 改用 readThemeQss 直接读取
2026-06-16 18:37:47 +08:00
6 changed files with 255 additions and 75 deletions
BIN
View File
Binary file not shown.
+8 -4
View File
@@ -211,12 +211,16 @@ class ALAutoScriptEditDialog(QDialog):
Layout.setSpacing(3)
Layout.setContentsMargins(3, 3, 3, 3)
ToolbarLayout = QHBoxLayout()
self.ZoomInBtn = QPushButton("")
self.ZoomInBtn = QPushButton("")
self.ZoomInBtn.setIcon(qta.icon("fa6s.plus", color=self._iconColor()))
self.ZoomInBtn.setIconSize(QSize(14, 14))
self.ZoomInBtn.setFixedSize(25, 25)
self.ZoomOutBtn = QPushButton("")
self.ZoomOutBtn = QPushButton("")
self.ZoomOutBtn.setIcon(qta.icon("fa6s.minus", color=self._iconColor()))
self.ZoomOutBtn.setIconSize(QSize(14, 14))
self.ZoomOutBtn.setFixedSize(25, 25)
self.ZoomResetBtn = QPushButton("")
self.ZoomResetBtn.setIcon(qta.icon("fa5s.undo", color=self._iconColor()))
self.ZoomResetBtn.setIcon(qta.icon("fa6s.rotate-left", color=self._iconColor()))
self.ZoomResetBtn.setIconSize(QSize(14, 14))
self.ZoomResetBtn.setFixedSize(25, 25)
self.ZoomResetBtn.setToolTip("重置缩放")
@@ -241,7 +245,7 @@ class ALAutoScriptEditDialog(QDialog):
ToolbarLayout.addWidget(self.ZoomLabel)
ToolbarLayout.addStretch()
self.CopyBtn = QPushButton("")
self.CopyBtn.setIcon(qta.icon("fa5s.copy", color=self._iconColor()))
self.CopyBtn.setIcon(qta.icon("fa6s.copy", color=self._iconColor()))
self.CopyBtn.setIconSize(QSize(14, 14))
self.CopyBtn.setFixedSize(25, 25)
self.CopyBtn.setToolTip("复制脚本")
+66 -16
View File
@@ -110,6 +110,13 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
self.NavigationList.setCurrentRow(0)
self.populateStyles()
self.setNavigationIcons()
color = QApplication.instance().palette().color(
QApplication.instance().palette().ColorRole.WindowText
).name()
self.BrowseQssButton.setIcon(qta.icon("fa6s.plus", color=color))
self.BrowseQssButton.setText("")
self.RemoveThemeButton.setIcon(qta.icon("fa6s.minus", color=color))
self.RemoveThemeButton.setText("")
self.ThemeInfoLabel.setTextFormat(Qt.TextFormat.RichText)
self.ThemeInfoLabel.setStyleSheet(
"border: 1px solid palette(mid);"\
@@ -125,7 +132,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
color = app.palette().color(app.palette().ColorRole.WindowText).name()
item = self.NavigationList.item(0)
if item:
item.setIcon(qta.icon("fa5s.palette", color=color))
item.setIcon(qta.icon("fa6s.palette", color=color))
def populateStyles(
self
@@ -139,6 +146,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
):
self.BrowseQssButton.clicked.connect(self.onImportThemeButtonClicked)
self.RemoveThemeButton.clicked.connect(self.onRemoveThemeButtonClicked)
self.ThemeComboBox.currentIndexChanged.connect(self.onThemeComboBoxChanged)
self.ResetThemeButton.clicked.connect(self.onResetThemeButtonClicked)
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
@@ -196,7 +204,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
self.StyleComboBox.setCurrentIndex(index)
self.populateThemeList()
if custom_theme:
idx = self.ThemeComboBox.findText(custom_theme)
idx = self.ThemeComboBox.findData(custom_theme)
if idx >= 0:
self.ThemeComboBox.setCurrentIndex(idx)
self.updateThemeStatus()
@@ -206,8 +214,10 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
self
):
name = self.ThemeComboBox.currentText()
if name and name != "默认":
file = self.ThemeComboBox.currentData()
t = self.__theme_cache.get(file) if file else None
name = t.get("name", "") if t else ""
if name:
self.QssStatusLabel.setText(f"当前使用 {name} 主题。")
else:
self.QssStatusLabel.setText("当前使用 默认 主题。")
@@ -216,13 +226,14 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
self
):
name = self.ThemeComboBox.currentText()
if not name or name == "默认":
file = self.ThemeComboBox.currentData()
if not file:
self.ThemeInfoLabel.setText("")
return
t = self.__theme_cache.get(name)
t = self.__theme_cache.get(file)
if t:
author = t.get("author", "未知")
name = t.get("name", "未知")
author = t.get("author", "未知作者")
need_theme = t.get("need_theme", "both")
brief = t.get("brief", "没有相关简介")
self.ThemeInfoLabel.setText(
@@ -257,8 +268,8 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
else:
theme = "system"
style = self.StyleComboBox.currentText()
custom_theme = self.ThemeComboBox.currentText()
if custom_theme == "默认":
custom_theme = self.ThemeComboBox.currentData() or ""
if not custom_theme:
custom_theme = ""
return theme, style, custom_theme
@@ -306,16 +317,55 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
self.ThemeComboBox.blockSignals(True)
self.ThemeComboBox.clear()
self.ThemeComboBox.addItem("默认")
self.ThemeComboBox.addItem("默认", "")
self.__theme_cache = {}
themes = themeInstance().listThemes()
for t in themes:
name = t.get("name", "")
file = t.get("file", name)
author = t.get("author", "")
if name:
self.__theme_cache[name] = t
self.ThemeComboBox.addItem(name)
self.__theme_cache[file] = t
self.ThemeComboBox.addItem(name, file)
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()
def onImportThemeButtonClicked(
self
@@ -330,9 +380,9 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
if not file_path:
return
try:
name = themeInstance().importTheme(file_path)
file_id = themeInstance().importTheme(file_path)
self.populateThemeList()
idx = self.ThemeComboBox.findText(name)
idx = self.ThemeComboBox.findData(file_id)
if idx >= 0:
self.ThemeComboBox.setCurrentIndex(idx)
self.updateThemeStatus()
@@ -359,7 +409,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
self.ThemeComboBox.blockSignals(True)
if self.__original_custom_theme:
idx = self.ThemeComboBox.findText(self.__original_custom_theme)
idx = self.ThemeComboBox.findData(self.__original_custom_theme)
if idx >= 0:
self.ThemeComboBox.setCurrentIndex(idx)
else:
+20 -1
View File
@@ -328,7 +328,26 @@
</size>
</property>
<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>
</widget>
</item>
+81 -51
View File
@@ -9,9 +9,7 @@ See the LICENSE file for details.
"""
import os
import shutil
import tempfile
import threading
import zipfile
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
@@ -23,9 +21,8 @@ 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 (
packTheme,
readThemeInfo,
unpackTheme,
readThemeQss,
validateTheme,
wrapQssToAtheme
)
@@ -94,6 +91,54 @@ class ThemeManager:
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._removeThemeFile(theme_name) # caller holds the lock
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(
self,
source_path: str
@@ -123,31 +168,17 @@ class ThemeManager:
with self.__lock:
if ext == ".qss":
name = os.path.splitext(os.path.basename(source_path))[0]
dest_path = os.path.join(self.__themes_dir, name + ".altheme")
if os.path.exists(dest_path):
raise ValueError(f"主题 '{name}' 已存在")
dest_path = self._resolveDestPath(name, "未知作者")
wrapQssToAtheme(source_path, dest_path, "both")
return name
return os.path.splitext(os.path.basename(dest_path))[0]
elif ext == ".altheme":
with zipfile.ZipFile(source_path, "r") as zf:
if "theme.qss" not in zf.namelist():
raise ValueError("无效的 .altheme: 缺少 theme.qss")
info = readThemeInfo(source_path)
info = validateTheme(source_path)
name = info.get("name", os.path.splitext(os.path.basename(source_path))[0])
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", "")
for existing in self.listThemes():
if (existing.get("name", "") == safe_name
and existing.get("author", "") == new_author):
raise ValueError(
f"主题名称 '{safe_name}' (作者 '{new_author}') 已存在"
)
dest_path = self._resolveDestPath(safe_name, new_author)
shutil.copy2(source_path, dest_path)
return safe_name
return os.path.splitext(os.path.basename(dest_path))[0]
else:
raise ValueError(f"不支持的文件类型: {ext}")
@@ -172,10 +203,7 @@ class ThemeManager:
if filename.endswith(".altheme"):
filepath = os.path.join(self.__themes_dir, filename)
try:
info = readThemeInfo(filepath)
with zipfile.ZipFile(filepath, "r") as zf:
if "theme.qss" not in zf.namelist():
raise ValueError("缺少 theme.qss")
info = validateTheme(filepath, check_qss=False) # skip QSS read for list scan
name = info.get("name", "")
author = info.get("author", "")
key = (name, author)
@@ -185,6 +213,7 @@ class ThemeManager:
)
continue
seen_keys.add(key)
info["file"] = os.path.splitext(filename)[0]
themes.append(info)
except Exception as e:
logInstance().getLogger("ThemeManager").warning(
@@ -196,6 +225,26 @@ 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
@@ -210,16 +259,8 @@ class ThemeManager:
name (str): The theme name to remove.
"""
filepath = os.path.join(self.__themes_dir, name + ".altheme")
with self.__lock:
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)
self._removeThemeFile(name)
def applyTheme(
self,
@@ -243,22 +284,11 @@ class ThemeManager:
if not os.path.isfile(filepath):
raise FileNotFoundError(filepath)
with self.__lock:
info = readThemeInfo(filepath)
with tempfile.TemporaryDirectory() as tmpdir:
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"
)
info = validateTheme(filepath)
qss = readThemeQss(filepath)
app = QApplication.instance()
if app:
app.setStyleSheet(qss)
need_theme = info.get("need_theme", "both")
app.styleHints().setColorScheme(
ThemeManager._colorSchemeFor(need_theme)
+80 -3
View File
@@ -37,7 +37,6 @@ def packTheme(
zf.writestr("info.json", json.dumps(info, ensure_ascii=False, indent=4))
zf.write(qss_path, "theme.qss")
def unpackTheme(
altheme_path: str,
output_dir: str
@@ -65,7 +64,6 @@ def unpackTheme(
raise ValueError(f"不安全的 .altheme 入口: {name}")
zf.extractall(output_dir)
def readThemeInfo(
altheme_path: str
) -> dict:
@@ -94,6 +92,85 @@ def readThemeInfo(
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}")
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
if ("author" not in info or not isinstance(info.get("author"), str)
or not info["author"].strip()):
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 check_qss and not qss_bytes.strip():
raise ValueError("无效的 .altheme: theme.qss 为空")
return info
def wrapQssToAtheme(
qss_path: str,
@@ -119,7 +196,7 @@ def wrapQssToAtheme(
filename = os.path.splitext(os.path.basename(qss_path))[0]
info = {
"name": filename,
"author": "未知",
"author": "未知作者",
"need_theme": current_theme,
"brief": "没有相关简介"
}