1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-06-18 15:33:03 +08:00

Compare commits

..

16 Commits

Author SHA1 Message Date
github-actions[bot] 95a3ae2a24 chore(release): v1.1.0 [auto release commit] 2026-02-26 15:04:42 +00:00
KenanZhu 896242a1e3 fix(Main, ALConfigWidget): 修复配置文件初始化问题 2026-02-26 22:59:26 +08:00
KenanZhu fd96fc235e ci(workflows): 修复 build.yml 中 Generate 'Main.spec' 步骤中的 name 参数 2026-02-26 21:27:35 +08:00
KenanZhu 25aab588a8 feat(utils): 添加 ConfigManager 与 JSON 配置读写,替换旧实现
add:
- src/utils/ConfigManager.py
- src/utils/JSONReader.py
- src/utils/JSONWriter.py
remove:
- src/utils/ConfigReader.py
- src/utils/ConfigWriter.py
refactor:
- 更新调用方以使用 ConfigManager / JSONReader / JSONWriter(见 ALConfigWidget.py、ALMainWindow.py、ALTimerTaskManageWidget.py、ALMainWorkers.py 等)
- 统一方法命名(initlize* -> initialize*)、改进错误提示与配置路径管理

BREAKING CHANGE: 删除 ConfigReader/ConfigWriter,外部调用需改为 JSONReader/JSONWriter 或通过 ConfigManager 访问配置
2026-02-26 21:18:18 +08:00
KenanZhu 6e1b8e6b10 ci(workflows): 修改 build.yml 中 PyInstaller 打包参数,发布压缩包修改为为文件夹模式 2026-02-24 17:39:49 +08:00
KenanZhu 5f2327cf61 style(gui.*): 修改一些 import 顺序和格式 2026-02-23 22:26:52 +08:00
KenanZhu 96e7adabb0 docs(readme): 修改自述文件 2026-02-23 00:07:58 +08:00
KenanZhu 42afbbe694 docs(readme): 修改自述文件 2026-02-22 23:19:42 +08:00
KenanZhu 3777970332 docs(readme): 修改自述文件,完善使用说明 2026-02-22 00:24:47 +08:00
KenanZhu 9fb28e1368 ci(release.yml): 修改 release.yml 中发布说明的默认内容 2026-02-21 23:32:19 +08:00
KenanZhu 4aeca08ce8 chore(ALMainWindow, ALMainWorkers): 修改统一部分函数和变量的命名 2026-02-21 23:18:17 +08:00
KenanZhu a1ff85256a refactor(ALConfigWidget, ALTimerTaskManageWidget): 优化界面的错误异常处理 2026-02-21 15:38:56 +08:00
KenanZhu 169de92d5b chore(ALConfigWidget): 删除了未使用的方法 def defaultGroup() 和 def defaultUsers() 2026-02-21 15:10:36 +08:00
KenanZhu 5ca4a14a14 chore(*): 更改一些界面类方法,局部变量和信号的命名:
(ALConfigWidget):
def initlizeDefaultConfigPaths() 中 script_path 和 script_dir 分别改为 executable_path 和 executable_dir
def fillUserTree() 更改为 def setUsersToTreeWidget()
def collectUserFromUserInfoWidget() 更改为 def collectUserFromWidget()
def collectUserConfigFromUserTreeWidget 更改为 def collectUsersFromTreeWidget()
交换了一些方法的位置

(ALSeatMapSelectDialog):
信号 seatMapSelectDialogClosed 改为 seatMapSelectDialogIsClosed

(ALTimerTaskManageWidget):
信号 timerTaskManageWidgetClosed 改为 timerTaskManageWidgetIsClosed

