mirror of
https://github.com/KenanZhu/AutoLibrary.git
synced 2026-06-17 23:13:03 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f9175371dc | |||
| 8e1b28f3fe | |||
| 57f1cfb3f2 | |||
| 007b4dc2ef | |||
| 67f297b434 | |||
| 86f0761eed |
Binary file not shown.
@@ -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
@@ -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:
|
||||
|
||||
@@ -21,11 +21,11 @@ QMainWindow::separator {
|
||||
QMenuBar {
|
||||
background-color: #0f1628;
|
||||
border-bottom: 1px solid #1c2840;
|
||||
padding: 2px 6px;
|
||||
padding: 2px 5px;
|
||||
color: #d0daf0;
|
||||
}
|
||||
QMenuBar::item {
|
||||
padding: 4px 10px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QMenuBar::item:selected {
|
||||
@@ -150,7 +150,6 @@ QSpinBox::up-arrow,
|
||||
QDoubleSpinBox::up-arrow,
|
||||
QDateEdit::up-arrow,
|
||||
QTimeEdit::up-arrow {
|
||||
/* image: none; */
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-bottom: 5px solid #7888b8;
|
||||
@@ -176,7 +175,6 @@ QSpinBox::down-arrow,
|
||||
QDoubleSpinBox::down-arrow,
|
||||
QDateEdit::down-arrow,
|
||||
QTimeEdit::down-arrow {
|
||||
image: none;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 5px solid #7888b8;
|
||||
@@ -255,7 +253,7 @@ QComboBox:disabled {
|
||||
/* ---- Check Box / Radio Button ---- */
|
||||
QCheckBox,
|
||||
QRadioButton {
|
||||
spacing: 8px;
|
||||
spacing: 5px;
|
||||
color: #d0daf0;
|
||||
}
|
||||
QCheckBox::indicator,
|
||||
@@ -263,9 +261,14 @@ QRadioButton::indicator {
|
||||
border-style: solid;
|
||||
border-color: #334478;
|
||||
border-width: 2px;
|
||||
border-radius: 3px;
|
||||
background-color: #0a1020;
|
||||
}
|
||||
QCheckBox::indicator {
|
||||
border-radius: 3px;
|
||||
}
|
||||
QRadioButton::indicator {
|
||||
border-radius: 7px;
|
||||
}
|
||||
QCheckBox::indicator:hover,
|
||||
QRadioButton::indicator:hover {
|
||||
border-color: #2dd4bf;
|
||||
@@ -274,9 +277,6 @@ QCheckBox::indicator:checked {
|
||||
background-color: #2dd4bf;
|
||||
border-color: #2dd4bf;
|
||||
}
|
||||
QRadioButton::indicator {
|
||||
border-radius: 10px;
|
||||
}
|
||||
QRadioButton::indicator:checked {
|
||||
background-color: #2dd4bf;
|
||||
border-color: #2dd4bf;
|
||||
@@ -330,20 +330,14 @@ QTableWidget::indicator:indeterminate {
|
||||
|
||||
/* ---- Group Box ---- */
|
||||
QGroupBox {
|
||||
margin-top: 5px;
|
||||
padding-top: 15px;
|
||||
color: #b4c2f5;
|
||||
font-weight: bold;
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
border-radius: 6px;
|
||||
margin-top: 12px;
|
||||
padding-top: 14px;
|
||||
color: #d0daf0;
|
||||
font-weight: bold;
|
||||
}
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
left: 12px;
|
||||
padding: 0 6px;
|
||||
color: #8b9ad0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* ---- Tab ---- */
|
||||
@@ -389,18 +383,10 @@ QTableWidget {
|
||||
QListWidget::item,
|
||||
QTreeWidget::item,
|
||||
QTableWidget::item {
|
||||
padding: 5px 10px;
|
||||
border: none;
|
||||
}
|
||||
QListWidget::item:selected,
|
||||
QTreeWidget::item:selected,
|
||||
QTableWidget::item:selected {
|
||||
background-color: #2dd4bf;
|
||||
color: #0f1119;
|
||||
padding: 5px 5px;
|
||||
}
|
||||
QHeaderView::section {
|
||||
background-color: #0f1628;
|
||||
border: none;
|
||||
border-right: 1px solid #253250;
|
||||
border-bottom: 1px solid #253250;
|
||||
padding: 5px 10px;
|
||||
|
||||
@@ -21,11 +21,11 @@ QMainWindow::separator {
|
||||
QMenuBar {
|
||||
background-color: #dce4ee;
|
||||
border-bottom: 1px solid #c0cdda;
|
||||
padding: 2px 6px;
|
||||
padding: 2px 5px;
|
||||
color: #1a2740;
|
||||
}
|
||||
QMenuBar::item {
|
||||
padding: 4px 10px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QMenuBar::item:selected {
|
||||
@@ -150,7 +150,6 @@ QSpinBox::up-arrow,
|
||||
QDoubleSpinBox::up-arrow,
|
||||
QDateEdit::up-arrow,
|
||||
QTimeEdit::up-arrow {
|
||||
/* image: none; */
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-bottom: 5px solid #6a7898;
|
||||
@@ -176,7 +175,6 @@ QSpinBox::down-arrow,
|
||||
QDoubleSpinBox::down-arrow,
|
||||
QDateEdit::down-arrow,
|
||||
QTimeEdit::down-arrow {
|
||||
image: none;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 5px solid #6a7898;
|
||||
@@ -255,7 +253,7 @@ QComboBox:disabled {
|
||||
/* ---- Check Box / Radio Button ---- */
|
||||
QCheckBox,
|
||||
QRadioButton {
|
||||
spacing: 8px;
|
||||
spacing: 5px;
|
||||
color: #1a2740;
|
||||
}
|
||||
QCheckBox::indicator,
|
||||
@@ -263,9 +261,14 @@ QRadioButton::indicator {
|
||||
border-style: solid;
|
||||
border-color: #90a4c4;
|
||||
border-width: 2px;
|
||||
border-radius: 3px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
QCheckBox::indicator {
|
||||
border-radius: 3px;
|
||||
}
|
||||
QRadioButton::indicator {
|
||||
border-radius: 7px;
|
||||
}
|
||||
QCheckBox::indicator:hover,
|
||||
QRadioButton::indicator:hover {
|
||||
border-color: #0ea58a;
|
||||
@@ -274,9 +277,6 @@ QCheckBox::indicator:checked {
|
||||
background-color: #0ea58a;
|
||||
border-color: #0ea58a;
|
||||
}
|
||||
QRadioButton::indicator {
|
||||
border-radius: 10px;
|
||||
}
|
||||
QRadioButton::indicator:checked {
|
||||
background-color: #0ea58a;
|
||||
border-color: #0ea58a;
|
||||
@@ -330,20 +330,14 @@ QTableWidget::indicator:indeterminate {
|
||||
|
||||
/* ---- Group Box ---- */
|
||||
QGroupBox {
|
||||
margin-top: 5px;
|
||||
padding-top: 15px;
|
||||
color: #4a6080;
|
||||
font-weight: bold;
|
||||
border-style: solid;
|
||||
border-color: #c0cdda;
|
||||
border-width: 1px;
|
||||
border-radius: 6px;
|
||||
margin-top: 12px;
|
||||
padding-top: 14px;
|
||||
color: #1a2740;
|
||||
font-weight: bold;
|
||||
}
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
left: 12px;
|
||||
padding: 0 6px;
|
||||
color: #4a6080;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* ---- Tab ---- */
|
||||
@@ -372,10 +366,6 @@ QTabBar::tab:selected {
|
||||
color: #0ea58a;
|
||||
border-bottom: 2px solid #0ea58a;
|
||||
}
|
||||
QTabBar::tab:hover:!selected {
|
||||
background-color: #d5dde8;
|
||||
color: #1a2740;
|
||||
}
|
||||
|
||||
/* ---- List / Tree ---- */
|
||||
QListWidget,
|
||||
@@ -393,18 +383,10 @@ QTableWidget {
|
||||
QListWidget::item,
|
||||
QTreeWidget::item,
|
||||
QTableWidget::item {
|
||||
padding: 5px 10px;
|
||||
border: none;
|
||||
}
|
||||
QListWidget::item:selected,
|
||||
QTreeWidget::item:selected,
|
||||
QTableWidget::item:selected {
|
||||
background-color: #0ea58a;
|
||||
color: #ffffff;
|
||||
padding: 5px 5px;
|
||||
}
|
||||
QHeaderView::section {
|
||||
background-color: #dce4ee;
|
||||
border: none;
|
||||
border-right: 1px solid #c0cdda;
|
||||
border-bottom: 1px solid #c0cdda;
|
||||
padding: 5px 10px;
|
||||
|
||||
@@ -1956,13 +1956,13 @@
|
||||
<widget class="QPushButton" name="ExportConfigButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<width>120</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<width>120</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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": "没有相关简介"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user