Compare commits

...
39 Commits
Author SHA1 Message Date
KenanZhuandClaude Opus 4.7 106463b9e5 refactor(autoscript): 对象化 ASEngine、移除旧变量导出、清理编排窗口解析逻辑
- ASEngine 转为类,目标变量注册作为 __init__ 接口,配套函数提取到 _helpers.py
- Lua 函数重命名 CURRENT_DATE→datenow, CURRENT_TIME→timenow, date_add→dateadd 等
- __init__.py 移除 ALL_VARIABLES/_TARGET_VAR_DEFS/_MOCK_TYPE_VALUES 导出,替换为接口函数
- 编排窗口移除脚本→控件的反向解析逻辑,合并常量定义为查询接口
- 编辑窗口新增工具函数 Tab、Tab 键插入 4 空格、图标改用 setIcon 加载

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:10:07 +08:00
KenanZhuandClaude Opus 4.7 5e898180c7 refactor(style): 统一项目代码风格,整理导入顺序、间距规范与方法排列
- GUI 模块统一 QtCore → QtGui → QtWidgets 导入排列,各类独占一行按字母排序
- 统一类间两空行、类内方法间一空行、函数间一空行的间距规范
- 统一方法排列顺序:__init__ → setupUi → connectSignals → public → Slot → private
- 统一 _widgets 中 ConditionRowFrame/ActionStepFrame 方法命名(populate* / toScript / updateValueWidget)
- LibTimeSelector 迁入 operators/abs 抽象层

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 13:14:27 +08:00
KenanZhuandClaude Opus 4.7 a03ab38279 refactor(autoscript): 完善 Lua 错误分类与 Date/Time 严格校验,清理死代码并补齐类型注解
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 01:02:17 +08:00
KenanZhu 4761cade26 refactor(gui): 统一资源路径前缀并更新新版 SVG Logo 2026-05-23 20:05:39 +08:00
KenanZhu 531b05651e refactor(gui): 重构更新 AutoLibrary Logo 样式为全新设计样式 2026-05-23 19:26:00 +08:00
KenanZhuandClaude Opus 4.7 3cea7df736 refactor(gui): 编排编辑窗口适配 Lua 引擎新接口
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:22:49 +08:00
KenanZhuandClaude Opus 4.7 a0fd03f12f refactor(autoscript): ASEngine 迁移至 Lua 沙箱引擎,强化类型安全与异常处理
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:22:36 +08:00
KenanZhuandClaude Opus 4.7 9b47886e5b fix(autoscript): SET 赋值强制强类型检查,禁止跨类型隐式转换
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 04:22:06 +08:00
KenanZhuandClaude Opus 4.7 82738be99a feat(gui): 编辑窗口支持调试运行与动态模拟目标数据输入
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 04:21:52 +08:00
KenanZhuandClaude Opus 4.7 e097b5afc9 refactor(gui): 编排窗口简化为纯代码生成器,移除脚本解析与预检逻辑
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 04:21:40 +08:00
KenanZhuandClaude Opus 4.7 fe7453fe02 feat(gui): 编排窗口支持算术表达式解析与回显
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 00:27:59 +08:00
KenanZhuandClaude Opus 4.7 1d4b03d162 feat(autoscript): 支持算术表达式与变量参与加减运算
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 00:27:43 +08:00
KenanZhu 4642916fd5 fix(gui): 修正编排窗口日期映射 CURRENT_DATE 误识别为前天的问题 2026-05-18 20:47:35 +08:00
KenanZhu 5800437ba2 fix(gui): 编排窗口代码生成统一使用 END IF 结束块 2026-05-18 20:43:48 +08:00
KenanZhu 23467c1d3d feat(autoscript): 支持 // 行内注释与完整注释行解析 2026-05-18 20:13:46 +08:00
KenanZhu b8c0a29c59 fix(gui): 调整定时任务对话框布局边距与间距 2026-05-18 17:59:04 +08:00
KenanZhu 87787ad3dc style(gui): 编辑器高亮配色更改为 VSCode C 风格并为布尔字面量独立配色 2026-05-18 17:59:00 +08:00
KenanZhu e800f6ece1 refactor(gui): 统一 setupUi 命名并调整按钮布局 2026-05-18 16:01:22 +08:00
KenanZhu 600a304ab8 style(gui): 规范编排对话框属性命名并消除冗余代码 2026-05-18 16:01:16 +08:00
KenanZhu c038c8005d refactor(autoscript): 公开 splitTopLevel 并导出常量,消除冗余委托与重复变量 2026-05-18 16:01:10 +08:00
KenanZhu 6cf182c8c8 refactor(gui): 编排窗口迁移至新包并移除旧的预览/编排对话框 2026-05-18 11:15:35 +08:00
KenanZhu 33c0f4414c fix(autoscript): 为异常添加行号信息并补充类型兼容性检查 2026-05-17 02:58:47 +08:00
KenanZhu 2843300cf9 refactor(autoscript): 使用观察者模式解耦解析与预检查/编排流程 2026-05-17 01:48:25 +08:00
KenanZhu 9bdc9a3de9 refactor(autoscript): 使用 ASTokenizer 和 NodeVisitor 重构解析与执行流程 2026-05-17 01:33:22 +08:00
KenanZhu 500ddd41c5 refactor(autoscript): 替换 dsl 包为 autoscript 引擎模块 2026-05-12 11:49:43 +08:00
KenanZhu 14c6db3384 refactor(config): 引入 ConfigPath 值对象消除 ConfigType/ConfigKey 的消费者 API 冗余 2026-05-10 16:14:36 +08:00
KenanZhu bbd97970a6 refactor(modules): 将 AutoScriptEngine 移至 dsl/,ConfigUtils 移至 managers/config/,修复单一职责和依赖倒置问题 2026-05-10 15:33:10 +08:00
github-actions[bot] 22d3c3462c chore(release): merge release/v1.3.0 to main [auto release commit] 2026-05-09 06:08:33 +00:00
github-actions[bot] dc287f3aa5 chore(release): v1.3.0 [auto release commit] 2026-05-09 06:05:24 +00:00
Kenan Zhu 7886379875 feat(*): 支持编辑定时任务,支持AutoScript的重复性定时任务预处理指令 (#7)
feat: 支持编辑定时任务,支持AutoScript的重复性定时任务预处理指令
2026-05-09 13:20:37 +08:00
KenanZhu 967ede4b04 fix(ALTimerTaskManageWidget): 修复右键菜单删除任务时 parent() 类型错误 2026-05-09 12:59:23 +08:00
KenanZhu 27250dba2f feat(ALTimerTask*): 实现定时任务编辑功能,统一代码规范并重命名重复任务历史字段 2026-05-09 10:07:25 +08:00
KenanZhu 46b3447d1e feat(autoscript): 将预处理脚本重构为 AutoScript DSL,新增可视化编排与预览对话框 2026-05-08 20:46:54 +08:00
Gogs 4d0d7a952c feat(preproc): 新增适用于重复性定时任务的预处理脚本以及可视化编排对话框 2026-05-08 15:23:24 +08:00
KenanZhu e11f696b76 style(*): 添加缺失的版权信息,并同一版权年份为文件创建时间的年份 2026-05-06 01:01:52 +08:00
KenanZhu ffae43d5bd fix(ConfigUtils): 添加未导入的 os 模块 2026-03-24 21:49:52 +08:00
Gogs baa4f23136 refactor(config): 新增 ConfigUtils 工具类并优化配置管理逻辑
- 新增 ConfigUtils 工具类,提供配置路径获取等工具方法
- 将 ConfigManager.getValidateAutomationConfigPaths() 重构为 ConfigUtils.getAutomationConfigPaths()
- 优化 MsgBase 中 LogManager 的导入方式,使用模块导入替代函数导入
- 规范化 TimerUtils.py 中 calculate_next_repeat_time() 的文档字符串格式
2026-03-23 13:31:06 +08:00
KenanZhu 1c88d3db7b chore(requirement): 移除 opencv-python 和 pywin32 冗余依赖 2026-03-22 22:56:43 +08:00
github-actions[bot] 3880f90916 chore(release): merge release/v1.2.1 to main [auto release commit] 2026-03-22 14:17:40 +00:00
62 changed files with 3547 additions and 669 deletions

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
# AutoLibrary
---
![AutoLibrary Logo](./src/gui/resources/icons/AutoLibrary_128x128.ico)
![AutoLibrary Logo](./src/gui/resources/icons/AutoLibrary_Logo_128.svg)
[![GitHub stars](https://img.shields.io/github/stars/KenanZhu/AutoLibrary.svg?style=social&label=Star)](https://github.com/KenanZhu/AutoLibrary)
![License](https://img.shields.io/github/license/KenanZhu/AutoLibrary?label=license)
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -22,7 +22,7 @@ def main():
app = QApplication(sys.argv)
translator = QTranslator()
if translator.load(":/res/trans/translators/qtbase_zh_CN.ts"):
if translator.load(":/res/translators/qtbase_zh_CN.ts"):
app.installTranslator(translator)
app.setStyle('Fusion')
app.setApplicationName("AutoLibrary")
+262
View File
@@ -0,0 +1,262 @@
# -*- 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.
"""
from datetime import (
date,
datetime,
)
from lupa import LuaRuntime as _LuaRuntime
from autoscript._helpers import (
_TYPE_DEFAULT_VAR,
_assignPath,
_checkDateFormat,
_checkTimeFormat,
_checkType,
_cleanLuaError,
_navigatePath,
)
try:
from lupa.lua55 import LuaError as _LuaError, LuaSyntaxError as _LuaSyntaxError
except ImportError:
try:
from lupa.lua54 import LuaError as _LuaError, LuaSyntaxError as _LuaSyntaxError
except ImportError:
_LuaError = Exception
_LuaSyntaxError = Exception
__all__ = ["ASEngine"]
class ASEngine:
@staticmethod
def getCurrentDate(
) -> str:
return date.today().isoformat()
@staticmethod
def getCurrentTime(
) -> str:
return datetime.now().strftime("%H:%M")
@staticmethod
def _sandbox(
lua,
) -> None:
lua.execute("""
io = nil
require = nil
dofile = nil
loadfile = nil
load = nil
package = nil
rawget = nil
rawset = nil
rawequal = nil
getfenv = nil
setfenv = nil
debug = nil
if os then
os.execute = nil
os.exit = nil
os.getenv = nil
os.remove = nil
os.rename = nil
os.tmpname = nil
os.setlocale = nil
end
""")
@staticmethod
def _registerHelpers(
lua,
) -> None:
lua.execute("""
function date(y, m, d)
return os.time({year = y, month = m, day = d})
end
function time(h, m)
return h * 60 + m
end
function datenow()
local now = os.date("*t")
return os.time({year = now.year, month = now.month, day = now.day})
end
function timenow()
local now = os.date("*t")
return now.hour * 60 + now.min
end
function dateadd(date_val, n)
return date_val + n * 86400
end
function timeadd(time_val, n)
return (time_val + n * 60) % 1440
end
function strtodate(iso_str)
local y, m, d = iso_str:match("(%d+)-(%d+)-(%d+)")
return os.time({year = y, month = m, day = d})
end
function strtotime(hm_str)
local h, m = hm_str:match("(%d+):(%d+)")
return h * 60 + m
end
function datetostr(ts)
return os.date("%Y-%m-%d", ts)
end
function timetostr(m)
return string.format("%02d:%02d", math.floor(m / 60), m % 60)
end
""")
def __init__(
self,
targetVars: list[tuple] = None,
):
self._targetVars: dict[str, dict] = {}
self._lua = None
if targetVars:
for item in targetVars:
name, varType, keyPath = item[0], item[1], item[2]
self.addTargetVar(name, varType, keyPath)
def _getLua(
self,
):
if self._lua is None:
self._lua = _LuaRuntime(unpack_returned_tuples=True)
self._sandbox(self._lua)
self._registerHelpers(self._lua)
return self._lua
def _push(
self,
targetData: dict,
) -> None:
lua = self._getLua()
g = lua.globals()
strToDate = g["strtodate"]
strToTime = g["strtotime"]
for varName, info in self._targetVars.items():
keyPath = info["keyPath"]
vt = info["type"]
raw = _navigatePath(targetData, keyPath)
if vt == "Date":
if not isinstance(raw, str) or not raw.strip():
raise ValueError(
f"Date 类型变量 '{varName}' 对应的数据为空或不是字符串类型,"
f"请检查路径 {keyPath} 的值是否为合法的日期字符串 (YYYY-MM-DD)"
)
raw = raw.strip()
_checkDateFormat(raw, varName)
g[varName] = strToDate(raw)
elif vt == "Time":
if not isinstance(raw, str) or not raw.strip():
raise ValueError(
f"Time 类型变量 '{varName}' 对应的数据为空或不是字符串类型,"
f"请检查路径 {keyPath} 的值是否为合法的时间字符串 (HH:MM)"
)
raw = raw.strip()
_checkTimeFormat(raw, varName)
g[varName] = strToTime(raw)
else:
if raw is None:
raw = _TYPE_DEFAULT_VAR.get(vt, False)
g[varName] = raw
def _pull(
self,
targetData: dict,
) -> None:
lua = self._getLua()
g = lua.globals()
dateToStr = g["datetostr"]
timeToStr = g["timetostr"]
for varName, info in self._targetVars.items():
try:
luaVal = g[varName]
except KeyError:
continue
vt = info["type"]
if vt == "Date":
luaVal = dateToStr(luaVal)
elif vt == "Time":
luaVal = timeToStr(luaVal)
elif vt == "Float" and isinstance(luaVal, int) and not isinstance(luaVal, bool):
luaVal = float(luaVal)
_checkType(varName, vt, luaVal)
_assignPath(targetData, info["keyPath"], luaVal)
def addTargetVar(
self,
name: str,
varType: str,
keyPath: list,
) -> None:
upperName = name.upper().strip()
self._targetVars[upperName] = {
"type": varType,
"keyPath": keyPath,
}
def execute(
self,
scriptText: str,
targetData: dict,
) -> None:
if not scriptText or not scriptText.strip():
return
try:
self._push(targetData)
self._getLua().execute(scriptText)
self._pull(targetData)
except _LuaSyntaxError as e:
raise ValueError(
f"AutoScript 语法错误: {_cleanLuaError(str(e))}"
)
except _LuaError as e:
raise ValueError(
f"AutoScript 运行时错误: {_cleanLuaError(str(e))}"
)
except ValueError as e:
raise ValueError(f"AutoScript 数据错误: {e}")
except Exception as e:
raise ValueError(f"AutoScript 未知错误: {e}")
def reset(
self,
) -> None:
self._targetVars = {}
self._lua = None
+66
View File
@@ -0,0 +1,66 @@
# -*- 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.
"""
from autoscript.ASEngine import ASEngine
__all__ = [
"ASEngine",
"createEngine",
"createMockTargetData",
"createAllVariablesTable",
"createTargetVarDefs",
]
_TARGET_VAR_DEFS = [
("USERNAME", "String", ["username"], "用户名"),
("USER_ENABLE", "Boolean", ["enabled"], "用户启用"),
("RESERVE_DATE", "Date", ["reserve_info", "date"], "预约日期"),
("RESERVE_BEGIN_TIME", "Time", ["reserve_info", "begin_time", "time"], "预约开始时间"),
("RESERVE_END_TIME", "Time", ["reserve_info", "end_time", "time"], "预约结束时间"),
]
_MOCK_TYPE_VALUES = {
"String": "__mock__",
"Boolean": True,
"Date": "2099-01-01",
"Time": "00:00",
"Int": 0,
"Float": 0.0,
}
def createAllVariablesTable(
) -> dict:
return {
displayName: (name, varType)
for name, varType, _, displayName in _TARGET_VAR_DEFS
}
def createTargetVarDefs(
) -> list:
return list(_TARGET_VAR_DEFS)
def createMockTargetData(
) -> dict:
data = {}
for _, varType, keyPath, _ in _TARGET_VAR_DEFS:
d = data
for key in keyPath[:-1]:
d = d.setdefault(key, {})
d[keyPath[-1]] = _MOCK_TYPE_VALUES.get(varType, "")
return data
def createEngine(
) -> ASEngine:
return ASEngine(_TARGET_VAR_DEFS)
+153
View File
@@ -0,0 +1,153 @@
# -*- 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.
"""
from datetime import (
date,
datetime,
)
_TYPE_DEFAULT_VAR: dict[str, str | int | float | bool] = {
"String": "",
"Int": 0,
"Float": 0.0,
"Boolean": False,
}
def _navigatePath(
data: dict,
keyPath: list,
default=None,
):
d = data
for key in keyPath[:-1]:
d = d.get(key, {})
if not isinstance(d, dict):
return default
return d.get(keyPath[-1], default)
def _assignPath(
data: dict,
keyPath: list,
value,
) -> None:
d = data
for key in keyPath[:-1]:
d = d.setdefault(key, {})
d[keyPath[-1]] = value
def _checkDateFormat(
dateStr: str,
varName: str = "",
) -> None:
prefix = f"Date 类型变量 '{varName}' 的" if varName else ""
try:
date.fromisoformat(dateStr)
except ValueError:
raise ValueError(
f"{prefix}值 '{dateStr}' 不是合法的日期格式,"
f"应为 YYYY-MM-DD"
)
def _checkTimeFormat(
timeStr: str,
varName: str = "",
) -> None:
prefix = f"Time 类型变量 '{varName}' 的" if varName else ""
try:
datetime.strptime(timeStr, "%H:%M")
except ValueError:
raise ValueError(
f"{prefix}值 '{timeStr}' 不是合法的时间格式,"
f"应为 HH:MM"
)
def _checkType(
varName: str,
varType: str,
value,
) -> None:
if varType == "Date":
if not isinstance(value, str):
raise ValueError(
f"Date 类型变量 '{varName}' 只能接受日期字符串,"
f"不能接受 {_pyTypeToASType(value)} 类型"
)
_checkDateFormat(value, varName)
return
if varType == "Time":
if not isinstance(value, str):
raise ValueError(
f"Time 类型变量 '{varName}' 只能接受时间字符串,"
f"不能接受 {_pyTypeToASType(value)} 类型"
)
_checkTimeFormat(value, varName)
return
if varType == "Int":
if isinstance(value, bool):
raise ValueError(
f"Int 类型变量 '{varName}' 不能接受 Boolean 类型的值"
)
if not isinstance(value, int) and not (isinstance(value, float) and value == int(value)):
raise ValueError(
f"Int 类型变量 '{varName}' 不能接受 {_pyTypeToASType(value)} 类型的值"
)
return
if varType == "Float":
if isinstance(value, bool):
raise ValueError(
f"Float 类型变量 '{varName}' 不能接受 Boolean 类型的值"
)
if not isinstance(value, (int, float)):
raise ValueError(
f"Float 类型变量 '{varName}' 不能接受 {_pyTypeToASType(value)} 类型的值"
)
return
if varType == "Boolean":
if not isinstance(value, bool):
raise ValueError(
f"Boolean 类型变量 '{varName}' 不能接受 {_pyTypeToASType(value)} 类型的值"
)
return
if varType == "String":
if not isinstance(value, str):
raise ValueError(
f"String 类型变量 '{varName}' 不能接受 {_pyTypeToASType(value)} 类型的值"
)
return
def _pyTypeToASType(
value,
) -> str:
if isinstance(value, bool):
return "Boolean"
if isinstance(value, int):
return "Int"
if isinstance(value, float):
return "Float"
if isinstance(value, str):
return "String"
return "Unknown"
def _cleanLuaError(
rawMsg: str,
) -> str:
msg = rawMsg.replace('[string "<python>"]:', "").strip()
stackIdx = msg.find("stack traceback:")
if stackIdx != -1:
msg = msg[:stackIdx].strip()
return msg
+1 -2
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -29,7 +29,6 @@ class LibOperator(MsgBase):
super().__init__(input_queue, output_queue)
def _waitResponseLoad(
self
) -> bool:
+3 -7
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -11,7 +11,7 @@ import logging
import queue
import datetime
from managers.log.LogManager import getLogger
import managers.log.LogManager as LogManager
class MsgBase:
@@ -54,11 +54,10 @@ class MsgBase:
self._input_queue = input_queue
self._output_queue = output_queue
try:
self._logger = getLogger(self._class_name)
self._logger = LogManager.getLogger(self._class_name)
except RuntimeError:
self._logger = None
def _showMsg(
self,
msg: str
@@ -66,7 +65,6 @@ class MsgBase:
self._output_queue.put(f"[{self._class_name:<15}] >>> : {msg}")
def _showTrace(
self,
msg: str,
@@ -79,7 +77,6 @@ class MsgBase:
if self._logger and not no_log:
self._logger.log(level, msg)
def _showLog(
self,
msg: str,
@@ -89,7 +86,6 @@ class MsgBase:
if self._logger:
self._logger.log(level, msg)
def _waitMsg(
self,
timeout: float = 1.0
+1 -2
View File
@@ -1,8 +1,7 @@
"""
Base module for the AutoLibrary project.
Here are the classes and modules in this package:
- MsgBase: Base class for messages.\
- MsgBase: Base class for messages.
- LibOperator: Base class for library operators.
"""
+13 -7
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -16,7 +16,7 @@ from managers.config.ConfigManager import instance as configInstance
from managers.driver.WebDriverManager import instance as webdriverInstance
def initializeLogManager(
def _initializeLogManager(
) -> bool:
app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
@@ -27,7 +27,7 @@ def initializeLogManager(
logInstance(log_dir)
return True
def initializeConfigManager(
def _initializeConfigManager(
) -> bool:
logger = logInstance().getLogger("AppInitializer")
@@ -49,7 +49,7 @@ def initializeConfigManager(
configInstance(new_config_dir)
return True
def initializeWebDriverManager(
def _initializeWebDriverManager(
) -> bool:
logger = logInstance().getLogger("AppInitializer")
@@ -66,11 +66,17 @@ def initializeWebDriverManager(
def initializeApp(
) -> bool:
"""
Initialize the application components
if not initializeLogManager():
Order:
LogManager -> ConfigManager -> WebDriverManager
"""
if not _initializeLogManager():
return False
if not initializeConfigManager():
if not _initializeConfigManager():
return False
if not initializeWebDriverManager():
if not _initializeWebDriverManager():
return False
return True
+9 -15
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -9,14 +9,14 @@ See the LICENSE file for details.
"""
import platform
from PySide6.QtGui import (
QIcon, QFont
)
from PySide6.QtWidgets import (
QDialog, QApplication
)
from PySide6.QtCore import (
QTimer, Qt
Qt,
QTimer
)
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import (
QApplication,
QDialog
)
from gui.ALVersionInfo import (
@@ -38,12 +38,11 @@ class ALAboutDialog(QDialog, Ui_ALAboutDialog):
self.modifyUi()
self.connectSignals()
def modifyUi(
self
):
self.LogoIconLabel.setPixmap(QIcon(":/res/icon/icons/AutoLibrary_32x32.ico").pixmap(48, 48))
self.LogoIconLabel.setPixmap(QIcon(":/res/icons/AutoLibrary_Logo_64.svg").pixmap(48, 48))
info_text = self.generateAboutText()
self.AboutInfoBrowser.setHtml(info_text)
browser_font = self.AboutInfoBrowser.font()
@@ -51,14 +50,12 @@ class ALAboutDialog(QDialog, Ui_ALAboutDialog):
self.AboutInfoBrowser.setFont(browser_font)
self.AboutInfoBrowser.setTextInteractionFlags(Qt.TextBrowserInteraction)
def connectSignals(
self
):
self.CopyButton.clicked.connect(self.copyAboutInfo)
def generateAboutText(
self
) -> str:
@@ -91,7 +88,6 @@ GitHub: <a href="https://www.github.com/KenanZhu" style="text-decoration: none;"
"""
return about_text
def getOSInfo(
self
):
@@ -123,7 +119,6 @@ GitHub: <a href="https://www.github.com/KenanZhu" style="text-decoration: none;"
'architecture': architecture
}
def getQtVersion(
self
):
@@ -134,7 +129,6 @@ GitHub: <a href="https://www.github.com/KenanZhu" style="text-decoration: none;"
except:
return "Unknown"
def copyAboutInfo(
self
):
+669
View File
@@ -0,0 +1,669 @@
# -*- 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.
"""
from copy import deepcopy
from PySide6.QtCore import (
QDate,
QSize,
Qt,
QTime,
QTimer,
Slot
)
from PySide6.QtGui import (
QColor,
QFont,
QIcon,
QSyntaxHighlighter,
QTextCharFormat,
)
from PySide6.QtWidgets import (
QApplication,
QComboBox,
QDateEdit,
QDialog,
QDialogButtonBox,
QDoubleSpinBox,
QFormLayout,
QFrame,
QGridLayout,
QGroupBox,
QHBoxLayout,
QHeaderView,
QLabel,
QLineEdit,
QMessageBox,
QPlainTextEdit,
QPushButton,
QSpinBox,
QSplitter,
QStyle,
QTabWidget,
QTableWidget,
QTableWidgetItem,
QTimeEdit,
QVBoxLayout,
QWidget,
)
from autoscript import (
createAllVariablesTable,
createMockTargetData,
createTargetVarDefs,
createEngine,
)
class ALScriptHighlighter(QSyntaxHighlighter):
"""
Syntax highlighter for Lua-based AutoScript.
"""
def __init__(
self,
parent = None
):
super().__init__(parent)
self._rules = []
keywordFmt = QTextCharFormat()
keywordFmt.setForeground(QColor("#569CD6"))
keywordFmt.setFontWeight(QFont.Weight.Bold)
for kw in [
"if", "elseif", "else", "end", "then",
"and", "or", "not",
"local", "function", "return", "nil",
]:
self._rules.append((r"\b" + kw + r"\b", keywordFmt))
boolFmt = QTextCharFormat()
boolFmt.setForeground(QColor("#4FC1FF"))
boolFmt.setFontWeight(QFont.Weight.Bold)
self._rules.append((r"\btrue\b", boolFmt))
self._rules.append((r"\bfalse\b", boolFmt))
cmpFmt = QTextCharFormat()
cmpFmt.setForeground(QColor("#C586C0"))
cmpFmt.setFontWeight(QFont.Weight.Normal)
for op in [r"==", r"~=", r">=", r"<=", r">", r"<"]:
self._rules.append((op, cmpFmt))
arithFmt = QTextCharFormat()
arithFmt.setForeground(QColor("#C586C0"))
arithFmt.setFontWeight(QFont.Weight.Normal)
for op in [r"\+", r"-", r"\*", r"/", r"\.\."]:
self._rules.append((op, arithFmt))
funcFmt = QTextCharFormat()
funcFmt.setForeground(QColor("#DCDCAA"))
funcFmt.setFontWeight(QFont.Weight.Normal)
for fn in [ "time", "date", "datenow", "timenow", "dateadd", "timeadd"]:
self._rules.append((r"\b" + fn + r"\b", funcFmt))
varFmt = QTextCharFormat()
varFmt.setForeground(QColor("#9CDCFE"))
varFmt.setFontWeight(QFont.Weight.Normal)
var_names = [name for _, (name, _) in createAllVariablesTable().items()]
for var in var_names:
self._rules.append((r"\b" + var + r"\b", varFmt))
strFmt = QTextCharFormat()
strFmt.setForeground(QColor("#CE9178"))
strFmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((r'"[^"]*"', strFmt))
self._rules.append((r"'[^']*'", strFmt))
numFmt = QTextCharFormat()
numFmt.setForeground(QColor("#B5CEA8"))
numFmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((r"\b\d+(?:\.\d+)?\b", numFmt))
commentFmt = QTextCharFormat()
commentFmt.setForeground(QColor("#6A9955"))
commentFmt.setFontItalic(True)
self._rules.append((r"--[^\n]*", commentFmt))
def highlightBlock(
self,
text
):
import re
for pattern, fmt in self._rules:
for match in re.finditer(pattern, text, re.IGNORECASE):
start = match.start()
length = match.end() - match.start()
self.setFormat(start, length, fmt)
class _DebugResultDialog(QDialog):
def __init__(
self,
changes: list,
parent = None
):
super().__init__(parent)
self.setWindowTitle("调试运行结果 - AutoLibrary")
self.setMinimumSize(600, 200)
layout = QVBoxLayout(self)
table = QTableWidget(len(changes), 3)
table.setHorizontalHeaderLabels(["目标变量", "原始数据", "运行后数据"])
table.horizontalHeader().setStretchLastSection(True)
table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
for row, (display_name, name, var_type, before_val, after_val) in enumerate(changes):
label = f"{display_name}: {name}({var_type})"
table.setItem(row, 0, QTableWidgetItem(label))
table.setItem(row, 1, QTableWidgetItem(str(before_val)))
table.setItem(row, 2, QTableWidgetItem(str(after_val)))
layout.addWidget(table)
btnBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
btnBox.accepted.connect(self.accept)
layout.addWidget(btnBox)
class _TabToSpacesEditor(QPlainTextEdit):
def keyPressEvent(
self,
event
):
if event.key() == Qt.Key.Key_Tab:
self.insertPlainText(" ")
return
super().keyPressEvent(event)
class ALAutoScriptEditDialog(QDialog):
def __init__(
self,
parent = None,
script: str = "",
mockData: dict = None
):
super().__init__(parent)
self._fontSize = 21
self._mockWidgets = {}
self.setupUi()
self.connectSignals()
self.textEdit.setPlainText(script)
self._highlighter = ALScriptHighlighter(
self.textEdit.document()
)
if mockData:
self.setMockData(mockData)
def setupUi(
self
):
self.setWindowTitle("AutoScript 编辑 - AutoLibrary")
self.setMinimumSize(660, 600)
layout = QVBoxLayout(self)
layout.setSpacing(3)
layout.setContentsMargins(3, 3, 3, 3)
toolbarLayout = QHBoxLayout()
self.zoomInBtn = QPushButton("+")
self.zoomInBtn.setFixedSize(25, 25)
self.zoomOutBtn = QPushButton("-")
self.zoomOutBtn.setFixedSize(25, 25)
self.zoomResetBtn = QPushButton("")
self.zoomResetBtn.setIcon(QIcon(":/res/icons/Reset.svg"))
self.zoomResetBtn.setIconSize(QSize(20, 20))
self.zoomResetBtn.setFixedSize(25, 25)
self.zoomResetBtn.setToolTip("重置缩放")
self.zoomLabel = QLabel(f"{self._fontSize}px")
self.zoomLabel.setFixedHeight(25)
self.orchBtn = QPushButton("编排")
self.orchBtn.setFixedHeight(25)
self.orchBtn.setToolTip("可视化生成 AutoScript 代码并插入到光标位置")
toolbarLayout.addWidget(self.orchBtn)
self.debugBtn = QPushButton("▶ 调试运行")
self.debugBtn.setFixedHeight(25)
self.debugBtn.setToolTip("使用右侧模拟数据执行脚本,查看目标变量变化")
toolbarLayout.addWidget(self.debugBtn)
sep = QFrame()
sep.setFrameShape(QFrame.Shape.VLine)
sep.setFrameShadow(QFrame.Shadow.Sunken)
sep.setFixedWidth(1)
toolbarLayout.addWidget(sep)
toolbarLayout.addWidget(self.zoomInBtn)
toolbarLayout.addWidget(self.zoomOutBtn)
toolbarLayout.addWidget(self.zoomResetBtn)
toolbarLayout.addWidget(self.zoomLabel)
toolbarLayout.addStretch()
self.copyBtn = QPushButton("")
self.copyBtn.setIcon(QIcon(":/res/icons/Copy.svg"))
self.copyBtn.setIconSize(QSize(20, 20))
self.copyBtn.setFixedSize(25, 25)
self.copyBtn.setToolTip("复制脚本")
toolbarLayout.addWidget(self.copyBtn)
layout.addLayout(toolbarLayout)
self.textEdit = _TabToSpacesEditor(self)
self.textEdit.setTabStopDistance(40)
self.textEdit.setLineWrapMode(
QPlainTextEdit.LineWrapMode.NoWrap
)
self.textEdit.setStyleSheet(
"QPlainTextEdit {"
" font-family: 'Courier New', 'Consolas', monospace;"
f" font-size: {self._fontSize}px;"
"}"
)
layout.addWidget(self.textEdit)
self.createButtonPanel(layout)
self.btnBox = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok |
QDialogButtonBox.StandardButton.Cancel
)
self.btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
self.btnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
layout.addWidget(self.btnBox)
def createButtonPanel(
self,
parent_layout
):
splitter = QSplitter(Qt.Orientation.Horizontal)
tabWidget = QTabWidget()
tabWidget.setMaximumHeight(150)
basicWidget = QWidget()
basicLayout = QGridLayout(basicWidget)
basicLayout.setSpacing(4)
basicLayout.setContentsMargins(4, 4, 4, 4)
basicLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
controlButtons = [
("如果 (if...)", "if then\n \nend"),
("再如果 (elseif...)", "elseif then\n "),
("否则 (else)", "else"),
("结束 (end)", "end"),
("跳过 (pass)", "-- pass"),
]
self.addButtonsToGrid(basicLayout, controlButtons, 0, 0, 3)
assignButtons = [
("赋值 (=)", " = "),
]
self.addButtonsToGrid(basicLayout, assignButtons, 1, 2, 3)
tabWidget.addTab(basicWidget, "基本语法")
operatorWidget = QWidget()
operatorLayout = QGridLayout(operatorWidget)
operatorLayout.setSpacing(4)
operatorLayout.setContentsMargins(4, 4, 4, 4)
operatorLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
arithmeticButtons = [
("加 (+)", " + "),
("减 (-)", " - "),
]
self.addButtonsToGrid(operatorLayout, arithmeticButtons, 0, 0, 3)
compareButtons = [
("等于 (==)", " == "),
("不等于 (~=)", " ~= "),
("大于 (>)", " > "),
("小于 (<)", " < "),
("大于等于 (>=)", " >= "),
("小于等于 (<=)", " <= "),
]
self.addButtonsToGrid(operatorLayout, compareButtons, 1, 0, 3)
logic_buttons = [
("且 (and)", " and "),
("或 (or)", " or "),
]
self.addButtonsToGrid(operatorLayout, logic_buttons, 2, 0, 3)
tabWidget.addTab(operatorWidget, "运算符")
literalWidget = QWidget()
literalLayout = QGridLayout(literalWidget)
literalLayout.setSpacing(4)
literalLayout.setContentsMargins(4, 4, 4, 4)
literalLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
bool_buttons = [
("真 (true)", "true"),
("假 (false)", "false"),
]
self.addButtonsToGrid(literalLayout, bool_buttons, 0, 0, 3)
dateTimeButtons = [
("日期", '"2099-01-01"'),
("时间", '"00:00"'),
]
self.addButtonsToGrid(literalLayout, dateTimeButtons, 1, 0, 3)
hintButtons = [
("字符串", '"请输入文本"'),
("数字", "123"),
("注释", "-- 请输入注释"),
]
self.addButtonsToGrid(literalLayout, hintButtons, 2, 0, 3)
tabWidget.addTab(literalWidget, "字面量")
varWidget = QWidget()
varLayout = QGridLayout(varWidget)
varLayout.setSpacing(4)
varLayout.setContentsMargins(4, 4, 4, 4)
varLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
varButtons = [
(display_name, name) for display_name, (name, _) in createAllVariablesTable().items()
]
self.addButtonsToGrid(varLayout, varButtons, 0, 0, 3)
tabWidget.addTab(varWidget, "变量")
funcWidget = QWidget()
funcLayout = QGridLayout(funcWidget)
funcLayout.setSpacing(4)
funcLayout.setContentsMargins(4, 4, 4, 4)
funcLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
funcButtons = [
("datenow()", "datenow()", "返回当前日期的 Unix 时间戳"),
("timenow()", "timenow()", "返回当前时间在一天中的分钟数"),
("dateadd(d, n)", "dateadd(, )", "日期偏移: dateadd(日期时间戳, 天数)"),
("timeadd(t, n)", "timeadd(, )", "时间偏移: timeadd(分钟数, 分钟数)"),
]
for i, (text, template, tooltip) in enumerate(funcButtons):
btn = QPushButton(text)
btn.setProperty("template", template)
btn.clicked.connect(self.insertTemplate)
btn.setFixedWidth(100)
btn.setFixedHeight(25)
btn.setToolTip(tooltip)
funcLayout.addWidget(btn, i // 2, i % 2)
tabWidget.addTab(funcWidget, "工具函数")
mockPanel = self.createMockPanel()
mockPanel.setMinimumWidth(260)
splitter.addWidget(tabWidget)
splitter.addWidget(mockPanel)
splitter.setStretchFactor(0, 1)
splitter.setStretchFactor(1, 1)
splitter.setSizes([530, 530])
parent_layout.addWidget(splitter)
def addButtonsToGrid(
self,
grid_layout,
buttons,
start_row,
start_col,
max_columns
):
col = start_col
row = start_row
for btn_text, template in buttons:
btn = QPushButton(btn_text)
btn.setProperty("template", template)
btn.clicked.connect(self.insertTemplate)
btn.setFixedWidth(100)
btn.setFixedHeight(25)
btn.setToolTip(f"插入: {template}")
grid_layout.addWidget(btn, row, col)
col += 1
if col >= start_col + max_columns:
col = start_col
row += 1
def createMockPanel(
self
) -> QGroupBox:
group = QGroupBox("模拟目标数据")
form = QFormLayout(group)
form.setSpacing(4)
form.setContentsMargins(5, 10, 5, 5)
self._mockWidgets = {}
mockData = createMockTargetData()
for name, var_type, key_path, display_name in createTargetVarDefs():
d = mockData
for key in key_path:
d = d[key]
default = d
widget = self.makeMockInput(var_type, default)
label = QLabel(f"{display_name}: {name}({var_type})")
form.addRow(label, widget)
self._mockWidgets[name] = (widget, var_type, key_path)
return group
def makeMockInput(
self,
var_type: str,
default
) -> QWidget:
if var_type == "String":
w = QLineEdit()
w.setText(str(default))
return w
if var_type == "Boolean":
w = QComboBox()
w.addItems(["是", "否"])
w.setCurrentIndex(0 if default else 1)
return w
if var_type == "Date":
w = QDateEdit()
w.setCalendarPopup(True)
w.setDisplayFormat("yyyy-MM-dd")
w.setDate(QDate.fromString(str(default), "yyyy-MM-dd"))
return w
if var_type == "Time":
w = QTimeEdit()
w.setDisplayFormat("HH:mm")
w.setTime(QTime.fromString(str(default), "HH:mm"))
return w
if var_type == "Int":
w = QSpinBox()
w.setMinimum(-999999)
w.setMaximum(999999)
w.setValue(int(default) if default else 0)
return w
if var_type == "Float":
w = QDoubleSpinBox()
w.setMinimum(-999999.0)
w.setMaximum(999999.0)
w.setDecimals(2)
w.setValue(float(default) if default else 0.0)
return w
w = QLineEdit()
w.setText(str(default))
return w
def getMockData(
self
) -> dict:
data = {}
for name, var_type, key_path, display_name in createTargetVarDefs():
widget, _, _ = self._mockWidgets[name]
value = self.getMockValue(widget, var_type)
d = data
for key in key_path[:-1]:
d = d.setdefault(key, {})
d[key_path[-1]] = value
return data
def setMockData(
self,
data: dict
):
if not data:
return
for name, var_type, key_path, display_name in createTargetVarDefs():
d = data
try:
for key in key_path:
d = d[key]
except (KeyError, TypeError):
continue
widget, _, _ = self._mockWidgets[name]
self.setMockValue(widget, var_type, d)
def getMockValue(
self,
widget: QWidget,
var_type: str
):
if var_type == "Boolean":
return widget.currentIndex() == 0
if var_type == "Date":
return widget.date().toString("yyyy-MM-dd")
if var_type == "Time":
return widget.time().toString("HH:mm")
if var_type == "Int":
return widget.value()
if var_type == "Float":
return widget.value()
return widget.text()
def setMockValue(
self,
widget: QWidget,
var_type: str,
value
):
if var_type == "Boolean":
widget.setCurrentIndex(0 if value else 1)
elif var_type == "Date":
widget.setDate(QDate.fromString(str(value), "yyyy-MM-dd"))
elif var_type == "Time":
widget.setTime(QTime.fromString(str(value), "HH:mm"))
elif var_type == "Int":
widget.setValue(int(value))
elif var_type == "Float":
widget.setValue(float(value))
else:
widget.setText(str(value))
def connectSignals(
self
):
self.btnBox.accepted.connect(self.accept)
self.btnBox.rejected.connect(self.reject)
self.orchBtn.clicked.connect(self.onOpenOrchDialog)
self.debugBtn.clicked.connect(self.onDebugRun)
self.zoomInBtn.clicked.connect(self.onZoomIn)
self.zoomOutBtn.clicked.connect(self.onZoomOut)
self.zoomResetBtn.clicked.connect(self.onZoomReset)
self.copyBtn.clicked.connect(self.onCopy)
def getScript(
self
) -> str:
return self.textEdit.toPlainText()
def updateFontSize(
self
):
self.textEdit.setStyleSheet(
"QPlainTextEdit {"
" font-family: 'Courier New', 'Consolas', monospace;"
f" font-size: {self._fontSize}px;"
"}"
)
self.zoomLabel.setText(f"{self._fontSize}px")
@Slot()
def insertTemplate(
self
):
btn = self.sender()
if not isinstance(btn, QPushButton):
return
template = btn.property("template")
if not template:
return
cursor = self.textEdit.textCursor()
cursor.insertText(template)
@Slot()
def onZoomIn(
self
):
self._fontSize = min(self._fontSize + 2, 40)
self.updateFontSize()
@Slot()
def onZoomOut(
self
):
self._fontSize = max(self._fontSize - 2, 8)
self.updateFontSize()
@Slot()
def onZoomReset(
self
):
self._fontSize = 21
self.updateFontSize()
@Slot()
def onCopy(
self
):
clipboard = QApplication.clipboard()
clipboard.setText(self.textEdit.toPlainText())
self.copyBtn.setEnabled(False)
QTimer.singleShot(2000, lambda: (
self.copyBtn.setEnabled(True)
))
@Slot()
def onOpenOrchDialog(
self
):
from gui.ALAutoScriptOrchDialog import ALAutoScriptOrchDialog
dlg = ALAutoScriptOrchDialog(self)
if dlg.exec() == QDialog.DialogCode.Accepted:
script = dlg.getScript()
if script:
cursor = self.textEdit.textCursor()
cursor.insertText(script)
dlg.deleteLater()
@Slot()
def onDebugRun(
self
):
script = self.textEdit.toPlainText().strip()
if not script:
QMessageBox.warning(self, "提示", "脚本内容为空。")
return
target_data = self.getMockData()
before = deepcopy(target_data)
try:
engine = createEngine()
engine.execute(script, target_data)
except ValueError as e:
QMessageBox.warning(self, "运行错误", str(e))
return
changes = []
for name, var_type, key_path, display_name in createTargetVarDefs():
before_val = before
after_val = target_data
try:
for key in key_path:
before_val = before_val[key]
after_val = after_val[key]
except (KeyError, TypeError):
continue
if before_val != after_val:
changes.append((display_name, name, var_type, before_val, after_val))
if not changes:
QMessageBox.information(self, "调试运行", "目标变量未发生变化。")
return
dlg = _DebugResultDialog(changes, self)
dlg.exec()
dlg.deleteLater()
@@ -0,0 +1,3 @@
from gui.ALAutoScriptOrchDialog._dialog import ALAutoScriptOrchDialog
__all__ = ["ALAutoScriptOrchDialog"]
+266
View File
@@ -0,0 +1,266 @@
# -*- 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.
"""
"""
Conditional block widget for the AutoScript orchestration dialog.
"""
from PySide6.QtCore import Slot
from PySide6.QtWidgets import (
QComboBox,
QGroupBox,
QHBoxLayout,
QLabel,
QPushButton,
QSizePolicy,
QVBoxLayout,
QWidget,
)
from gui.ALAutoScriptOrchDialog._widgets import (
ActionStepFrame,
ConditionRowFrame,
)
class ConditionalBlock(QGroupBox):
def __init__(
self,
blockIndex: int,
varMgr = None,
parent = None
):
super().__init__(parent)
self.blockIndex = blockIndex
self._varMgr = varMgr
self._actionWidgets = []
self._conditionRows = []
self.setupUi()
self.connectSignals()
self.addInitialConditionRow()
def setupUi(
self
):
self.setUpdatesEnabled(False)
self.setStyleSheet(
"QGroupBox { font-weight: bold; border: 1px solid #ccc; "
"margin-top: 5px; padding-top: 5px; }"
)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
mainLayout = QVBoxLayout(self)
mainLayout.setSpacing(6)
mainLayout.setContentsMargins(8, 8, 8, 8)
headerLayout = QHBoxLayout()
headerLayout.setSpacing(8)
self.typeCombo = QComboBox(self)
self.typeCombo.addItem("IF", "IF")
self.typeCombo.addItem("ELSE IF", "ELSE IF")
self.typeCombo.addItem("ELSE", "ELSE")
self.typeCombo.setFixedHeight(25)
if self.blockIndex == 0:
self.typeCombo.setEnabled(False)
headerLayout.addWidget(QLabel("类型:", self))
headerLayout.addWidget(self.typeCombo)
headerLayout.addStretch()
self.deleteBlockBtn = QPushButton("删除此块", self)
self.deleteBlockBtn.setStyleSheet("color: red;")
self.deleteBlockBtn.setFixedHeight(25)
headerLayout.addWidget(self.deleteBlockBtn)
mainLayout.addLayout(headerLayout)
self.conditionWidget = QWidget(self)
self.conditionWidget.setSizePolicy(
QSizePolicy.Preferred, QSizePolicy.Preferred
)
condLayout = QVBoxLayout(self.conditionWidget)
condLayout.setContentsMargins(4, 4, 4, 4)
condLayout.setSpacing(6)
self.condRowsLayout = QVBoxLayout()
self.condRowsLayout.setSpacing(4)
condLayout.addLayout(self.condRowsLayout)
self.addCondBtn = QPushButton("+ 添加条件", self.conditionWidget)
self.addCondBtn.setFixedHeight(25)
condLayout.addWidget(self.addCondBtn)
mainLayout.addWidget(self.conditionWidget)
self.actionLabel = QLabel("执行步骤:", self)
self.actionLabel.setFixedHeight(25)
mainLayout.addWidget(self.actionLabel)
self.actionsLayout = QVBoxLayout()
self.actionsLayout.setSpacing(4)
mainLayout.addLayout(self.actionsLayout)
self.addActionBtn = QPushButton("+ 添加执行步骤", self)
self.addActionBtn.setFixedHeight(25)
mainLayout.addWidget(self.addActionBtn)
self.setUpdatesEnabled(True)
def connectSignals(
self
):
self.typeCombo.currentIndexChanged.connect(self.onTypeChanged)
self.addCondBtn.clicked.connect(self.addConditionRow)
self.addActionBtn.clicked.connect(self.addActionStep)
def addInitialConditionRow(
self
):
row = ConditionRowFrame(
self._varMgr, self.blockIndex,
isFirst=True, parent=self
)
self._conditionRows.append(row)
self.condRowsLayout.addWidget(row)
def addConditionRow(
self
):
row = ConditionRowFrame(
self._varMgr, self.blockIndex,
isFirst=False, parent=self
)
row.deleteBtn.clicked.connect(lambda: self.removeConditionRow(row))
self._conditionRows.append(row)
self.condRowsLayout.addWidget(row)
def removeConditionRow(
self,
row: ConditionRowFrame
):
if row in self._conditionRows and len(self._conditionRows) > 1:
self._conditionRows.remove(row)
self.condRowsLayout.removeWidget(row)
row.hide()
row.deleteLater()
def addActionStep(
self
):
step = ActionStepFrame(self._varMgr, self.blockIndex, parent=self)
step.deleteBtn.clicked.connect(lambda: self.removeActionStep(step))
self._actionWidgets.append(step)
self.actionsLayout.addWidget(step)
def removeActionStep(
self,
step: ActionStepFrame
):
if step in self._actionWidgets:
self._actionWidgets.remove(step)
self.actionsLayout.removeWidget(step)
step.hide()
step.deleteLater()
def getBlockType(
self
) -> str:
return self.typeCombo.currentData()
def getConditionRows(
self
):
return list(self._conditionRows)
def getActionSteps(
self
):
return list(self._actionWidgets)
def countActionSteps(
self
) -> int:
return len(self._actionWidgets)
def toScript(
self
) -> list:
"""
Generate Lua script lines for this conditional block.
"""
blockType = self.getBlockType()
lines = []
if blockType in ("IF", "ELSE IF"):
condTexts = [
r.toScript() for r in self._conditionRows if r.toScript()
]
if not condTexts:
condTexts = ["true"]
if len(condTexts) == 1:
combined = condTexts[0]
else:
parts = []
for i, ct in enumerate(condTexts):
if i > 0:
logic = self._conditionRows[i].getLogic() or "and"
parts.append(f" {logic} ")
parts.append(f"({ct})")
combined = "".join(parts)
if blockType == "IF":
lines.append(f"if {combined} then")
else:
lines.append(f"elseif {combined} then")
else:
lines.append("else")
for step in self._actionWidgets:
scriptLine = step.toScript()
if scriptLine:
lines.append(scriptLine)
return lines
def refreshVarCombos(
self
):
for row in self._conditionRows:
row.refreshVarCombos()
for step in self._actionWidgets:
step.refreshVarCombos()
def setPrevBlockType(
self,
prevType: str | None
):
model = self.typeCombo.model()
if model is None:
return
for data in ("ELSE IF", "ELSE"):
idx = self.typeCombo.findData(data)
if idx < 0:
continue
item = model.item(idx)
shouldEnable = prevType != "ELSE"
item.setEnabled(shouldEnable)
if prevType == "ELSE" and self.typeCombo.currentData() in ("ELSE IF", "ELSE"):
self.typeCombo.setCurrentIndex(0)
@Slot(int)
def onTypeChanged(
self,
_idx
):
isCond = self.typeCombo.currentData() in ("IF", "ELSE IF")
self.conditionWidget.setVisible(isCond)
self.actionLabel.setText(
"执行步骤:" if isCond else "ELSE 执行步骤:"
)
+164
View File
@@ -0,0 +1,164 @@
# -*- 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.
"""
"""
Orchestration dialog for visually composing AutoScript scripts.
"""
from PySide6.QtCore import Slot
from PySide6.QtWidgets import (
QDialog,
QDialogButtonBox,
QFrame,
QMessageBox,
QPushButton,
QScrollArea,
QVBoxLayout,
QWidget,
)
from gui.ALAutoScriptOrchDialog._helpers import VariableManager
from gui.ALAutoScriptOrchDialog._blocks import ConditionalBlock
class ALAutoScriptOrchDialog(QDialog):
def __init__(
self,
parent = None
):
super().__init__(parent)
self._blocks = []
self._varMgr = VariableManager(self)
self.setupUi()
self.connectSignals()
self.addBlock()
self.scrollLayout.addStretch()
def setupUi(
self
):
self.setWindowTitle("AutoScript 指令编排 - AutoLibrary")
self.setMinimumSize(640, 600)
self.setModal(True)
mainLayout = QVBoxLayout(self)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.NoFrame)
scrollContent = QWidget()
self.scrollLayout = QVBoxLayout(scrollContent)
self.scrollLayout.setSpacing(5)
scroll.setWidget(scrollContent)
mainLayout.addWidget(scroll)
self.addBlockBtn = QPushButton("+ 添加判断块")
self.addBlockBtn.setFixedHeight(25)
mainLayout.addWidget(self.addBlockBtn)
self.btnBox = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok |
QDialogButtonBox.StandardButton.Cancel
)
self.btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
self.btnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
mainLayout.addWidget(self.btnBox)
def connectSignals(
self
):
self.btnBox.accepted.connect(self.onAccept)
self.btnBox.rejected.connect(self.reject)
self.addBlockBtn.clicked.connect(self.addBlock)
def updateBlockTypeRestrictions(
self
):
prevType = None
for block in self._blocks:
block.setPrevBlockType(prevType)
prevType = block.getBlockType()
def addBlock(
self
):
block = ConditionalBlock(
len(self._blocks), self._varMgr, parent=self
)
block.deleteBlockBtn.clicked.connect(lambda: self.removeBlock(block))
block.typeCombo.currentIndexChanged.connect(self.updateBlockTypeRestrictions)
block.addActionStep()
self._blocks.append(block)
self.updateBlockTypeRestrictions()
if self.scrollLayout.count() > 0:
lastItem = self.scrollLayout.itemAt(
self.scrollLayout.count() - 1
)
if lastItem and lastItem.spacerItem():
self.scrollLayout.insertWidget(
self.scrollLayout.count() - 1, block
)
return
self.scrollLayout.addWidget(block)
def removeBlock(
self,
block: ConditionalBlock
):
if len(self._blocks) <= 1:
QMessageBox.information(self, "提示", "至少保留一个判断块。")
return
if block in self._blocks:
self._blocks.remove(block)
self.scrollLayout.removeWidget(block)
block.hide()
block.deleteLater()
for i, blk in enumerate(self._blocks):
blk.blockIndex = i
if i == 0:
blk.typeCombo.setEnabled(False)
blk.typeCombo.setCurrentIndex(0)
else:
blk.typeCombo.setEnabled(True)
blk.refreshVarCombos()
self.updateBlockTypeRestrictions()
def getScript(
self
) -> str:
"""
Generate the complete Lua script from all blocks.
"""
parts = []
prevType = None
for block in self._blocks:
blockType = block.getBlockType()
if blockType == "IF" and prevType is not None:
parts.append("end")
lines = block.toScript()
parts.extend(lines)
prevType = blockType
if self._blocks and self._blocks[0].getBlockType() == "IF":
parts.append("end")
return "\n".join(parts)
@Slot()
def onAccept(
self
):
script = self.getScript().strip()
if not script:
QMessageBox.warning(self, "提示", "脚本内容为空,请添加至少一个操作步骤。")
return
self.accept()
+516
View File
@@ -0,0 +1,516 @@
# -*- 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.
"""
"""
Helper utilities and constants for the AutoScript orchestration dialog.
"""
import re
from PySide6.QtCore import QObject
from PySide6.QtWidgets import (
QComboBox,
QDateEdit,
QDoubleSpinBox,
QHBoxLayout,
QLabel,
QLineEdit,
QSizePolicy,
QSpinBox,
QStackedWidget,
QTimeEdit,
QWidget,
)
from autoscript import createAllVariablesTable
VARTYPE_INFOS = [
# varType, isArithType
("String", False),
("Int", True),
("Float", True),
("Boolean", False),
("Date", True),
("Time", True),
]
def getTypeOrder(
) -> list:
return [t for t, _ in VARTYPE_INFOS]
def getArithType(
varType: str
) -> bool:
for t, a in VARTYPE_INFOS:
if t == varType:
return a
def getPresetVars(
) -> list:
return [
{"name": name.upper(), "type": vtype, "display": display}
for display, (name, vtype) in createAllVariablesTable().items()
]
COMPARE_OPTIONS = [
("等于", "=="),
("不等于", "~="),
("大于", ">"),
("小于", "<"),
("大于等于", ">="),
("小于等于", "<="),
]
LOGIC_OPTIONS = [
("并且 (and)", "and"),
("或者 (or)", "or"),
]
ACTION_OPTIONS = [
("设置为", "set"),
("增加", "add"),
("减少", "sub"),
]
DATE_OPTIONS = [
("前天", "day_before_yesterday"),
("昨天", "yesterday"),
("今天", "today"),
("明天", "tomorrow"),
("后天", "day_after_tomorrow")
]
DATE_OFFSET_OPTIONS = [
("天", "days"),
("周", "weeks"),
# NOTE: "月" and "年" use fixed day counts (30 / 365), not calendar months/years,
# because date_add() works with second-level offsets (n * 86400).
("月", "months"),
("年", "years"),
]
class _DateInputContainer(QWidget):
def __init__(
self,
parent = None
):
super().__init__(parent)
self.setupUi()
def setupUi(
self
):
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(4)
self._modeCombo = QComboBox(self)
self._modeCombo.addItem("相对日期", "relative")
self._modeCombo.addItem("绝对日期", "absolute")
self._modeCombo.setFixedHeight(25)
self._stack = QStackedWidget(self)
self._relCombo = QComboBox(self)
for display, data in DATE_OPTIONS:
self._relCombo.addItem(display, data)
self._relCombo.setFixedHeight(25)
self._stack.addWidget(self._relCombo)
self._dateEdit = QDateEdit(self)
self._dateEdit.setDisplayFormat("yyyy-MM-dd")
self._dateEdit.setCalendarPopup(True)
self._dateEdit.setFixedHeight(25)
self._stack.addWidget(self._dateEdit)
self._modeCombo.currentIndexChanged.connect(
lambda i: self._stack.setCurrentIndex(i)
)
layout.addWidget(self._modeCombo)
layout.addWidget(self._stack)
layout.addStretch()
def getValue(
self
) -> str:
mode = self._modeCombo.currentData()
if mode == "relative":
return self._relCombo.currentText()
return self._dateEdit.date().toString("yyyy-MM-dd")
class _TimeInputContainer(QWidget):
def __init__(
self,
parent = None
):
super().__init__(parent)
self._timeEdit = QTimeEdit(self)
self._timeEdit.setDisplayFormat("HH:mm")
self._timeEdit.setFixedHeight(25)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self._timeEdit)
def getValue(
self
) -> str:
return self._timeEdit.time().toString("HH:mm")
class _DateOffsetContainer(QWidget):
def __init__(
self,
parent = None
):
super().__init__(parent)
self._spinBox = QSpinBox(self)
self._spinBox.setRange(0, 99999)
self._spinBox.setFixedHeight(25)
self._unitCombo = QComboBox(self)
for display, data in DATE_OFFSET_OPTIONS:
self._unitCombo.addItem(display, data)
self._unitCombo.setFixedHeight(25)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(4)
layout.addWidget(self._spinBox)
layout.addWidget(self._unitCombo)
layout.addStretch()
def getValue(
self
) -> str:
return str(self.getOffsetDays())
def getOffsetDays(
self
) -> int:
val = self._spinBox.value()
unit = self._unitCombo.currentData()
if unit == "weeks":
return val * 7
if unit == "months":
return val * 30
if unit == "years":
return val * 365
return val
class _TimeOffsetContainer(QWidget):
def __init__(
self,
parent = None
):
super().__init__(parent)
self._spinBox = QSpinBox(self)
self._spinBox.setRange(0, 99999)
self._spinBox.setSuffix(" 小时")
self._spinBox.setFixedHeight(25)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self._spinBox)
def getValue(
self
) -> str:
return str(self.getOffsetHours())
def getOffsetHours(
self
) -> int:
return self._spinBox.value()
class VariableManager(QObject):
def __init__(
self,
parent = None
):
super().__init__(parent)
self._vars = []
self._nameMap = {}
self.initPresetVars()
def initPresetVars(
self
):
for p in getPresetVars():
entry = {"name": p["name"], "type": p["type"], "display": p["display"]}
self._vars.append(entry)
self._nameMap[p["name"]] = entry
def getInfoByName(
self,
name: str
):
return self._nameMap.get(name.upper().strip())
def populateCombo(
self,
combo: QComboBox
):
currentData = combo.currentData()
combo.blockSignals(True)
combo.clear()
for entry in self._vars:
combo.addItem(
entry["display"],
(entry["name"], entry["type"])
)
if currentData:
for i in range(combo.count()):
d = combo.itemData(i)
if d and d[0] == currentData[0]:
combo.setCurrentIndex(i)
break
combo.blockSignals(False)
def makeValueWidget(
var_type: str,
parent: QWidget = None
) -> QWidget:
if var_type == "Int":
w = QSpinBox(parent)
w.setRange(-999999, 999999)
w.setFixedHeight(25)
w.setMinimumWidth(100)
return w
if var_type == "Float":
w = QDoubleSpinBox(parent)
w.setRange(-999999.0, 999999.0)
w.setDecimals(2)
w.setFixedHeight(25)
w.setMinimumWidth(100)
return w
if var_type == "String":
w = QLineEdit(parent)
w.setPlaceholderText("输入值")
w.setFixedHeight(25)
w.setMinimumWidth(120)
return w
if var_type == "Boolean":
w = QComboBox(parent)
w.addItem("是 (true)", "true")
w.addItem("否 (false)", "false")
w.setFixedHeight(25)
w.setMinimumWidth(100)
return w
if var_type == "Date":
return _DateInputContainer(parent)
if var_type == "Time":
return _TimeInputContainer(parent)
w = QLineEdit(parent)
w.setPlaceholderText("输入值")
w.setFixedHeight(25)
w.setMinimumWidth(120)
return w
def makeOffsetWidget(
var_type: str,
parent: QWidget = None
) -> QWidget:
if var_type == "Int":
w = QSpinBox(parent)
w.setRange(-999999, 999999)
w.setFixedHeight(25)
w.setMinimumWidth(100)
return w
if var_type == "Float":
w = QDoubleSpinBox(parent)
w.setRange(-999999.0, 999999.0)
w.setDecimals(2)
w.setFixedHeight(25)
w.setMinimumWidth(100)
return w
if var_type == "Date":
return _DateOffsetContainer(parent)
if var_type == "Time":
return _TimeOffsetContainer(parent)
w = QLabel("(不支持该操作)", parent)
w.setFixedHeight(25)
return w
def makeVarRefCombo(
parent: QWidget = None
) -> QComboBox:
cb = QComboBox(parent)
cb.setFixedHeight(25)
cb.setMinimumWidth(120)
cb.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
return cb
def makeComboWidget(
items,
min_width: int = 80,
parent: QWidget = None
) -> QComboBox:
cb = QComboBox(parent)
for display, data in items:
cb.addItem(display, data)
cb.setFixedHeight(25)
cb.setMinimumWidth(min_width)
return cb
def makeLabel(
text: str,
parent: QWidget = None,
width: int = None
) -> QLabel:
lbl = QLabel(text, parent)
lbl.setFixedHeight(25)
if width:
lbl.setFixedWidth(width)
return lbl
def getValueFromWidget(
w: QWidget
) -> str:
if hasattr(w, "getValue"):
return w.getValue()
if isinstance(w, QTimeEdit):
return w.time().toString("HH:mm")
if isinstance(w, QDateEdit):
return w.date().toString("yyyy-MM-dd")
if isinstance(w, QComboBox):
return w.currentData() or w.currentText()
if isinstance(w, QSpinBox):
return str(w.value())
if isinstance(w, QDoubleSpinBox):
return str(w.value())
if isinstance(w, QLineEdit):
return w.text()
return ""
def encodeValueStr(
raw_value: str,
var_type: str
) -> str:
"""
Encode a raw widget value as a Lua expression.
Arithmetic expressions (A + B) are passed through for numeric types;
Date/Time arithmetic is translated to ``date_add()`` / ``time_add()`` calls.
"""
if var_type in ("Date", "Time"):
return encodeDateOrTime(str(raw_value), var_type)
if isinstance(raw_value, bool):
return "true" if raw_value else "false"
s = str(raw_value)
if isArithExpr(s):
return s
if var_type == "Boolean":
up = s.upper().strip()
if up in ("TRUE", "FALSE"):
return up.lower()
return "true" if raw_value else "false"
if var_type == "String":
escaped = s.replace("\\", "\\\\").replace('"', '\\"')
return f'"{escaped}"'
return s
def encodeDateOrTime(
raw_value: str,
var_type: str
) -> str:
"""
Translate a date/time widget value into a Lua expression.
"""
s = raw_value.strip()
up = s.upper()
# Input comes from widget values — single binary expressions only (e.g. "A + 3",
# "CURRENT_DATE + 5"). Multi-operator expressions are not produced by the UI.
m_arith_spaced = re.match(r'^(.+?)\s+([+-])\s+(.+)$', s)
m_arith_nospace = re.match(r'^([A-Za-z_]\w*)([+-])(\d+|[A-Za-z_]\w*)$', s)
m_arith = m_arith_spaced or m_arith_nospace
if m_arith:
left = m_arith.group(1).strip().upper()
sign = m_arith.group(2)
right = m_arith.group(3).strip()
operand = right if sign == "+" else f"-{right}"
if left == "CURRENT_DATE":
return f"date_add(CURRENT_DATE(), {operand})"
if left == "CURRENT_TIME":
return f"time_add(CURRENT_TIME(), {operand})"
if var_type == "Date":
return f"date_add({left}, {operand})"
if var_type == "Time":
return f"time_add({left}, {operand})"
return f"{left} {sign} {right}"
if up == "CURRENT_DATE":
return "CURRENT_DATE()"
if up == "CURRENT_TIME":
return "CURRENT_TIME()"
_REL_MAP = {
"前天": "date_add(CURRENT_DATE(), -2)",
"昨天": "date_add(CURRENT_DATE(), -1)",
"今天": "CURRENT_DATE()",
"明天": "date_add(CURRENT_DATE(), 1)",
"后天": "date_add(CURRENT_DATE(), 2)",
}
if s in _REL_MAP:
return _REL_MAP[s]
if var_type == "Date":
m_date = re.match(r"^(\d{4})-(\d{2})-(\d{2})$", s)
if m_date:
y, m, d = int(m_date.group(1)), int(m_date.group(2)), int(m_date.group(3))
return f"date({y}, {m}, {d})"
if var_type == "Time":
m_time = re.match(r"^(\d{1,2}):(\d{2})$", s)
if m_time:
h, m = int(m_time.group(1)), int(m_time.group(2))
return f"time({h}, {m})"
if re.match(r"^[+-]?\d+$", s):
return s
if re.match(r"^[A-Za-z_]\w*$", s):
return s
return f'"{s}"'
# Pre-compiled patterns for detecting arithmetic expressions (A + B / A - B)
_RE_ARITH_SPACED = re.compile(r'^(.+?)\s+([+-])\s+(.+)$')
_RE_ARITH_NOSPACE = re.compile(r'^([A-Za-z_]\w*)([+-])(\d+|[A-Za-z_]\w*)$')
def isArithExpr(
expr: str
) -> bool:
"""
Return True if expr looks like a two-operand arithmetic expression (A ± B).
"""
s = expr.strip()
return bool(_RE_ARITH_SPACED.match(s) or _RE_ARITH_NOSPACE.match(s))
+464
View File
@@ -0,0 +1,464 @@
# -*- 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.
"""
"""
Widget components for the AutoScript orchestration dialog.
"""
from PySide6.QtCore import Slot
from PySide6.QtWidgets import (
QComboBox,
QFrame,
QHBoxLayout,
QLabel,
QPushButton,
QSizePolicy,
QStackedWidget
)
from gui.ALAutoScriptOrchDialog._helpers import (
ACTION_OPTIONS,
COMPARE_OPTIONS,
LOGIC_OPTIONS,
encodeValueStr,
getPresetVars,
getTypeOrder,
getValueFromWidget,
getArithType,
makeComboWidget,
makeLabel,
makeOffsetWidget,
makeValueWidget,
makeVarRefCombo,
)
class ConditionRowFrame(QFrame):
def __init__(
self,
varMgr,
parentBlockIndex: int = 0,
isFirst: bool = False,
parent = None
):
super().__init__(parent)
self._varMgr = varMgr
self._blockIndex = parentBlockIndex
self._isFirst = isFirst
self._isBoolMode = False
self._rawRhsExpr = ""
self.setupUi()
self.connectSignals()
def setupUi(
self
):
self.setUpdatesEnabled(False)
self.setFrameShape(QFrame.StyledPanel)
self.setFrameShadow(QFrame.Raised)
self.setFixedHeight(32)
layout = QHBoxLayout(self)
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(4)
if self._isFirst:
self.logicCombo = None
else:
self.logicCombo = makeComboWidget(LOGIC_OPTIONS, min_width=110, parent=self)
layout.addWidget(self.logicCombo)
self.leftVarCombo = QComboBox(self)
self.leftVarCombo.setFixedHeight(25)
self.leftVarCombo.setMinimumWidth(120)
self.leftVarCombo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.populateLeftVarCombo()
layout.addWidget(self.leftVarCombo)
self.opCombo = makeComboWidget(COMPARE_OPTIONS, min_width=80, parent=self)
layout.addWidget(self.opCombo)
self._compTypeCombo = makeComboWidget([
("特定值", "literal"),
("变量", "variable"),
], min_width=70, parent=self)
layout.addWidget(self._compTypeCombo)
self.rhsStack = QStackedWidget(self)
self.rhsStack.setFixedHeight(25)
self.initLiteralStack()
self.rhsVarCombo = makeVarRefCombo(self)
self.rhsStack.addWidget(self.rhsVarCombo)
self.rhsStack.setCurrentIndex(0)
layout.addWidget(self.rhsStack)
if not self._isFirst:
self.deleteBtn = QPushButton("×", self)
self.deleteBtn.setFixedSize(25, 25)
self.deleteBtn.setStyleSheet("color: red; font-weight: bold;")
layout.addWidget(self.deleteBtn)
else:
self.deleteBtn = None
layout.addStretch()
self.setUpdatesEnabled(True)
def populateLeftVarCombo(
self
):
wasBool = self._isBoolMode
boolName = None
if wasBool:
data = self.leftVarCombo.currentData()
if data:
boolName = data[0]
self._varMgr.populateCombo(self.leftVarCombo)
# Append boolean literal sentinels at the end
self.leftVarCombo.insertSeparator(self.leftVarCombo.count())
self.leftVarCombo.addItem("true", ("true", "Boolean"))
self.leftVarCombo.addItem("false", ("false", "Boolean"))
if wasBool and boolName:
for ci in range(self.leftVarCombo.count()):
d = self.leftVarCombo.itemData(ci)
if d and d[0] == boolName:
self.leftVarCombo.setCurrentIndex(ci)
break
def populateRHSVarCombo(
self
):
self._varMgr.populateCombo(self.rhsVarCombo)
def initLiteralStack(
self
):
self.literalStack = QStackedWidget(self)
self.literalStack.setFixedHeight(25)
self._literalWidgets = {}
for vt in getTypeOrder():
w = makeValueWidget(vt, self.literalStack)
self._literalWidgets[vt] = w
self.literalStack.addWidget(w)
self.literalStack.setCurrentWidget(self._literalWidgets.get("String"))
self.rhsStack.addWidget(self.literalStack)
def connectSignals(
self
):
self.leftVarCombo.currentIndexChanged.connect(self.onLeftVarChanged)
self._compTypeCombo.currentIndexChanged.connect(self.onCompTypeChanged)
def getLogic(
self
) -> str:
return self.logicCombo.currentData() if self.logicCombo else ""
def updateRHSLiteralWidget(
self,
vartype: str
):
if vartype not in self._literalWidgets:
vartype = "String"
self.literalStack.setCurrentWidget(self._literalWidgets[vartype])
def toScript(
self
) -> str:
data = self.leftVarCombo.currentData()
if self._isBoolMode and data:
return data[0]
if not data:
return ""
name, vartype = data
# CURRENT_DATE / CURRENT_TIME are Lua functions — call them, not reference them
if name in ("CURRENT_DATE", "CURRENT_TIME"):
name = f"{name}()"
opSym = self.opCombo.currentData()
if self._rawRhsExpr:
return f"{name} {opSym} {self._rawRhsExpr}"
isVarRef = (self._compTypeCombo.currentData() == "variable")
if isVarRef:
rd = self.rhsVarCombo.currentData()
if rd:
rhsName = rd[0]
if rhsName in ("CURRENT_DATE", "CURRENT_TIME"):
rhsName = f"{rhsName}()"
return f"{name} {opSym} {rhsName}"
rhsText = self.rhsVarCombo.currentText().strip()
if rhsText:
return f"{name} {opSym} {rhsText}"
return ""
w = self._literalWidgets.get(vartype)
if w:
rawVal = getValueFromWidget(w)
encoded = encodeValueStr(rawVal, vartype)
return f"{name} {opSym} {encoded}"
return ""
def refreshVarCombos(
self
):
self.populateLeftVarCombo()
self.populateRHSVarCombo()
@Slot(int)
def onLeftVarChanged(
self,
idx
):
self._rawRhsExpr = ""
if idx < 0:
return
data = self.leftVarCombo.itemData(idx)
if not data:
return
name, vartype = data
isBool = name in ("true", "false")
self._isBoolMode = isBool
self.opCombo.setVisible(not isBool)
self._compTypeCombo.setVisible(not isBool)
self.rhsStack.setVisible(not isBool)
if not isBool:
self.updateRHSLiteralWidget(vartype)
@Slot(int)
def onCompTypeChanged(
self,
idx
):
self._rawRhsExpr = ""
isVar = (self._compTypeCombo.currentData() == "variable")
self.rhsStack.setCurrentIndex(1 if isVar else 0)
if isVar:
self.populateRHSVarCombo()
class ActionStepFrame(QFrame):
def __init__(
self,
varMgr,
parentBlockIndex: int = 0,
parent = None
):
super().__init__(parent)
self._varMgr = varMgr
self._blockIndex = parentBlockIndex
self._currentTargetType = "String"
self.setupUi()
self.connectSignals()
def setupUi(
self
):
self.setUpdatesEnabled(False)
self.setFrameShape(QFrame.StyledPanel)
self.setFrameShadow(QFrame.Raised)
self.setFixedHeight(35)
layout = QHBoxLayout(self)
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(4)
self.opTypeCombo = makeComboWidget(ACTION_OPTIONS, min_width=70, parent=self)
layout.addWidget(self.opTypeCombo)
layout.addWidget(makeLabel("设置", self))
self.targetCombo = QComboBox(self)
self.targetCombo.setFixedHeight(25)
self.targetCombo.setMinimumWidth(120)
self.populateTargetCombo()
layout.addWidget(self.targetCombo)
layout.addWidget(makeLabel("为", self))
self.valueSrcCombo = makeComboWidget([
("特定值", "literal"),
("变量", "variable"),
], min_width=70, parent=self)
layout.addWidget(self.valueSrcCombo)
self.valueStack = QStackedWidget(self)
self.valueStack.setFixedHeight(25)
self.initValueStacks()
layout.addWidget(self.valueStack)
self.existingVarCombo = makeVarRefCombo(self)
self.existingVarCombo.setVisible(False)
layout.addWidget(self.existingVarCombo)
self.deleteBtn = QPushButton("×", self)
self.deleteBtn.setFixedSize(25, 25)
self.deleteBtn.setStyleSheet("color: red; font-weight: bold;")
layout.addWidget(self.deleteBtn)
self.setUpdatesEnabled(True)
def populateTargetCombo(
self
):
self.targetCombo.blockSignals(True)
self.targetCombo.clear()
for p in getPresetVars():
if p["name"] in ("CURRENT_TIME", "CURRENT_DATE"):
continue
info = self._varMgr.getInfoByName(p["name"])
if info:
self.targetCombo.addItem(
info["display"],
(info["name"], info["type"])
)
self.targetCombo.blockSignals(False)
def initValueStacks(
self
):
self._literalWidgets = {}
self._offsetWidgets = {}
for vt in getTypeOrder():
self._literalWidgets[vt] = makeValueWidget(vt, self.valueStack)
self.valueStack.addWidget(self._literalWidgets[vt])
if getArithType(vt):
self._offsetWidgets[vt] = makeOffsetWidget(vt, self.valueStack)
self.valueStack.addWidget(self._offsetWidgets[vt])
else:
lbl = QLabel("(不支持该操作)", self.valueStack)
lbl.setFixedHeight(25)
self._offsetWidgets[vt] = lbl
self.valueStack.addWidget(lbl)
def connectSignals(
self
):
self.opTypeCombo.currentIndexChanged.connect(self.onOpTypeChanged)
self.targetCombo.currentIndexChanged.connect(self.onTargetChanged)
self.valueSrcCombo.currentIndexChanged.connect(self.onValueSrcChanged)
def getTargetName(
self
) -> str:
data = self.targetCombo.currentData()
return data[0] if data else ""
def updateValueWidget(
self
):
op = self.opTypeCombo.currentData()
isArith = (op in ("add", "sub"))
actualType = self._currentTargetType
if isArith and actualType in self._offsetWidgets:
self.valueStack.setCurrentWidget(self._offsetWidgets[actualType])
elif actualType in self._literalWidgets:
self.valueStack.setCurrentWidget(self._literalWidgets[actualType])
else:
self.valueStack.setCurrentWidget(self._literalWidgets.get("String"))
def toScript(
self
) -> str:
"""
Generate a single line of Lua script from the current widget state.
"""
target = self.getTargetName()
op = self.opTypeCombo.currentData()
if op == "pass":
return " -- pass"
if not target:
return ""
rawVal = self.getValueRaw()
vartype = self._currentTargetType
if op == "set":
encoded = encodeValueStr(rawVal, vartype)
return f" {target} = {encoded}"
elif op == "add":
if vartype == "Date" and hasattr(self.valueStack.currentWidget(), "getOffsetDays"):
days = self.valueStack.currentWidget().getOffsetDays()
return f" {target} = date_add({target}, {days})"
if vartype == "Time" and hasattr(self.valueStack.currentWidget(), "getOffsetHours"):
hours = self.valueStack.currentWidget().getOffsetHours()
return f" {target} = time_add({target}, {hours})"
return f" {target} = {target} + {rawVal}"
elif op == "sub":
if vartype == "Date" and hasattr(self.valueStack.currentWidget(), "getOffsetDays"):
days = self.valueStack.currentWidget().getOffsetDays()
return f" {target} = date_add({target}, -{days})"
if vartype == "Time" and hasattr(self.valueStack.currentWidget(), "getOffsetHours"):
hours = self.valueStack.currentWidget().getOffsetHours()
return f" {target} = time_add({target}, -{hours})"
return f" {target} = {target} - {rawVal}"
return ""
def getValueRaw(
self
) -> str:
if self.valueSrcCombo.currentData() == "variable":
data = self.existingVarCombo.currentData()
return data[0] if data else ""
w = self.valueStack.currentWidget()
if w:
return getValueFromWidget(w)
return ""
def refreshVarCombos(
self
):
currentData = self.targetCombo.currentData()
self.populateTargetCombo()
if currentData:
for i in range(self.targetCombo.count()):
d = self.targetCombo.itemData(i)
if d and d[0] == currentData[0]:
self.targetCombo.setCurrentIndex(i)
break
self._varMgr.populateCombo(self.existingVarCombo)
@Slot(int)
def onTargetChanged(
self,
idx
):
if idx < 0:
return
data = self.targetCombo.itemData(idx)
if not data:
return
_, vartype = data
self._currentTargetType = vartype
self.updateValueWidget()
self.onValueSrcChanged(self.valueSrcCombo.currentIndex())
@Slot(int)
def onOpTypeChanged(
self,
idx
):
self.updateValueWidget()
@Slot(int)
def onValueSrcChanged(
self,
idx
):
isVar = (self.valueSrcCombo.currentData() == "variable")
self.valueStack.setVisible(not isVar)
self.existingVarCombo.setVisible(isVar)
if isVar:
self._varMgr.populateCombo(self.existingVarCombo)
else:
self.updateValueWidget()
+38 -51
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -10,26 +10,46 @@ See the LICENSE file for details.
import os
from PySide6.QtCore import (
Qt, Signal, Slot, QTime, QDate, QDir, QFileInfo
)
from PySide6.QtWidgets import (
QDialog, QWidget, QLineEdit, QMessageBox, QFileDialog,
QTreeWidgetItem, QMenu, QInputDialog
QDate,
QDir,
QFileInfo,
Qt,
QTime,
Signal,
Slot
)
from PySide6.QtGui import (
QCloseEvent, QAction
QAction,
QCloseEvent
)
from PySide6.QtWidgets import (
QDialog,
QFileDialog,
QInputDialog,
QLineEdit,
QMenu,
QMessageBox,
QTreeWidgetItem,
QWidget
)
import managers.config.ConfigManager as ConfigManager
from utils.JSONReader import JSONReader
from utils.JSONWriter import JSONWriter
from gui.resources.ui.Ui_ALConfigWidget import Ui_ALConfigWidget
from gui.ALSeatMapSelectDialog import ALSeatMapSelectDialog
from gui.ALSeatMapTable import ALSeatMapTable
from gui.ALUserTreeWidget import ALUserTreeWidget, ALUserTreeItemType
from gui.ALUserTreeWidget import (
ALUserTreeItemType,
ALUserTreeWidget
)
from gui.ALWebDriverDownloadDialog import ALWebDriverDownloadDialog
from gui.resources.ui.Ui_ALConfigWidget import Ui_ALConfigWidget
from interfaces.ConfigProvider import (
CfgKey,
ConfigProvider
)
from managers.config.ConfigUtils import ConfigUtils
from utils.JSONReader import JSONReader
from utils.JSONWriter import JSONWriter
class ALConfigWidget(QWidget, Ui_ALConfigWidget):
@@ -42,8 +62,8 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
):
super().__init__(parent)
self.__cfg_mgr = ConfigManager.instance()
self.__config_paths = ConfigManager.getValidateAutomationConfigPaths()
self.__cfg_mgr: ConfigProvider = ConfigManager.instance()
self.__config_paths = ConfigUtils.getAutomationConfigPaths()
self.__config_data = {"run": {}, "user": {}}
self.setupUi(self)
@@ -52,7 +72,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
if not self.initializeConfigs():
self.close()
def modifyUi(
self
):
@@ -68,7 +87,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.initializeFloorRoomMap()
self.initializeUserInfoWidget()
def connectSignals(
self
):
@@ -92,7 +110,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
def showEvent(
self,
event
@@ -116,7 +133,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
return result
def closeEvent(
self,
event: QCloseEvent
@@ -125,7 +141,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.configWidgetIsClosed.emit()
super().closeEvent(event)
def initializeFloorRoomMap(
self
):
@@ -159,7 +174,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
"五层": ["五层考研"]
}
def initializeConfigToWidget(
self,
which: str,
@@ -174,7 +188,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.setUsersToTreeWidget(config_data)
self.CurrentUserConfigEdit.setText(self.__config_paths["user"])
def initializeConfig(
self,
which: str
@@ -208,7 +221,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
is_success = False
return is_success
def initializeConfigs(
self
) -> bool:
@@ -221,7 +233,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.initializeConfigToWidget(which, self.__config_data[which])
return is_success
def defaultRunConfig(
self
) -> dict:
@@ -245,7 +256,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
}
}
def defaultUserConfig(
self
) -> dict:
@@ -255,7 +265,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
]
}
def collectRunConfigFromWidget(
self
) -> dict:
@@ -277,7 +286,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
run_config["mode"]["run_mode"] = run_mode
return run_config
def setRunConfigToWidget(
self,
run_config: dict
@@ -316,7 +324,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
"文件可能被意外修改或已经损坏\n"
)
def initializeUserInfoWidget(
self
):
@@ -341,7 +348,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.MaxRenewTimeDiffSpinBox.setValue(30)
self.PreferLateRenewTimeCheckBox.setChecked(False)
def collectUserFromWidget(
self
) -> dict:
@@ -374,7 +380,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
user["reserve_info"]["renew_time"]["prefer_early"] = not self.PreferLateRenewTimeCheckBox.isChecked()
return user
def collectUsersFromTreeWidget(
self
) -> dict:
@@ -397,7 +402,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
user_config["groups"].append(group_config)
return user_config
def setUserToWidget(
self,
user: dict
@@ -439,7 +443,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
"文件可能被意外修改或已经损坏\n"
)
def setUsersToTreeWidget(
self,
users: dict
@@ -481,7 +484,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
finally:
self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged)
def loadRunConfig(
self,
run_config_path: str
@@ -505,7 +507,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
)
return None
def saveRunConfig(
self,
run_config_path: str,
@@ -527,7 +528,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
)
return False
def loadUserConfig(
self,
user_config_path: str
@@ -561,7 +561,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
)
return None
def saveUserConfig(
self,
user_config_path: str,
@@ -583,7 +582,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
)
return False
def saveConfigs(
self,
run_config_path: str,
@@ -606,7 +604,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
return False
return True
def loadConfig(
self,
config_path: str
@@ -635,7 +632,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
except:
return False
def addGroup(
self,
group_name: str = ""
@@ -652,7 +648,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged)
return group_item
def delGroup(
self,
group_item: QTreeWidgetItem = None
@@ -665,7 +660,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
index = self.UserTreeWidget.indexOfTopLevelItem(group_item)
self.UserTreeWidget.takeTopLevelItem(index)
def addUser(
self,
group_item: QTreeWidgetItem = None
@@ -720,7 +714,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged)
return user_item
def delUser(
self,
user_item: QTreeWidgetItem = None
@@ -736,7 +729,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
if parent_item.childCount() == 0:
self.UserTreeWidget.setCurrentItem(None)
def renameItem(
self,
item: QTreeWidgetItem,
@@ -860,7 +852,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
is_checked = item.checkState(1) == Qt.CheckState.Checked
item.setText(1, "" if is_checked else "跳过")
def showTreeMenu(
self,
menu: QMenu
@@ -870,7 +861,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
add_group_action.triggered.connect(self.addGroup)
menu.addAction(add_group_action)
def showGroupMenu(
self,
menu: QMenu,
@@ -890,7 +880,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
if group_item.checkState(1) == Qt.CheckState.Unchecked:
add_user_action.setEnabled(False)
def showUserMenu(
self,
menu: QMenu,
@@ -950,7 +939,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
if browser_driver_path:
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(browser_driver_path))
@Slot()
def onAutoDownloadWebDriverButtonClicked(
self
@@ -964,7 +952,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.BrowserTypeComboBox.setCurrentText(selected_driver_info.driver_type.value)
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(str(selected_driver_info.driver_path)))
@Slot()
def onBrowseCurrentRunConfigButtonClicked(
self
@@ -984,13 +971,13 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.setRunConfigToWidget(data)
self.__config_paths["run"] = run_config_path
self.CurrentRunConfigEdit.setText(run_config_path)
paths = self.__cfg_mgr.get(ConfigManager.ConfigType.GLOBAL, "automation.run_path.paths", [])
paths = self.__cfg_mgr.get(CfgKey.GLOBAL.AUTOMATION.RUN_PATH.PATHS, [])
if run_config_path not in paths:
paths.append(run_config_path)
index = len(paths) - 1
else:
index = paths.index(run_config_path)
self.__cfg_mgr.set(ConfigManager.ConfigType.GLOBAL, "automation.run_path", {"current": index, "paths": paths})
self.__cfg_mgr.set(CfgKey.GLOBAL.AUTOMATION.RUN_PATH.ROOT, {"current": index, "paths": paths})
else:
QMessageBox.warning(
self,
@@ -1019,13 +1006,13 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.setUsersToTreeWidget(data)
self.__config_paths["user"] = user_config_path
self.CurrentUserConfigEdit.setText(user_config_path)
paths = self.__cfg_mgr.get(ConfigManager.ConfigType.GLOBAL, "automation.user_path.paths", [])
paths = self.__cfg_mgr.get(CfgKey.GLOBAL.AUTOMATION.USER_PATH.PATHS, [])
if user_config_path not in paths:
paths.append(user_config_path)
index = len(paths) - 1
else:
index = paths.index(user_config_path)
self.__cfg_mgr.set(ConfigManager.ConfigType.GLOBAL, "automation.user_path", {"current": index, "paths": paths})
self.__cfg_mgr.set(CfgKey.GLOBAL.AUTOMATION.USER_PATH.ROOT, {"current": index, "paths": paths})
else:
QMessageBox.warning(
self,
+29 -31
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -10,25 +10,37 @@ See the LICENSE file for details.
import queue
from PySide6.QtCore import (
Qt, Signal, Slot, QTimer, QUrl,
)
from PySide6.QtWidgets import (
QMainWindow, QMenu, QSystemTrayIcon, QMessageBox
QTimer,
QUrl,
Qt,
Signal,
Slot
)
from PySide6.QtGui import (
QTextCursor, QCloseEvent, QFont, QIcon, QDesktopServices
QCloseEvent,
QDesktopServices,
QFont,
QIcon,
QTextCursor
)
from PySide6.QtWidgets import (
QMainWindow,
QMenu,
QMessageBox,
QSystemTrayIcon
)
import managers.config.ConfigManager as ConfigManager
from base.MsgBase import MsgBase
from gui.resources.ui.Ui_ALMainWindow import Ui_ALMainWindow
from gui.resources import ALResource
from gui.ALConfigWidget import ALConfigWidget
from gui.ALTimerTaskManageWidget import ALTimerTaskManageWidget
from gui.ALAboutDialog import ALAboutDialog
from gui.ALMainWorkers import TimerTaskWorker, AutoLibWorker
from gui.ALConfigWidget import ALConfigWidget
from gui.ALMainWorkers import (
AutoLibWorker,
TimerTaskWorker
)
from gui.ALTimerTaskManageWidget import ALTimerTaskManageWidget
from gui.resources import ALResource
from gui.resources.ui.Ui_ALMainWindow import Ui_ALMainWindow
from managers.config.ConfigUtils import ConfigUtils
class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
@@ -44,9 +56,8 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
MsgBase.__init__(self, queue.Queue(), queue.Queue())
QMainWindow.__init__(self)
self.__cfg_mgr = ConfigManager.instance()
self.__timer_task_queue = queue.Queue()
self.__config_paths = ConfigManager.getValidateAutomationConfigPaths()
self.__config_paths = ConfigUtils.getAutomationConfigPaths()
self.__alTimerTaskManageWidget = None
self.__alConfigWidget = None
self.__auto_lib_thread = None
@@ -61,12 +72,11 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.startTimerTaskPolling()
self._showLog("主窗口初始化完成")
def modifyUi(
self
):
self.icon = QIcon(":/res/icon/icons/AutoLibrary_32x32.ico")
self.icon = QIcon(":/res/icons/AutoLibrary_Logo_64.svg")
self.setWindowIcon(self.icon)
self.MessageIOTextEdit.setFont(QFont("Courier New", 10))
self.ManualAction.triggered.connect(self.onManualActionTriggered)
@@ -92,7 +102,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.__alTimerTaskManageWidget.timerTaskManageWidgetIsClosed.connect(self.onTimerTaskManageWidgetClosed)
self.__alTimerTaskManageWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint)
def onAboutActionTriggered(
self
):
@@ -100,7 +109,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
about_dialog = ALAboutDialog(self)
about_dialog.exec()
def onManualActionTriggered(
self
):
@@ -108,7 +116,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
url = QUrl("https://www.autolibrary.kenanzhu.com/manuals")
QDesktopServices.openUrl(url)
def setupTray(
self
):
@@ -130,7 +137,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.TrayIcon.activated.connect(self.onTrayIconActivated)
self.TrayIcon.show()
def hideToTray(
self
):
@@ -143,7 +149,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
2000
)
def onTrayIconActivated(
self,
reason: QSystemTrayIcon.ActivationReason
@@ -152,7 +157,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
if reason == QSystemTrayIcon.DoubleClick:
self.showNormal()
def connectSignals(
self
):
@@ -164,7 +168,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.SendButton.clicked.connect(self.onSendButtonClicked)
self.MessageEdit.returnPressed.connect(self.onSendButtonClicked)
def closeEvent(
self,
event: QCloseEvent
@@ -190,7 +193,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self._showLog("主窗口关闭")
QMainWindow.closeEvent(self, event)
def appendToTextEdit(
self,
text: str
@@ -204,7 +206,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
scrollbar = self.MessageIOTextEdit.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
def startMsgPolling(
self
):
@@ -213,7 +214,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.__msg_queue_timer.timeout.connect(self.pollMsgQueue)
self.__msg_queue_timer.start(100)
def startTimerTaskPolling(
self
):
@@ -222,7 +222,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.__timer_task_timer.timeout.connect(self.pollTimerTaskQueue)
self.__timer_task_timer.start(500)
def pollTimerTaskQueue(
self
):
@@ -256,7 +255,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.__is_running_timer_task = False
pass
def setControlButtons(
self,
config_button_enabled: bool,
@@ -300,7 +298,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.__alConfigWidget.configWidgetIsClosed.disconnect(self.onConfigWidgetClosed)
self.__alConfigWidget.deleteLater()
self.__alConfigWidget = None
self.__config_paths = ConfigManager.getValidateAutomationConfigPaths()
self.__config_paths = ConfigUtils.getAutomationConfigPaths()
self.setControlButtons(True, None, None)
self._showLog("配置窗口已关闭,配置文件路径已更新")
+90 -21
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -18,6 +18,7 @@ from PySide6.QtCore import (
from base.MsgBase import MsgBase
from operators.AutoLib import AutoLib
from utils.JSONReader import JSONReader
from autoscript import createEngine
class AutoLibWorker(MsgBase, QThread):
@@ -36,7 +37,6 @@ class AutoLibWorker(MsgBase, QThread):
QThread.__init__(self)
self.__config_paths = config_paths
def checkTimeAvailable(
self,
) -> bool:
@@ -51,7 +51,6 @@ class AutoLibWorker(MsgBase, QThread):
self._showLog(f"时间检查通过, 当前时间: {current_time}", self.TraceLevel.INFO)
return True
def checkConfigPaths(
self,
) -> bool:
@@ -67,7 +66,6 @@ class AutoLibWorker(MsgBase, QThread):
self._showLog(f"配置文件路径检查通过, 路径: {self.__config_paths}", self.TraceLevel.INFO)
return True
def loadConfigs(
self
) -> bool:
@@ -76,28 +74,30 @@ class AutoLibWorker(MsgBase, QThread):
f"正在加载配置文件, 运行配置文件路径: {self.__config_paths["run"]}",
no_log=True
)
self.__run_config = JSONReader(self.__config_paths["run"]).data()
self._run_config = JSONReader(self.__config_paths["run"]).data()
self._showTrace(
f"正在加载配置文件, 用户配置文件路径: {self.__config_paths["user"]}",
no_log=True
)
self.__user_config = JSONReader(self.__config_paths["user"]).data()
if self.__run_config is None or self.__user_config is None:
self._user_config = JSONReader(self.__config_paths["user"]).data()
if self._run_config is None or self._user_config is None:
self._showTrace(
"配置文件加载失败, 请检查配置文件是否正确",
self.TraceLevel.ERROR
)
return False
if not self.__user_config.get("groups"):
if not self._user_config.get("groups"):
self._showTrace(
"用户配置文件中无有效任务组, 请检查用户配置文件是否正确",
self.TraceLevel.WARNING
)
return False
self._showLog(f"配置文件加载成功, 任务组数量: {len(self.__user_config.get('groups', []))}", self.TraceLevel.INFO)
self._showLog(
f"配置文件加载成功, 任务组数量: {len(self._user_config.get('groups', []))}",
self.TraceLevel.INFO
)
return True
def run(
self
):
@@ -115,9 +115,9 @@ class AutoLibWorker(MsgBase, QThread):
auto_lib = AutoLib(
self._input_queue,
self._output_queue,
self.__run_config
self._run_config
)
groups = self.__user_config.get("groups")
groups = self._user_config.get("groups")
for group in groups:
if not group["enabled"]:
self._showTrace(f"任务组 {group["name"]} 已跳过", no_log=True)
@@ -162,7 +162,84 @@ class TimerTaskWorker(AutoLibWorker):
):
self._showTrace(f"定时任务 {self.__timer_task['name']} 开始运行")
super().run()
if not self.checkTimeAvailable() or not self.checkConfigPaths():
self._showTrace("定时任务跳过执行 (时间或配置文件检查未通过)")
self.timerTaskWorkerIsFinished.emit(False, self.__timer_task)
return
try:
if not self.loadConfigs():
raise Exception("配置文件加载失败")
self.applyRepeatAutoScript()
auto_lib = AutoLib(
self._input_queue,
self._output_queue,
self._run_config
)
groups = self._user_config.get("groups")
for group in groups:
if not group["enabled"]:
self._showTrace(
f"任务组 {group['name']} 已跳过",
no_log=True
)
continue
self._showTrace(
f"正在运行任务组 {group['name']}",
no_log=True
)
auto_lib.run(
{"users": group.get("users", [])}
)
auto_lib.close()
except Exception as e:
self._showTrace(
f"定时任务 {self.__timer_task['name']} 运行时发生异常: {e}",
self.TraceLevel.ERROR
)
self.timerTaskWorkerIsFinished.emit(True, self.__timer_task)
return
self._showTrace(f"定时任务 {self.__timer_task['name']} 运行结束")
self.timerTaskWorkerIsFinished.emit(False, self.__timer_task)
def applyRepeatAutoScript(
self
):
auto_script = self.__timer_task.get("repeat_auto_script", "")
if not auto_script or not auto_script.strip():
return
self._showTrace(
f"检测到重复定时任务 AutoScript, 开始执行...",
no_log=True
)
groups = self._user_config.get("groups", [])
affected_count = 0
for group in groups:
if not group.get("enabled", False):
continue
for user in group.get("users", []):
try:
engine = createEngine()
engine.execute(auto_script, user)
affected_count += 1
except ValueError as e:
self._showTrace(
f"AutoScript 执行错误 (用户 {user['username']}): {e}",
self.TraceLevel.ERROR
)
self._showLog(
f"AutoScript 执行完毕, "
f"影响 {affected_count} 个用户",
self.TraceLevel.INFO
)
@Slot()
def onTimerTaskIsFinished(
self
):
self._showTrace(f"定时任务 {self.__timer_task['name']} 运行结束")
self.timerTaskWorkerIsFinished.emit(False, self.__timer_task)
@Slot()
def onTimerTaskFinishedWithError(
@@ -174,11 +251,3 @@ class TimerTaskWorker(AutoLibWorker):
self.TraceLevel.ERROR
)
self.timerTaskWorkerIsFinished.emit(True, self.__timer_task)
@Slot()
def onTimerTaskIsFinished(
self
):
self._showTrace(f"定时任务 {self.__timer_task['name']} 运行结束")
self.timerTaskWorkerIsFinished.emit(False, self.__timer_task)
+1 -3
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -63,14 +63,12 @@ class ALSeatFrame(QFrame):
self.toggleSelection()
self.clicked.emit(self.__seat_number)
def isSelected(
self
):
return self.__is_selected
def toggleSelection(self):
self.__is_selected = not self.__is_selected
+11 -14
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -8,15 +8,20 @@ You may use, modify, and distribute this file under the terms of the MIT License
See the LICENSE file for details.
"""
from PySide6.QtCore import (
Qt, Slot, Signal
)
from PySide6.QtWidgets import (
QDialog, QLabel, QHBoxLayout, QVBoxLayout,
QPushButton,
Qt,
Signal,
Slot
)
from PySide6.QtGui import (
QCloseEvent
)
from PySide6.QtWidgets import (
QDialog,
QHBoxLayout,
QLabel,
QPushButton,
QVBoxLayout
)
from gui.ALSeatMapView import ALSeatMapView
@@ -42,7 +47,6 @@ class ALSeatMapSelectDialog(QDialog):
self.setupUi()
self.connectSignals()
def setupUi(
self
):
@@ -85,7 +89,6 @@ class ALSeatMapSelectDialog(QDialog):
self.SeatMapWidgetControlLayout.addWidget(self.ConfirmButton)
self.SeatMapWidgetMainLayout.addLayout(self.SeatMapWidgetControlLayout)
def connectSignals(
self
):
@@ -93,7 +96,6 @@ class ALSeatMapSelectDialog(QDialog):
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
def showEvent(
self,
event
@@ -117,7 +119,6 @@ class ALSeatMapSelectDialog(QDialog):
return result
def closeEvent(
self,
event: QCloseEvent
@@ -131,7 +132,6 @@ class ALSeatMapSelectDialog(QDialog):
self.seatMapSelectDialogIsClosed.emit(self.getSelectedSeats())
super().closeEvent(event)
def selectSeat(
self,
seat_number: str
@@ -139,7 +139,6 @@ class ALSeatMapSelectDialog(QDialog):
self.SeatMapGraphicsView.selectSeat(seat_number)
def selectSeats(
self,
seat_numbers: list[str]
@@ -147,14 +146,12 @@ class ALSeatMapSelectDialog(QDialog):
return self.SeatMapGraphicsView.selectSeats(seat_numbers)
def getSelectedSeats(
self
) -> list[str]:
return self.SeatMapGraphicsView.getSelectedSeats()
def clearSelections(
self
):
+21 -24
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -11,11 +11,16 @@ from PySide6.QtCore import (
Qt, Slot, QEvent
)
from PySide6.QtWidgets import (
QFrame, QWidget,
QGridLayout, QGraphicsView, QGraphicsScene, QGraphicsItem
QFrame,
QWidget,
QGridLayout,
QGraphicsView,
QGraphicsScene,
QGraphicsItem
)
from PySide6.QtGui import (
QPainter, QWheelEvent
QPainter,
QWheelEvent
)
from gui.ALSeatFrame import ALSeatFrame
@@ -35,18 +40,6 @@ class ALSeatMapView(QGraphicsView):
self.setupUi()
@staticmethod
def formatSeatNumber(
seat_number: str
) -> str:
if seat_number and not seat_number[-1].isdigit():
digits = seat_number[:-1]
letter = seat_number[-1]
return digits.zfill(3) + letter
return seat_number.zfill(3)
def eventFilter(
self,
watched,
@@ -61,7 +54,6 @@ class ALSeatMapView(QGraphicsView):
return True
return super().eventFilter(watched, event)
def zoomGraphicsView(
self,
event: QWheelEvent
@@ -80,7 +72,6 @@ class ALSeatMapView(QGraphicsView):
self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
self.scale(zoom_factor, zoom_factor)
def setupUi(
self
):
@@ -100,7 +91,6 @@ class ALSeatMapView(QGraphicsView):
self.ContainerProxy = self.SeatMapGraphicsScene.addWidget(self.SeatsContainerWidget)
self.ContainerProxy.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, False)
def setupSeatMap(
self
):
@@ -125,7 +115,6 @@ class ALSeatMapView(QGraphicsView):
self.SeatsContainerLayout.setContentsMargins(20, 20, 20, 20)
self.SeatsContainerWidget.adjustSize()
def selectSeat(
self,
seat_number: str
@@ -142,7 +131,6 @@ class ALSeatMapView(QGraphicsView):
widget.toggleSelection()
self.__selected_seats.append(seat_number)
def selectSeats(
self,
selected_seats: list
@@ -152,14 +140,12 @@ class ALSeatMapView(QGraphicsView):
for seat_number in selected_seats:
self.selectSeat(seat_number)
def getSelectedSeats(
self
) -> list[str]:
return self.__selected_seats
def clearSelections(
self
):
@@ -185,4 +171,15 @@ class ALSeatMapView(QGraphicsView):
if len(self.__selected_seats) < 1:
self.__selected_seats.append(seat_number)
else:
self.__seat_frames[seat_number].toggleSelection()
self.__seat_frames[seat_number].toggleSelection()
@staticmethod
def formatSeatNumber(
seat_number: str
) -> str:
if seat_number and not seat_number[-1].isdigit():
digits = seat_number[:-1]
letter = seat_number[-1]
return digits.zfill(3) + letter
return seat_number.zfill(3)
+19 -9
View File
@@ -1,14 +1,28 @@
# -*- 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.
"""
from enum import Enum
from PySide6.QtWidgets import (
QLabel
)
from PySide6.QtCore import (
Qt, Property, QPropertyAnimation, QEasingCurve
Property,
QEasingCurve,
QPropertyAnimation,
Qt
)
from PySide6.QtGui import (
QPainter, QColor, QConicalGradient, QPalette
QColor,
QConicalGradient,
QPainter,
QPalette
)
from PySide6.QtWidgets import (
QLabel
)
@@ -36,7 +50,6 @@ class ALStatusLabel(QLabel):
self.setupUi()
def setupUi(
self
):
@@ -51,14 +64,12 @@ class ALStatusLabel(QLabel):
self.RunningAnimation.setLoopCount(-1)
self.RunningAnimation.setEasingCurve(QEasingCurve.Type.Linear)
def isDarkMode(
self
) -> bool:
return self.palette().color(QPalette.ColorRole.Window).value() < 128
def getMarkColor(
self
) -> QColor:
@@ -103,7 +114,6 @@ class ALStatusLabel(QLabel):
self.__icon_angle = value
self.update()
def paintEvent(
self,
event
+150 -20
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -12,11 +12,12 @@ import uuid
from enum import Enum
from datetime import datetime, timedelta
from PySide6.QtCore import Slot, QDateTime
from PySide6.QtWidgets import QLabel, QDialog, QWidget, QSpinBox, QHBoxLayout, QGridLayout, QDateTimeEdit
from PySide6.QtCore import Slot, QDateTime, QUrl
from PySide6.QtGui import QDesktopServices
from PySide6.QtWidgets import QLabel, QDialog, QWidget, QSpinBox, QHBoxLayout, QVBoxLayout, QGridLayout, QDateTimeEdit, QGroupBox, QPushButton
from gui.resources.ui.Ui_ALTimerTaskAddDialog import Ui_ALTimerTaskAddDialog
import utils.TimerUtils as TimerUtils
from utils.TimerUtils import TimerUtils
class ALTimerTaskStatus(Enum):
@@ -34,15 +35,19 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
def __init__(
self,
parent = None
parent = None,
timer_task: dict = None
):
super().__init__(parent)
self.__edit_timer_task = timer_task
self.setupUi(self)
self.modifyUi()
self.connectSignals()
if self.__edit_timer_task:
self.loadTask(self.__edit_timer_task)
def modifyUi(
self
@@ -51,6 +56,8 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.TimerTypeComboBox.setCurrentIndex(0)
self.SpecificTimerWidget = QWidget()
self.SpecificTimerLayout = QHBoxLayout(self.SpecificTimerWidget)
self.SpecificTimerLayout.setContentsMargins(0, 0, 0, 0)
self.SpecificTimerLayout.setSpacing(5)
self.SpecificTimerLayout.addWidget(QLabel("定时时间:"))
self.SpecificDateTimeEdit = QDateTimeEdit()
self.SpecificDateTimeEdit.setCalendarPopup(True)
@@ -62,6 +69,8 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.RelativeTimerWidget = QWidget()
self.RelativeTimerLayout = QHBoxLayout(self.RelativeTimerWidget)
self.RelativeTimerLayout.setContentsMargins(0, 0, 0, 0)
self.RelativeTimerLayout.setSpacing(5)
self.RelativeTimerLayout.addWidget(QLabel("相对时间:"))
self.RelativeDaySpinBox = QSpinBox()
self.RelativeDaySpinBox.setMinimum(0)
@@ -86,6 +95,82 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.TimerConfigLayout.addWidget(self.RelativeTimerWidget)
self.RelativeTimerWidget.setVisible(False)
self.AutoScriptGroupBox = QGroupBox("AutoScript 指令")
self.AutoScriptLayout = QVBoxLayout(self.AutoScriptGroupBox)
self.AutoScriptLayout.setContentsMargins(3, 3, 3, 3)
self.AutoScriptLayout.setSpacing(3)
autoScriptBtnLayout = QHBoxLayout()
self.AutoScriptEditButton = QPushButton("编辑")
self.AutoScriptEditButton.setMinimumHeight(25)
self.AutoScriptEditButton.setFixedWidth(80)
autoScriptBtnLayout.addWidget(self.AutoScriptEditButton)
autoScriptBtnLayout.addStretch()
self.AutoScriptHelpButton = QPushButton("?")
self.AutoScriptHelpButton.setFixedSize(20, 20)
self.AutoScriptHelpButton.setToolTip(
"AutoScript 是一种轻量级 DSL 语言,基于 Lua 实现。\n"
"用于在重复定时任务执行前,对用户的预约数据进行预处理\n"
"\n"
"点击查看完整在线文档"
)
self.AutoScriptHelpButton.setStyleSheet(
"QPushButton { border-radius: 10px; border: 1px solid #999; "
"font-weight: bold; color: #555; }"
"QPushButton:hover { background-color: #E0E0E0; }"
)
autoScriptBtnLayout.addWidget(self.AutoScriptHelpButton)
self.AutoScriptStatusLabel = QLabel("未设置")
self.AutoScriptStatusLabel.setStyleSheet("color: #969696;")
self.AutoScriptStatusLabel.setFixedHeight(25)
autoScriptBtnLayout.addWidget(self.AutoScriptStatusLabel)
self.AutoScriptLayout.addLayout(autoScriptBtnLayout)
self.ALAddTimerTaskLayout.insertWidget(
self.ALAddTimerTaskLayout.indexOf(self.TaskConfigGroupBox) + 1,
self.AutoScriptGroupBox
)
self.AutoScriptGroupBox.setVisible(False)
self.__auto_script = ""
self.__mock_target_data = None
def loadTask(
self,
task: dict
):
self.TaskNameLineEdit.setText(task.get("name", ""))
time_type = task.get("time_type", "特定时间")
self.TimerTypeComboBox.setCurrentText(time_type)
self.SpecificDateTimeEdit.setDateTime(
QDateTime(task["execute_time"])
)
self.RelativeDaySpinBox.setValue(0)
self.RelativeHourSpinBox.setValue(0)
self.RelativeMinuteSpinBox.setValue(0)
self.RelativeSecondSpinBox.setValue(0)
if task.get("silent", False):
self.SilentlyRunRadioButton.setChecked(True)
else:
self.ShowBeforeRunRadioButton.setChecked(True)
repeat = task.get("repeat", False)
self.RepeatCheckBox.setChecked(repeat)
if repeat:
repeat_days = task.get("repeat_days", [])
self.MonCheckBox.setChecked(0 in repeat_days)
self.TueCheckBox.setChecked(1 in repeat_days)
self.WedCheckBox.setChecked(2 in repeat_days)
self.ThuCheckBox.setChecked(3 in repeat_days)
self.FriCheckBox.setChecked(4 in repeat_days)
self.SatCheckBox.setChecked(5 in repeat_days)
self.SunCheckBox.setChecked(6 in repeat_days)
auto_script = task.get("repeat_auto_script", "")
if auto_script:
self.__auto_script = auto_script
self.AutoScriptStatusLabel.setText("已设置")
self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;")
mock_data = task.get("mock_target_data")
if mock_data:
self.__mock_target_data = mock_data
self.ConfirmButton.setText("保存")
def connectSignals(
self
@@ -95,7 +180,8 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.ConfirmButton.clicked.connect(self.accept)
self.TimerTypeComboBox.currentIndexChanged.connect(self.onTimerTypeComboBoxIndexChanged)
self.RepeatCheckBox.toggled.connect(self.onRepeatCheckBoxToggled)
self.AutoScriptEditButton.clicked.connect(self.onPreviewAutoScript)
self.AutoScriptHelpButton.clicked.connect(self.onAutoScriptHelp)
def getTimerTask(
self
@@ -119,18 +205,36 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
minutes = self.RelativeMinuteSpinBox.value(),
seconds = self.RelativeSecondSpinBox.value()
)
task_data = {
"name": name,
"uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}",
"time_type": self.TimerTypeComboBox.currentText(),
"execute_time": execute_time,
"silent": silent,
"added_time": added_time,
"status": ALTimerTaskStatus.PENDING,
"executed": False,
"repeat": self.RepeatCheckBox.isChecked(),
}
if task_data["repeat"]:
if self.__edit_timer_task:
task_data = dict(self.__edit_timer_task)
task_data["name"] = name
task_data["execute_time"] = execute_time
task_data["silent"] = silent
task_data["status"] = ALTimerTaskStatus.PENDING
task_data["executed"] = False
task_data["repeat_auto_script"] = self.__auto_script
task_data["mock_target_data"] = self.__mock_target_data
else:
task_data = {
"name": name,
"uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}",
"time_type": self.TimerTypeComboBox.currentText(),
"execute_time": execute_time,
"silent": silent,
"added_time": added_time,
"status": ALTimerTaskStatus.PENDING,
"executed": False,
"repeat": self.RepeatCheckBox.isChecked(),
"repeat_auto_script": self.__auto_script,
"mock_target_data": self.__mock_target_data,
}
repeat = self.RepeatCheckBox.isChecked()
task_data["repeat"] = repeat
if repeat:
if "repeat_history" not in task_data:
task_data["repeat_history"] = []
repeat_days = []
if self.MonCheckBox.isChecked():
repeat_days.append(0)
@@ -152,7 +256,7 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
task_data["repeat_hour"] = execute_time.hour
task_data["repeat_minute"] = execute_time.minute
task_data["repeat_second"] = execute_time.second
task_data["execute_time"] = TimerUtils.calculateNextRepeatTime(
task_data["execute_time"] = TimerUtils.getNextTimerRepeatTime(
task_data["repeat_days"],
task_data["repeat_hour"],
task_data["repeat_minute"],
@@ -181,4 +285,30 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.ThuCheckBox.setEnabled(checked)
self.FriCheckBox.setEnabled(checked)
self.SatCheckBox.setEnabled(checked)
self.SunCheckBox.setEnabled(checked)
self.SunCheckBox.setEnabled(checked)
self.AutoScriptGroupBox.setVisible(checked)
@Slot()
def onPreviewAutoScript(self):
from gui.ALAutoScriptEditDialog import ALAutoScriptEditDialog
dlg = ALAutoScriptEditDialog(self, self.__auto_script, self.__mock_target_data)
if dlg.exec() == QDialog.DialogCode.Accepted:
script = dlg.getScript()
self.__auto_script = script
self.__mock_target_data = dlg.getMockData()
if script:
self.AutoScriptStatusLabel.setText("已设置")
self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;")
else:
self.AutoScriptStatusLabel.setText("未设置")
self.AutoScriptStatusLabel.setStyleSheet("color: #969696;")
dlg.deleteLater()
@Slot()
def onAutoScriptHelp(
self
):
QDesktopServices.openUrl(
QUrl("https://www.autolibrary.kenanzhu.com/manuals/autoscript")
)
+9 -15
View File
@@ -28,15 +28,13 @@ class ALTimerTaskHistoryDialog(QDialog):
):
super().__init__(parent)
self.__task_data = task_data
self.__history = task_data.get("history", [])
self.__history = task_data.get("repeat_history", [])
self.modifyUi()
self.setupUi()
self.connectSignals()
def modifyUi(
def setupUi(
self
):
@@ -83,7 +81,6 @@ class ALTimerTaskHistoryDialog(QDialog):
ButtonLayout.addWidget(self.CloseButton)
MainLayout.addLayout(ButtonLayout)
def connectSignals(
self
):
@@ -91,7 +88,6 @@ class ALTimerTaskHistoryDialog(QDialog):
self.CloseButton.clicked.connect(self.accept)
self.ClearHistoryButton.clicked.connect(self.onClearHistoryButtonClicked)
def loadHistory(
self
):
@@ -100,6 +96,11 @@ class ALTimerTaskHistoryDialog(QDialog):
for row, record in enumerate(self.__history):
self.addHistoryRow(row, record)
def getHistory(
self
) -> list:
return self.__history
def addHistoryRow(
self,
@@ -137,11 +138,4 @@ class ALTimerTaskHistoryDialog(QDialog):
self.__history.clear()
self.HistoryTableWidget.setRowCount(0)
self.__task_data["history"] = self.__history
def getHistory(
self
) -> list:
return self.__history
self.__task_data["repeat_history"] = self.__history
+82 -41
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -15,26 +15,46 @@ from enum import Enum
from datetime import datetime, timedelta
from PySide6.QtCore import (
Qt, Signal, Slot, QTimer
)
from PySide6.QtWidgets import (
QDialog, QWidget, QListWidgetItem, QMessageBox,
QHBoxLayout, QVBoxLayout, QLabel, QPushButton
QTimer,
Qt,
Signal,
Slot
)
from PySide6.QtGui import (
QAction,
QCloseEvent
)
from PySide6.QtWidgets import (
QDialog,
QHBoxLayout,
QLabel,
QListWidgetItem,
QMenu,
QMessageBox,
QPushButton,
QVBoxLayout,
QWidget
)
import managers.config.ConfigManager as ConfigManager
import utils.TimerUtils as TimerUtils
from gui.resources.ui.Ui_ALTimerTaskManageWidget import Ui_ALTimerTaskManageWidget
from gui.ALTimerTaskAddDialog import ALTimerTaskAddDialog, ALTimerTaskStatus
from gui.ALTimerTaskAddDialog import (
ALTimerTaskAddDialog,
ALTimerTaskStatus
)
from gui.ALTimerTaskHistoryDialog import ALTimerTaskHistoryDialog
from gui.resources.ui.Ui_ALTimerTaskManageWidget import Ui_ALTimerTaskManageWidget
from interfaces.ConfigProvider import (
CfgKey,
ConfigProvider
)
from utils.TimerUtils import TimerUtils
class ALTimerTaskItemWidget(QWidget):
editRequested = Signal(dict)
def __init__(
self,
parent = None,
@@ -43,9 +63,11 @@ class ALTimerTaskItemWidget(QWidget):
super().__init__(parent)
self.__timer_task = timer_task
self.__manage_widget = parent
self.modifyUi()
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.customContextMenuRequested.connect(self.showContextMenu)
def modifyUi(
self
@@ -145,6 +167,27 @@ class ALTimerTaskItemWidget(QWidget):
self.DeleteButton.setEnabled(False)
self.setFixedHeight(55)
@Slot(object)
def showContextMenu(
self,
pos
):
menu = QMenu(self)
edit_action = QAction("编辑", self)
edit_action.triggered.connect(
lambda: self.editRequested.emit(self.__timer_task)
)
menu.addAction(edit_action)
if self.__timer_task["status"] != ALTimerTaskStatus.RUNNING\
and self.__timer_task["status"] != ALTimerTaskStatus.READY:
delete_action = QAction("删除", self)
delete_action.triggered.connect(
lambda: self.__manage_widget.deleteTask(self.__timer_task)
)
menu.addAction(delete_action)
menu.exec(self.mapToGlobal(pos))
class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
@@ -164,7 +207,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
):
super().__init__(parent)
self.__cfg_mgr = ConfigManager.instance()
self.__cfg_mgr: ConfigProvider = ConfigManager.instance()
self.__timer_tasks = []
self.__check_timer = None
self.__sort_policy = self.SortPolicy.BY_EXECUTE_TIME
@@ -176,7 +219,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
if not self.initializeTimerTasks():
raise Exception("定时任务配置文件初始化失败 !")
def connectSignals(
self
):
@@ -187,7 +229,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self.TimerTaskSortOrderToggleButton.clicked.connect(self.onSortOrderToggleButtonClicked)
self.timerTasksChanged.connect(self.onTimerTasksChanged)
def setupTimer(
self
):
@@ -196,7 +237,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self.__check_timer.timeout.connect(self.checkTasks)
self.__check_timer.start(500)
def initializeTimerTasks(
self
) -> bool:
@@ -212,20 +252,19 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
return True
return False
def getTimerTasks(
self
) -> list:
try:
timer_tasks = self.__cfg_mgr.get(ConfigManager.ConfigType.TIMERTASK)
timer_tasks = self.__cfg_mgr.get(CfgKey.TIMERTASK.ROOT)
if timer_tasks and "timer_tasks" in timer_tasks:
for task in timer_tasks["timer_tasks"]:
task["added_time"] = datetime.strptime(task["added_time"], "%Y-%m-%d %H:%M:%S")
task["execute_time"] = datetime.strptime(task["execute_time"], "%Y-%m-%d %H:%M:%S")
task["status"] = ALTimerTaskStatus(task["status"])
if "history" in task:
for item in task["history"]:
if "repeat_history" in task:
for item in task["repeat_history"]:
item["result"] = ALTimerTaskStatus(item["result"])
return timer_tasks["timer_tasks"]
raise Exception("定时任务配置文件格式错误")
@@ -237,7 +276,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
)
return None
def setTimerTasks(
self,
timer_tasks: list
@@ -248,10 +286,10 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
task["added_time"] = task["added_time"].strftime("%Y-%m-%d %H:%M:%S")
task["execute_time"] = task["execute_time"].strftime("%Y-%m-%d %H:%M:%S")
task["status"] = task["status"].value
if "history" in task:
for item in task["history"]:
if "repeat_history" in task:
for item in task["repeat_history"]:
item["result"] = item["result"].value
self.__cfg_mgr.set(ConfigManager.ConfigType.TIMERTASK, "", { "timer_tasks": timer_tasks })
self.__cfg_mgr.set(CfgKey.TIMERTASK.ROOT, { "timer_tasks": timer_tasks })
return True
except Exception as e:
QMessageBox.warning(
@@ -261,7 +299,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
)
return False
def showEvent(
self,
event
@@ -285,7 +322,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
return result
def closeEvent(
self,
event: QCloseEvent
@@ -295,7 +331,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self.timerTaskManageWidgetIsClosed.emit()
event.ignore()
def sortTimerTasks(
self,
policy: SortPolicy = SortPolicy.BY_EXECUTE_TIME,
@@ -318,7 +353,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
reverse = order is Qt.SortOrder.DescendingOrder
)
def updateStat(
self
):
@@ -345,7 +379,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self.ExecutedTaskLabel.setText(f"已执行:{executed}")
self.InvalidTaskLabel.setText(f"无效的:{invalid}")
def updateTimerTaskList(
self
):
@@ -363,11 +396,11 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
widget.HistoryButton.clicked.connect(
lambda _, task = timer_task: self.showTaskHistory(task)
)
widget.editRequested.connect(self.editTask)
item.setSizeHint(widget.size())
self.TimerTasksListWidget.addItem(item)
self.TimerTasksListWidget.setItemWidget(item, widget)
def addTask(
self
):
@@ -378,15 +411,29 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self.__timer_tasks.append(timer_task)
self.timerTasksChanged.emit()
def editTask(
self,
timer_task: dict
):
dialog = ALTimerTaskAddDialog(self, timer_task)
if dialog.exec() == QDialog.DialogCode.Accepted:
updated = dialog.getTimerTask()
for i, task in enumerate(self.__timer_tasks):
if task["uuid"] == updated["uuid"]:
self.__timer_tasks[i] = updated
break
self.timerTasksChanged.emit()
@staticmethod
def getTimerTaskDetailMessage(
timer_task: dict
):
if "history" not in timer_task:
if "repeat_history" not in timer_task:
history = []
else:
history = timer_task["history"]
history = timer_task["repeat_history"]
history_count = len(history)
return (
f"任务名称:{timer_task["name"]}\n"
@@ -396,7 +443,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
f"已记录次数:{history_count}"
)
def deleteTask(
self,
timer_task: dict
@@ -425,7 +471,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
]
self.timerTasksChanged.emit()
def clearAllTasks(
self
):
@@ -487,7 +532,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self.__timer_tasks.clear()
self.timerTasksChanged.emit()
def showTaskHistory(
self,
task: dict
@@ -497,7 +541,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
if dialog.exec() == QDialog.DialogCode.Accepted:
self.timerTasksChanged.emit()
def checkTasks(
self
):
@@ -559,7 +602,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self.updateTimerTaskList()
self.updateStat()
@Slot(dict)
def onTimerTaskIsRunning(
self,
@@ -572,7 +614,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
break
self.timerTasksChanged.emit()
def onRepeatTimerTaskIs(
self,
status: ALTimerTaskStatus,
@@ -584,12 +625,12 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
ALTimerTaskStatus.OUTDATED}
if status not in valid_statuses:
return timer_task
if "history" not in timer_task:
timer_task["history"] = []
if "repeat_history" not in timer_task:
timer_task["repeat_history"] = []
if status != ALTimerTaskStatus.OUTDATED:
executed_time = datetime.now()
duration = (executed_time - timer_task["execute_time"]).total_seconds()
timer_task["history"].append({
timer_task["repeat_history"].append({
"execute_time": timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S"),
"executed_time": executed_time.strftime("%Y-%m-%d %H:%M:%S"),
"result": status,
@@ -603,14 +644,14 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
delta_days = (current_time - execute_time).days
for i in range(delta_days + 1):
if (execute_weekday + i)%7 in timer_task["repeat_days"]:
timer_task["history"].append({
timer_task["repeat_history"].append({
"execute_time": (execute_time + timedelta(days=i)).strftime("%Y-%m-%d %H:%M:%S"),
"executed_time": (execute_time + timedelta(days=i)).strftime("%Y-%m-%d %H:%M:%S"),
"result": status,
"duration": 0,
"uuid": timer_task["uuid"]
})
next_time = TimerUtils.calculateNextRepeatTime(
next_time = TimerUtils.getNextTimerRepeatTime(
timer_task["repeat_days"],
timer_task["repeat_hour"],
timer_task["repeat_minute"],
+13 -11
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -10,14 +10,22 @@ See the LICENSE file for details.
from enum import Enum
from PySide6.QtCore import (
Qt, QSize, QCoreApplication, QRect, QPoint
Qt,
QSize,
QCoreApplication,
QRect,
QPoint
)
from PySide6.QtWidgets import (
QAbstractScrollArea, QAbstractItemView,
QTreeWidget, QTreeWidgetItem
QAbstractScrollArea,
QAbstractItemView,
QTreeWidget,
QTreeWidgetItem
)
from PySide6.QtGui import (
QDragEnterEvent, QDragMoveEvent, QDropEvent
QDragEnterEvent,
QDragMoveEvent,
QDropEvent
)
@@ -39,7 +47,6 @@ class ALUserTreeWidget(QTreeWidget):
self.setupUi()
self.translateUi()
def setupUi(
self
):
@@ -70,7 +77,6 @@ class ALUserTreeWidget(QTreeWidget):
self.header().setHighlightSections(False)
self.header().setProperty(u"showSortIndicator", True)
def translateUi(
self
):
@@ -78,7 +84,6 @@ class ALUserTreeWidget(QTreeWidget):
___qtreewidgetitem = self.headerItem()
___qtreewidgetitem.setText(1, QCoreApplication.translate("ALConfigWidget", u"\u72b6\u6001", None));
@staticmethod
def isDragPositionValid(
target_rect: QRect,
@@ -90,7 +95,6 @@ class ALUserTreeWidget(QTreeWidget):
y_offset < target_rect.height()*0.8)
return valid
def dragEnterEvent(
self,
event: QDragEnterEvent
@@ -98,7 +102,6 @@ class ALUserTreeWidget(QTreeWidget):
super().dragEnterEvent(event)
def dragMoveEvent(
self,
event: QDragMoveEvent
@@ -136,7 +139,6 @@ class ALUserTreeWidget(QTreeWidget):
return
event.acceptProposedAction()
def dropEvent(
self,
event: QDropEvent
+3 -3
View File
@@ -5,11 +5,11 @@
workflow process. Do not edit manually.
This file is auto-generated during the workflow process.
Last updated: 2026-03-22 14:14:19 UTC
Last updated: 2026-05-09 06:05:13 UTC
"""
AL_VERSION = "1.2.1"
AL_TAG = "v1.2.1"
AL_VERSION = "1.3.0"
AL_TAG = "v1.3.0"
AL_COMMIT_SHA = "local"
AL_COMMIT_DATE = "null" # time zone : UTC
AL_BUILD_DATE = "null" # time zone : UTC
+137 -140
View File
@@ -11,20 +11,30 @@ import threading
from typing import Optional
from PySide6.QtCore import (
Qt, Slot, QThread, Signal
Qt,
Slot,
QThread,
Signal
)
from PySide6.QtWidgets import (
QDialog, QLabel, QComboBox, QProgressBar,
QPushButton, QVBoxLayout, QHBoxLayout,
QMessageBox, QFrame, QLineEdit
)
from PySide6.QtGui import (
QCloseEvent
QDialog,
QLabel,
QComboBox,
QProgressBar,
QPushButton,
QVBoxLayout,
QHBoxLayout,
QMessageBox,
QFrame,
QLineEdit
)
from PySide6.QtGui import QCloseEvent
from managers.driver.WebDriverManager import (
instance as webdriver_manager_instance,
WebDriverManager, WebDriverInfo, WebDriverType,
instance as webdriverInstance,
WebDriverManager,
WebDriverInfo,
WebDriverType,
WebDriverStatus
)
from gui.ALStatusLabel import ALStatusLabel
@@ -142,7 +152,6 @@ class ALWebDriverDownloadDialog(QDialog):
self.initializeDriverManager()
self.refreshDriverList()
def showEvent(
self,
event
@@ -165,7 +174,6 @@ class ALWebDriverDownloadDialog(QDialog):
self.move(target_pos)
return result
def setupUi(
self
):
@@ -237,7 +245,6 @@ class ALWebDriverDownloadDialog(QDialog):
self.ControlLayout.addWidget(self.ConfirmButton)
self.MainLayout.addLayout(self.ControlLayout)
def connectSignals(
self
):
@@ -249,18 +256,16 @@ class ALWebDriverDownloadDialog(QDialog):
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
self.DriverComboBox.currentIndexChanged.connect(self.onDriverComboBoxChanged)
def initializeDriverManager(
self
):
try:
self.__driver_manager = webdriver_manager_instance(self.__driver_dir)
self.__driver_manager = webdriverInstance(self.__driver_dir)
except ValueError as e:
QMessageBox.warning(self, "初始化失败", f"WebDriverManager 初始化失败:\n{str(e)}")
self.reject()
def refreshDriverList(
self
):
@@ -270,18 +275,20 @@ class ALWebDriverDownloadDialog(QDialog):
self.__driver_manager.refresh()
self.__driver_infos = self.__driver_manager.getDriverInfos()
self.DriverComboBox.clear()
installed = 0
installed_idx = 0
for i, driver_info in enumerate(self.__driver_infos):
if driver_info.driver_status == WebDriverStatus.INSTALLED:
installed_idx = i # get the installed driver index
display_text = f"{driver_info.driver_type.value} - {driver_info.browser_version}"
if driver_info.driver_status == WebDriverStatus.INSTALLED:
installed += 1
installed_idx = i # get the installed driver index
display_text += " : 已安装"
self.DriverComboBox.addItem(display_text)
count = len(self.__driver_infos)
self.BrowserCountLabel.setText(f"检测到 {count} 个可用浏览器:")
self.BrowserCountLabel.setText(f"检测到 {count} 个可用浏览器,{installed} 个已安装驱动:")
if self.__driver_infos:
self.DriverComboBox.setCurrentIndex(installed_idx)
def onDriverComboBoxChanged(
self,
index: int
@@ -294,6 +301,116 @@ class ALWebDriverDownloadDialog(QDialog):
self.updateProgressBarStates(driver_info)
self.updateButtonStates(driver_info)
def closeEvent(
self,
event: QCloseEvent
):
if self.__download_thread and self.__download_thread.isRunning():
reply = QMessageBox.question(
self,
"确认关闭 - AutoLibrary",
"驱动正在下载中, 确定要取消并关闭对话框吗 ?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.No:
event.ignore()
return
self.__download_thread.stop()
if not self.__confirmed:
self.__selected_driver_info = None
event.accept()
super().closeEvent(event)
def onThreadFinished(
self
):
if self.__download_thread:
self.__download_thread.deleteLater()
self.__download_thread = None
def getSelectedDriverInfo(
self
) -> Optional[WebDriverInfo]:
return self.__selected_driver_info
def updateDriverInfoDisplay(
self,
driver_info: WebDriverInfo
):
if driver_info.driver_type == WebDriverType.CHROME:
driver_type = "Google Chrome"
elif driver_info.driver_type == WebDriverType.FIREFOX:
driver_type = "Mozilla Firefox"
elif driver_info.driver_type == WebDriverType.EDGE:
driver_type = "Microsoft Edge"
else:
driver_type = "未知"
self.BrowserTypeLabel.setText(f"类型:{driver_type}")
self.VersionLabel.setText(f"版本:{driver_info.driver_version}")
if driver_info.driver_path:
self.PathLabel.setText(str(driver_info.driver_path))
else:
self.PathLabel.setText("未安装")
match driver_info.driver_status:
case WebDriverStatus.NOT_INSTALLED:
self.StatusLabel.status = ALStatusLabel.Status.WAITING
case WebDriverStatus.INSTALLED:
self.StatusLabel.status = ALStatusLabel.Status.SUCCESS
case WebDriverStatus.DOWNLOADING:
self.StatusLabel.status = ALStatusLabel.Status.RUNNING
case WebDriverStatus.ERROR:
self.StatusLabel.status = ALStatusLabel.Status.FAILURE
def updateProgressBarStates(
self,
driver_info: WebDriverInfo
):
if driver_info.driver_status == WebDriverStatus.NOT_INSTALLED:
self.ProgressBar.setValue(0)
self.ProgressText.setText("未安装")
elif driver_info.driver_status == WebDriverStatus.INSTALLED:
self.ProgressBar.setValue(100)
self.ProgressText.setText("已安装")
elif driver_info.driver_status == WebDriverStatus.DOWNLOADING:
pass # update by worker thread
elif driver_info.driver_status == WebDriverStatus.ERROR:
self.ProgressBar.setValue(0)
self.ProgressText.setText("下载失败")
def updateButtonStates(
self,
driver_info: WebDriverInfo
):
if driver_info.driver_status == WebDriverStatus.NOT_INSTALLED:
self.RefreshButton.setEnabled(True)
self.DeleteButton.setEnabled(False)
self.DownloadButton.setEnabled(True)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(False)
elif driver_info.driver_status == WebDriverStatus.INSTALLED:
self.RefreshButton.setEnabled(True)
self.DownloadButton.setEnabled(False)
self.DeleteButton.setEnabled(True)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(True)
elif driver_info.driver_status == WebDriverStatus.DOWNLOADING:
self.RefreshButton.setEnabled(False)
self.DownloadButton.setEnabled(False)
self.DeleteButton.setEnabled(False)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(False)
elif driver_info.driver_status == WebDriverStatus.ERROR:
self.RefreshButton.setEnabled(True)
self.DownloadButton.setEnabled(True)
self.DeleteButton.setEnabled(False)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(False)
@Slot()
def onRefreshButtonClicked(
@@ -302,7 +419,6 @@ class ALWebDriverDownloadDialog(QDialog):
self.refreshDriverList()
@Slot()
def onDeleteButtonClicked(
self
@@ -355,7 +471,7 @@ class ALWebDriverDownloadDialog(QDialog):
self.__download_thread.downloadFinished.connect(self.onDownloadFinished)
self.__download_thread.downloadError.connect(self.onDownloadError)
self.__download_thread.downloadCancelled.connect(self.onDownloadCancelled)
self.__download_thread.finished.connect(self.__onThreadFinished)
self.__download_thread.finished.connect(self.onThreadFinished)
self.__download_thread.start()
@Slot()
@@ -406,7 +522,6 @@ class ALWebDriverDownloadDialog(QDialog):
self.updateButtonStates(driver_info)
QMessageBox.critical(self, "下载失败 - AutoLibrary", error_message)
@Slot()
def onDownloadCancelled(
self
@@ -423,7 +538,6 @@ class ALWebDriverDownloadDialog(QDialog):
self.updateButtonStates(driver_info)
self.ProgressText.setText("下载已取消")
@Slot()
def onConfirmButtonClicked(
self
@@ -439,7 +553,6 @@ class ALWebDriverDownloadDialog(QDialog):
self.__confirmed = True
self.accept()
@Slot()
def onCancelButtonClicked(
self
@@ -458,119 +571,3 @@ class ALWebDriverDownloadDialog(QDialog):
self.__confirmed = False
self.__selected_driver_info = None
self.reject()
def closeEvent(
self,
event: QCloseEvent
):
if self.__download_thread and self.__download_thread.isRunning():
reply = QMessageBox.question(
self,
"确认关闭 - AutoLibrary",
"驱动正在下载中, 确定要取消并关闭对话框吗 ?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.No:
event.ignore()
return
self.__download_thread.stop()
if not self.__confirmed:
self.__selected_driver_info = None
event.accept()
super().closeEvent(event)
def __onThreadFinished(
self
):
if self.__download_thread:
self.__download_thread.deleteLater()
self.__download_thread = None
def getSelectedDriverInfo(
self
) -> Optional[WebDriverInfo]:
return self.__selected_driver_info
def updateDriverInfoDisplay(
self,
driver_info: WebDriverInfo
):
if driver_info.driver_type == WebDriverType.CHROME:
driver_type = "Google Chrome"
elif driver_info.driver_type == WebDriverType.FIREFOX:
driver_type = "Mozilla Firefox"
elif driver_info.driver_type == WebDriverType.EDGE:
driver_type = "Microsoft Edge"
else:
driver_type = "未知"
self.BrowserTypeLabel.setText(f"类型:{driver_type}")
self.VersionLabel.setText(f"版本:{driver_info.driver_version}")
if driver_info.driver_path:
self.PathLabel.setText(str(driver_info.driver_path))
else:
self.PathLabel.setText("未安装")
match driver_info.driver_status:
case WebDriverStatus.NOT_INSTALLED:
self.StatusLabel.status = ALStatusLabel.Status.WAITING
case WebDriverStatus.INSTALLED:
self.StatusLabel.status = ALStatusLabel.Status.SUCCESS
case WebDriverStatus.DOWNLOADING:
self.StatusLabel.status = ALStatusLabel.Status.RUNNING
case WebDriverStatus.ERROR:
self.StatusLabel.status = ALStatusLabel.Status.FAILURE
def updateProgressBarStates(
self,
driver_info: WebDriverInfo
):
if driver_info.driver_status == WebDriverStatus.NOT_INSTALLED:
self.ProgressBar.setValue(0)
self.ProgressText.setText("未安装")
elif driver_info.driver_status == WebDriverStatus.INSTALLED:
self.ProgressBar.setValue(100)
self.ProgressText.setText("已安装")
elif driver_info.driver_status == WebDriverStatus.DOWNLOADING:
pass # update by worker thread
elif driver_info.driver_status == WebDriverStatus.ERROR:
self.ProgressBar.setValue(0)
self.ProgressText.setText("下载失败")
def updateButtonStates(
self,
driver_info: WebDriverInfo
):
if driver_info.driver_status == WebDriverStatus.NOT_INSTALLED:
self.RefreshButton.setEnabled(True)
self.DeleteButton.setEnabled(False)
self.DownloadButton.setEnabled(True)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(False)
elif driver_info.driver_status == WebDriverStatus.INSTALLED:
self.RefreshButton.setEnabled(True)
self.DownloadButton.setEnabled(False)
self.DeleteButton.setEnabled(True)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(True)
elif driver_info.driver_status == WebDriverStatus.DOWNLOADING:
self.RefreshButton.setEnabled(False)
self.DownloadButton.setEnabled(False)
self.DeleteButton.setEnabled(False)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(False)
elif driver_info.driver_status == WebDriverStatus.ERROR:
self.RefreshButton.setEnabled(True)
self.DownloadButton.setEnabled(True)
self.DeleteButton.setEnabled(False)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(False)
+1
View File
@@ -10,6 +10,7 @@
- ALSeatMapTable: Seat map table class.
- ALSeatMapSelectDialog: Seat map select dialog class.
- ALTimerTaskAddDialog: Timer task add dialog class.
- ALAutoScriptOrchDialog: AutoScript orchestration dialog class.
- ALTimerTaskHistoryDialog: Timer task history dialog class.
- ALTimerTaskManageWidget: Timer task manage widget class.
- ALUserTreeWidget: User tree widget class.
+7 -4
View File
@@ -1,8 +1,11 @@
<RCC>
<qresource prefix="/res/icon">
<file>icons/AutoLibrary_32x32.ico</file>
</qresource>
<qresource prefix="/res/trans">
<qresource prefix="/res">
<file>icons/AutoLibrary_Logo_64.svg</file>
<file>icons/AutoLibrary_Logo_128.svg</file>
<file>icons/Copy.svg</file>
<file>icons/Reset.svg</file>
<file>translators/qtbase_zh_CN.qm</file>
</qresource>
</RCC>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 785 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="128px" height="128px" viewBox="0 0 128 128" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,36.862746%,56.862748%);fill-opacity:1;" d="M 14.609375 17.722656 C 14.691406 17.722656 14.773438 17.722656 14.855469 17.722656 C 15.125 17.722656 15.394531 17.722656 15.664062 17.726562 C 15.855469 17.726562 16.046875 17.730469 16.238281 17.730469 C 16.824219 17.734375 17.414062 17.742188 18 17.75 C 18.085938 17.75 18.171875 17.753906 18.261719 17.753906 C 29.371094 17.914062 40.535156 19.359375 57.875 27.625 C 57.957031 27.679688 58.039062 27.734375 58.125 27.789062 C 58.90625 28.308594 59.65625 28.855469 60.375 29.460938 C 60.671875 29.65625 60.898438 29.71875 61.25 29.75 C 61.714844 29.507812 62.042969 29.214844 62.402344 28.835938 C 62.839844 28.398438 63.300781 28.023438 63.796875 27.65625 C 63.882812 27.59375 63.96875 27.527344 64.058594 27.460938 C 69.78125 23.273438 76.863281 20.476562 83.71875 18.8125 C 83.808594 18.789062 83.894531 18.769531 83.988281 18.746094 C 85.8125 18.308594 85.8125 18.308594 86.625 18.25 C 86.667969 18.292969 86.707031 18.332031 86.75 18.375 C 86.808594 18.292969 86.867188 18.210938 86.925781 18.128906 C 87.410156 17.75 87.847656 17.804688 88.449219 17.8125 C 88.636719 17.8125 88.636719 17.8125 88.824219 17.8125 C 89.238281 17.808594 89.652344 17.8125 90.0625 17.8125 C 90.359375 17.8125 90.65625 17.8125 90.953125 17.8125 C 91.671875 17.8125 92.394531 17.8125 93.117188 17.816406 C 93.957031 17.816406 94.796875 17.816406 95.636719 17.816406 C 97.132812 17.816406 98.632812 17.820312 100.132812 17.824219 C 101.585938 17.828125 103.039062 17.828125 104.492188 17.828125 C 104.582031 17.828125 104.671875 17.828125 104.765625 17.828125 C 104.855469 17.828125 104.945312 17.828125 105.035156 17.828125 C 106.691406 17.828125 108.347656 17.828125 110 17.832031 C 110.585938 17.832031 111.171875 17.832031 111.757812 17.832031 C 112.546875 17.832031 113.335938 17.835938 114.121094 17.835938 C 114.410156 17.839844 114.703125 17.839844 114.992188 17.839844 C 115.386719 17.839844 115.78125 17.839844 116.175781 17.839844 C 116.347656 17.839844 116.347656 17.839844 116.523438 17.839844 C 116.628906 17.839844 116.734375 17.84375 116.84375 17.84375 C 116.933594 17.84375 117.027344 17.84375 117.121094 17.84375 C 117.550781 17.898438 117.808594 18.046875 118.089844 18.375 C 118.320312 18.917969 118.300781 19.414062 118.296875 20 C 118.296875 20.128906 118.296875 20.253906 118.296875 20.386719 C 118.296875 20.8125 118.296875 21.238281 118.292969 21.664062 C 118.292969 21.96875 118.292969 22.273438 118.296875 22.578125 C 118.296875 23.320312 118.292969 24.0625 118.292969 24.804688 C 118.289062 25.671875 118.289062 26.535156 118.289062 27.402344 C 118.289062 28.941406 118.289062 30.484375 118.285156 32.027344 C 118.28125 33.527344 118.28125 35.023438 118.28125 36.519531 C 118.28125 36.613281 118.28125 36.707031 118.28125 36.800781 C 118.28125 36.988281 118.28125 37.171875 118.28125 37.359375 C 118.28125 37.496094 118.28125 37.496094 118.28125 37.636719 C 118.28125 37.726562 118.28125 37.820312 118.28125 37.914062 C 118.277344 39.339844 118.277344 40.765625 118.277344 42.191406 C 118.277344 42.710938 118.273438 43.226562 118.273438 43.742188 C 118.273438 44.640625 118.273438 45.539062 118.269531 46.433594 C 118.269531 46.734375 118.269531 47.03125 118.269531 47.328125 C 118.269531 47.734375 118.269531 48.140625 118.265625 48.546875 C 118.269531 48.726562 118.269531 48.726562 118.269531 48.910156 C 118.265625 49.015625 118.265625 49.125 118.265625 49.238281 C 118.265625 49.378906 118.265625 49.378906 118.265625 49.519531 C 118.25 49.75 118.25 49.75 118.125 50 C 117.742188 50.132812 117.742188 50.132812 117.25 50.125 C 116.308594 49.523438 115.511719 48.53125 114.726562 47.742188 C 114.492188 47.507812 114.257812 47.273438 114.023438 47.039062 C 112.652344 45.671875 112.652344 45.671875 112.046875 44.945312 C 111.796875 44.664062 111.511719 44.4375 111.222656 44.199219 C 110.816406 43.835938 110.476562 43.414062 110.125 43 C 110.082031 42.957031 110.042969 42.917969 110 42.875 C 110 43.003906 110 43.128906 110 43.261719 C 109.996094 48.882812 109.992188 54.5 109.984375 60.121094 C 109.984375 60.207031 109.984375 60.292969 109.984375 60.378906 C 109.984375 62.226562 109.980469 64.074219 109.976562 65.921875 C 109.976562 66.828125 109.976562 67.734375 109.972656 68.640625 C 109.972656 68.734375 109.972656 68.824219 109.972656 68.914062 C 109.96875 71.839844 109.96875 74.765625 109.964844 77.691406 C 109.964844 80.695312 109.960938 83.699219 109.957031 86.703125 C 109.957031 87.128906 109.953125 87.554688 109.953125 87.976562 C 109.953125 88.101562 109.953125 88.101562 109.953125 88.230469 C 109.953125 89.492188 109.949219 90.753906 109.949219 92.015625 C 109.953125 93.449219 109.949219 94.886719 109.945312 96.320312 C 109.945312 97.054688 109.941406 97.785156 109.945312 98.519531 C 109.945312 99.1875 109.945312 99.859375 109.941406 100.527344 C 109.941406 100.773438 109.941406 101.015625 109.941406 101.257812 C 109.941406 101.585938 109.9414Line truncated
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(99.215686%,93.333334%,87.450981%);fill-opacity:1;" d="M 88.449219 17.8125 C 88.636719 17.8125 88.636719 17.8125 88.824219 17.8125 C 89.238281 17.808594 89.652344 17.8125 90.0625 17.8125 C 90.359375 17.8125 90.65625 17.8125 90.953125 17.8125 C 91.671875 17.8125 92.394531 17.8125 93.117188 17.816406 C 93.957031 17.816406 94.796875 17.816406 95.636719 17.816406 C 97.132812 17.816406 98.632812 17.820312 100.132812 17.824219 C 101.585938 17.828125 103.039062 17.828125 104.492188 17.828125 C 104.582031 17.828125 104.671875 17.828125 104.765625 17.828125 C 104.855469 17.828125 104.945312 17.828125 105.035156 17.828125 C 106.691406 17.828125 108.347656 17.828125 110 17.832031 C 110.585938 17.832031 111.171875 17.832031 111.757812 17.832031 C 112.546875 17.832031 113.335938 17.835938 114.121094 17.835938 C 114.410156 17.839844 114.703125 17.839844 114.992188 17.839844 C 115.386719 17.839844 115.78125 17.839844 116.175781 17.839844 C 116.347656 17.839844 116.347656 17.839844 116.523438 17.839844 C 116.628906 17.839844 116.734375 17.84375 116.84375 17.84375 C 116.980469 17.84375 116.980469 17.84375 117.121094 17.84375 C 117.550781 17.898438 117.808594 18.046875 118.089844 18.375 C 118.320312 18.917969 118.300781 19.414062 118.296875 20 C 118.296875 20.191406 118.296875 20.191406 118.296875 20.386719 C 118.296875 20.8125 118.296875 21.238281 118.292969 21.664062 C 118.292969 21.96875 118.292969 22.273438 118.296875 22.578125 C 118.296875 23.320312 118.292969 24.0625 118.292969 24.804688 C 118.289062 25.671875 118.289062 26.535156 118.289062 27.402344 C 118.289062 28.941406 118.289062 30.484375 118.285156 32.027344 C 118.28125 33.527344 118.28125 35.023438 118.28125 36.519531 C 118.28125 36.613281 118.28125 36.707031 118.28125 36.800781 C 118.28125 36.988281 118.28125 37.171875 118.28125 37.359375 C 118.28125 37.449219 118.28125 37.542969 118.28125 37.636719 C 118.28125 37.773438 118.28125 37.773438 118.28125 37.914062 C 118.277344 39.339844 118.277344 40.765625 118.277344 42.191406 C 118.277344 42.710938 118.273438 43.226562 118.273438 43.742188 C 118.273438 44.640625 118.273438 45.539062 118.269531 46.433594 C 118.269531 46.734375 118.269531 47.03125 118.269531 47.328125 C 118.269531 47.734375 118.269531 48.140625 118.265625 48.546875 C 118.269531 48.726562 118.269531 48.726562 118.269531 48.910156 C 118.265625 49.015625 118.265625 49.125 118.265625 49.238281 C 118.265625 49.378906 118.265625 49.378906 118.265625 49.519531 C 118.25 49.75 118.25 49.75 118.125 50 C 117.742188 50.132812 117.742188 50.132812 117.25 50.125 C 116.308594 49.523438 115.511719 48.53125 114.726562 47.742188 C 114.492188 47.507812 114.257812 47.273438 114.023438 47.039062 C 112.652344 45.671875 112.652344 45.671875 112.046875 44.945312 C 111.796875 44.660156 111.507812 44.433594 111.214844 44.191406 C 110.992188 43.992188 110.808594 43.785156 110.613281 43.558594 C 110.054688 42.910156 109.457031 42.308594 108.851562 41.703125 C 108.742188 41.589844 108.628906 41.476562 108.515625 41.363281 C 108.40625 41.253906 108.300781 41.148438 108.191406 41.035156 C 108.09375 40.9375 107.996094 40.84375 107.898438 40.742188 C 107.566406 40.449219 107.304688 40.257812 106.855469 40.234375 C 105.742188 40.355469 105.175781 40.929688 104.414062 41.710938 C 104.070312 42.054688 103.710938 42.371094 103.339844 42.6875 C 103.160156 42.84375 102.992188 43.011719 102.828125 43.179688 C 102.625 43.375 102.625 43.375 102.375 43.375 C 102.332031 43.5 102.292969 43.621094 102.25 43.75 C 102.054688 43.925781 101.847656 44.089844 101.640625 44.25 C 100.984375 44.773438 100.40625 45.328125 99.820312 45.929688 C 99.464844 46.285156 99.09375 46.613281 98.714844 46.941406 C 98.347656 47.257812 98.011719 47.605469 97.679688 47.957031 C 97.5 48.140625 97.3125 48.320312 97.125 48.5 C 97.042969 48.5 96.960938 48.5 96.875 48.5 C 96.875 48.582031 96.875 48.664062 96.875 48.75 C 96.792969 48.75 96.710938 48.75 96.625 48.75 C 96.601562 48.816406 96.574219 48.886719 96.546875 48.953125 C 96.304688 49.371094 95.960938 49.65625 95.605469 49.972656 C 94.746094 50.757812 93.914062 51.570312 93.089844 52.394531 C 93.027344 52.457031 92.964844 52.515625 92.902344 52.582031 C 92.585938 52.898438 92.265625 53.214844 91.949219 53.535156 C 91.625 53.859375 91.300781 54.183594 90.972656 54.507812 C 90.71875 54.765625 90.464844 55.019531 90.210938 55.273438 C 90.03125 55.449219 89.855469 55.628906 89.675781 55.804688 C 89.117188 56.367188 88.570312 56.9375 88.054688 57.542969 C 87.710938 57.941406 87.328125 58.296875 86.953125 58.664062 C 86.613281 59.011719 86.296875 59.375 85.980469 59.738281 C 85.613281 60.15625 85.230469 60.554688 84.851562 60.953125 C 84.058594 61.792969 83.300781 62.652344 82.558594 63.535156 C 82.304688 63.832031 82.050781 64.128906 81.792969 64.425781 C 80.683594 65.714844 79.617188 67.035156 78.570312 68.375 C 78.359375 68.644531 78.144531 68.917969 77.933594 69.1875 C 77.78125 69.382812 77.628906 69.578125 77.47656Line truncated
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.529412%,40.392157%,55.29412%);fill-opacity:1;" d="M 109.5 42.5 C 109.6875 42.625 109.6875 42.625 109.875 42.75 C 109.914062 43.085938 109.925781 43.390625 109.921875 43.726562 C 109.921875 43.828125 109.921875 43.929688 109.921875 44.035156 C 109.921875 44.382812 109.921875 44.730469 109.921875 45.078125 C 109.921875 45.328125 109.921875 45.578125 109.921875 45.828125 C 109.921875 46.515625 109.921875 47.203125 109.917969 47.894531 C 109.917969 48.636719 109.917969 49.378906 109.917969 50.121094 C 109.917969 51.40625 109.917969 52.695312 109.914062 53.980469 C 109.914062 55.332031 109.910156 56.679688 109.910156 58.03125 C 109.910156 58.113281 109.910156 58.199219 109.910156 58.285156 C 109.910156 58.710938 109.910156 59.136719 109.910156 59.5625 C 109.910156 62.582031 109.90625 65.597656 109.902344 68.617188 C 109.898438 71.550781 109.898438 74.484375 109.894531 77.414062 C 109.894531 77.503906 109.894531 77.597656 109.894531 77.6875 C 109.894531 78.597656 109.894531 79.503906 109.894531 80.410156 C 109.894531 82.261719 109.890625 84.109375 109.890625 85.957031 C 109.890625 86.042969 109.890625 86.125 109.890625 86.214844 C 109.886719 91.894531 109.882812 97.570312 109.875 103.25 C 109.792969 103.25 109.710938 103.25 109.625 103.25 C 109.582031 83.203125 109.542969 63.15625 109.5 42.5 Z M 109.5 42.5 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(96.078432%,94.117647%,90.588236%);fill-opacity:1;" d="M 80.5 42.875 C 80.5 43.671875 79.515625 44.359375 78.96875 44.90625 C 78.757812 45.117188 78.546875 45.328125 78.332031 45.539062 C 77.90625 45.964844 77.496094 46.398438 77.105469 46.859375 C 76.789062 47.226562 76.460938 47.582031 76.132812 47.9375 C 76.039062 48.039062 76.039062 48.039062 75.945312 48.144531 C 75.796875 48.304688 75.648438 48.464844 75.5 48.625 C 75.417969 48.582031 75.335938 48.542969 75.25 48.5 C 75.359375 48.382812 75.472656 48.265625 75.585938 48.140625 C 75.734375 47.980469 75.886719 47.820312 76.039062 47.65625 C 76.117188 47.570312 76.199219 47.488281 76.28125 47.398438 C 76.769531 46.875 77.257812 46.34375 77.742188 45.8125 C 78.167969 45.347656 78.597656 44.898438 79.046875 44.457031 C 79.355469 44.140625 79.640625 43.808594 79.929688 43.476562 C 80.109375 43.265625 80.300781 43.070312 80.5 42.875 Z M 80.5 42.875 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(95.294118%,93.725491%,90.980393%);fill-opacity:1;" d="M 86.625 36.875 C 86.625 37.359375 86.433594 37.488281 86.105469 37.816406 C 86.027344 37.890625 85.953125 37.964844 85.875 38.039062 C 85.800781 38.117188 85.722656 38.191406 85.644531 38.269531 C 85.484375 38.429688 85.320312 38.59375 85.15625 38.753906 C 84.90625 39 84.660156 39.246094 84.414062 39.492188 C 84.253906 39.648438 84.097656 39.804688 83.9375 39.960938 C 83.867188 40.035156 83.792969 40.109375 83.714844 40.183594 C 83.367188 40.523438 83.027344 40.847656 82.625 41.125 C 82.542969 41.082031 82.460938 41.042969 82.375 41 C 83.949219 39.402344 83.949219 39.402344 84.726562 38.746094 C 85.386719 38.152344 86 37.507812 86.625 36.875 Z M 86.625 36.875 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,34.509805%,52.156866%);fill-opacity:1;" d="M 91.625 53.875 C 91.707031 53.917969 91.789062 53.957031 91.875 54 C 91.804688 54.070312 91.734375 54.140625 91.660156 54.210938 C 91.117188 54.75 90.59375 55.292969 90.097656 55.875 C 89.710938 56.3125 89.292969 56.71875 88.875 57.125 C 88.832031 57 88.792969 56.878906 88.75 56.75 C 89.699219 55.800781 90.648438 54.851562 91.625 53.875 Z M 91.625 53.875 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(95.686275%,93.725491%,90.588236%);fill-opacity:1;" d="M 89.125 34.5 C 89.207031 34.542969 89.289062 34.582031 89.375 34.625 C 89.054688 34.953125 88.734375 35.28125 88.414062 35.609375 C 88.277344 35.75 88.277344 35.75 88.140625 35.890625 C 88.050781 35.980469 87.964844 36.070312 87.875 36.164062 C 87.792969 36.246094 87.710938 36.328125 87.628906 36.414062 C 87.351562 36.644531 87.09375 36.757812 86.75 36.875 C 86.84375 36.785156 86.9375 36.695312 87.03125 36.601562 C 87.367188 36.269531 87.367188 36.269531 87.601562 35.953125 C 87.917969 35.570312 88.273438 35.265625 88.648438 34.945312 C 88.882812 34.746094 88.882812 34.746094 89.125 34.5 Z M 89.125 34.5 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(94.509804%,92.941177%,91.37255%);fill-opacity:1;" d="M 82.25 41.125 C 82.292969 41.207031 82.332031 41.289062 82.375 41.375 C 81.910156 41.925781 81.464844 42.449219 80.875 42.875 C 80.792969 42.832031 80.710938 42.792969 80.625 42.75 C 80.796875 42.5625 80.796875 42.5625 81 42.375 C 81.082031 42.375 81.164062 42.375 81.25 42.375 C 81.277344 42.308594 81.300781 42.242188 81.328125 42.171875 C 81.574219 41.75 81.898438 41.460938 82.25 41.125 Z M 82.25 41.125 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(95.686275%,93.725491%,90.196079%);fill-opacity:1;" d="M 74.625 49.25 C 74.667969 49.332031 74.707031 49.414062 74.75 49.5 C 74.613281 49.667969 74.476562 49.835938 74.335938 50 C 74.21875 50.140625 74.21875 50.140625 74.101562 50.28125 C 73.875 50.5 73.875 50.5 73.5 50.5 C 73.871094 50.085938 74.242188 49.675781 74.625 49.25 Z M 74.625 49.25 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.921569%,40.000001%,55.686277%);fill-opacity:1;" d="M 99.375 46.25 C 99.5 46.292969 99.621094 46.332031 99.75 46.375 C 99.378906 46.703125 99.007812 47.035156 98.625 47.375 C 98.542969 47.332031 98.460938 47.292969 98.375 47.25 C 98.703125 46.921875 99.035156 46.589844 99.375 46.25 Z M 99.375 46.25 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(94.509804%,94.509804%,92.156863%);fill-opacity:1;" d="M 60.625 70.5 C 60.667969 70.5 60.707031 70.5 60.75 70.5 C 60.828125 71.140625 60.550781 71.585938 60.25 72.125 C 60.167969 72.125 60.085938 72.125 60 72.125 C 60.058594 71.949219 60.113281 71.769531 60.171875 71.59375 C 60.21875 71.445312 60.21875 71.445312 60.269531 71.296875 C 60.367188 71.015625 60.488281 70.761719 60.625 70.5 Z M 60.625 70.5 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(20.784314%,38.431373%,55.29412%);fill-opacity:1;" d="M 101.625 44.25 C 101.707031 44.292969 101.789062 44.332031 101.875 44.375 C 101.441406 44.746094 101.441406 44.746094 101 45.125 C 100.917969 45.082031 100.835938 45.042969 100.75 45 C 101.039062 44.753906 101.328125 44.503906 101.625 44.25 Z M 101.625 44.25 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 29 KiB

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="64px" height="64px" viewBox="0 0 64 64" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,36.862746%,56.862748%);fill-opacity:1;" d="M 7.304688 8.859375 C 7.34375 8.859375 7.386719 8.859375 7.429688 8.859375 C 7.5625 8.859375 7.699219 8.863281 7.832031 8.863281 C 7.929688 8.863281 8.023438 8.863281 8.121094 8.863281 C 8.414062 8.867188 8.707031 8.871094 9 8.875 C 9.042969 8.875 9.085938 8.875 9.128906 8.875 C 14.683594 8.957031 20.269531 9.679688 28.9375 13.8125 C 28.980469 13.839844 29.019531 13.867188 29.0625 13.894531 C 29.453125 14.15625 29.828125 14.429688 30.1875 14.730469 C 30.335938 14.828125 30.449219 14.859375 30.625 14.875 C 30.859375 14.753906 31.019531 14.609375 31.203125 14.417969 C 31.417969 14.199219 31.648438 14.011719 31.898438 13.828125 C 31.941406 13.796875 31.984375 13.765625 32.03125 13.730469 C 34.890625 11.636719 38.429688 10.238281 41.859375 9.40625 C 41.902344 9.394531 41.949219 9.382812 41.992188 9.375 C 42.90625 9.152344 42.90625 9.152344 43.3125 9.125 C 43.332031 9.144531 43.355469 9.167969 43.375 9.1875 C 43.402344 9.148438 43.433594 9.105469 43.460938 9.0625 C 43.707031 8.875 43.925781 8.902344 44.226562 8.90625 C 44.316406 8.90625 44.316406 8.90625 44.410156 8.90625 C 44.617188 8.90625 44.824219 8.90625 45.03125 8.90625 C 45.179688 8.90625 45.328125 8.90625 45.476562 8.90625 C 45.835938 8.90625 46.199219 8.90625 46.558594 8.90625 C 46.976562 8.910156 47.398438 8.910156 47.816406 8.910156 C 48.566406 8.910156 49.316406 8.910156 50.066406 8.910156 C 50.792969 8.914062 51.519531 8.914062 52.246094 8.914062 C 52.292969 8.914062 52.335938 8.914062 52.382812 8.914062 C 52.425781 8.914062 52.472656 8.914062 52.519531 8.914062 C 53.347656 8.914062 54.171875 8.914062 55 8.917969 C 55.292969 8.917969 55.585938 8.917969 55.878906 8.917969 C 56.273438 8.917969 56.667969 8.917969 57.0625 8.917969 C 57.207031 8.917969 57.351562 8.917969 57.496094 8.917969 C 57.691406 8.917969 57.890625 8.917969 58.085938 8.921875 C 58.175781 8.921875 58.175781 8.921875 58.261719 8.921875 C 58.316406 8.921875 58.367188 8.921875 58.421875 8.921875 C 58.46875 8.921875 58.511719 8.921875 58.558594 8.921875 C 58.777344 8.949219 58.90625 9.023438 59.046875 9.1875 C 59.160156 9.460938 59.152344 9.707031 59.148438 10 C 59.148438 10.0625 59.148438 10.128906 59.148438 10.191406 C 59.148438 10.40625 59.148438 10.617188 59.148438 10.832031 C 59.148438 10.984375 59.148438 11.136719 59.148438 11.289062 C 59.148438 11.660156 59.148438 12.03125 59.144531 12.402344 C 59.144531 12.835938 59.144531 13.269531 59.144531 13.699219 C 59.144531 14.472656 59.144531 15.242188 59.140625 16.015625 C 59.140625 16.761719 59.140625 17.511719 59.140625 18.261719 C 59.140625 18.304688 59.140625 18.351562 59.140625 18.398438 C 59.140625 18.492188 59.140625 18.585938 59.140625 18.679688 C 59.140625 18.746094 59.140625 18.746094 59.140625 18.816406 C 59.140625 18.863281 59.140625 18.910156 59.140625 18.957031 C 59.140625 19.667969 59.140625 20.382812 59.136719 21.097656 C 59.136719 21.355469 59.136719 21.613281 59.136719 21.871094 C 59.136719 22.320312 59.136719 22.769531 59.136719 23.21875 C 59.136719 23.367188 59.136719 23.515625 59.136719 23.664062 C 59.136719 23.867188 59.132812 24.070312 59.132812 24.273438 C 59.132812 24.363281 59.132812 24.363281 59.132812 24.453125 C 59.132812 24.507812 59.132812 24.5625 59.132812 24.617188 C 59.132812 24.6875 59.132812 24.6875 59.132812 24.761719 C 59.125 24.875 59.125 24.875 59.0625 25 C 58.871094 25.066406 58.871094 25.066406 58.625 25.0625 C 58.15625 24.761719 57.757812 24.265625 57.363281 23.871094 C 57.246094 23.753906 57.128906 23.636719 57.011719 23.519531 C 56.328125 22.835938 56.328125 22.835938 56.023438 22.472656 C 55.898438 22.332031 55.753906 22.21875 55.609375 22.097656 C 55.40625 21.917969 55.238281 21.707031 55.0625 21.5 C 55.042969 21.480469 55.019531 21.457031 55 21.4375 C 55 21.5 55 21.566406 55 21.628906 C 55 24.441406 54.996094 27.25 54.992188 30.0625 C 54.992188 30.101562 54.992188 30.144531 54.992188 30.191406 C 54.992188 31.113281 54.988281 32.039062 54.988281 32.960938 C 54.988281 33.414062 54.988281 33.867188 54.988281 34.320312 C 54.988281 34.367188 54.988281 34.410156 54.988281 34.457031 C 54.984375 35.921875 54.984375 37.382812 54.984375 38.84375 C 54.980469 40.347656 54.980469 41.851562 54.976562 43.351562 C 54.976562 43.5625 54.976562 43.777344 54.976562 43.988281 C 54.976562 44.050781 54.976562 44.050781 54.976562 44.113281 C 54.976562 44.746094 54.976562 45.378906 54.976562 46.007812 C 54.976562 46.726562 54.976562 47.441406 54.972656 48.160156 C 54.972656 48.527344 54.972656 48.894531 54.972656 49.257812 C 54.972656 49.59375 54.972656 49.929688 54.96875 50.265625 C 54.96875 50.386719 54.96875 50.507812 54.972656 50.628906 C 54.972656 50.792969 54.96875 50.957031 54.96875 51.121094 C 54.96875 51.171875 54.96875 51.21875 54.972656 51.265625 C 54.96875 51.484375 54.953125 51.601562 54.820312 51.777344 C 54.652344 51.902344 54.53125 51.917969 54.324219 51.941406 C 54.25 51.949219 54.175781 51.9Line truncated
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(99.215686%,93.333334%,87.450981%);fill-opacity:1;" d="M 44.226562 8.90625 C 44.316406 8.90625 44.316406 8.90625 44.410156 8.90625 C 44.617188 8.90625 44.824219 8.90625 45.03125 8.90625 C 45.179688 8.90625 45.328125 8.90625 45.476562 8.90625 C 45.835938 8.90625 46.199219 8.90625 46.558594 8.90625 C 46.976562 8.910156 47.398438 8.910156 47.816406 8.910156 C 48.566406 8.910156 49.316406 8.910156 50.066406 8.910156 C 50.792969 8.914062 51.519531 8.914062 52.246094 8.914062 C 52.292969 8.914062 52.335938 8.914062 52.382812 8.914062 C 52.425781 8.914062 52.472656 8.914062 52.519531 8.914062 C 53.347656 8.914062 54.171875 8.914062 55 8.917969 C 55.292969 8.917969 55.585938 8.917969 55.878906 8.917969 C 56.273438 8.917969 56.667969 8.917969 57.0625 8.917969 C 57.207031 8.917969 57.351562 8.917969 57.496094 8.917969 C 57.691406 8.917969 57.890625 8.917969 58.085938 8.921875 C 58.175781 8.921875 58.175781 8.921875 58.261719 8.921875 C 58.316406 8.921875 58.367188 8.921875 58.421875 8.921875 C 58.492188 8.921875 58.492188 8.921875 58.558594 8.921875 C 58.777344 8.949219 58.90625 9.023438 59.046875 9.1875 C 59.160156 9.460938 59.152344 9.707031 59.148438 10 C 59.148438 10.09375 59.148438 10.09375 59.148438 10.191406 C 59.148438 10.40625 59.148438 10.617188 59.148438 10.832031 C 59.148438 10.984375 59.148438 11.136719 59.148438 11.289062 C 59.148438 11.660156 59.148438 12.03125 59.144531 12.402344 C 59.144531 12.835938 59.144531 13.269531 59.144531 13.699219 C 59.144531 14.472656 59.144531 15.242188 59.144531 16.015625 C 59.140625 16.761719 59.140625 17.511719 59.140625 18.261719 C 59.140625 18.304688 59.140625 18.351562 59.140625 18.398438 C 59.140625 18.492188 59.140625 18.585938 59.140625 18.679688 C 59.140625 18.726562 59.140625 18.769531 59.140625 18.816406 C 59.140625 18.886719 59.140625 18.886719 59.140625 18.957031 C 59.140625 19.667969 59.140625 20.382812 59.136719 21.097656 C 59.136719 21.355469 59.136719 21.613281 59.136719 21.871094 C 59.136719 22.320312 59.136719 22.769531 59.136719 23.21875 C 59.136719 23.367188 59.136719 23.515625 59.136719 23.664062 C 59.136719 23.867188 59.132812 24.070312 59.132812 24.273438 C 59.132812 24.363281 59.132812 24.363281 59.132812 24.453125 C 59.132812 24.507812 59.132812 24.5625 59.132812 24.617188 C 59.132812 24.6875 59.132812 24.6875 59.132812 24.761719 C 59.125 24.875 59.125 24.875 59.0625 25 C 58.871094 25.066406 58.871094 25.066406 58.625 25.0625 C 58.15625 24.761719 57.757812 24.265625 57.363281 23.871094 C 57.246094 23.753906 57.128906 23.636719 57.011719 23.519531 C 56.328125 22.835938 56.328125 22.835938 56.023438 22.472656 C 55.898438 22.332031 55.753906 22.214844 55.609375 22.097656 C 55.496094 21.996094 55.402344 21.894531 55.308594 21.777344 C 55.027344 21.457031 54.726562 21.152344 54.425781 20.851562 C 54.371094 20.796875 54.316406 20.738281 54.257812 20.679688 C 54.203125 20.628906 54.148438 20.574219 54.09375 20.519531 C 54.046875 20.46875 54 20.421875 53.949219 20.371094 C 53.78125 20.222656 53.652344 20.128906 53.429688 20.117188 C 52.871094 20.179688 52.585938 20.464844 52.207031 20.855469 C 52.035156 21.027344 51.855469 21.183594 51.671875 21.34375 C 51.582031 21.421875 51.496094 21.503906 51.414062 21.589844 C 51.3125 21.6875 51.3125 21.6875 51.1875 21.6875 C 51.167969 21.75 51.144531 21.8125 51.125 21.875 C 51.027344 21.960938 50.925781 22.042969 50.820312 22.125 C 50.492188 22.386719 50.203125 22.664062 49.910156 22.964844 C 49.734375 23.140625 49.546875 23.304688 49.355469 23.46875 C 49.171875 23.628906 49.007812 23.800781 48.839844 23.976562 C 48.75 24.070312 48.65625 24.160156 48.5625 24.25 C 48.519531 24.25 48.480469 24.25 48.4375 24.25 C 48.4375 24.292969 48.4375 24.332031 48.4375 24.375 C 48.394531 24.375 48.355469 24.375 48.3125 24.375 C 48.300781 24.410156 48.289062 24.441406 48.273438 24.476562 C 48.152344 24.683594 47.980469 24.828125 47.804688 24.988281 C 47.375 25.378906 46.957031 25.785156 46.546875 26.195312 C 46.515625 26.226562 46.484375 26.257812 46.453125 26.289062 C 46.292969 26.449219 46.132812 26.609375 45.976562 26.765625 C 45.8125 26.929688 45.648438 27.09375 45.488281 27.253906 C 45.359375 27.382812 45.230469 27.507812 45.105469 27.636719 C 45.015625 27.726562 44.925781 27.8125 44.839844 27.902344 C 44.558594 28.183594 44.285156 28.46875 44.027344 28.773438 C 43.855469 28.96875 43.664062 29.148438 43.476562 29.332031 C 43.308594 29.503906 43.148438 29.6875 42.988281 29.871094 C 42.808594 30.078125 42.617188 30.277344 42.425781 30.476562 C 42.03125 30.894531 41.652344 31.324219 41.277344 31.765625 C 41.152344 31.917969 41.023438 32.0625 40.898438 32.210938 C 40.34375 32.855469 39.808594 33.515625 39.285156 34.1875 C 39.179688 34.324219 39.074219 34.457031 38.96875 34.59375 C 38.890625 34.691406 38.8125 34.789062 38.738281 34.886719 C 38.703125 34.929688 38.667969 34.976562 38.632812 35.023438 C 38.167969 35.617188 37.734375 36.230469 37.320312 36.859375 C 37.234375 36.99218Line truncated
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.529412%,40.392157%,55.29412%);fill-opacity:1;" d="M 54.75 21.25 C 54.84375 21.3125 54.84375 21.3125 54.9375 21.375 C 54.957031 21.542969 54.960938 21.695312 54.960938 21.863281 C 54.960938 21.914062 54.960938 21.964844 54.960938 22.019531 C 54.960938 22.191406 54.960938 22.363281 54.960938 22.539062 C 54.960938 22.664062 54.960938 22.789062 54.960938 22.914062 C 54.960938 23.257812 54.960938 23.601562 54.960938 23.945312 C 54.957031 24.316406 54.957031 24.6875 54.957031 25.058594 C 54.957031 25.703125 54.957031 26.347656 54.957031 26.992188 C 54.957031 27.664062 54.957031 28.339844 54.957031 29.015625 C 54.957031 29.058594 54.957031 29.097656 54.957031 29.140625 C 54.957031 29.355469 54.957031 29.566406 54.953125 29.78125 C 54.953125 31.289062 54.953125 32.800781 54.953125 34.308594 C 54.949219 35.773438 54.949219 37.242188 54.949219 38.707031 C 54.949219 38.753906 54.949219 38.796875 54.949219 38.84375 C 54.949219 39.296875 54.945312 39.753906 54.945312 40.207031 C 54.945312 41.128906 54.945312 42.054688 54.945312 42.976562 C 54.945312 43.019531 54.945312 43.0625 54.945312 43.105469 C 54.945312 45.945312 54.941406 48.785156 54.9375 51.625 C 54.894531 51.625 54.855469 51.625 54.8125 51.625 C 54.792969 41.601562 54.769531 31.578125 54.75 21.25 Z M 54.75 21.25 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(96.078432%,94.117647%,90.588236%);fill-opacity:1;" d="M 40.25 21.4375 C 40.25 21.835938 39.757812 22.179688 39.484375 22.453125 C 39.378906 22.558594 39.273438 22.664062 39.167969 22.769531 C 38.953125 22.984375 38.746094 23.199219 38.550781 23.429688 C 38.394531 23.613281 38.230469 23.789062 38.066406 23.96875 C 38.019531 24.019531 38.019531 24.019531 37.972656 24.070312 C 37.898438 24.152344 37.824219 24.230469 37.75 24.3125 C 37.707031 24.292969 37.667969 24.269531 37.625 24.25 C 37.679688 24.191406 37.734375 24.132812 37.792969 24.070312 C 37.867188 23.992188 37.945312 23.910156 38.019531 23.828125 C 38.058594 23.785156 38.097656 23.742188 38.140625 23.699219 C 38.386719 23.4375 38.628906 23.171875 38.871094 22.90625 C 39.082031 22.675781 39.300781 22.449219 39.523438 22.226562 C 39.675781 22.070312 39.820312 21.90625 39.964844 21.738281 C 40.054688 21.632812 40.152344 21.535156 40.25 21.4375 Z M 40.25 21.4375 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(95.294118%,93.725491%,90.980393%);fill-opacity:1;" d="M 43.3125 18.4375 C 43.3125 18.679688 43.214844 18.742188 43.050781 18.910156 C 43.015625 18.945312 42.976562 18.980469 42.9375 19.019531 C 42.898438 19.058594 42.863281 19.097656 42.824219 19.136719 C 42.742188 19.214844 42.660156 19.296875 42.578125 19.375 C 42.453125 19.5 42.332031 19.621094 42.207031 19.746094 C 42.128906 19.824219 42.046875 19.902344 41.96875 19.980469 C 41.933594 20.015625 41.894531 20.054688 41.859375 20.089844 C 41.683594 20.261719 41.511719 20.421875 41.3125 20.5625 C 41.269531 20.542969 41.230469 20.519531 41.1875 20.5 C 41.976562 19.703125 41.976562 19.703125 42.363281 19.371094 C 42.695312 19.074219 43 18.753906 43.3125 18.4375 Z M 43.3125 18.4375 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,34.509805%,52.156866%);fill-opacity:1;" d="M 45.8125 26.9375 C 45.855469 26.957031 45.894531 26.980469 45.9375 27 C 45.902344 27.035156 45.867188 27.070312 45.828125 27.105469 C 45.558594 27.375 45.296875 27.648438 45.046875 27.9375 C 44.855469 28.15625 44.644531 28.359375 44.4375 28.5625 C 44.417969 28.5 44.394531 28.4375 44.375 28.375 C 44.847656 27.902344 45.324219 27.425781 45.8125 26.9375 Z M 45.8125 26.9375 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(95.686275%,93.725491%,90.588236%);fill-opacity:1;" d="M 44.5625 17.25 C 44.605469 17.269531 44.644531 17.292969 44.6875 17.3125 C 44.527344 17.476562 44.367188 17.640625 44.207031 17.804688 C 44.140625 17.875 44.140625 17.875 44.070312 17.945312 C 44.027344 17.992188 43.980469 18.035156 43.9375 18.082031 C 43.898438 18.121094 43.855469 18.164062 43.816406 18.207031 C 43.675781 18.324219 43.546875 18.378906 43.375 18.4375 C 43.421875 18.390625 43.46875 18.347656 43.515625 18.300781 C 43.683594 18.136719 43.683594 18.136719 43.800781 17.976562 C 43.960938 17.785156 44.136719 17.632812 44.324219 17.472656 C 44.441406 17.375 44.441406 17.375 44.5625 17.25 Z M 44.5625 17.25 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(94.509804%,92.941177%,91.37255%);fill-opacity:1;" d="M 41.125 20.5625 C 41.144531 20.605469 41.167969 20.644531 41.1875 20.6875 C 40.953125 20.964844 40.730469 21.222656 40.4375 21.4375 C 40.394531 21.417969 40.355469 21.394531 40.3125 21.375 C 40.398438 21.28125 40.398438 21.28125 40.5 21.1875 C 40.542969 21.1875 40.582031 21.1875 40.625 21.1875 C 40.636719 21.152344 40.652344 21.121094 40.664062 21.085938 C 40.785156 20.875 40.949219 20.730469 41.125 20.5625 Z M 41.125 20.5625 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(95.686275%,93.725491%,90.196079%);fill-opacity:1;" d="M 37.3125 24.625 C 37.332031 24.667969 37.355469 24.707031 37.375 24.75 C 37.308594 24.832031 37.238281 24.917969 37.167969 25 C 37.109375 25.070312 37.109375 25.070312 37.050781 25.140625 C 36.9375 25.25 36.9375 25.25 36.75 25.25 C 36.9375 25.042969 37.121094 24.835938 37.3125 24.625 Z M 37.3125 24.625 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.921569%,40.000001%,55.686277%);fill-opacity:1;" d="M 49.6875 23.125 C 49.75 23.144531 49.8125 23.167969 49.875 23.1875 C 49.6875 23.351562 49.503906 23.515625 49.3125 23.6875 C 49.269531 23.667969 49.230469 23.644531 49.1875 23.625 C 49.351562 23.460938 49.515625 23.296875 49.6875 23.125 Z M 49.6875 23.125 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(94.509804%,94.509804%,92.156863%);fill-opacity:1;" d="M 30.3125 35.25 C 30.332031 35.25 30.355469 35.25 30.375 35.25 C 30.414062 35.570312 30.273438 35.792969 30.125 36.0625 C 30.082031 36.0625 30.042969 36.0625 30 36.0625 C 30.027344 35.972656 30.058594 35.886719 30.085938 35.796875 C 30.109375 35.722656 30.109375 35.722656 30.132812 35.648438 C 30.183594 35.507812 30.242188 35.378906 30.3125 35.25 Z M 30.3125 35.25 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(20.784314%,38.431373%,55.29412%);fill-opacity:1;" d="M 50.8125 22.125 C 50.855469 22.144531 50.894531 22.167969 50.9375 22.1875 C 50.722656 22.375 50.722656 22.375 50.5 22.5625 C 50.457031 22.542969 50.417969 22.519531 50.375 22.5 C 50.519531 22.375 50.664062 22.253906 50.8125 22.125 Z M 50.8125 22.125 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 30 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 7H7V5H13V7Z" fill="currentColor"/>
<path d="M13 11H7V9H13V11Z" fill="currentColor"/>
<path d="M7 15H13V13H7V15Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 19V1H17V5H21V23H7V19H3ZM15 17V3H5V17H15ZM17 7V19H9V21H19V7H17Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 396 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.817 11.186a8.94 8.94 0 0 0-1.355-3.219 9.053 9.053 0 0 0-2.43-2.43 8.95 8.95 0 0 0-3.219-1.355 9.028 9.028 0 0 0-1.838-.18V2L8 5l3.975 3V6.002c.484-.002.968.044 1.435.14a6.961 6.961 0 0 1 2.502 1.053 7.005 7.005 0 0 1 1.892 1.892A6.967 6.967 0 0 1 19 13a7.032 7.032 0 0 1-.55 2.725 7.11 7.11 0 0 1-.644 1.188 7.2 7.2 0 0 1-.858 1.039 7.028 7.028 0 0 1-3.536 1.907 7.13 7.13 0 0 1-2.822 0 6.961 6.961 0 0 1-2.503-1.054 7.002 7.002 0 0 1-1.89-1.89A6.996 6.996 0 0 1 5 13H3a9.02 9.02 0 0 0 1.539 5.034 9.096 9.096 0 0 0 2.428 2.428A8.95 8.95 0 0 0 12 22a9.09 9.09 0 0 0 1.814-.183 9.014 9.014 0 0 0 3.218-1.355 8.886 8.886 0 0 0 1.331-1.099 9.228 9.228 0 0 0 1.1-1.332A8.952 8.952 0 0 0 21 13a9.09 9.09 0 0 0-.183-1.814z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 866 B

+1 -1
View File
@@ -13,7 +13,7 @@
<property name="minimumSize">
<size>
<width>350</width>
<height>400</height>
<height>460</height>
</size>
</property>
<property name="maximumSize">
+119
View File
@@ -0,0 +1,119 @@
# -*- 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.
"""
from dataclasses import dataclass
from enum import Enum
from typing import Any, Optional, Protocol
class ConfigType(Enum):
"""
Config type enum. Values represent the default filename.
"""
GLOBAL = "autolibrary.json"
BULLETIN = "bulletin.json"
TIMERTASK = "timer_task.json"
@dataclass(frozen=True)
class ConfigPath:
"""
A typed configuration path that carries both the config file
and the dot-separated key in a single object.
Consumers pass this directly to ConfigProvider.get/set,
eliminating the need to import ConfigType separately.
"""
config_type: ConfigType
key: str = ""
class CfgKey:
"""
Type-safe hierarchical configuration key constants.
Each leaf is a ConfigPath that can be passed directly to
``ConfigProvider.get()`` or ``ConfigProvider.set()``.
Usage::
CfgKey.GLOBAL.AUTOMATION.RUN_PATH.PATHS
# -> ConfigPath(ConfigType.GLOBAL, "automation.run_path.paths")
config.get(CfgKey.GLOBAL.AUTOMATION.RUN_PATH.PATHS, [])
config.set(CfgKey.GLOBAL.AUTOMATION.RUN_PATH.PATHS, value)
"""
class GLOBAL:
class AUTOMATION:
ROOT = ConfigPath(ConfigType.GLOBAL, "automation")
class RUN_PATH:
ROOT = ConfigPath(ConfigType.GLOBAL, "automation.run_path")
CURRENT = ConfigPath(ConfigType.GLOBAL, "automation.run_path.current")
PATHS = ConfigPath(ConfigType.GLOBAL, "automation.run_path.paths")
class USER_PATH:
ROOT = ConfigPath(ConfigType.GLOBAL, "automation.user_path")
CURRENT = ConfigPath(ConfigType.GLOBAL, "automation.user_path.current")
PATHS = ConfigPath(ConfigType.GLOBAL, "automation.user_path.paths")
class TIMERTASK:
ROOT = ConfigPath(ConfigType.TIMERTASK, "")
TIMER_TASKS = ConfigPath(ConfigType.TIMERTASK, "timer_tasks")
class BULLETIN:
ROOT = ConfigPath(ConfigType.BULLETIN, "")
BULLETIN = ConfigPath(ConfigType.BULLETIN, "bulletin")
LAST_SYNC_TIME = ConfigPath(ConfigType.BULLETIN, "last_sync_time")
class ConfigProvider(Protocol):
"""
Abstract interface for configuration storage access.
Concrete implementations (e.g. ConfigManager) conform to
this protocol structurally rather than through explicit
inheritance.
"""
def get(
self,
key: ConfigPath,
default: Optional[Any] = None
) -> Any:
"""
Retrieve a configuration value.
Args:
key: A ConfigPath object specifying which config file
and key to read from.
default: Fallback value if the key is not found.
Returns:
The configuration value at the given key path.
"""
...
def set(
self,
key: ConfigPath,
value: Any = None
) -> None:
"""
Set a configuration value and persist to disk.
Args:
key: A ConfigPath object specifying which config file
and key to write to.
value: The value to store.
"""
...
+11
View File
@@ -0,0 +1,11 @@
"""
Interfaces module for the AutoLibrary project.
Defines abstract interfaces (Protocols) and shared type definitions
used across layers to decouple consumers from concrete implementations.
Key components:
- ConfigProvider: Abstract interface for configuration access.
- ConfigType: Enumeration of configuration file types.
- ConfigKey: Type-safe hierarchical key constants for config lookups.
"""
+14 -73
View File
@@ -10,26 +10,17 @@ See the LICENSE file for details.
import os
import threading
from enum import Enum
from typing import Any, Optional
from utils.JSONReader import JSONReader
from utils.JSONWriter import JSONWriter
from interfaces.ConfigProvider import ConfigType, ConfigPath
# This config manager class only responsible for global and other
# unconfigurable config files.
class ConfigType(Enum):
"""
Config type class. Values represent the default filename.
"""
GLOBAL = "autolibrary.json" # Global config file.
BULLETIN = "bulletin.json" # Bulletin board config file.
TIMERTASK = "timer_task.json" # Timer task config file.
class ConfigTemplate:
"""
Config template class.
@@ -42,7 +33,6 @@ class ConfigTemplate:
self.__config_type = config_type
def template(
self
) -> dict:
@@ -92,7 +82,6 @@ class ConfigManager:
self.initialize()
def initialize(
self
):
@@ -100,7 +89,6 @@ class ConfigManager:
for config_type in ConfigType:
self.load(config_type)
def load(
self,
config_type: ConfigType
@@ -117,47 +105,42 @@ class ConfigManager:
self.__config_data[config_type.value] = ConfigTemplate(config_type).template()
JSONWriter(config_path, self.__config_data[config_type.value])
def get(
self,
config_type: ConfigType,
key: str = "",
key: ConfigPath,
default: Optional[Any] = None
) -> Any:
with self.__config_lock:
config_data = self.__config_data[config_type.value]
if key == "":
config_data = self.__config_data[key.config_type.value]
if key.key == "":
return config_data
keys = key.split('.')
keys = key.key.split('.')
for k in keys[:-1]:
config_data = config_data.get(k, None)
if config_data is None:
return default
return config_data.get(keys[-1], default)
def set(
self,
config_type: ConfigType,
key: str = "",
key: ConfigPath,
value: Any = None
):
with self.__config_lock:
root_data = self.__config_data[config_type.value]
if key == "":
self.__config_data[config_type.value] = value
root_data = self.__config_data[key.config_type.value]
if key.key == "":
self.__config_data[key.config_type.value] = value
else:
keys = key.split('.')
keys = key.key.split('.')
config_data = root_data
for k in keys[:-1]:
if k not in config_data:
config_data[k] = {}
config_data = config_data[k]
config_data[keys[-1]] = value
self.save(config_type)
self.save(key.config_type)
def save(
self,
@@ -167,7 +150,6 @@ class ConfigManager:
config_path = os.path.join(self.__config_dir, config_type.value)
JSONWriter(config_path, self.__config_data[config_type.value])
def configDir(
self
) -> str:
@@ -176,52 +158,11 @@ class ConfigManager:
# ConfigManager singleton instance.
_config_manager_instance = None
# Utility functions.
#
# Utility function to get validated automation config paths.
def getValidateAutomationConfigPaths(
) -> dict:
"""
Get validated automation config paths from ConfigManager instance.
These function will validate the config paths and return the validated paths in a dict.
Returns:
dict: Validated automation config paths.
"""
config_paths = {"run": "", "user": ""}
auto_config = _config_manager_instance.get(ConfigType.GLOBAL, "automation", {})
for cfg_type in ["run", "user"]:
paths = auto_config.get(f"{cfg_type}_path", {}).get("paths", [])
index = auto_config.get(f"{cfg_type}_path", {}).get("current", 0)
if paths == []:
paths.append(os.path.join(_config_manager_instance.configDir(), f"{cfg_type}.json"))
if index < 0:
index = 0
if index >= len(paths):
index = len(paths) - 1
config_paths[cfg_type] = paths[index]
data = {"current": index, "paths": paths}
auto_config[f"{cfg_type}_path"] = data
_config_manager_instance.set(ConfigType.GLOBAL, "automation", auto_config)
return config_paths
# Utility function to get base config directory.
def getBaseConfigDir(
) -> str:
"""
Get base config directory, on Windows, it is usually at :
'C:\\Users\\<username>\\AppData\\Local\\AutoLibrary\\config'.
Returns:
str: Base config directory.
"""
return _config_manager_instance.configDir()
_config_manager_instance : ConfigManager | None = None
# Singleton instance of ConfigManager.
_instance_lock = threading.Lock()
def instance(
config_dir: str = ""
) -> ConfigManager:
@@ -240,6 +181,6 @@ def instance(
else:
if config_dir == "":
return _config_manager_instance
if getBaseConfigDir() != config_dir:
if _config_manager_instance.configDir() != config_dir:
raise ValueError("ConfigManager 的实例已初始化,不能使用不同的配置目录。")
return _config_manager_instance
+48
View File
@@ -0,0 +1,48 @@
# -*- 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 os
import managers.config.ConfigManager as ConfigManager
from interfaces.ConfigProvider import CfgKey
class ConfigUtils:
"""
Config utilities class.
"""
@staticmethod
def getAutomationConfigPaths(
) -> dict[str]:
"""
Get validated automation config paths from ConfigManager instance.
These function will validate the config paths and return the validated paths in a dict.
Returns:
dict[str]: Validated automation config paths (include user and run config paths).
"""
cfg_mgr = ConfigManager.instance() # config manager instance
config_paths = {"run": "", "user": ""}
auto_config = cfg_mgr.get(CfgKey.GLOBAL.AUTOMATION.ROOT, {})
for cfg_type in ["run", "user"]:
paths = auto_config.get(f"{cfg_type}_path", {}).get("paths", [])
index = auto_config.get(f"{cfg_type}_path", {}).get("current", 0)
if paths == []:
paths.append(os.path.join(cfg_mgr.configDir(), f"{cfg_type}.json"))
if index < 0:
index = 0
if index >= len(paths):
index = len(paths) - 1
config_paths[cfg_type] = paths[index]
data = {"current": index, "paths": paths}
auto_config[f"{cfg_type}_path"] = data
cfg_mgr.set(CfgKey.GLOBAL.AUTOMATION.ROOT, auto_config)
return config_paths
+10 -2
View File
@@ -1,3 +1,12 @@
# -*- 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 platform
import browsers
@@ -32,6 +41,7 @@ class WebBrowserArch(Enum):
MACX86_64 = 6
MACARM = 7
@dataclass
class WebBrowserInfo:
"""
@@ -61,7 +71,6 @@ class WebBrowserArchDetector:
pass
def detect(
self
) -> WebBrowserArch:
@@ -114,7 +123,6 @@ class WebBrowserDetector:
self.browser_arch = WebBrowserArchDetector().detect()
self.browser_infos : list[WebBrowserInfo] = []
def detect(
self
) -> list[WebBrowserInfo]:
+32 -31
View File
@@ -1,3 +1,12 @@
# -*- 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 os
import time
import shutil
@@ -86,7 +95,6 @@ class WebDriverName:
self.driver_type = driver_type
def __str__(
self
) -> str:
@@ -116,7 +124,6 @@ class WebDriverExecName:
self.driver_type = driver_type
self.arch = arch
def __str__(
self
) -> str:
@@ -191,7 +198,6 @@ class WebDriverURL:
self.arch = arch
self.file_name = str(WebDriverFileName(self.version, self.driver_type, self.arch))
def __str__(
self
) -> str:
@@ -241,31 +247,6 @@ class WebDriverDownloader:
self.download_dir.mkdir(mode=0o0755, parents=True, exist_ok=True)
self.download_path = self.download_dir/str(WebDriverFileName(self.version, self.driver_type, self.arch))
def download(
self,
progress_callback: Optional[Callable[[float, int, float, str], None]] = None,
cancel_event: Optional[threading.Event] = None
) -> Optional[Path]:
try:
# downlaod file : 0% - 98%
if not self._download(progress_callback, cancel_event=cancel_event):
return None
# verify file : 98% - 99%
if not self._verify(progress_callback):
progress_callback(0, 100, 0.0, "验证失败")
return None
# extract file : 99% - 100%
driver_path = self._extract(progress_callback)
if not driver_path:
progress_callback(0, 100, 0.0, "解压失败")
return None
return driver_path
except Exception as e:
raise e
def _download(
self,
progress_callback: Optional[Callable[[float, int, float, str], None]] = None,
@@ -343,7 +324,6 @@ class WebDriverDownloader:
continue
raise e
def _verify(
self,
progress_callback: Optional[Callable[[float, int, float, str], None]] = None
@@ -352,7 +332,6 @@ class WebDriverDownloader:
progress_callback(98, 100, 0.0, "验证完成")
return True
def _extract(
self,
progress_callback: Optional[Callable[[float, int, float, str], None]] = None
@@ -388,7 +367,6 @@ class WebDriverDownloader:
except Exception:
return None
def _cleanup(
self,
driver_file: Path
@@ -401,6 +379,29 @@ class WebDriverDownloader:
else:
item.unlink()
def download(
self,
progress_callback: Optional[Callable[[float, int, float, str], None]] = None,
cancel_event: Optional[threading.Event] = None
) -> Optional[Path]:
try:
# downlaod file : 0% - 98%
if not self._download(progress_callback, cancel_event=cancel_event):
return None
# verify file : 98% - 99%
if not self._verify(progress_callback):
progress_callback(0, 100, 0.0, "验证失败")
return None
# extract file : 99% - 100%
driver_path = self._extract(progress_callback)
if not driver_path:
progress_callback(0, 100, 0.0, "解压失败")
return None
return driver_path
except Exception as e:
raise e
class ChromeDriverDownloader(WebDriverDownloader):
"""
-16
View File
@@ -81,7 +81,6 @@ class WebDriverManager:
self.initialize()
def initialize(
self
):
@@ -93,7 +92,6 @@ class WebDriverManager:
self._checkDriverStatus()
self.__initialized = True
def _detectBrowsers(
self
):
@@ -105,7 +103,6 @@ class WebDriverManager:
for info in browser_infos
]
def _checkDriverStatus(
self
):
@@ -117,7 +114,6 @@ class WebDriverManager:
driver_info.driver_path = driver_path
driver_info.driver_status = WebDriverStatus.INSTALLED
def _mapWebBrowserTypeToDriver(
self,
browser_type: WebBrowserType
@@ -132,7 +128,6 @@ class WebDriverManager:
else:
raise ValueError(f"不支持的 Web 浏览器类型 : {browser_type}")
def _mapWebBrowserArchToDriver(
self,
browser_type: WebBrowserType,
@@ -199,7 +194,6 @@ class WebDriverManager:
else:
raise ValueError(f"不支持的 Web 浏览器类型 : {browser_type}")
def _mapFirefoxDriverVersion(
self,
version: str
@@ -240,7 +234,6 @@ class WebDriverManager:
except Exception as e:
raise ValueError(f"无效的 Firefox 版本格式 : {version}") from e
def _getDriverInfo(
self,
browser_info: WebBrowserInfo
@@ -256,7 +249,6 @@ class WebDriverManager:
driver_info.browser_version = browser_info.browser_version
return driver_info
def _getDriverPath(
self,
driver_info: WebDriverInfo
@@ -286,7 +278,6 @@ class WebDriverManager:
driver_path = driver_dir/exe_name
return driver_path
def refresh(
self
):
@@ -294,7 +285,6 @@ class WebDriverManager:
self._detectBrowsers()
self._checkDriverStatus()
def getDriverInfos(
self
) -> list[WebDriverInfo]:
@@ -302,7 +292,6 @@ class WebDriverManager:
with self.__lock:
return self.__driver_infos.copy()
def getDriverInfo(
self,
driver_type: WebDriverType
@@ -315,7 +304,6 @@ class WebDriverManager:
if info.driver_type == driver_type
]
def getDriverPath(
self,
driver_info: WebDriverInfo
@@ -325,7 +313,6 @@ class WebDriverManager:
return driver_info.driver_path
return None
def installDriver(
self,
driver_info: WebDriverInfo,
@@ -390,7 +377,6 @@ class WebDriverManager:
driver_info.driver_status = WebDriverStatus.ERROR
raise e
def cancelDriverDownload(
self,
driver_info: WebDriverInfo
@@ -411,7 +397,6 @@ class WebDriverManager:
except Exception:
return False
def uninstallDriver(
self,
driver_info: WebDriverInfo,
@@ -441,7 +426,6 @@ class WebDriverManager:
driver_info.driver_status = WebDriverStatus.ERROR
raise
def driverDir(
self
) -> str:
+2 -5
View File
@@ -89,7 +89,6 @@ class LogManager:
self.initialize()
def initialize(
self
):
@@ -139,7 +138,6 @@ class LogManager:
self.__initialized = True
def getLogger(
self,
name: Optional[str] = None
@@ -149,7 +147,6 @@ class LogManager:
return self.__logger.getChild(name)
return self.__logger
def setLevel(
self,
level: int
@@ -158,7 +155,6 @@ class LogManager:
if self.__logger:
self.__logger.setLevel(level)
def logDir(
self
) -> str:
@@ -171,6 +167,7 @@ _log_manager_instance = None
# Singleton instance lock.
_instance_lock = threading.Lock()
def instance(
log_dir: str = ""
) -> LogManager:
@@ -186,7 +183,7 @@ def instance(
raise ValueError("LogManager 的实例已初始化, 不能使用不同的日志目录")
return _log_manager_instance
# export function to get logger
def getLogger(
name: Optional[str] = None
) -> logging.Logger:
+1 -8
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -49,7 +49,6 @@ class AutoLib(MsgBase):
raise Exception("浏览器驱动URL初始化失败 !")
self.__initLibOperators()
def __initBrowserDriver(
self
) -> bool:
@@ -142,7 +141,6 @@ class AutoLib(MsgBase):
self._showTrace(f"浏览器驱动已初始化, 类型: {self.__driver_type}, 路径: {self.__driver_path}")
return True
def __initLibOperators(
self
):
@@ -157,7 +155,6 @@ class AutoLib(MsgBase):
self.__lib_checkin = LibCheckin(self._input_queue, self._output_queue, self.__driver)
self.__lib_renew = LibRenew(self._input_queue, self._output_queue, self.__driver)
def __waitResponseLoad(
self
) -> bool:
@@ -184,7 +181,6 @@ class AutoLib(MsgBase):
self._showTrace(f"登录页面加载失败 !", self.TraceLevel.ERROR)
return False
def __initDriverUrl(
self,
) -> bool:
@@ -207,7 +203,6 @@ class AutoLib(MsgBase):
return False
return True
def __run(
self,
username: str,
@@ -292,7 +287,6 @@ class AutoLib(MsgBase):
return -1
return result
def run(
self,
user_config: dict
@@ -339,7 +333,6 @@ class AutoLib(MsgBase):
)
return
def close(
self
) -> bool:
+1 -13
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -33,7 +33,6 @@ class LibChecker(LibOperator):
self.__driver = driver
def _waitResponseLoad(
self
) -> bool:
@@ -50,7 +49,6 @@ class LibChecker(LibOperator):
seconds = int(seconds%60)
return f"{hours} 时 {minutes} 分 {seconds} 秒"
def __navigateToReserveRecordPage(
self
) -> bool:
@@ -67,7 +65,6 @@ class LibChecker(LibOperator):
return False
return True
def __decodeReserveTime(
self,
time_element
@@ -105,7 +102,6 @@ class LibChecker(LibOperator):
}
}
def __decodeReserveInfo(
self,
info_elements
@@ -133,7 +129,6 @@ class LibChecker(LibOperator):
"status": status,
}
def __decodeReserveRecord(
self,
reservation
@@ -160,7 +155,6 @@ class LibChecker(LibOperator):
"info": info
}
def __loadReserveRecords(
self
) -> list:
@@ -177,7 +171,6 @@ class LibChecker(LibOperator):
self._showTrace("加载预约记录失败 !", self.TraceLevel.ERROR)
return None
def __showMoreReserveRecords(
self
) -> bool:
@@ -203,7 +196,6 @@ class LibChecker(LibOperator):
self._showTrace("加载更多预约记录失败 !", self.TraceLevel.ERROR)
return False
def __getReserveRecord(
self,
wanted_date: str,
@@ -253,7 +245,6 @@ class LibChecker(LibOperator):
break
return None
def canReserve(
self,
date: str
@@ -270,7 +261,6 @@ class LibChecker(LibOperator):
self._showTrace(f"用户在 {date} 已存在有效预约, 无法预约")
return False
def canCheckin(
self
) -> bool:
@@ -307,7 +297,6 @@ class LibChecker(LibOperator):
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法签到")
return False
def canRenew(
self
) -> tuple[bool, dict]:
@@ -335,7 +324,6 @@ class LibChecker(LibOperator):
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法续约")
return False, None
def postRenewCheck(
self,
record: dict
+1 -4
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -31,7 +31,6 @@ class LibCheckin(LibOperator):
self.__driver = driver
def _waitResponseLoad(
self
) -> bool:
@@ -87,7 +86,6 @@ class LibCheckin(LibOperator):
ok_btn.click()
return False
def __enableCheckinBtn(
self
) -> bool:
@@ -112,7 +110,6 @@ class LibCheckin(LibOperator):
self._showTrace("签到按钮启用失败", self.TraceLevel.WARNING)
return result
def checkin(
self,
username: str
+1 -2
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -33,7 +33,6 @@ class LibCheckout(LibOperator):
self.__driver = driver
def _waitResponseLoad(
self
) -> bool:
+1 -9
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -34,7 +34,6 @@ class LibLogin(LibOperator):
self.__driver = driver
self.__ddddocr = ddddocr.DdddOcr()
def _waitResponseLoad(
self
) -> bool:
@@ -58,7 +57,6 @@ class LibLogin(LibOperator):
)
return False
def __fillLogInElements(
self,
username: str,
@@ -78,7 +76,6 @@ class LibLogin(LibOperator):
return False
return True
def __autoRecognizeCaptcha(
self
) -> str:
@@ -100,7 +97,6 @@ class LibLogin(LibOperator):
self._showTrace(f"验证码识别失败 ! : {e}", self.TraceLevel.ERROR)
return ""
def __manualRecognizeCaptcha(
self
) -> str:
@@ -118,7 +114,6 @@ class LibLogin(LibOperator):
self._showTrace(f"输入验证码失败 ! : {e}", self.TraceLevel.ERROR)
return ""
def __refreshCaptcha(
self
):
@@ -134,7 +129,6 @@ class LibLogin(LibOperator):
self._showTrace(f"刷新验证码失败 ! : {e}", self.TraceLevel.ERROR)
return False
def __solveCaptcha(
self,
auto_captcha: bool = True
@@ -158,7 +152,6 @@ class LibLogin(LibOperator):
)
return ""
def __fillCaptchaElement(
self,
captcha_text: str
@@ -173,7 +166,6 @@ class LibLogin(LibOperator):
self._showTrace(f"验证码填写失败 ! : {e}", self.TraceLevel.ERROR)
return False
def login(
self,
username: str,
+1 -3
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -28,14 +28,12 @@ class LibLogout(LibOperator):
self.__driver = driver
def _waitResponseLoad(
self
) -> bool:
return True
def logout(
self,
username: str
+2 -8
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -14,7 +14,7 @@ from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from base.LibTimeSelector import LibTimeSelector
from operators.abs.LibTimeSelector import LibTimeSelector
class LibRenew(LibTimeSelector):
@@ -30,7 +30,6 @@ class LibRenew(LibTimeSelector):
self.__driver = driver
def _waitResponseLoad(
self
) -> bool:
@@ -38,7 +37,6 @@ class LibRenew(LibTimeSelector):
self.__driver.refresh()
return True
def __waitRenewDialog(
self
) -> bool:
@@ -77,7 +75,6 @@ class LibRenew(LibTimeSelector):
return False
return True
def __selectNearestTime(
self,
record: dict,
@@ -116,7 +113,6 @@ class LibRenew(LibTimeSelector):
self._showTrace(f"当前可供续约的时间有: {free_times}")
return False
def __validateAndAdjustRenewTime(
self,
end_time: str,
@@ -139,7 +135,6 @@ class LibRenew(LibTimeSelector):
return True
return True
def __confirmRenewal(
self,
best_opt,
@@ -167,7 +162,6 @@ class LibRenew(LibTimeSelector):
self._showTrace("确认续约时发生错误 !", self.TraceLevel.ERROR)
return False
def renew(
self,
username: str,
+4 -23
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -15,7 +15,7 @@ from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from base.LibTimeSelector import LibTimeSelector
from operators.abs.LibTimeSelector import LibTimeSelector
class LibReserve(LibTimeSelector):
@@ -48,7 +48,6 @@ class LibReserve(LibTimeSelector):
"8": "五层考研"
}
def _waitResponseLoad(
self,
) -> bool:
@@ -99,7 +98,6 @@ class LibReserve(LibTimeSelector):
self._showTrace(f"预约结果加载失败 !", self.TraceLevel.ERROR)
return False
def __containRequiredInfo(
self,
reserve_info: dict
@@ -135,7 +133,6 @@ class LibReserve(LibTimeSelector):
)
return False
def __isValidDate(
self,
reserve_info: dict
@@ -157,7 +154,6 @@ class LibReserve(LibTimeSelector):
reserve_info["date"] = cur_date_str
return True
def __isValidBeginTime(
self,
reserve_info: dict
@@ -177,7 +173,6 @@ class LibReserve(LibTimeSelector):
self._showTrace(f"是否优先选择更早开始时间未指定, 自动设置为 True")
return True
def __isValidExpectDuration(
self,
reserve_info: dict
@@ -192,7 +187,6 @@ class LibReserve(LibTimeSelector):
self._showTrace("需要满足预约持续时间, 但未指定, 使用默认时长为 4 小时")
return True
def __isValidEndTime(
self,
reserve_info: dict
@@ -222,7 +216,6 @@ class LibReserve(LibTimeSelector):
self._showTrace(f"是否优先选择较晚结束时间未指定, 自动设置为 True")
return True
def __finalCheck(
self,
reserve_info: dict
@@ -275,7 +268,6 @@ class LibReserve(LibTimeSelector):
reserve_info["end_time"]["time"] = self._minsToTimeStr(begin_mins + 8*60)
return True
def __checkReserveInfo(
self,
reserve_info: dict
@@ -305,7 +297,6 @@ class LibReserve(LibTimeSelector):
)
return True
def __clickElement(
self,
trigger_locator: tuple,
@@ -330,7 +321,6 @@ class LibReserve(LibTimeSelector):
self._showTrace(fail_msg)
return False
def __clickElementByJS(
self,
trigger_locator_id: str,
@@ -364,7 +354,6 @@ class LibReserve(LibTimeSelector):
self._showTrace(fail_msg)
return result
def __selectDate(
self,
date_str: str
@@ -384,7 +373,6 @@ class LibReserve(LibTimeSelector):
fail_msg=f"选择日期失败 ! : {date_str} 不可用"
)
def __selectPlace(
self,
place: str
@@ -406,7 +394,6 @@ class LibReserve(LibTimeSelector):
fail_msg=f"选择预约场所失败 ! : {display_place} 不可用"
)
def __selectFloor(
self,
floor: str
@@ -427,7 +414,6 @@ class LibReserve(LibTimeSelector):
fail_msg=f"选择楼层失败 ! : {display_floor} 不可用"
)
def __selectRoom(
self,
room: str
@@ -453,7 +439,6 @@ class LibReserve(LibTimeSelector):
self._showTrace(f"选择房间失败 ! : {display_room} 不可用", self.TraceLevel.ERROR)
return False
def __selectSeat(
self,
seat_id: str
@@ -492,7 +477,6 @@ class LibReserve(LibTimeSelector):
self._showTrace(f"座位选择失败 !", self.TraceLevel.ERROR)
return False
def __selectNearestTime(
self,
time_id: str,
@@ -547,7 +531,6 @@ class LibReserve(LibTimeSelector):
self._showTrace(f"当前可供预约的 {time_type} 有: {free_times}")
return -1
def __selectSeatTime(
self,
begin_time: dict,
@@ -583,7 +566,7 @@ class LibReserve(LibTimeSelector):
# If 'satisfy_duration' is True, select end time based on actual begin time
if satisfy_duration:
exp_end_mins = int(self.validateAndAdjustEndTime(act_beg_mins, expect_duration))
exp_end_mins = int(self.__validateAndAdjustEndTime(act_beg_mins, expect_duration))
exp_end_tm_str = self._minsToTimeStr(exp_end_mins)
self._showTrace(
f"需要满足期望预约持续时间: {expect_duration} 小时, "
@@ -607,8 +590,7 @@ class LibReserve(LibTimeSelector):
)
return True
def validateAndAdjustEndTime(
def __validateAndAdjustEndTime(
self,
begin_mins: int,
duration: int
@@ -627,7 +609,6 @@ class LibReserve(LibTimeSelector):
)
return expect_end_mins
def reserve(
self,
username: str,
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -16,7 +16,7 @@ from base.LibOperator import LibOperator
class LibTimeSelector(LibOperator):
"""
Base class for time selection operations.
Abstract base class for time selection operations.
This class provides common time selection logic for reservation and renewal
operations, including time conversion utilities and best time option finding.
@@ -60,7 +60,6 @@ class LibTimeSelector(LibOperator):
hour, minute = divmod(int(mins), 60)
return f"{hour:02d}:{minute:02d}"
def _formatTimeRelation(
self,
abs_diff: int,
@@ -78,7 +77,6 @@ class LibTimeSelector(LibOperator):
else:
return f"正好等于 {time_type}"
def _findBestTimeOption(
self,
time_options: list,
+6
View File
@@ -0,0 +1,6 @@
"""
Abstract layer class of the LibOperator
Here are the classes and modules in this package:
- LibTimeSelector: Abstract base class for time selection operations.
"""
-4
View File
@@ -42,7 +42,6 @@ class JSONReader:
self.__json_data = None
self.__read()
def __read(
self
):
@@ -59,7 +58,6 @@ class JSONReader:
except Exception as e:
raise Exception(f"读取文件时发生未知错误: {e}") from e
def read(
self
) -> bool:
@@ -70,14 +68,12 @@ class JSONReader:
return False
return True
def data(
self
) -> dict:
return self.__json_data.copy()
def path(
self
) -> str:
-3
View File
@@ -46,7 +46,6 @@ class JSONWriter:
self.__json_data = json_data.copy() if json_data is not None else {}
self.__write()
def __write(
self
):
@@ -63,7 +62,6 @@ class JSONWriter:
except Exception as e:
raise Exception(f"写入文件时发生未知错误: {e}") from e
def write(
self
) -> bool:
@@ -74,7 +72,6 @@ class JSONWriter:
return False
return True
def path(
self
) -> str:
+42 -36
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
@@ -10,41 +10,47 @@ See the LICENSE file for details.
from datetime import datetime, timedelta
def calculateNextRepeatTime(
repeat_days: list,
hour: int,
minute: int,
second: int
) -> datetime:
class TimerUtils:
"""
Calculate the next repeat time based on repeat days and target time.
This function calculates the next execution time for a repeatable task.
If the current day is in repeat_days and the target time has not passed,
it returns today's target time. Otherwise, it finds the next matching day.
Args:
repeat_days (list): List of weekdays to repeat (0=Monday, 6=Sunday).
hour (int): Target hour (0-23).
minute (int): Target minute (0-59).
second (int): Target second (0-59).
Returns:
datetime: The next repeat execution time.
Timer utilities class.
"""
current_time = datetime.now()
current_weekday = current_time.weekday()
target_time = current_time.replace(hour=hour, minute=minute, second=second, microsecond=0)
if current_weekday in repeat_days:
if target_time > current_time:
return target_time
repeat_days_sorted = sorted(repeat_days)
for day in repeat_days_sorted:
if day > current_weekday:
days_until = day - current_weekday
next_time = target_time + timedelta(days=days_until)
return next_time
days_until = 7 - current_weekday + repeat_days_sorted[0]
next_time = target_time + timedelta(days=days_until)
return next_time
@staticmethod
def getNextTimerRepeatTime(
repeat_days: list[int],
hour: int,
minute: int,
second: int
) -> datetime:
"""
Calculate the next repeat time based on repeat days and target time.
This function calculates the next execution time for a repeatable task.
If the current day is in repeat_days and the target time has not passed,
it returns today's target time. Otherwise, it finds the next matching day.
Args:
repeat_days (list[int]): List of weekdays to repeat (0=Monday, 6=Sunday).
hour (int): Target hour (0-23).
minute (int): Target minute (0-59).
second (int): Target second (0-59).
Returns:
datetime: The next repeat execution time.
"""
current_time = datetime.now()
current_weekday = current_time.weekday()
target_time = current_time.replace(hour=hour, minute=minute, second=second, microsecond=0)
if current_weekday in repeat_days:
if target_time > current_time:
return target_time
repeat_days_sorted = sorted(repeat_days)
for day in repeat_days_sorted:
if day > current_weekday:
days_until = day - current_weekday
next_time = target_time + timedelta(days=days_until)
return next_time
days_until = 7 - current_weekday + repeat_days_sorted[0]
next_time = target_time + timedelta(days=days_until)
return next_time
+1 -1
View File
@@ -5,4 +5,4 @@
- TimerUtils: Timer utils class for the AutoLibrary project.
- JSONReader: JSON reader class for the AutoLibrary project.
- JSONWriter: JSON writer class for the AutoLibrary project.
"""
"""