1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-08-02 06:09:36 +08:00

feat(theme): 引入 .altheme 主题文件格式与主题管理系统

- 新增 .altheme 文件格式(zip 压缩包包含 info.json 与 theme.qss)
- 新增 utils/ThemeUtils.py:主题文件打包/解包/读取工具函数
- 新增 managers/theme/ThemeManager:主题目录管理器,支持导入/列举/删除/应用
- 新增 LightLake 浅色主题 QSS 文件
- 新增 CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME 配置键
- 配置模板新增 custom_theme 字段
- ALSettingsWidget 接入 ThemeManager,替换裸 QSS 路径模式
- AppInitializer 启动时恢复自定义主题状态
- Zip Slip 防护与线程安全保护
This commit is contained in:
2026-05-30 21:01:18 +08:00
parent c0b6e0899c
commit 35253dadbb
8 changed files with 913 additions and 46 deletions
+123
View File
@@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import json
import os
import zipfile
def packTheme(
qss_path: str,
info: dict,
output_path: str
):
"""
Pack a .qss file and info dict into a .altheme file.
The .altheme file is a zip archive containing info.json and theme.qss.
Args:
qss_path (str): Path to the .qss stylesheet file.
info (dict): Theme metadata dict with keys name, author, need_theme, brief.
output_path (str): Destination path for the .altheme file.
Raises:
FileNotFoundError: If qss_path does not exist.
"""
if not os.path.isfile(qss_path):
raise FileNotFoundError(qss_path)
with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
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
):
"""
Extract a .altheme file to a directory.
Performs Zip Slip validation before extraction.
Args:
altheme_path (str): Path to the .altheme file.
output_dir (str): Directory to extract contents into.
Raises:
FileNotFoundError: If altheme_path does not exist.
ValueError: If a zip entry contains an unsafe path.
"""
if not os.path.isfile(altheme_path):
raise FileNotFoundError(altheme_path)
os.makedirs(output_dir, exist_ok=True)
with zipfile.ZipFile(altheme_path, "r") as zf:
for name in zf.namelist():
if name.startswith("/") or ".." in name:
raise ValueError(f"不安全的 .altheme 入口: {name}")
zf.extractall(output_dir)
def readThemeInfo(
altheme_path: str
) -> dict:
"""
Read only the info.json metadata from a .altheme file.
Args:
altheme_path (str): Path to the .altheme file.
Returns:
dict: The theme metadata dictionary.
Raises:
FileNotFoundError: If altheme_path does not exist.
ValueError: If the .altheme does not contain info.json.
"""
if not os.path.isfile(altheme_path):
raise FileNotFoundError(altheme_path)
with zipfile.ZipFile(altheme_path, "r") as zf:
if "info.json" not in zf.namelist():
raise ValueError("无效的 .altheme: 缺少 info.json")
with zf.open("info.json") as fh:
return json.loads(fh.read().decode("utf-8"))
def wrapQssToAtheme(
qss_path: str,
output_path: str,
current_theme: str
):
"""
Wrap a bare .qss file into a .altheme file with auto-generated metadata.
The generated info.json uses the filename as the theme name
and sets default values for author and brief.
Args:
qss_path (str): Path to the bare .qss stylesheet file.
output_path (str): Destination path for the .altheme file.
current_theme (str): The need_theme value to embed in metadata
("light", "dark", or "both").
Raises:
FileNotFoundError: If qss_path does not exist.
"""
filename = os.path.splitext(os.path.basename(qss_path))[0]
info = {
"name": filename,
"author": "未知",
"need_theme": current_theme,
"brief": "没有相关简介"
}
packTheme(qss_path, info, output_path)