(ALMainWindow):
def __init__() 中 script_path 和 script_dir 分别改为 executable_path 和 executable_dir
更改 ALSeatMapSelectDialog 和 ALTimerTaskManageWidget 中相关的信号命名
2026-02-21 14:26:54 +08:00
KenanZhu 155b3fe3ca style(LibRenew): 删除多余注释,修改部分注释的格式 2026-02-19 17:09:15 +08:00
KenanZhu 99d454a566 refactor(LibChecker, AutoLib): 重构 LibChecker 类中 canRenew 方法的返回值类型:
将 canRenew 方法的返回值类型指定为 tuple(bool, dict),并随之修改返回值以及调用
模块的调用逻辑。
2026-02-19 17:05:42 +08:00
23 changed files with 737 additions and 524 deletions
+18 -11
View File
@@ -164,10 +164,7 @@ jobs:
"exe = EXE("
" pyz,"
" a.scripts,"
" a.binaries,"
" a.datas,"
" [],"
" name='$exeName',"
" name='AutoLibrary',"
" debug=False,"
" bootloader_ignore_signals=False,"
" strip=False,"
@@ -182,10 +179,20 @@ jobs:
" entitlements_file=None,"
" icon=['src\\gui\\resources\\icons\\AutoLibrary_32x32.ico'],"
")"
""
"coll = COLLECT("
" exe,"
" a.binaries,"
" a.datas,"
" strip=False,"
" upx=True,"
" upx_exclude=[],"
" name='$exeName'"
")"
)
$specLines | Out-File -FilePath "Main.spec" -Encoding UTF8
Write-Host "✓ Main.spec generated successfully"
Write-Host "✓ Main.spec (non-single file) generated successfully"
Write-Host "`nGenerated Main.spec ============"
Get-Content "Main.spec" | Write-Host
Write-Host "==================================`n"
@@ -200,17 +207,17 @@ jobs:
run: |
$tagName = "${{ steps.get_version.outputs.TAG_NAME }}"
$version = "${{ steps.get_version.outputs.VERSION }}"
$exeName = "AutoLibrary-$version.exe"
$distDir = "dist/AutoLibrary-$version"
$zipName = "AutoLibrary.$tagName-windows-x86_64.zip"
echo "ZIP_PATH=$zipName" >> $env:GITHUB_OUTPUT
Write-Host "Looking for executable: dist/$exeName"
if (Test-Path "dist/$exeName") {
Compress-Archive -Path "dist/$exeName" -DestinationPath $zipName
Write-Host "✓ Created release archive: $zipName"
Write-Host "Looking for distribution directory: $distDir"
if (Test-Path $distDir) {
Compress-Archive -Path "$distDir/*" -DestinationPath $zipName
Write-Host "✓ Created release archive (directory mode): $zipName"
} else {
Write-Error "✗ Executable not found: dist/$exeName"
Write-Error "✗ Distribution directory not found: $distDir"
Write-Host "Files in dist directory:"
Get-ChildItem "dist" | ForEach-Object { Write-Host " - $($_.Name)" }
exit 1
-12
View File
@@ -125,18 +125,6 @@ jobs:
prerelease: false
generate_release_notes: true
body: |
### 下载获取
- **Windows x86_64**: `AutoLibrary.${{ needs.build.outputs.tag_name }}-windows-x86_64.zip`
### 如何使用
1. 下载 `AutoLibrary.${{ needs.build.outputs.tag_name }}-windows-x86_64.zip` 文件
2. 解压到任意目录
3. 下载对应浏览器的驱动文件
4. 运行 `AutoLibrary-${{ needs.build.outputs.version }}.exe` (首次运行会初始化配置文件)
5. 按照提示操作即可
更多详情请访问 [AutoLibrary 网站](http://www.autolibrary.top) 和查看 [帮助手册](https://www.autolibrary.top/manuals)
---
**完整更新日志见下方自动生成的 Release Notes**
env:
+13 -12
View File
@@ -5,10 +5,10 @@
![AutoLibrary Logo](./src/gui/resources/icons/AutoLibrary_128x128.ico)
[![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)
[![Build](https://img.shields.io/github/actions/workflow/status/KenanZhu/AutoLibrary/release.yml)](https://github.com/KenanZhu/AutoLibrary/actions/workflows/release.yml)
[![Release](https://img.shields.io/github/v/release/KenanZhu/AutoLibrary)](https://github.com/KenanZhu/AutoLibrary/releases)
![Downloads](https://img.shields.io/github/downloads/KenanZhu/AutoLibrary/total)
![License](https://img.shields.io/github/license/KenanZhu/AutoLibrary?label=license)
[![Build](https://img.shields.io/github/actions/workflow/status/KenanZhu/AutoLibrary/release.yml?label=release&logo=github-actions&logoColor=white)](https://github.com/KenanZhu/AutoLibrary/actions/workflows/release.yml)
[![Release](https://img.shields.io/github/v/release/KenanZhu/AutoLibrary?label=latest&logo=github&logoColor=white)](https://github.com/KenanZhu/AutoLibrary/releases/latest)
![Downloads](https://img.shields.io/github/downloads/KenanZhu/AutoLibrary/total?label=downloads)
了解更多请访问 [_AutoLibrary 网站_](http://www.autolibrary.top)
@@ -26,11 +26,12 @@
### 如何使用
1. 下载最新版本的 [AutoLibrary 压缩包](https://github.com/KenanZhu/AutoLibrary/releases)。
1. 下载最新版本的 [AutoLibrary 压缩包](https://github.com/KenanZhu/AutoLibrary/releases/latest)。
2. 解压下载的文件到任意目录。
3. 下载对应浏览器的驱动文件,并在配置界面的运行配置选项卡对应位置选择你下载好的浏览器驱动
4. 运行 `AutoLibrary.exe` 文件
5. 按照提示操作即可
3. 下载对应浏览器类型和版本(具体操作请参考适用软件版本的 [帮助手册](https://www.autolibrary.top/manuals)的驱动文件,并在配置界面的运行配置选项卡对应位置选择你下载好的浏览器驱动
4. 运行 `AutoLibrary-[主版本号].[次版本号].[修订版本号].Z.exe` 文件 (如 `AutoLibrary-1.0.0.exe`
5. 点击 [配置] 按钮,在配置界面填写好预约信息和运行配置后,点击 [确认] 按钮
6. 点击 [启动脚本] 按钮,即可开始自动预约、续约、签到等操作。
*注意 1*: 关于浏览器驱动的下载和其它相关问题,请参考我们的 [帮助手册](https://www.autolibrary.top/manuals) 中对应软件版本的内容。
@@ -98,11 +99,11 @@ def classification(self, img: bytes):
#### 后续会有哪些功能?
当前版本的功能对于正常使用已经足够,不过后续会着重考虑完善 2-4 人预约时的使用体验,暂时有以下构想:
当前版本的功能对于正常使用已经足够,不过后续会着重完善预约时的使用体验,暂时有以下构想:
1. 2-4 人一起预约时,往往会偏向于预约并排或对面的整个空座位,这时候工具会按照一定策略查询搜索符合条件的座位,并预约并排或对面的整个座位,而不是各自独立预约
2. 预约时会考虑到组内用户的预约时间是否冲突,若冲突则会提示用户是否继续预约,若用户选择继续预约,则会按需要调整预约时间,再进行预约
3. 对于比较固定的用户,会考虑在定时任务管理中添加如 ‘每日任务’ ‘每周任务’ 等选项,用户可以根据需要设置定时任务重复的日期范围,自动完成预约,签到,续约等操作
- 引入交互预约面板功能,预约时直接在座位分布图中选择可用座位,并按用户分配,无需事先配置预约信息
- 优化定时任务管理功能,用户可以在定时任务管理界面设置重复运行的定时任务,如每日预约、每周预约等
- 软件的自动更新以及公告栏功能,用户可以自动更新最新版本并获取最新公告事项
不过由于本人的时间和能力有限,也需要考虑到图书馆的正常运行,所以后续功能会有所取舍,但也许会进行一些小的功能验证。
+14 -1
View File
@@ -7,14 +7,25 @@ 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 sys
from PySide6.QtCore import QTranslator
from PySide6.QtCore import QTranslator, QStandardPaths, QDir
from PySide6.QtWidgets import QApplication
from gui.ALMainWindow import ALMainWindow
from gui.resources import ALResource
from utils.ConfigManager import instance
def initializeConfigManager():
app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
config_dir = os.path.join(app_dir, "config")
if not QDir(config_dir).exists():
QDir().mkpath(config_dir)
instance(config_dir)
def main():
@@ -23,6 +34,8 @@ def main():
if translator.load(":/res/trans/translators/qtbase_zh_CN.ts"):
app.installTranslator(translator)
app.setStyle('Fusion')
app.setApplicationName("AutoLibrary")
initializeConfigManager()
window = ALMainWindow()
window.show()
sys.exit(app.exec_())
-1
View File
@@ -23,7 +23,6 @@ from gui.ALVersionInfo import (
AL_VERSION, AL_COMMIT_SHA, AL_COMMIT_DATE, AL_BUILD_DATE
)
from gui.resources.ui.Ui_ALAboutDialog import Ui_ALAboutDialog
from gui.resources import ALResource
+169 -132
View File
@@ -21,38 +21,35 @@ from PySide6.QtGui import (
QCloseEvent, QAction
)
from utils.JSONReader import JSONReader
from utils.JSONWriter import JSONWriter
from utils.ConfigManager import ConfigType, instance
from utils.ConfigManager import getValidateAutomationConfigPaths
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 utils.ConfigReader import ConfigReader
from utils.ConfigWriter import ConfigWriter
class ALConfigWidget(QWidget, Ui_ALConfigWidget):
configWidgetIsClosed = Signal(dict)
configWidgetIsClosed = Signal()
def __init__(
self,
parent = None,
config_paths = {
"run": "",
"user": ""
}
):
super().__init__(parent)
self.__config_paths = config_paths
self.__cfg_mgr = instance()
self.__config_paths = getValidateAutomationConfigPaths()
self.__config_data = {"run": {}, "user": {}}
self.setupUi(self)
self.modifyUi()
self.connectSignals()
self.initlizeFloorRoomMap()
self.initlizeDefaultConfigPaths()
if not self.initlizeConfigs():
if not self.initializeConfigs():
self.close()
@@ -68,8 +65,8 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.UserListLayout.insertWidget(0, self.UserTreeWidget)
self.UserTreeWidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.UserTreeWidget.customContextMenuRequested.connect(self.onUserTreeWidgetContextMenu)
self.initlizeFloorRoomMap()
self.initilizeUserInfoWidget()
self.initializeFloorRoomMap()
self.initializeUserInfoWidget()
def connectSignals(
@@ -124,11 +121,11 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
event: QCloseEvent
):
self.configWidgetIsClosed.emit(self.__config_paths)
self.configWidgetIsClosed.emit()
super().closeEvent(event)
def initlizeFloorRoomMap(
def initializeFloorRoomMap(
self
):
@@ -162,19 +159,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
}
def initlizeDefaultConfigPaths(
self
):
script_path = sys.executable
script_dir = QFileInfo(script_path).absoluteDir()
self.__default_config_paths = {
"user": QDir.toNativeSeparators(script_dir.absoluteFilePath("user.json")),
"run": QDir.toNativeSeparators(script_dir.absoluteFilePath("run.json"))
}
def initlizeConfigToWidget(
def initializeConfigToWidget(
self,
which: str,
config_data: dict
@@ -184,12 +169,12 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.setRunConfigToWidget(config_data)
self.CurrentRunConfigEdit.setText(self.__config_paths["run"])
elif which == "user":
self.initilizeUserInfoWidget()
self.fillUserTree(config_data)
self.initializeUserInfoWidget()
self.setUsersToTreeWidget(config_data)
self.CurrentUserConfigEdit.setText(self.__config_paths["user"])
def initlizeConfig(
def initializeConfig(
self,
which: str
) -> bool:
@@ -200,7 +185,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
run_config_path = self.__config_paths[which]
if not os.path.exists(run_config_path):
self.__config_data[which] = self.defaultRunConfig()
self.__config_paths[which] = self.__default_config_paths[which]
if self.saveRunConfig(self.__config_paths[which], self.__config_data[which]):
msg += f"运行配置文件已初始化, 文件路径: \n{self.__config_paths[which]}\n"
else:
@@ -213,7 +197,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
user_config_path = self.__config_paths[which]
if not os.path.exists(user_config_path):
self.__config_data[which] = self.defaultUserConfig()
self.__config_paths[which] = self.__default_config_paths[which]
if self.saveUserConfig(self.__config_paths[which], self.__config_data[which]):
msg += f"用户配置文件已初始化, 文件路径: \n{self.__config_paths[which]}\n"
else:
@@ -225,18 +208,16 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
return is_success
def initlizeConfigs(
def initializeConfigs(
self
) -> bool:
is_success = True
for which in ["run", "user"]:
if not self.__config_paths[which]:
self.__config_paths[which] = self.__default_config_paths[which]
if not self.initlizeConfig(which):
if not self.initializeConfig(which):
is_success = False
break
self.initlizeConfigToWidget(which, self.__config_data[which])
self.initializeConfigToWidget(which, self.__config_data[which])
return is_success
@@ -269,27 +250,8 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
) -> dict:
return {
"groups": []
}
def defaultGroup(
self
) -> dict:
return {
"name": "默认分组",
"enabled": True,
"users": []
}
def defaultUsers(
self
) -> dict:
return {
"users": []
"groups": [
]
}
@@ -320,6 +282,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
run_config: dict
):
try:
self.HostUrlEdit.setText(run_config["library"]["host_url"])
self.LoginUrlEdit.setText(run_config["library"]["login_url"])
self.AutoCaptchaCheckBox.setChecked(run_config["login"]["auto_captcha"])
@@ -335,9 +298,25 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.AutoReserveCheckBox.setChecked(run_mode&0x01)
self.AutoCheckinCheckBox.setChecked(run_mode&0x02)
self.AutoRenewalCheckBox.setChecked(run_mode&0x04)
except KeyError as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"运行配置文件读取键 '{e}' 时发生错误 ! :\n"
f"文件路径: {self.__config_paths['run']}\n"
"文件可能被意外修改或已经损坏\n"
)
except Exception as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"运行配置文件读取键 '{e}' 时发生未知错误 ! :\n"
f"文件路径: {self.__config_paths['run']}\n"
"文件可能被意外修改或已经损坏\n"
)
def initilizeUserInfoWidget(
def initializeUserInfoWidget(
self
):
@@ -362,7 +341,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.PreferLateRenewTimeCheckBox.setChecked(False)
def collectUserFromUserInfoWidget(
def collectUserFromWidget(
self
) -> dict:
@@ -395,7 +374,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
return user
def collectUserConfigFromUserTreeWidget(
def collectUsersFromTreeWidget(
self
) -> dict:
@@ -442,13 +421,64 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.ExpectRenewDurationSpinBox.setValue(user["reserve_info"]["renew_time"]["expect_duration"])
self.MaxRenewTimeDiffSpinBox.setValue(user["reserve_info"]["renew_time"]["max_diff"])
self.PreferLateRenewTimeCheckBox.setChecked(not user["reserve_info"]["renew_time"]["prefer_early"])
except:
except KeyError as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
"用户配置文件读取发生错误 !\n"\
f"用户: {user['username']} 配置文件可能已损坏"
f"用户配置文件读取'{e}'发生错误 ! :\n"
f"文件路径: {self.__config_paths['user']}\n"
"文件可能被意外修改或已经损坏\n"
)
except Exception as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"用户配置文件读取键 '{e}' 时发生未知错误 ! :\n"
f"文件路径: {self.__config_paths['user']}\n"
"文件可能被意外修改或已经损坏\n"
)
def setUsersToTreeWidget(
self,
users: dict
):
self.UserTreeWidget.clear()
self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged)
try:
if "groups" in users:
for group_config in users["groups"]:
group_item = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value)
group_item.setText(0, group_config["name"])
group_item.setFlags(group_item.flags() | Qt.ItemIsEditable)
group_item.setCheckState(1, Qt.Checked if group_config.get("enabled", True) else Qt.Unchecked)
for user_config in group_config["users"]:
user_item = QTreeWidgetItem(group_item, ALUserTreeItemType.USER.value)
user_item.setText(0, user_config["username"])
user_item.setText(1, "" if user_config.get("enabled", True) else "跳过")
user_item.setData(0, Qt.UserRole, user_config)
user_item.setCheckState(1, Qt.Checked if user_config.get("enabled", True) else Qt.Unchecked)
user_item.setDisabled(not group_config.get("enabled", True))
group_item.setExpanded(True)
except KeyError as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"用户配置文件读取键 '{e}' 时发生错误 ! :\n"
f"文件路径: {self.__config_paths['user']}\n"
"文件可能被意外修改或已经损坏\n"
)
except Exception as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"用户配置文件读取键 '{e}' 时发生未知错误 ! :\n"
f"文件路径: {self.__config_paths['user']}\n"
"文件可能被意外修改或已经损坏\n"
)
finally:
self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged)
def loadRunConfig(
@@ -459,18 +489,18 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
try:
if not run_config_path or not os.path.exists(run_config_path):
raise Exception("文件路径不存在")
run_config = ConfigReader(run_config_path).getConfigs()
run_config = JSONReader(run_config_path).data()
if run_config and "library" in run_config\
and "web_driver" in run_config\
and "login" in run_config:
return run_config
else:
return None
except Exception as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"运行配置文件读取发生错误 ! : {e}\n"\
f"文件路径: {run_config_path}"
f"运行配置文件读取发生错误 ! :\n{e}"
)
return None
@@ -486,14 +516,13 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
raise Exception("文件路径为空")
if not run_config_data or not isinstance(run_config_data, dict):
raise Exception("运行配置数据为空或类型错误")
ConfigWriter(run_config_path, run_config_data)
JSONWriter(run_config_path, run_config_data)
return True
except Exception as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"配置文件写入发生错误 ! : {e}\n"\
f"文件路径: {run_config_path}"
f"配置文件写入发生错误 ! : \n{e}"
)
return False
@@ -506,11 +535,11 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
try:
if not user_config_path or not os.path.exists(user_config_path):
raise Exception("文件路径不存在")
user_config = ConfigReader(user_config_path).getConfigs()
user_config = JSONReader(user_config_path).data()
if user_config and "groups" in user_config:
return user_config
# compatibility with old version config format
if user_config and "users" in user_config:
elif user_config and "users" in user_config:
user_config = {
"groups": [
{
@@ -521,13 +550,13 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
]
}
return user_config
else:
return None
except Exception as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"用户配置文件读取发生错误 ! : {e}\n"\
f"文件路径: {user_config_path}"
f"用户配置文件读取发生错误 ! :\n{e}"
)
return None
@@ -543,14 +572,13 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
raise Exception("文件路径为空")
if not user_config_data or not isinstance(user_config_data, dict):
raise Exception("用户配置数据为空或类型错误")
ConfigWriter(user_config_path, user_config_data)
JSONWriter(user_config_path, user_config_data)
return True
except Exception as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"用户配置文件写入发生错误 ! : {e}\n"\
f"文件路径: \n{user_config_path}"
f"用户配置文件写入发生错误 ! :\n{e}"
)
return False
@@ -562,7 +590,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
) -> bool:
if user_config_path:
self.__config_data["user"] = self.collectUserConfigFromUserTreeWidget()
self.__config_data["user"] = self.collectUsersFromTreeWidget()
if not self.saveUserConfig(
user_config_path,
self.__config_data["user"]
@@ -601,38 +629,12 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
return True
if user_config is not None:
self.__config_data["user"].update(user_config)
self.fillUserTree(self.__config_data["user"])
self.setUsersToTreeWidget(self.__config_data["user"])
return True
except:
return False
def fillUserTree(
self,
user_config_data: dict
):
self.UserTreeWidget.clear()
self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged)
try:
if "groups" in user_config_data:
for group_config in user_config_data["groups"]:
group_item = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value)
group_item.setText(0, group_config["name"])
group_item.setFlags(group_item.flags() | Qt.ItemIsEditable)
group_item.setCheckState(1, Qt.Checked if group_config.get("enabled", True) else Qt.Unchecked)
for user_config in group_config["users"]:
user_item = QTreeWidgetItem(group_item, ALUserTreeItemType.USER.value)
user_item.setText(0, user_config["username"])
user_item.setText(1, "" if user_config.get("enabled", True) else "跳过")
user_item.setData(0, Qt.UserRole, user_config)
user_item.setCheckState(1, Qt.Checked if user_config.get("enabled", True) else Qt.Unchecked)
user_item.setDisabled(not group_config.get("enabled", True))
group_item.setExpanded(True)
finally:
self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged)
def addGroup(
self,
group_name: str = ""
@@ -650,6 +652,19 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
return group_item
def delGroup(
self,
group_item: QTreeWidgetItem = None
):
if group_item is None:
return
if group_item.type() != ALUserTreeItemType.GROUP.value:
return
index = self.UserTreeWidget.indexOfTopLevelItem(group_item)
self.UserTreeWidget.takeTopLevelItem(index)
def addUser(
self,
group_item: QTreeWidgetItem = None
@@ -721,19 +736,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.UserTreeWidget.setCurrentItem(None)
def delGroup(
self,
group_item: QTreeWidgetItem = None
):
if group_item is None:
return
if group_item.type() != ALUserTreeItemType.GROUP.value:
return
index = self.UserTreeWidget.indexOfTopLevelItem(group_item)
self.UserTreeWidget.takeTopLevelItem(index)
def renameItem(
self,
item: QTreeWidgetItem,
@@ -762,7 +764,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
item.setData(0, Qt.UserRole, user)
self.setUserToWidget(user)
@Slot()
def onShowPasswordCheckBoxChecked(
self,
@@ -818,7 +819,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
# possiblity of frequency edit. we just let the QListWidget
# help us.
if previous and previous.type() == ALUserTreeItemType.USER.value:
user = self.collectUserFromUserInfoWidget()
user = self.collectUserFromWidget()
if user:
self.UsernameEdit.textEdited.disconnect()
user["enabled"] = previous.checkState(1) == Qt.Checked
@@ -826,7 +827,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
previous.setText(1, "" if user.get("enabled", True) else "跳过")
previous.setData(0, Qt.UserRole, user)
if current is None:
self.initilizeUserInfoWidget()
self.initializeUserInfoWidget()
return
if current.type() == ALUserTreeItemType.USER.value:
user = current.data(0, Qt.UserRole)
@@ -834,7 +835,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.setUserToWidget(user)
self.UsernameEdit.textEdited.connect(lambda text: current.setText(0, text))
else:
self.initilizeUserInfoWidget()
self.initializeUserInfoWidget()
@Slot()
def onUserTreeWidgetItemChanged(
@@ -961,9 +962,27 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
)[0]
if run_config_path:
run_config_path = QDir.toNativeSeparators(run_config_path)
if self.loadConfig(run_config_path):
data = self.loadRunConfig(run_config_path)
if data is not None:
self.__config_data["run"].update(data)
self.setRunConfigToWidget(data)
self.__config_paths["run"] = run_config_path
self.CurrentRunConfigEdit.setText(run_config_path)
paths = self.__cfg_mgr.get(ConfigType.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(ConfigType.GLOBAL, "automation.run_path", {"current": index, "paths": paths})
else:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
"运行配置文件读取发生错误 ! :\n"\
"无法从选择的运行配置文件中加载数据 ! :\n"\
"可能选择了错误的配置文件类型"
)
@Slot()
def onBrowseCurrentUserConfigButtonClicked(
@@ -978,9 +997,27 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
)[0]
if user_config_path:
user_config_path = QDir.toNativeSeparators(user_config_path)
if self.loadConfig(user_config_path):
data = self.loadUserConfig(user_config_path)
if data is not None:
self.__config_data["user"].update(data)
self.setUsersToTreeWidget(data)
self.__config_paths["user"] = user_config_path
self.CurrentUserConfigEdit.setText(user_config_path)
paths = self.__cfg_mgr.get(ConfigType.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(ConfigType.GLOBAL, "automation.user_path", {"current": index, "paths": paths})
else:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
"用户配置文件读取发生错误 ! :\n"\
"无法从选择的用户配置文件中加载数据 ! :\n"\
"可能选择了错误的配置文件类型"
)
@Slot()
def onBrowseExportRunConfigButtonClicked(
@@ -1067,9 +1104,9 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
if run_exists or user_exists:
exist_files = []
if run_exists:
exist_files.append(run_config_path)
exist_files.append(f"运行配置文件: \n{run_config_path}")
if user_exists:
exist_files.append(user_config_path)
exist_files.append(f"用户配置文件: \n{user_config_path}")
reply = QMessageBox.information(
self,
"提示 - AutoLibrary",
@@ -1085,8 +1122,8 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
"run": run_config_path,
"user": user_config_path
}
self.initlizeConfigToWidget("run", self.__config_data["run"])
self.initlizeConfigToWidget("user", self.__config_data["user"])
self.initializeConfigToWidget("run", self.__config_data["run"])
self.initializeConfigToWidget("user", self.__config_data["user"])
@Slot()
def onConfirmButtonClicked(
@@ -1103,7 +1140,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
QMessageBox.information(
self,
"提示 - AutoLibrary",
"配置文件保存成功 !\n"
"配置文件保存成功 ! :\n"
f"运行配置文件路径: \n{self.__config_paths['run']}\n"\
f"用户配置文件路径: \n{self.__config_paths['user']}"
)
@@ -1111,7 +1148,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
QMessageBox.warning(
self,
"警告 - AutoLibrary",
"配置文件保存失败, 请检查文件路径权限"
"配置文件保存失败 !\n"
)
self.close()
+49 -46
View File
@@ -7,15 +7,14 @@ 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 sys
import time
import os
import queue
from PySide6.QtCore import (
Qt, Signal, Slot, QDir, QFileInfo, QTimer, QUrl,
Qt, Signal, Slot, QTimer, QDir, QUrl,
)
from PySide6.QtWidgets import (
QMainWindow, QMenu, QSystemTrayIcon
QMainWindow, QMenu, QSystemTrayIcon, QMessageBox
)
from PySide6.QtGui import (
QTextCursor, QCloseEvent, QFont, QIcon, QDesktopServices
@@ -23,17 +22,20 @@ from PySide6.QtGui import (
from base.MsgBase import MsgBase
from utils.ConfigManager import ConfigType, instance
from utils.ConfigManager import getValidateAutomationConfigPaths
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.resources import ALResource
class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
# signal : timer task
timerTaskIsRunning = Signal(dict)
timerTaskIsExecuted = Signal(dict)
timerTaskIsError = Signal(dict)
@@ -44,15 +46,10 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
MsgBase.__init__(self, queue.Queue(), queue.Queue())
QMainWindow.__init__(self)
self.__cfg_mgr = instance()
self.__timer_task_queue = queue.Queue()
script_path = sys.executable
script_dir = QFileInfo(script_path).absoluteDir()
self.__config_paths = {
"run": QDir.toNativeSeparators(script_dir.absoluteFilePath("run.json")),
"user": QDir.toNativeSeparators(script_dir.absoluteFilePath("user.json")),
"timer_task": QDir.toNativeSeparators(script_dir.absoluteFilePath("timer_task.json")),
}
self.__alTimerTaskWidget = None
self.__config_paths = getValidateAutomationConfigPaths()
self.__alTimerTaskManageWidget = None
self.__alConfigWidget = None
self.__auto_lib_thread = None
self.__current_timer_task_thread = None
@@ -77,13 +74,24 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.AboutAction.triggered.connect(self.onAboutActionTriggered)
# initialize timer task widget, but not show it
self.__alTimerTaskWidget = ALTimerTaskManageWidget(self, self.__config_paths["timer_task"])
self.timerTaskIsRunning.connect(self.__alTimerTaskWidget.onTimerTaskIsRunning)
self.timerTaskIsExecuted.connect(self.__alTimerTaskWidget.onTimerTaskIsExecuted)
self.timerTaskIsError.connect(self.__alTimerTaskWidget.onTimerTaskIsError)
self.__alTimerTaskWidget.timerTaskIsReady.connect(self.onTimerTaskIsReady)
self.__alTimerTaskWidget.timerTaskManageWidgetClosed.connect(self.onTimerTaskWidgetClosed)
self.__alTimerTaskWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint)
try:
self.__alTimerTaskManageWidget = ALTimerTaskManageWidget(self)
except Exception as e:
QMessageBox.critical(
self,
"错误 - AutoLibrary",
f"初始化定时任务功能失败: \n{e}"
)
self.__alTimerTaskManageWidget = None
self.TimerTaskManageWidgetButton.setEnabled(False)
self.TimerTaskManageWidgetButton.setToolTip("定时任务功能初始化失败, 请检查配置文件。")
return
self.timerTaskIsRunning.connect(self.__alTimerTaskManageWidget.onTimerTaskIsRunning)
self.timerTaskIsExecuted.connect(self.__alTimerTaskManageWidget.onTimerTaskIsExecuted)
self.timerTaskIsError.connect(self.__alTimerTaskManageWidget.onTimerTaskIsError)
self.__alTimerTaskManageWidget.timerTaskIsReady.connect(self.onTimerTaskIsReady)
self.__alTimerTaskManageWidget.timerTaskManageWidgetIsClosed.connect(self.onTimerTaskManageWidgetClosed)
self.__alTimerTaskManageWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint)
def onAboutActionTriggered(
@@ -116,7 +124,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.TrayMenu = QMenu()
self.TrayMenu.addAction("显示主窗口", self.showNormal)
self.TrayMenu.addAction("显示定时窗口", self.onTimerTaskWidgetButtonClicked)
self.TrayMenu.addAction("显示定时窗口", self.onTimerTaskManageWidgetButtonClicked)
self.TrayMenu.addAction("最小化到托盘", self.hideToTray)
self.TrayMenu.addSeparator()
self.TrayMenu.addAction("退出", self.close)
@@ -154,7 +162,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
):
self.ConfigButton.clicked.connect(self.onConfigButtonClicked)
self.TimerTaskWidgetButton.clicked.connect(self.onTimerTaskWidgetButtonClicked)
self.TimerTaskManageWidgetButton.clicked.connect(self.onTimerTaskManageWidgetButtonClicked)
self.StartButton.clicked.connect(self.onStartButtonClicked)
self.StopButton.clicked.connect(self.onStopButtonClicked)
self.SendButton.clicked.connect(self.onSendButtonClicked)
@@ -173,9 +181,9 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
if self.__is_running_timer_task:
self.__current_timer_task_thread.wait(2000)
self.__current_timer_task_thread.deleteLater()
if self.__alTimerTaskWidget:
self.__alTimerTaskWidget.close()
self.__alTimerTaskWidget.deleteLater()
if self.__alTimerTaskManageWidget:
self.__alTimerTaskManageWidget.close()
self.__alTimerTaskManageWidget.deleteLater()
if self.__alConfigWidget:
self.__alConfigWidget.close()
# the config widget is already deleted in the 'self.onConfigWidgetClosed'
@@ -241,7 +249,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self._output_queue,
self.__config_paths
)
self.__current_timer_task_thread.TimerTaskWorkerIsFinished.connect(self.onTimerTaskFinished)
self.__current_timer_task_thread.timerTaskWorkerIsFinished.connect(self.onTimerTaskFinished)
self.__current_timer_task_thread.start()
except queue.Empty:
self.__is_running_timer_task = False
@@ -276,16 +284,15 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
pass
@Slot()
def onTimerTaskWidgetClosed(
def onTimerTaskManageWidgetClosed(
self
):
self.TimerTaskWidgetButton.setEnabled(True)
self.TimerTaskManageWidgetButton.setEnabled(True)
@Slot(dict)
def onConfigWidgetClosed(
self,
config_paths: dict
self
):
if self.__alConfigWidget:
@@ -293,7 +300,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.__alConfigWidget.deleteLater()
self.__alConfigWidget = None
self.setControlButtons(True, None, None)
self.__config_paths = config_paths
@Slot(dict)
def onTimerTaskIsReady(
@@ -311,7 +317,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
):
self.__current_timer_task_thread.wait(1000)
self.__current_timer_task_thread.TimerTaskWorkerIsFinished.disconnect(self.onTimerTaskFinished)
self.__current_timer_task_thread.timerTaskWorkerIsFinished.disconnect(self.onTimerTaskFinished)
self.__current_timer_task_thread.deleteLater()
self.__current_timer_task_thread = None
self.setControlButtons(None, False, True)
@@ -333,14 +339,14 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.timerTaskIsError.emit(timer_task)
@Slot()
def onTimerTaskWidgetButtonClicked(
def onTimerTaskManageWidgetButtonClicked(
self
):
self.__alTimerTaskWidget.show()
self.__alTimerTaskWidget.raise_()
self.__alTimerTaskWidget.activateWindow()
self.TimerTaskWidgetButton.setEnabled(False)
self.__alTimerTaskManageWidget.show()
self.__alTimerTaskManageWidget.raise_()
self.__alTimerTaskManageWidget.activateWindow()
self.TimerTaskManageWidgetButton.setEnabled(False)
@Slot()
def onConfigButtonClicked(
@@ -348,10 +354,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
):
if self.__alConfigWidget is None:
self.__alConfigWidget = ALConfigWidget(
self,
self.__config_paths
)
self.__alConfigWidget = ALConfigWidget(self)
self.__alConfigWidget.configWidgetIsClosed.connect(self.onConfigWidgetClosed)
self.__alConfigWidget.show()
self.__alConfigWidget.raise_()
@@ -370,8 +373,8 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self._output_queue,
self.__config_paths
)
self.__auto_lib_thread.AutoLibWorkerIsFinished.connect(self.onStopButtonClicked)
self.__auto_lib_thread.AutoLibWorkerFinishedWithError.connect(self.onStopButtonClicked)
self.__auto_lib_thread.autoLibWorkerIsFinished.connect(self.onStopButtonClicked)
self.__auto_lib_thread.autoLibWorkerFinishedWithError.connect(self.onStopButtonClicked)
self.__auto_lib_thread.start()
@Slot()
@@ -383,8 +386,8 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self._showTrace("正在停止操作......")
self.__auto_lib_thread.wait(2000)
self._showTrace("操作已停止")
self.__auto_lib_thread.AutoLibWorkerIsFinished.disconnect(self.onStopButtonClicked)
self.__auto_lib_thread.AutoLibWorkerFinishedWithError.disconnect(self.onStopButtonClicked)
self.__auto_lib_thread.autoLibWorkerIsFinished.disconnect(self.onStopButtonClicked)
self.__auto_lib_thread.autoLibWorkerFinishedWithError.disconnect(self.onStopButtonClicked)
self.__auto_lib_thread.deleteLater()
self.__auto_lib_thread = None
self.setControlButtons(None, False, True)
+14 -14
View File
@@ -17,13 +17,13 @@ from PySide6.QtCore import (
from base.MsgBase import MsgBase
from operators.AutoLib import AutoLib
from utils.ConfigReader import ConfigReader
from utils.JSONReader import JSONReader
class AutoLibWorker(MsgBase, QThread):
AutoLibWorkerIsFinished = Signal()
AutoLibWorkerFinishedWithError = Signal()
autoLibWorkerIsFinished = Signal()
autoLibWorkerFinishedWithError = Signal()
def __init__(
self,
@@ -69,11 +69,11 @@ class AutoLibWorker(MsgBase, QThread):
self._showTrace(
f"正在加载配置文件, 运行配置文件路径: {self.__config_paths["run"]}"
)
self.__run_config = ConfigReader(self.__config_paths["run"]).getConfigs()
self.__run_config = JSONReader(self.__config_paths["run"]).data()
self._showTrace(
f"正在加载配置文件, 用户配置文件路径: {self.__config_paths["user"]}"
)
self.__user_config = ConfigReader(self.__config_paths["user"]).getConfigs()
self.__user_config = JSONReader(self.__config_paths["user"]).data()
if self.__run_config is None or self.__user_config is None:
self._showTrace("配置文件加载失败, 请检查配置文件是否正确")
self._showTrace("配置文件加载失败, 请检查配置文件是否正确")
@@ -116,17 +116,17 @@ class AutoLibWorker(MsgBase, QThread):
)
except Exception as e:
self._showTrace(f"AutoLibrary 运行时发生异常 : {e}")
self.AutoLibWorkerFinishedWithError.emit()
self.autoLibWorkerFinishedWithError.emit()
return
if auto_lib:
auto_lib.close()
self._showTrace("AutoLibrary 运行结束")
self.AutoLibWorkerIsFinished.emit()
self.autoLibWorkerIsFinished.emit()
class TimerTaskWorker(AutoLibWorker):
TimerTaskWorkerIsFinished = Signal(bool, dict)
timerTaskWorkerIsFinished = Signal(bool, dict)
def __init__(
self,
@@ -137,10 +137,10 @@ class TimerTaskWorker(AutoLibWorker):
):
super().__init__(input_queue, output_queue, config_paths)
self.__timer_task = timer_task
self.AutoLibWorkerIsFinished.connect(self.onTimerTaskIsFinished)
self.AutoLibWorkerFinishedWithError.connect(self.onTimerTaskIsError)
self.autoLibWorkerIsFinished.connect(self.onTimerTaskIsFinished)
self.autoLibWorkerFinishedWithError.connect(self.onTimerTaskFinishedWithError)
def run(
self
@@ -150,12 +150,12 @@ class TimerTaskWorker(AutoLibWorker):
super().run()
@Slot()
def onTimerTaskIsError(
def onTimerTaskFinishedWithError(
self
):
self._showTrace(f"定时任务 {self.__timer_task['name']} 运行时发生异常")
self.TimerTaskWorkerIsFinished.emit(True, self.__timer_task)
self.timerTaskWorkerIsFinished.emit(True, self.__timer_task)
@Slot()
def onTimerTaskIsFinished(
@@ -163,4 +163,4 @@ class TimerTaskWorker(AutoLibWorker):
):
self._showTrace(f"定时任务 {self.__timer_task['name']} 运行结束")
self.TimerTaskWorkerIsFinished.emit(False, self.__timer_task)
self.timerTaskWorkerIsFinished.emit(False, self.__timer_task)
+3 -2
View File
@@ -17,12 +17,13 @@ from PySide6.QtWidgets import (
from PySide6.QtGui import (
QCloseEvent
)
from gui.ALSeatMapView import ALSeatMapView
class ALSeatMapSelectDialog(QDialog):
seatMapSelectDialogClosed = Signal(list)
seatMapSelectDialogIsClosed = Signal(list)
def __init__(
self,
@@ -127,7 +128,7 @@ class ALSeatMapSelectDialog(QDialog):
self.reject()
else:
self.accept()
self.seatMapSelectDialogClosed.emit(self.getSelectedSeats())
self.seatMapSelectDialogIsClosed.emit(self.getSelectedSeats())
super().closeEvent(event)
+1
View File
@@ -17,6 +17,7 @@ from PySide6.QtWidgets import (
from PySide6.QtGui import (
QPainter, QWheelEvent
)
from gui.ALSeatFrame import ALSeatFrame
+23 -30
View File
@@ -8,13 +8,14 @@ You may use, modify, and distribute this file under the terms of the MIT License
See the LICENSE file for details.
"""
import os
import sys
import copy
from enum import Enum
from datetime import datetime, timedelta
from PySide6.QtCore import (
Qt, Signal, Slot, QTimer
Qt, Signal, Slot, QTimer, QFileInfo, QDir
)
from PySide6.QtWidgets import (
QDialog, QWidget, QListWidgetItem, QMessageBox,
@@ -24,12 +25,11 @@ from PySide6.QtGui import (
QCloseEvent
)
from utils.ConfigManager import ConfigType, instance
from gui.resources.ui.Ui_ALTimerTaskManageWidget import Ui_ALTimerTaskManageWidget
from gui.ALTimerTaskAddDialog import ALTimerTaskAddDialog, ALTimerTaskStatus
from utils.ConfigReader import ConfigReader
from utils.ConfigWriter import ConfigWriter
class ALTimerTaskItemWidget(QWidget):
@@ -137,20 +137,19 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
timerTaskIsReady = Signal(dict)
timerTasksChanged = Signal()
timerTaskManageWidgetClosed = Signal()
timerTaskManageWidgetIsClosed = Signal()
def __init__(
self,
parent = None,
timer_tasks_config_path: str = ""
parent = None
):
super().__init__(parent)
self.__cfg_mgr = instance()
self.__timer_tasks = []
self.__check_timer = None
self.__sort_policy = self.SortPolicy.BY_EXECUTE_TIME
self.__sort_order = Qt.SortOrder.AscendingOrder
self.__timer_tasks_config_path = timer_tasks_config_path
self.setupUi(self)
self.connectSignals()
@@ -183,28 +182,24 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self
) -> bool:
timer_tasks = self.loadTimerTasks(self.__timer_tasks_config_path)
timer_tasks = self.getTimerTasks()
if timer_tasks is not None:
self.__timer_tasks = timer_tasks
self.timerTasksChanged.emit()
return True
timer_tasks = []
if self.saveTimerTasks(self.__timer_tasks_config_path, copy.deepcopy(timer_tasks)):
if self.setTimerTasks(copy.deepcopy(timer_tasks)):
self.__timer_tasks = timer_tasks
self.updateTimerTaskList()
return True
return False
def loadTimerTasks(
self,
timer_tasks_config_path: str
def getTimerTasks(
self
) -> list:
try:
if not timer_tasks_config_path or not os.path.exists(timer_tasks_config_path):
raise Exception("定时任务配置文件不存在")
timer_tasks = ConfigReader(timer_tasks_config_path).getConfigs()
timer_tasks = self.__cfg_mgr.get(ConfigType.TIMERTASK)
if timer_tasks and "timer_tasks" in timer_tasks:
for task in timer_tasks["timer_tasks"]:
task["add_time"] = datetime.strptime(task["add_time"], "%Y-%m-%d %H:%M:%S")
@@ -212,34 +207,32 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
task["status"] = ALTimerTaskStatus(task["status"])
return timer_tasks["timer_tasks"]
raise Exception("定时任务配置文件格式错误")
except Exception:
except Exception as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"加载定时任务配置发生错误 ! : \n{e}"
)
return None
def saveTimerTasks(
def setTimerTasks(
self,
timer_tasks_config_path: str,
timer_tasks: list
) -> bool:
try:
if not timer_tasks_config_path:
raise Exception("配置文件路径为空")
for task in timer_tasks:
task["add_time"] = task["add_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
ConfigWriter(
timer_tasks_config_path,
{ "timer_tasks": timer_tasks }
)
self.__cfg_mgr.set(ConfigType.TIMERTASK, "", { "timer_tasks": timer_tasks })
return True
except Exception as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"保存定时任务配置发生错误 ! : {e}\n"\
f"文件路径: {timer_tasks_config_path}"
f"保存定时任务配置发生错误 ! : \n{e}"
)
return False
@@ -274,7 +267,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
):
self.hide()
self.timerTaskManageWidgetClosed.emit()
self.timerTaskManageWidgetIsClosed.emit()
event.ignore()
@@ -453,7 +446,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self
):
self.saveTimerTasks(self.__timer_tasks_config_path, copy.deepcopy(self.__timer_tasks))
self.setTimerTasks(copy.deepcopy(self.__timer_tasks))
self.updateTimerTaskList()
self.updateStat()
+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-02-16 07:04:48 UTC
Last updated: 2026-02-26 15:04:28 UTC
"""
AL_VERSION = "1.0.5"
AL_TAG = "v1.0.5"
AL_VERSION = "1.1.0"
AL_TAG = "v1.1.0"
AL_COMMIT_SHA = "local"
AL_COMMIT_DATE = "null" # time zone : UTC
AL_BUILD_DATE = "null" # time zone : UTC
+1 -1
View File
@@ -51,7 +51,7 @@
<number>5</number>
</property>
<item>
<widget class="QPushButton" name="TimerTaskWidgetButton">
<widget class="QPushButton" name="TimerTaskManageWidgetButton">
<property name="minimumSize">
<size>
<width>25</width>
@@ -25,7 +25,7 @@
<property name="windowTitle">
<string>定时任务管理 - AutoLibrary</string>
</property>
<layout class="QVBoxLayout" name="ALTimerTaskWidgetLayout">
<layout class="QVBoxLayout" name="ALTimerTaskManageWidgetLayout">
<property name="spacing">
<number>5</number>
</property>
+2 -1
View File
@@ -252,7 +252,8 @@ class AutoLib(MsgBase):
result = 2
# renewal
if run_mode["auto_renewal"] and result == 2:
if record := self.__lib_checker.canRenew():
can_renew, record = self.__lib_checker.canRenew()
if can_renew:
if self.__lib_renew.renew(username, record, reserve_info):
if self.__lib_checker.postRenewCheck(record):
result = 0
+5 -4
View File
@@ -309,7 +309,7 @@ class LibChecker(LibOperator):
def canRenew(
self
):
) -> tuple[bool, dict]:
# only check the current date
date = time.strftime("%Y-%m-%d", time.localtime())
@@ -326,12 +326,13 @@ class LibChecker(LibOperator):
)
if abs(time_diff_seconds) < 120*60:
self._showTrace(f"{trace_msg}, 可以续约")
return record
return True, record
else:
self._showTrace(f"{trace_msg}, 无法续约")
return None
return False, None # we do not need to return the record, because if current
# time is not available for renewal, the record is not required
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法续约")
return None
return False, None
def postRenewCheck(
-2
View File
@@ -138,9 +138,7 @@ class LibRenew(LibOperator):
abs_diff = abs(actual_diff)
if abs_diff < best_time_diff or (
abs_diff == best_time_diff and (
# 优先选择更早的时间
(prefer_earlier and actual_diff <= 0) or
# 优先选择更晚的时间
(not prefer_earlier and actual_diff >= 0)
)
):
+233
View File
@@ -0,0 +1,233 @@
# -*- 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 threading
from enum import Enum
from typing import Any, Optional
from utils.JSONReader import JSONReader
from utils.JSONWriter import JSONWriter
# 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.
"""
def __init__(
self,
config_type: ConfigType
):
self.__config_type = config_type
def template(
self
) -> dict:
"""
Get config template.
Returns:
dict: Config template.
"""
match self.__config_type:
case ConfigType.GLOBAL:
return {
"automation": {
"run_path": {
"current": 0,
"paths": []
},
"user_path": {
"current": 0,
"paths": []
}
}
}
case ConfigType.BULLETIN:
return {
"bulletin": [],
"last_sync_time": None
}
case ConfigType.TIMERTASK:
return {
"timer_tasks": []
}
case _:
return {}
class ConfigManager:
def __init__(
self,
config_dir: str
):
self.__config_dir = os.path.abspath(config_dir)
self.__config_lock = threading.Lock()
self.__config_data = {}
self.initialize()
def initialize(
self
):
for config_type in ConfigType:
self.load(config_type)
def load(
self,
config_type: ConfigType
):
config_path = os.path.join(self.__config_dir, config_type.value)
if os.path.exists(config_path):
try:
config_data = JSONReader(config_path).data()
self.__config_data[config_type.value] = config_data
return
except:
pass
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 = "",
default: Optional[Any] = None
) -> Any:
with self.__config_lock:
config_data = self.__config_data[config_type.value]
if key == "":
return config_data
keys = 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 = "",
value: Any = None
):
with self.__config_lock:
root_data = self.__config_data[config_type.value]
if key == "":
self.__config_data[config_type.value] = value
else:
keys = 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)
def save(
self,
config_type: ConfigType
):
config_path = os.path.join(self.__config_dir, config_type.value)
JSONWriter(config_path, self.__config_data[config_type.value])
def appDir(
self
) -> str:
return self.__config_dir
_config_manager_instance = None
# Utility function to get config data (thread-safe and validated) from ConfigManager instance.
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.appDir(), 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
def getBaseConfigDir(
) -> str:
return _config_manager_instance.appDir()
# Singleton instance of ConfigManager.
_instance_lock = threading.Lock()
def instance(
config_dir: str = ""
) -> ConfigManager:
"""
Initialize ConfigManager singleton instance.
Args:
config_dir (str): Config directory.
"""
global _config_manager_instance
with _instance_lock:
if _config_manager_instance is None:
_config_manager_instance = ConfigManager(config_dir)
else:
if config_dir == "":
return _config_manager_instance
if _config_manager_instance.appDir() != config_dir:
raise ValueError(
"ConfigManager 的实例已初始化,不能使用不同的配置目录。")
return _config_manager_instance
-115
View File
@@ -1,115 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import json
import copy
from typing import Any
class ConfigReader:
"""
Config reader class.
This class is used to read config file in JSON format.
Args:
config_path (str): The path of config file.
Examples:
>>> print(open("config.json", "r", encoding="utf-8").read())
{
"key1": {
"key2": "value1"
}
}
>>> config_reader = ConfigReader("config.json")
>>> config_reader.get("key1/key2")
"value1"
"""
def __init__(
self,
config_path: str
):
self.__config_path = config_path
self.__config_data = None
self.__readConfig()
def __readConfig(
self
):
try:
with open(self.__config_path, 'r', encoding='utf-8') as file:
self.__config_data = json.load(file)
except FileNotFoundError as e:
raise Exception(f"配置文件不存在: {self.__config_path}") from e
except PermissionError as e:
raise Exception(f"没有足够的权限读取配置文件: {self.__config_path}") from e
except json.JSONDecodeError as e:
raise Exception(f"JSON 解析错误: {self.__config_path}") from e
except Exception as e:
raise Exception(f"读取配置文件时未知错误: {e}") from e
def getConfigs(
self
) -> dict:
return self.__config_data.copy()
def getConfig(
self,
key: str
) -> Any:
config = self.__config_data.get(key, {})
return copy.deepcopy(config)
def get(
self,
key: str,
default: Any = None
) -> Any:
keys = key.split('/')
current = self.__config_data
for k in keys:
if isinstance(current, dict) and k in current:
current = current[k]
else:
return default
return copy.deepcopy(current)
def hasConfig(
self,
key: str
) -> bool:
return self.getConfig(key) != {}
def reReadConfig(
self
) -> bool:
return self.__readConfig()
def configPath(
self
) -> str:
return self.__config_path
-116
View File
@@ -1,116 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import json
from typing import Any
class ConfigWriter:
"""
Config writer class.
This class is used to write config file in JSON format.
Args:
config_path (str): The path of config file.
config_data (dict): The config data to be written.
Examples:
>>> config_data = {
... "key1": {
... "key2": "value1"
... }
... }
>>> config_writer = ConfigWriter("config.json", config_data)
>>> config_writer.set("key1/key2", "value1")
True
>>> print(open("config.json", "r", encoding="utf-8").read())
{
"key1": {
"key2": "value1"
}
}
"""
def __init__(
self,
config_path: str,
config_data: dict
):
self.__config_path = config_path
self.__config_data = config_data.copy() if config_data is not None else {}
self.__writeConfig()
def __writeConfig(
self
):
try:
with open(self.__config_path, "w", encoding="utf-8") as f:
json.dump(self.__config_data, f, indent=4, sort_keys=False)
except PermissionError as e:
raise Exception(f"没有足够的权限写入配置文件: {self.__config_path}") from e
except IOError as e:
raise Exception(f"写入配置文件时发生 IO 错误: {self.__config_path}") from e
except TypeError as e:
raise Exception(f"配置数据包含无法 JSON 序列化的类型: {e}") from e
except Exception as e:
raise Exception(f"写入配置文件时未知错误: {e}") from e
def setConfigs(
self,
configs: dict
) -> bool:
self.__config_data = configs
return self.__writeConfig()
def setConfig(
self,
key: str,
value: dict
) -> bool:
self.__config_data[key] = value
return self.__writeConfig()
def set(
self,
key: str,
value: Any
) -> bool:
keys = key.replace("\\", "/").split("/")
current = self.__config_data
for k in keys[:-1]:
if k not in current or not isinstance(current[k], dict):
current[k] = {}
current = current[k]
current[keys[-1]] = value
return self.__writeConfig()
def reWriteConfig(
self
) -> bool:
return self.__writeConfig()
def configPath(
self
) -> str:
return self.__config_path
+85
View File
@@ -0,0 +1,85 @@
# -*- 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 json
class JSONReader:
"""
JSON reader class.
This class is used to read JSON file.
Args:
json_path (str): The path of JSON file.
Examples:
>>> print(open("config.json", "r", encoding="utf-8").read())
{
"key1": {
"key2": "value1"
}
}
>>> json_reader = JSONReader("config.json")
>>> data = json_reader.data()
>>> data["key1"]["key2"]
"value1"
"""
def __init__(
self,
json_path: str
):
self.__json_path = os.path.abspath(json_path)
self.__json_data = None
self.__read()
def __read(
self
):
try:
with open(self.__json_path, 'r', encoding='utf-8') as file:
self.__json_data = json.load(file)
except FileNotFoundError as e:
raise Exception(f"文件不存在: {self.__json_path}") from e
except PermissionError as e:
raise Exception(f"没有足够的权限读取文件: {self.__json_path}") from e
except json.JSONDecodeError as e:
raise Exception(f"JSON 解析错误: {self.__json_path}") from e
except Exception as e:
raise Exception(f"读取文件时发生未知错误: {e}") from e
def read(
self
) -> bool:
try:
self.__read()
except:
return False
return True
def data(
self
) -> dict:
return self.__json_data.copy()
def path(
self
) -> str:
return self.__json_path
+82
View File
@@ -0,0 +1,82 @@
# -*- 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 json
class JSONWriter:
"""
JSON writer class.
This class is used to write JSON file.
Args:
json_path (str): The path of JSON file.
json_data (dict): The JSON data to be written.
Examples:
>>> json_data = {
... "key1": {
... "key2": "value1"
... }
... }
>>> json_writer = JSONWriter("config.json", json_data)
>>> print(open("config.json", "r", encoding="utf-8").read())
{
"key1": {
"key2": "value1"
}
}
"""
def __init__(
self,
json_path: str,
json_data: dict
):
self.__json_path = os.path.abspath(json_path)
self.__json_data = json_data.copy() if json_data is not None else {}
self.__write()
def __write(
self
):
try:
with open(self.__json_path, "w", encoding="utf-8") as f:
json.dump(self.__json_data, f, indent=4, sort_keys=False)
except PermissionError as e:
raise Exception(f"没有足够的权限写入文件: {self.__json_path}") from e
except IOError as e:
raise Exception(f"写入文件时发生 IO 错误: {self.__json_path}") from e
except TypeError as e:
raise Exception(f"JSON 数据包含无法 JSON 序列化的类型: {e}") from e
except Exception as e:
raise Exception(f"写入文件时发生未知错误: {e}") from e
def write(
self
) -> bool:
try:
self.__write()
except:
return False
return True
def path(
self
) -> str:
return self.__json_path
+3 -2
View File
@@ -2,6 +2,7 @@
Utils module for the AutoLibrary project.
Here are the classes and modules in this package:
- ConfigReader: Configuration reader class for the AutoLibrary project.
- ConfigWriter: Configuration writer class for the AutoLibrary project.
- ConfigManager: Configuration manager class for the AutoLibrary project.
- JSONReader: JSON reader class for the AutoLibrary project.
- JSONWriter: JSON writer class for the AutoLibrary project.
"""