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

Compare commits

..

31 Commits

Author SHA1 Message Date
KenanZhu 1cd39ec84c docs(readme): 更新 “后续会有哪些功能?” 部分,可重复性定时任务功能已完成 2026-03-17 16:08:54 +08:00
Kenan Zhu 73aab7b957 feat(LibCheckin, gui.*): 支持校园网环境下图书馆远程签到;定时任务管理支持可重复性定时任务 (#5)
- feat(LibCheckin): 支持校园网环境下图书馆远程签到 
- feat(TimerUtils): 新增重复任务时间计算工具 
- feat(TimerTask): 新增任务执行历史对话框 
- feat(TimerTaskAddDialog): 添加重复任务 UI 支持 
- feat(TimerTaskManageWidget): 实现重复任务执行与历史记录 
- fix(ALTimerTaskAddDialog): 修改添加定时任务对话框的重复选项的 Label 描述和布局
- fix(ALTimerTaskAddDialog): 修改定时任务时间类型中相对时间控件的布局样式 
- fix(ALTimerTaskAddDialog): 删除定时任务数据中多余的字段 ‘repeat_records’
- fix(ALTimerTaskAddDialog): 修改添加定时任务对话框的重复选项的 Label 描述和布局 
- style(ALTimerTaskManageWidget): 统一 import 语句的格式 
- refactor(ALTimerTaskItemWidget): 一些变量重构
- optimze(gui): 优化删除按钮样式,使其更加醒目;优化 ALTimerTaskManageWidget 的宽度 
- optimize(gui): 优化定时任务管理功能 
- fix(ALTimerTaskManageWidget): 修复删除任务的信号槽参数传递问题
2026-03-17 16:04:59 +08:00
KenanZhu 0a94c344d5 ci(workflows): 修复 Release 工作流的触发条件
- 当创建 'release/v*' 分支时,自动进行 Release 构建

/! Release 流程必须手动创建分支,工作流结束后会将对应分支提交合并
/! 到 main 分支上,且对应分支会被删除
2026-03-17 15:46:32 +08:00
KenanZhu 68e002ba8e fix(ALTimerTaskManageWidget): 修复删除任务的信号槽参数传递问题
- 修复删除任务的信号槽参数传递问题,此次修复通过 lambda 表达式将当前的 task 作为参数传递,避免了闭包陷阱。
2026-03-17 15:27:03 +08:00
KenanZhu 94dc22819f optimize(gui): 优化定时任务管理功能
- 优化任务历史查看对话框的界面布局和交互体验
- 新增任务状态枚举值以支持更完整的状态管理
- 统一重复任务执行后的历史记录处理逻辑
- 增强删除任务时的确认机制,删除可重复任务前展示详细执行记录
- 完善批量清除任务的验证流程,检查运行中任务并确认重复任务删除
2026-03-17 14:51:55 +08:00
KenanZhu d55d2075cb optimze(gui): 优化删除按钮样式,使其更加醒目;优化 ALTimerTaskManageWidget 的宽度
- 优化了 ALConfigWidget, ALTimerTaskManageWidget 中的删除按钮样式(字体颜色更改为红色),使其更加醒目
- 优化了 ALTimerTaskManageWidget 的宽度,使其适应内容宽度
2026-03-17 14:46:19 +08:00
KenanZhu 82744e3a2d refactor(ALTimerTaskItemWidget): 一些变量重构 2026-03-17 14:42:47 +08:00
KenanZhu 67493349dd style(ALTimerTaskManageWidget): 统一 import 语句的格式
- 对 gui.ALTimerTaskAddDialog 的 import 语句进行格式化
2026-03-17 14:42:07 +08:00
KenanZhu 0aea9b1540 fix(ALTimerTaskAddDialog): 修改添加定时任务对话框的重复选项的 Label 描述和布局
- 对 (b73242be00) 的补充提交
2026-03-17 14:39:01 +08:00
KenanZhu c02c6ddbe3 fix(ALTimerTaskAddDialog): 删除定时任务数据中多余的字段 ‘repeat_records’ 2026-03-17 14:37:33 +08:00
KenanZhu c679a1c79e fix(ALTimerTaskAddDialog): 修改定时任务时间类型中相对时间控件的布局样式
- 由栅格布局改为水平布局,该区域的高度与绝对时间控件的高度一致
2026-03-17 14:35:47 +08:00
KenanZhu b73242be00 fix(ALTimerTaskAddDialog): 修改添加定时任务对话框的重复选项的 Label 描述和布局 2026-03-17 14:33:37 +08:00
KenanZhu 9accf5ddc1 ci(workflows): 添加 push 触发器 2026-03-16 21:20:54 +08:00
KenanZhu 883859d1f9 feat(TimerTaskManageWidget): 实现重复任务执行与历史记录
- onTimerTaskIsExecuted/onTimerTaskIsError 添加历史记录
- 历史记录包含:execute_time、executed_time、result、duration
- 重复任务执行后自动计算并更新下次执行时间
2026-03-16 21:17:48 +08:00
KenanZhu f37bcf836b feat(TimerTaskAddDialog): 添加重复任务 UI 支持
- UI 添加重复配置控件:复选框、周一到周日复选框
- 新增 onRepeatCheckBoxToggled 槽函数控制日期选择显示
- getTimerTask 支持提取重复配置(日期、时分秒)
- 调用 TimerUtils 计算首次执行时间
- 重构导入语句格式
2026-03-16 21:16:46 +08:00
KenanZhu b0d1c0e99e feat(TimerTask): 新增任务执行历史对话框
- 新增 ALTimerTaskHistoryDialog 显示重复任务执行历史
- 支持查看执行时间、运行结果、运行耗时
- 提供清空历史记录功能
- 表格显示:执行时间、结果、耗时(秒/s)、uuid
2026-03-16 21:15:56 +08:00
KenanZhu 5af6120be8 feat(TimerUtils): 新增重复任务时间计算工具
- 新增 TimerUtils.calculateNextRepeatTime 方法
- 支持基于重复日期和目标时间计算下次执行时间
- 如果当天在重复日期且目标时间未过,则返回今天;否则查找下一个匹配日期
2026-03-16 21:15:15 +08:00
KenanZhu 60e055f6bb docs(readme): 添加 build-test 状态图标 2026-03-16 17:08:47 +08:00
KenanZhu 01e8100774 feat(LibCheckin): 支持校园网环境下图书馆远程签到
- 新增 __enableCheckinBtn 方法,通过 JavaScript 移除签到按钮的 disabled 属性
- 在检测到签到按钮不可用时,自动尝试启用按钮而非直接失败
- 支持在校园网环境下无需连接图书馆网络即可完成签到
- 优化签到流程的用户提示信息"
2026-03-16 16:55:52 +08:00
Kenan Zhu cf8493565e ci(workflows): 修改一些字符格式 (#2) 2026-03-16 16:40:13 +08:00
KenanZhu 24bb76d039 ci(workflows): 修改一些字符格式 2026-03-16 16:26:34 +08:00
KenanZhu 7111411115 ci(workflows): 优化 CI/CD 工作流配置
- 新增 build-test.yml 用于PR测试构建
- 升级 actions/checkout 和 actions/upload-artifact 到 v6 版本
- 完善 release.yml 的清理流程和摘要输出
2026-03-16 15:59:39 +08:00
KenanZhu 7df6a9157d refactor(LibReserve, LibRenew): 提取时间选择公共逻辑到 LibTimeSelector 基类
将 LibReserve 和 LibRenew 中重复的时间转换和选择逻辑提取到
LibTimeSelector 基类,消除代码重复,提升可维护性。

主要变更:
- 新增 LibTimeSelector 基类,提供时间转换和最佳时间选择算法
- LibReserve 和 LibRenew 继承 LibTimeSelector,移除重复代码
- 拆分过长方法,提升代码可读性
- 修正方法命名 __selectNearstTime -> __selectNearestTime

同时修复续约功能业务逻辑漏洞:
- 新增续约时间上限校验,防止续约时间超过图书馆闭馆时间(23:30)
2026-03-14 14:48:35 +08:00
KenanZhu ebe3910df5 fix(AutoLib): 修复自动预约,签到和续约功能的顺序处理逻辑问题
边缘情况下,即用户当前的预约时间满足签到或者续约的时间范围要求时,预期的处理顺序是先进行预约,再进行签到或者续约。
该提交修复了对这种情况的处理逻辑,确保先进行预约,再进行签到或者续约。
2026-03-10 11:00:01 +08:00
KenanZhu 84367e4abe chore(*): 更新网站地址为 www.autolibrary.kenanzhu.com 2026-03-10 10:59:13 +08:00
KenanZhu 3a50991860 fix(ALMainWindow): 修复程序最小化到托盘图标后,退出菜单异常处理问题 2026-03-10 10:58:27 +08:00
KenanZhu e4482b01da fix(ALMainWindow): 修复托盘图标初始化问题
1. 修复托盘图标初始化上下文菜单的重复调用问题
2. 修复托盘图标初始化忘记更改消息方法的问题
2026-03-05 07:55:36 +08:00
KenanZhu c06e0e05da fix(ALMainWindow): 修复定时任务的消息通知图标与运行状态不一致的问题 2026-03-05 07:54:18 +08:00
KenanZhu ff083884b6 style(utils.ConfigManager): 添加一些注释,并为 getBaseConfigDir 添加文档字符串。 2026-03-04 23:53:31 +08:00
KenanZhu 9ae89b61a4 chore(utils.ConfigManager): 将 ConfigManager 类的 appDir 重命名为 configDir 2026-03-04 23:52:28 +08:00
KenanZhu 2152cc46a3 style(*): 修改 ConfigManager 模块的 import 方式,并移除未使用的 import 语句 2026-03-04 23:52:01 +08:00
23 changed files with 1578 additions and 339 deletions
+221
View File
@@ -0,0 +1,221 @@
name: Build Test
# This workflow builds the application for testing purposes.
# It is triggered when a pull request is opened, synchronized, or reopened against the main branch.
on:
push:
branches:
- main
pull_request:
branches:
- main
types:
- opened
- synchronize
- reopened
# Allow manual trigger for testing
workflow_dispatch:
#
# Build Windows
#
jobs:
build-windows:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
- name: Get version info
id: get_version
run: |
$version = "pr-test"
$tagName = "pr-test"
Write-Host "✓ Mode: Pull Request Test Build"
Write-Host "✓ Tag: $tagName"
Write-Host "✓ Version: $version"
"VERSION=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"TAG_NAME=$tagName" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
shell: pwsh
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.13'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirement.txt
- name: Solve ddddocr compatibility and copy model files
run: |
$ddddocrPath = python -c "import ddddocr, os; print(os.path.dirname(ddddocr.__file__))"
Write-Host "ddddocr package location: $ddddocrPath"
$initFile = Join-Path $ddddocrPath "__init__.py"
if (Test-Path $initFile) {
Write-Host "Fixing ddddocr compatibility in: $initFile"
(Get-Content $initFile) -replace 'Image\.ANTIALIAS', 'Image.Resampling.LANCZOS' | Set-Content $initFile
Write-Host "✓ Fixed: Image.ANTIALIAS -> Image.Resampling.LANCZOS"
} else {
Write-Error "✗ ddddocr __init__.py not found"
exit 1
}
if (-not (Test-Path "models")) {
New-Item -ItemType Directory -Path "models" | Out-Null
Write-Host "✓ Created models directory"
}
$onnxSource = Join-Path $ddddocrPath "common.onnx"
$onnxDest = "models/common.onnx"
if (Test-Path $onnxSource) {
Copy-Item $onnxSource $onnxDest -Force
Write-Host "✓ Copied ONNX model from: $onnxSource"
Write-Host "✓ ONNX model copied to: $onnxDest"
} else {
Write-Error "✗ ONNX model not found in ddddocr package: $onnxSource"
exit 1
}
if (Test-Path $onnxDest) {
$fileSize = (Get-Item $onnxDest).Length / 1MB
Write-Host "✓ Model file verified: $onnxDest (Size: $([math]::Round($fileSize, 2)) MB)"
} else {
Write-Error "✗ Failed to copy model file"
exit 1
}
shell: pwsh
- name: Compile Qt Resource files
run: |
cd batchs
./compile_rc.bat
shell: cmd
- name: Compile Qt UI files
run: |
cd batchs
./compile_ui.bat
shell: cmd
- name: Generate 'Main.spec'
run: |
$version = "${{ steps.get_version.outputs.VERSION }}"
$exeName = "AutoLibrary-$version"
Write-Host "Generating Main.spec for version: $version"
Write-Host "Executable name: $exeName"
$specLines = @(
"# -*- mode: python ; coding: utf-8 -*-"
""
""
"a = Analysis("
" ['src\\Main.py'],"
" pathex=[],"
" binaries=[],"
" datas=["
" ('models\\common.onnx', 'ddddocr'),"
" ('src\\gui\\resources\\icons\\AutoLibrary_32x32.ico', 'gui\\resources\\icons'),"
" ],"
" hiddenimports=[],"
" hookspath=[],"
" hooksconfig={},"
" runtime_hooks=[],"
" excludes=[],"
" noarchive=False,"
" optimize=0,"
")"
"pyz = PYZ(a.pure)"
""
"exe = EXE("
" pyz,"
" a.scripts,"
" name='AutoLibrary',"
" debug=False,"
" bootloader_ignore_signals=False,"
" strip=False,"
" upx=True,"
" upx_exclude=[],"
" runtime_tmpdir=None,"
" console=False,"
" disable_windowed_traceback=False,"
" argv_emulation=False,"
" target_arch=None,"
" codesign_identity=None,"
" 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 (non-single file) generated successfully"
Write-Host "`nGenerated Main.spec ============"
Get-Content "Main.spec" | Write-Host
Write-Host "==================================`n"
shell: pwsh
- name: Build with PyInstaller
run: |
pyinstaller Main.spec
- name: Zip windows release
id: zip_release
run: |
$tagName = "${{ steps.get_version.outputs.TAG_NAME }}"
$version = "${{ steps.get_version.outputs.VERSION }}"
$distDir = "dist/AutoLibrary-$version"
$zipName = "AutoLibrary.$tagName-windows-x86_64.zip"
echo "ZIP_NAME=$zipName" >> $env:GITHUB_OUTPUT
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 "✗ Distribution directory not found: $distDir"
Write-Host "Files in dist directory:"
Get-ChildItem "dist" | ForEach-Object { Write-Host " - $($_.Name)" }
exit 1
}
shell: pwsh
- name: Archive artifacts
uses: actions/upload-artifact@v6
with:
name: AutoLibrary.${{ steps.get_version.outputs.TAG_NAME }}-windows-x86_64
path: |
${{ steps.zip_release.outputs.ZIP_NAME }}
retention-days: 7
- name: Upload build summary
run: |
Write-Host "## Build Test Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
Write-Host "" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "✓ Pull request build test completed successfully!" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Version: ${{ steps.get_version.outputs.VERSION }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Pull Request #${{ github.event.pull_request.number }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Branch: ${{ github.event.pull_request.head.ref }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Event: ${{ github.event_name }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
shell: pwsh
+46 -21
View File
@@ -8,21 +8,19 @@ name: Build
on: on:
workflow_call: workflow_call:
inputs: inputs:
version:
description: 'Version number'
required: true
type: string
tag_name: tag_name:
description: 'Tag name' description: 'Tag name'
required: true required: false
type: string type: string
outputs:
version: version:
description: 'The version number' description: 'Version number'
value: ${{ jobs.build-windows.outputs.version }} required: false
tag_name: type: string
description: 'The tag name' is_test:
value: ${{ jobs.build-windows.outputs.tag_name }} description: 'Whether this is a test build (not a release)'
required: false
type: string
default: 'true'
# #
# Build Windows # Build Windows
@@ -32,18 +30,18 @@ jobs:
build-windows: build-windows:
runs-on: windows-latest runs-on: windows-latest
outputs: outputs:
version: ${{ steps.get_version.outputs.VERSION }}
tag_name: ${{ steps.get_version.outputs.TAG_NAME }} tag_name: ${{ steps.get_version.outputs.TAG_NAME }}
version: ${{ steps.get_version.outputs.VERSION }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
ref: main ref: ${{ github.ref }}
# here we download the build version of ALVersionInfo.py from artifacts # here we download the build version of ALVersionInfo.py from artifacts
# and replace the committed version # and replace the committed version
- name: Download build version of ALVersionInfo.py - name: Download build version of ALVersionInfo.py
uses: actions/download-artifact@v4 uses: actions/download-artifact@v6
with: with:
name: updated-version-info-for-build name: updated-version-info-for-build
path: src/gui/ path: src/gui/
@@ -51,13 +49,27 @@ jobs:
- name: Get version info - name: Get version info
id: get_version id: get_version
run: | run: |
$isTest = "${{ inputs.is_test }}"
if ($isTest -eq "true") {
$version = "test"
$tagName = "test"
Write-Host "✓ Mode: Test Build"
} else {
$version = "${{ inputs.version }}" $version = "${{ inputs.version }}"
$tagName = "${{ inputs.tag_name }}" $tagName = "${{ inputs.tag_name }}"
echo "TAG_NAME=$tagName" >> $env:GITHUB_OUTPUT if ([string]::IsNullOrEmpty($version)) {
echo "VERSION=$version" >> $env:GITHUB_OUTPUT $version = "test"
$tagName = "test"
Write-Host "✓ Mode: Independent Build (No inputs provided)"
}
}
Write-Host "✓ Tag: $tagName" Write-Host "✓ Tag: $tagName"
Write-Host "✓ Version: $version" Write-Host "✓ Version: $version"
"VERSION=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"TAG_NAME=$tagName" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
shell: pwsh shell: pwsh
- name: Verify 'ALVersionInfo.py' was updated - name: Verify 'ALVersionInfo.py' was updated
@@ -70,7 +82,7 @@ jobs:
shell: pwsh shell: pwsh
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v6
with: with:
python-version: '3.13' python-version: '3.13'
@@ -210,7 +222,7 @@ jobs:
$distDir = "dist/AutoLibrary-$version" $distDir = "dist/AutoLibrary-$version"
$zipName = "AutoLibrary.$tagName-windows-x86_64.zip" $zipName = "AutoLibrary.$tagName-windows-x86_64.zip"
echo "ZIP_PATH=$zipName" >> $env:GITHUB_OUTPUT echo "ZIP_NAME=$zipName" >> $env:GITHUB_OUTPUT
Write-Host "Looking for distribution directory: $distDir" Write-Host "Looking for distribution directory: $distDir"
if (Test-Path $distDir) { if (Test-Path $distDir) {
@@ -225,8 +237,21 @@ jobs:
shell: pwsh shell: pwsh
- name: Archive artifacts - name: Archive artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: AutoLibrary.${{ steps.get_version.outputs.TAG_NAME }}-windows-x86_64 name: AutoLibrary.${{ steps.get_version.outputs.TAG_NAME }}-windows-x86_64
path: | path: |
${{ steps.zip_release.outputs.ZIP_PATH }} ${{ steps.zip_release.outputs.ZIP_NAME }}
retention-days: ${{ github.event_name != 'workflow_call' && 7 || 90 }}
- name: Upload build summary
if: ${{ github.event_name != 'workflow_call' }}
run: |
Write-Host "## Build Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
Write-Host "" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "✓ Build test completed successfully!" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Version: ${{ steps.get_version.outputs.VERSION }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Event: ${{ github.event_name }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Ref: ${{ github.ref }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
shell: pwsh
+57 -13
View File
@@ -1,7 +1,7 @@
name: Commit Release name: Commit Release
# This workflow commits version changes in 'ALVersionInfo.py' (get from artifacts) and # This workflow commits version changes in 'ALVersionInfo.py' (get from artifacts) and
# moves the release tag to this new release commit. # creates/moves the release tag to this new release commit.
# #
# It is triggered when called by the release workflow. # It is triggered when called by the release workflow.
@@ -9,7 +9,7 @@ on:
workflow_call: workflow_call:
inputs: inputs:
tag_name: tag_name:
description: 'Tag name to move (e.g., v1.0.0)' description: 'Tag name to create/move (e.g., v1.0.0 or v1.0.0-rc1)'
required: true required: true
type: string type: string
version: version:
@@ -20,10 +20,33 @@ on:
description: 'File path to commit' description: 'File path to commit'
required: true required: true
type: string type: string
create_tag:
description: 'Whether to create new tag (true) or move existing tag (false)'
required: false
type: string
default: 'false'
is_rc:
description: 'Whether this is a release candidate (pre-release)'
required: false
type: string
default: 'false'
ref:
description: 'Git ref to checkout (release branch)'
required: true
type: string
outputs: outputs:
tag_name:
description: 'The tag name created/moved'
value: ${{ inputs.tag_name }}
version:
description: 'Version number for commit message'
value: ${{ inputs.version }}
new_commit_sha: new_commit_sha:
description: 'The new commit SHA after moving the tag' description: 'The new commit SHA after creating/moving the tag'
value: ${{ jobs.commit-release.outputs.new_commit_sha }} value: ${{ jobs.commit-release.outputs.new_commit_sha }}
branch_name:
description: 'The branch name where the commit was made'
value: ${{ jobs.commit-release.outputs.branch_name }}
jobs: jobs:
commit-release: commit-release:
@@ -32,18 +55,19 @@ jobs:
contents: write contents: write
outputs: outputs:
new_commit_sha: ${{ steps.commit_info.outputs.commit_sha }} new_commit_sha: ${{ steps.commit_info.outputs.commit_sha }}
branch_name: ${{ steps.push_release.outputs.branch_name }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
ref: main ref: ${{ inputs.ref }}
fetch-depth: 0 fetch-depth: 0
# here we download the commit version of ALVersionInfo.py from artifacts # here we download the commit version of ALVersionInfo.py from artifacts
# and replace the original file with it. # and replace the original file with it.
- name: Download commit version of ALVersionInfo.py - name: Download commit version of ALVersionInfo.py
uses: actions/download-artifact@v4 uses: actions/download-artifact@v6
with: with:
name: updated-version-info-for-commit name: updated-version-info-for-commit
path: downloaded-file/ path: downloaded-file/
@@ -81,18 +105,38 @@ jobs:
git commit -m "chore(release): v${VERSION} [auto release commit]" git commit -m "chore(release): v${VERSION} [auto release commit]"
echo "✓ Changes committed" echo "✓ Changes committed"
- name: Push to main branch - name: Push to release branch
id: push_release
run: | run: |
MAIN_BRANCH=$(git remote show origin | grep 'HEAD branch' | cut -d' ' -f5) # Get the release branch name from the input ref
if [ -z "$MAIN_BRANCH" ]; then BRANCH_NAME=$(echo "${{ inputs.ref }}" | sed 's|refs/heads/||')
MAIN_BRANCH="main"
if [ -z "$BRANCH_NAME" ]; then
echo "✗ Error: Could not determine branch name from ref: ${{ inputs.ref }}"
exit 1
fi fi
echo "Pushing to branch: ${MAIN_BRANCH}" echo "Pushing to branch: ${BRANCH_NAME}"
git push origin HEAD:${MAIN_BRANCH} git push origin HEAD:${BRANCH_NAME}
echo "✓ Changes pushed to ${MAIN_BRANCH}" echo "✓ Changes pushed to ${BRANCH_NAME}"
# Output branch name for downstream jobs
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
- name: Create tag for release
if: ${{ inputs.create_tag == 'true' }}
run: |
TAG_NAME="${{ inputs.tag_name }}"
IS_RC="${{ inputs.is_rc }}"
echo "Creating new tag ${TAG_NAME} at this commit..."
echo "Release type: $([ "$IS_RC" = "true" ] && echo "Release Candidate (Pre-release)" || echo "Stable Release")"
git tag ${TAG_NAME}
git push origin ${TAG_NAME}
echo "✓ Tag ${TAG_NAME} created at commit $(git rev-parse --short HEAD)"
- name: Move tag to new release commit - name: Move tag to new release commit
if: ${{ inputs.create_tag != 'true' }}
run: | run: |
TAG_NAME="${{ inputs.tag_name }}" TAG_NAME="${{ inputs.tag_name }}"
+199 -21
View File
@@ -1,36 +1,48 @@
name: Release name: Release
# This workflow automates the complete release process for AutoLibrary application # This workflow automates the complete release process for AutoLibrary application
# It is triggered when a new version tag (vX.Y.Z) is pushed to the repository # It is triggered when a new release branch is created (release/vX.Y.Z or release/vX.Y.Z-rc*)
# #
# Workflow Steps: # Workflow Steps:
# START > # START >
# 1. Update Version: # 1. Extract Version:
# Extracts version number from branch name:
# - release/v1.0.0 -> v1.0.0 (stable release)
# - release/v1.0.0-rc1 -> v1.0.0 (release candidate)
# 2. Update Version:
# Updates version information in 'ALVersionInfo.py' with build metadata and archives # Updates version information in 'ALVersionInfo.py' with build metadata and archives
# the updated version file as an artifact. # the updated version file as an artifact.
# #
# for more information, please refer to the comment in the workflow 'update-version.yml' # for more information, please refer to the comment in the workflow 'update-version.yml'
# 2. Commit Release: # 3. Commit Release:
# Commits version changes and moves the release tag to this new release commit. # Commits version changes to release branch and creates the release tag.
# 3. Build: # 4. Build:
# Compiles the application for Windows platform using PyInstaller, and # Compiles the application for Windows platform using PyInstaller, and
# archives the built artifacts as 'AutoLibrary.<tag_name>-windows-x86_64.zip'. # archives the built artifacts as 'AutoLibrary.<tag_name>-windows-x86_64.zip'.
# 4. Release: # 5. Release:
# Creates GitHub release with generated artifacts and release notes # Creates GitHub release with generated artifacts and release notes
# < END # < END
# #
# 6. Merge back:
# Merges release branch back to main branch, and clean/delete the release branch
#
# The workflow ensures version consistency between source code, built artifacts, and GitHub releases # The workflow ensures version consistency between source code, built artifacts, and GitHub releases
# while maintaining proper commit history and tag management. # while maintaining proper commit history and tag management.
#
# IMPORTANT: This workflow only triggers on branch CREATION, not on pushes to release branches.
# If you need to fix issues on a release branch, delete the tag, merge fixes to main,
# and create a new release branch.
on: on:
push: push:
tags: branches:
- 'v[0-9]+.[0-9]+.[0-9]+' - 'release/v*'
jobs: jobs:
# #
@@ -44,6 +56,62 @@ jobs:
- name: Start release - name: Start release
run: | run: |
echo "✓ Starting release" echo "✓ Starting release"
echo "Branch: ${{ github.ref_name }}"
echo "Ref: ${{ github.ref }}"
#
# Extract version :
# this job extracts version from branch name
#
extract-version:
needs:
- start
runs-on: ubuntu-latest
outputs:
tag_name: ${{ steps.extract.outputs.tag_name }}
version: ${{ steps.extract.outputs.version }}
is_rc: ${{ steps.extract.outputs.is_rc }}
steps:
- name: Extract version from branch name
id: extract
run: |
BRANCH_NAME="${{ github.ref_name }}"
# Validate branch name starts with 'release/v'
if ! echo "$BRANCH_NAME" | grep -qE '^release/v'; then
echo "✗ Error: Branch '$BRANCH_NAME' does not start with 'release/v'"
echo "✗ This workflow should only be triggered by release branches"
exit 1
fi
# Extract version from branch name:
# - release/v1.0.0 -> v1.0.0 (stable)
# - release/v1.0.0-rc1 -> v1.0.0 (release candidate)
# - release/v1.0.0-alpha.1 -> v1.0.0-alpha.1 (pre-release)
if echo "$BRANCH_NAME" | grep -qE '^release/v[0-9]+\.[0-9]+\.[0-9]+$'; then
# Stable release: release/v1.0.0 -> v1.0.0
TAG_NAME=$(echo "$BRANCH_NAME" | sed -E 's|^release/(v[0-9]+\.[0-9]+\.[0-9]+)$|\1|')
IS_RC=false
elif echo "$BRANCH_NAME" | grep -qE '^release/v[0-9]+\.[0-9]+\.[0-9]+-'; then
# Pre-release: release/v1.0.0-rc1 -> v1.0.0-rc1
TAG_NAME=$(echo "$BRANCH_NAME" | sed -E 's|^release/(v[0-9]+\.[0-9]+\.[0-9]+-.*)$|\1|')
IS_RC=true
else
echo "✗ Error: Branch '$BRANCH_NAME' does not match expected format"
echo "✗ Expected format: release/vX.Y.Z or release/vX.Y.Z-rcX"
exit 1
fi
VERSION="${TAG_NAME#v}"
echo "TAG_NAME=$TAG_NAME" >> $GITHUB_OUTPUT
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "IS_RC=$IS_RC" >> $GITHUB_OUTPUT
echo "✓ Branch: $BRANCH_NAME"
echo "✓ Tag: $TAG_NAME"
echo "✓ Version: $VERSION"
echo "✓ Is RC: $IS_RC"
# #
# Update version : # Update version :
@@ -52,31 +120,35 @@ jobs:
update-version: update-version:
needs: needs:
- start - extract-version
uses: ./.github/workflows/update-version.yml uses: ./.github/workflows/update-version.yml
permissions: permissions:
contents: write contents: write
with: with:
tag_name: ${{ github.ref_name }} tag_name: ${{ needs.extract-version.outputs.tag_name }}
ref: ${{ github.ref }} ref: ${{ github.ref }}
# #
# Commit release : # Commit release :
# this job commits the updated version file and move the release # this job commits the updated version file to main and creates
# tag to this new commit # the release tag (not moving an existing tag)
# #
commit-release: commit-release:
needs: needs:
- extract-version
- update-version - update-version
if: ${{ needs.update-version.outputs.has_changes == 'true' }} if: ${{ needs.update-version.outputs.has_changes == 'true' }}
uses: ./.github/workflows/commit-release.yml uses: ./.github/workflows/commit-release.yml
permissions: permissions:
contents: write contents: write
with: with:
tag_name: ${{ needs.update-version.outputs.tag_name }} tag_name: ${{ needs.extract-version.outputs.tag_name }}
version: ${{ needs.update-version.outputs.version }} version: ${{ needs.extract-version.outputs.version }}
file_path: src/gui/ALVersionInfo.py file_path: src/gui/ALVersionInfo.py
create_tag: 'true'
is_rc: ${{ needs.extract-version.outputs.is_rc }}
ref: ${{ github.ref }}
# #
# Build : # Build :
@@ -91,8 +163,9 @@ jobs:
permissions: permissions:
contents: write contents: write
with: with:
version: ${{ needs.update-version.outputs.version }}
tag_name: ${{ needs.update-version.outputs.tag_name }} tag_name: ${{ needs.update-version.outputs.tag_name }}
version: ${{ needs.update-version.outputs.version }}
is_test: 'false'
# #
# Release : # Release :
@@ -102,27 +175,28 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- build - build
- extract-version
if: always() && needs.build.result == 'success' if: always() && needs.build.result == 'success'
permissions: permissions:
contents: write contents: write
steps: steps:
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v6
with: with:
name: AutoLibrary.${{ needs.build.outputs.tag_name }}-windows-x86_64 name: AutoLibrary.${{ needs.extract-version.outputs.tag_name }}-windows-x86_64
path: artifacts/ path: artifacts/
- name: Create release - name: Create release
id: create_release id: create_release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: ${{ needs.build.outputs.tag_name }} tag_name: ${{ needs.extract-version.outputs.tag_name }}
name: AutoLibrary ${{ needs.build.outputs.tag_name }} name: AutoLibrary ${{ needs.extract-version.outputs.tag_name }}
files: | files: |
artifacts/AutoLibrary.${{ needs.build.outputs.tag_name }}-windows-x86_64.zip artifacts/AutoLibrary.${{ needs.extract-version.outputs.tag_name }}-windows-x86_64.zip
draft: false draft: false
prerelease: false prerelease: ${{ needs.extract-version.outputs.is_rc == 'true' }}
generate_release_notes: true generate_release_notes: true
body: | body: |
--- ---
@@ -142,3 +216,107 @@ jobs:
- name: End release - name: End release
run: | run: |
echo "✓ Ending release" echo "✓ Ending release"
#
# Merge Back :
# this job merges the release branch to main after successful release
#
merge-back:
needs:
- release
- extract-version
- commit-release
if: ${{ needs.release.result == 'success' && needs.commit-release.result == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Merge release branch to main
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Use the release branch name from the original trigger
BRANCH_NAME="${{ needs.extract-version.outputs.tag_name }}"
# Extract branch name: v1.0.0 -> release/v1.0.0
if [[ ! "$BRANCH_NAME" =~ ^release/ ]]; then
BRANCH_NAME="release/${BRANCH_NAME}"
fi
MAIN_BRANCH=$(git remote show origin | grep 'HEAD branch' | cut -d' ' -f5)
if [ -z "$MAIN_BRANCH" ]; then
MAIN_BRANCH="main"
fi
echo "Merging ${BRANCH_NAME} to ${MAIN_BRANCH}..."
echo "Current commit info:"
git log --oneline -3
# Fetch all branches including the release branch
git fetch origin ${BRANCH_NAME}
git fetch origin ${MAIN_BRANCH}
# Checkout main branch
git checkout ${MAIN_BRANCH}
# Show branch status before merge
echo "=== Branch status before merge ==="
git log --oneline --graph --all -5
echo "=== Diff between ${MAIN_BRANCH} and origin/${BRANCH_NAME} ==="
git diff ${MAIN_BRANCH} origin/${BRANCH_NAME} --stat || echo "No differences found"
# Force create a merge commit even if there are no changes
# This ensures the release history is properly recorded
git merge origin/${BRANCH_NAME} \
--no-ff \
-m "chore(release): merge ${BRANCH_NAME} to ${MAIN_BRANCH} [auto release commit]"
# Show merge result
echo "=== Merge result ==="
git log --oneline --graph -3
# Push to main
git push origin ${MAIN_BRANCH}
echo "✓ Successfully merged ${BRANCH_NAME} to ${MAIN_BRANCH}"
- name: Delete release branch
run: |
BRANCH_NAME="${{ needs.extract-version.outputs.tag_name }}"
# Extract branch name: v1.0.0 -> release/v1.0.0
if [[ ! "$BRANCH_NAME" =~ ^release/ ]]; then
BRANCH_NAME="release/${BRANCH_NAME}"
fi
echo "Deleting release branch: ${BRANCH_NAME}"
git push origin --delete ${BRANCH_NAME}
echo "✓ Deleted branch ${BRANCH_NAME}"
- name: Release cleanup summary
run: |
BRANCH_NAME="${{ github.ref_name }}"
TAG_NAME="${{ needs.extract-version.outputs.tag_name }}"
MAIN_BRANCH=$(git remote show origin | grep 'HEAD branch' | cut -d' ' -f5)
if [ -z "$MAIN_BRANCH" ]; then
MAIN_BRANCH="main"
fi
echo "## Release Cleanup Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "✓ Release completed successfully!" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Actions Performed:" >> $GITHUB_STEP_SUMMARY
echo "- Merged \`${BRANCH_NAME}\` to \`${MAIN_BRANCH}\`" >> $GITHUB_STEP_SUMMARY
echo "- Deleted release branch \`${BRANCH_NAME}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Release Details:" >> $GITHUB_STEP_SUMMARY
echo "- Tag: \`${TAG_NAME}\`" >> $GITHUB_STEP_SUMMARY
echo "- Version: \`${{ needs.extract-version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
echo "- Release Type: $([ "${{ needs.extract-version.outputs.is_rc }}" = "true" ] && echo "Release Candidate" || echo "Stable Release")" >> $GITHUB_STEP_SUMMARY
+3 -3
View File
@@ -42,7 +42,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
ref: ${{ inputs.ref }} ref: ${{ inputs.ref }}
fetch-depth: 0 fetch-depth: 0
@@ -148,7 +148,7 @@ jobs:
- name: Upload modified ALVersionInfo.py ready for build - name: Upload modified ALVersionInfo.py ready for build
if: steps.check_changes.outputs.has_changes == 'true' if: steps.check_changes.outputs.has_changes == 'true'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: updated-version-info-for-build name: updated-version-info-for-build
path: src/gui/temp/ALVersionInfo.py path: src/gui/temp/ALVersionInfo.py
@@ -156,7 +156,7 @@ jobs:
- name: Upload modified ALVersionInfo.py ready for commit - name: Upload modified ALVersionInfo.py ready for commit
if: steps.check_changes.outputs.has_changes == 'true' if: steps.check_changes.outputs.has_changes == 'true'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: updated-version-info-for-commit name: updated-version-info-for-commit
path: src/gui/ALVersionInfo.py path: src/gui/ALVersionInfo.py
+1 -1
View File
@@ -1,3 +1,3 @@
This folder is used to store the manuals. This folder is used to store the manuals.
Our manuals are available at https://www.autolibrary.top/manuals Our manuals are available at https://www.autolibrary.kenanzhu.com/manuals
+7 -6
View File
@@ -6,11 +6,12 @@
[![GitHub stars](https://img.shields.io/github/stars/KenanZhu/AutoLibrary.svg?style=social&label=Star)](https://github.com/KenanZhu/AutoLibrary) [![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) ![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) [![Build](https://img.shields.io/github/actions/workflow/status/KenanZhu/AutoLibrary/build-test.yml?label=build-test&logo=github-actions&logoColor=white)](https://github.com/KenanZhu/AutoLibrary/actions/workflows/build-test.yml)
[![Release](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) [![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) ![Downloads](https://img.shields.io/github/downloads/KenanZhu/AutoLibrary/total?label=downloads)
了解更多请访问 [_AutoLibrary 网站_](http://www.autolibrary.top) 了解更多请访问 [_AutoLibrary 网站_](http://www.autolibrary.kenanzhu.com)
--- ---
@@ -22,18 +23,18 @@
4. 批量操作 - 支持同时预约多个用户,可以指定当前需要跳过的用户,并将用户分成多个组 4. 批量操作 - 支持同时预约多个用户,可以指定当前需要跳过的用户,并将用户分成多个组
5. 定时任务 - 使用内置定时任务管理,添加定时任务,指定时间后按当前预约信息自动运行 5. 定时任务 - 使用内置定时任务管理,添加定时任务,指定时间后按当前预约信息自动运行
*1,2,3 的具体操作方法和注意事项请访问我们的 [帮助手册](https://www.autolibrary.top/manuals)* *1,2,3 的具体操作方法和注意事项请访问我们的 [帮助手册](https://www.autolibrary.kenanzhu.com/manuals)*
### 如何使用 ### 如何使用
1. 下载最新版本的 [AutoLibrary 压缩包](https://github.com/KenanZhu/AutoLibrary/releases/latest)。 1. 下载最新版本的 [AutoLibrary 压缩包](https://github.com/KenanZhu/AutoLibrary/releases/latest)。
2. 解压下载的文件到任意目录。 2. 解压下载的文件到任意目录。
3. 下载对应浏览器类型和版本(具体操作请参考适用软件版本的 [帮助手册](https://www.autolibrary.top/manuals))的驱动文件,并在配置界面的运行配置选项卡对应位置选择你下载好的浏览器驱动。 3. 下载对应浏览器类型和版本(具体操作请参考适用软件版本的 [帮助手册](https://www.autolibrary.kenanzhu.com/manuals))的驱动文件,并在配置界面的运行配置选项卡对应位置选择你下载好的浏览器驱动。
4. 运行 `AutoLibrary-[主版本号].[次版本号].[修订版本号].Z.exe` 文件 (如 `AutoLibrary-1.0.0.exe`)。 4. 运行 `AutoLibrary-[主版本号].[次版本号].[修订版本号].Z.exe` 文件 (如 `AutoLibrary-1.0.0.exe`)。
5. 点击 [配置] 按钮,在配置界面填写好预约信息和运行配置后,点击 [确认] 按钮。 5. 点击 [配置] 按钮,在配置界面填写好预约信息和运行配置后,点击 [确认] 按钮。
6. 点击 [启动脚本] 按钮,即可开始自动预约、续约、签到等操作。 6. 点击 [启动脚本] 按钮,即可开始自动预约、续约、签到等操作。
*注意 1*: 关于浏览器驱动的下载和其它相关问题,请参考我们的 [帮助手册](https://www.autolibrary.top/manuals) 中对应软件版本的内容。 *注意 1*: 关于浏览器驱动的下载和其它相关问题,请参考我们的 [帮助手册](https://www.autolibrary.kenanzhu.com/manuals) 中对应软件版本的内容。
#### 平台支持 & 编译步骤 #### 平台支持 & 编译步骤
@@ -102,7 +103,7 @@ def classification(self, img: bytes):
当前版本的功能对于正常使用已经足够,不过后续会着重完善预约时的使用体验,暂时有以下构想: 当前版本的功能对于正常使用已经足够,不过后续会着重完善预约时的使用体验,暂时有以下构想:
- 引入交互预约面板功能,预约时直接在座位分布图中选择可用座位,并按用户分配,无需事先配置预约信息。 - 引入交互预约面板功能,预约时直接在座位分布图中选择可用座位,并按用户分配,无需事先配置预约信息。
- 优化定时任务管理功能,用户可以在定时任务管理界面设置重复运行的定时任务,如每日预约、每周预约等。 - ~~优化定时任务管理功能,用户可以在定时任务管理界面设置重复运行的定时任务,如每日预约、每周预约等。~~ (已完成)
- 软件的自动更新以及公告栏功能,用户可以自动更新最新版本并获取最新公告事项。 - 软件的自动更新以及公告栏功能,用户可以自动更新最新版本并获取最新公告事项。
不过由于本人的时间和能力有限,也需要考虑到图书馆的正常运行,所以后续功能会有所取舍,但也许会进行一些小的功能验证。 不过由于本人的时间和能力有限,也需要考虑到图书馆的正常运行,所以后续功能会有所取舍,但也许会进行一些小的功能验证。
+135
View File
@@ -0,0 +1,135 @@
# -*- 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 queue
from base.LibOperator import LibOperator
class LibTimeSelector(LibOperator):
"""
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.
"""
def __init__(
self,
input_queue: queue.Queue,
output_queue: queue.Queue
):
super().__init__(input_queue, output_queue)
@staticmethod
def _timeToMins(
time_str: str
) -> int:
"""
Convert time string "HH:MM" to minutes since midnight.
"""
hour, minute = map(int, time_str.split(":"))
return hour*60 + minute
@staticmethod
def _minsToTime(
mins: int
) -> str:
"""
Convert minutes since midnight to time string "HH:MM".
"""
hour, minute = divmod(mins, 60)
return f"{hour:02d}:{minute:02d}"
def _formatTimeRelation(
self,
abs_diff: int,
actual_diff: int,
time_type: str
) -> str:
"""
Format time difference relation string.
"""
if actual_diff < 0:
return f"早了 {abs_diff} 分钟"
elif actual_diff > 0:
return f"晚了 {abs_diff} 分钟"
else:
return f"正好等于 {time_type}"
def _findBestTimeOption(
self,
time_options: list,
target_time: int,
max_time_diff: int,
prefer_earlier: bool,
is_reserve: bool = True
) -> tuple:
"""
Find the best time option from available times.
Args:
time_options: List of WebElement time options
target_time: Target time in minutes
max_time_diff: Maximum acceptable time difference in minutes
prefer_earlier: If True, prefer earlier times when diffs are equal
is_reserve: If True, parse 'time' attribute; if False, parse 'id' attribute
Returns:
Tuple of (best_time_element, best_time_text, actual_diff, free_times_list)
or (None, None, None, []) if no suitable option found
"""
free_times = []
best_time_diff = max_time_diff
best_actual_diff = None
best_time_opt = None
for time_opt in time_options:
# Parse time value based on context
if is_reserve:
time_attr = time_opt.get_attribute("time")
if time_attr == "now":
from datetime import datetime
now = datetime.now()
time_val = now.hour * 60 + now.minute
elif time_attr and time_attr.isdigit():
time_val = int(time_attr)
else:
continue
else:
# Renewal context: parse 'id' attribute
time_attr = time_opt.get_attribute("id")
if not (time_attr and time_attr.isdigit()):
continue
time_val = int(time_attr)
free_times.append(time_opt.text.strip() if not is_reserve else self._minsToTime(time_val))
actual_diff = time_val - target_time
abs_diff = abs(actual_diff)
# Update best option if current is better
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)))):
best_time_diff = abs_diff
best_actual_diff = actual_diff
best_time_opt = time_opt
if best_time_opt is not None:
return (best_time_opt, best_time_opt.text.strip(), best_actual_diff, free_times)
return (None, None, None, free_times)
+1 -1
View File
@@ -82,7 +82,7 @@ System architecture: {os_info['architecture']}<br>
<h4>Project Information:</h4> <h4>Project Information:</h4>
License: MIT License<br> License: MIT License<br>
Project repository: <a href="https://www.github.com/KenanZhu/AutoLibrary" style="text-decoration: none;">https://www.github.com/KenanZhu/AutoLibrary</a><br> Project repository: <a href="https://www.github.com/KenanZhu/AutoLibrary" style="text-decoration: none;">https://www.github.com/KenanZhu/AutoLibrary</a><br>
Project website: <a href="https://www.autolibrary.top" style="text-decoration: none;">https://www.autolibrary.top</a><br> Project website: <a href="https://www.autolibrary.kenanzhu.com" style="text-decoration: none;">https://www.autolibrary.kenanzhu.com</a><br>
<h4>Author Information:</h4> <h4>Author Information:</h4>
Developer: KenanZhu<br> Developer: KenanZhu<br>
+8 -8
View File
@@ -21,10 +21,10 @@ from PySide6.QtGui import (
QCloseEvent, QAction QCloseEvent, QAction
) )
import utils.ConfigManager as ConfigManager
from utils.JSONReader import JSONReader from utils.JSONReader import JSONReader
from utils.JSONWriter import JSONWriter 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.resources.ui.Ui_ALConfigWidget import Ui_ALConfigWidget
from gui.ALSeatMapSelectDialog import ALSeatMapSelectDialog from gui.ALSeatMapSelectDialog import ALSeatMapSelectDialog
@@ -42,8 +42,8 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
): ):
super().__init__(parent) super().__init__(parent)
self.__cfg_mgr = instance() self.__cfg_mgr = ConfigManager.instance()
self.__config_paths = getValidateAutomationConfigPaths() self.__config_paths = ConfigManager.getValidateAutomationConfigPaths()
self.__config_data = {"run": {}, "user": {}} self.__config_data = {"run": {}, "user": {}}
self.setupUi(self) self.setupUi(self)
@@ -968,13 +968,13 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.setRunConfigToWidget(data) self.setRunConfigToWidget(data)
self.__config_paths["run"] = run_config_path self.__config_paths["run"] = run_config_path
self.CurrentRunConfigEdit.setText(run_config_path) self.CurrentRunConfigEdit.setText(run_config_path)
paths = self.__cfg_mgr.get(ConfigType.GLOBAL, "automation.run_path.paths", []) paths = self.__cfg_mgr.get(ConfigManager.ConfigType.GLOBAL, "automation.run_path.paths", [])
if run_config_path not in paths: if run_config_path not in paths:
paths.append(run_config_path) paths.append(run_config_path)
index = len(paths) - 1 index = len(paths) - 1
else: else:
index = paths.index(run_config_path) index = paths.index(run_config_path)
self.__cfg_mgr.set(ConfigType.GLOBAL, "automation.run_path", {"current": index, "paths": paths}) self.__cfg_mgr.set(ConfigManager.ConfigType.GLOBAL, "automation.run_path", {"current": index, "paths": paths})
else: else:
QMessageBox.warning( QMessageBox.warning(
self, self,
@@ -1003,13 +1003,13 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.setUsersToTreeWidget(data) self.setUsersToTreeWidget(data)
self.__config_paths["user"] = user_config_path self.__config_paths["user"] = user_config_path
self.CurrentUserConfigEdit.setText(user_config_path) self.CurrentUserConfigEdit.setText(user_config_path)
paths = self.__cfg_mgr.get(ConfigType.GLOBAL, "automation.user_path.paths", []) paths = self.__cfg_mgr.get(ConfigManager.ConfigType.GLOBAL, "automation.user_path.paths", [])
if user_config_path not in paths: if user_config_path not in paths:
paths.append(user_config_path) paths.append(user_config_path)
index = len(paths) - 1 index = len(paths) - 1
else: else:
index = paths.index(user_config_path) index = paths.index(user_config_path)
self.__cfg_mgr.set(ConfigType.GLOBAL, "automation.user_path", {"current": index, "paths": paths}) self.__cfg_mgr.set(ConfigManager.ConfigType.GLOBAL, "automation.user_path", {"current": index, "paths": paths})
else: else:
QMessageBox.warning( QMessageBox.warning(
self, self,
+13 -14
View File
@@ -7,11 +7,10 @@ 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. You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details. See the LICENSE file for details.
""" """
import os
import queue import queue
from PySide6.QtCore import ( from PySide6.QtCore import (
Qt, Signal, Slot, QTimer, QDir, QUrl, Qt, Signal, Slot, QTimer, QUrl,
) )
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QMainWindow, QMenu, QSystemTrayIcon, QMessageBox QMainWindow, QMenu, QSystemTrayIcon, QMessageBox
@@ -20,10 +19,9 @@ from PySide6.QtGui import (
QTextCursor, QCloseEvent, QFont, QIcon, QDesktopServices QTextCursor, QCloseEvent, QFont, QIcon, QDesktopServices
) )
from base.MsgBase import MsgBase import utils.ConfigManager as ConfigManager
from utils.ConfigManager import ConfigType, instance from base.MsgBase import MsgBase
from utils.ConfigManager import getValidateAutomationConfigPaths
from gui.resources.ui.Ui_ALMainWindow import Ui_ALMainWindow from gui.resources.ui.Ui_ALMainWindow import Ui_ALMainWindow
from gui.resources import ALResource from gui.resources import ALResource
@@ -46,9 +44,9 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
MsgBase.__init__(self, queue.Queue(), queue.Queue()) MsgBase.__init__(self, queue.Queue(), queue.Queue())
QMainWindow.__init__(self) QMainWindow.__init__(self)
self.__cfg_mgr = instance() self.__cfg_mgr = ConfigManager.instance()
self.__timer_task_queue = queue.Queue() self.__timer_task_queue = queue.Queue()
self.__config_paths = getValidateAutomationConfigPaths() self.__config_paths = ConfigManager.getValidateAutomationConfigPaths()
self.__alTimerTaskManageWidget = None self.__alTimerTaskManageWidget = None
self.__alConfigWidget = None self.__alConfigWidget = None
self.__auto_lib_thread = None self.__auto_lib_thread = None
@@ -106,7 +104,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self self
): ):
url = QUrl("https://www.autolibrary.top/manuals") url = QUrl("https://www.autolibrary.kenanzhu.com/manuals")
QDesktopServices.openUrl(url) QDesktopServices.openUrl(url)
@@ -115,9 +113,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
): ):
if not QSystemTrayIcon.isSystemTrayAvailable(): if not QSystemTrayIcon.isSystemTrayAvailable():
self.showTraceSignal.emit( self._showTrace("操作系统不支持系统托盘功能, 无法创建系统托盘图标")
"系统不支持系统托盘功能, 无法创建系统托盘图标。"
)
return return
self.TrayIcon = QSystemTrayIcon(self.icon, self) self.TrayIcon = QSystemTrayIcon(self.icon, self)
self.TrayIcon.setToolTip("AutoLibrary") self.TrayIcon.setToolTip("AutoLibrary")
@@ -130,7 +126,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.TrayMenu.addAction("退出", self.close) self.TrayMenu.addAction("退出", self.close)
self.TrayIcon.setContextMenu(self.TrayMenu) self.TrayIcon.setContextMenu(self.TrayMenu)
self.TrayIcon.setContextMenu(self.TrayMenu)
self.TrayIcon.activated.connect(self.onTrayIconActivated) self.TrayIcon.activated.connect(self.onTrayIconActivated)
self.TrayIcon.show() self.TrayIcon.show()
@@ -174,6 +169,10 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
event: QCloseEvent event: QCloseEvent
): ):
if not self.isVisible():
self.showNormal()
event.ignore()
return
if self.__msg_queue_timer and self.__msg_queue_timer.isActive(): if self.__msg_queue_timer and self.__msg_queue_timer.isActive():
self.__msg_queue_timer.stop() self.__msg_queue_timer.stop()
if self.__timer_task_timer and self.__timer_task_timer.isActive(): if self.__timer_task_timer and self.__timer_task_timer.isActive():
@@ -187,7 +186,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
if self.__alConfigWidget: if self.__alConfigWidget:
self.__alConfigWidget.close() self.__alConfigWidget.close()
# the config widget is already deleted in the 'self.onConfigWidgetClosed' # the config widget is already deleted in the 'self.onConfigWidgetClosed'
super().closeEvent(event) QMainWindow.closeEvent(self, event)
def appendToTextEdit( def appendToTextEdit(
@@ -327,7 +326,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.TrayIcon.showMessage( self.TrayIcon.showMessage(
"定时任务 - AutoLibrary", "定时任务 - AutoLibrary",
f"\n定时任务 '{timer_task['name']}' 执行{'失败' if is_error else '完成'}", f"\n定时任务 '{timer_task['name']}' 执行{'失败' if is_error else '完成'}",
QSystemTrayIcon.MessageIcon.Information, QSystemTrayIcon.MessageIcon.Warning if is_error else QSystemTrayIcon.MessageIcon.Information,
1000 1000
) )
self._showTrace( self._showTrace(
+59 -17
View File
@@ -12,15 +12,11 @@ import uuid
from enum import Enum from enum import Enum
from datetime import datetime, timedelta from datetime import datetime, timedelta
from PySide6.QtCore import ( from PySide6.QtCore import Slot, QDateTime
Slot, QDateTime from PySide6.QtWidgets import QLabel, QDialog, QWidget, QSpinBox, QHBoxLayout, QGridLayout, QDateTimeEdit
)
from PySide6.QtWidgets import (
QLabel, QDialog, QWidget, QSpinBox,
QHBoxLayout, QGridLayout, QDateTimeEdit
)
from gui.resources.ui.Ui_ALTimerTaskAddDialog import Ui_ALTimerTaskAddDialog from gui.resources.ui.Ui_ALTimerTaskAddDialog import Ui_ALTimerTaskAddDialog
import utils.TimerUtils as TimerUtils
class ALTimerTaskStatus(Enum): class ALTimerTaskStatus(Enum):
@@ -31,6 +27,7 @@ class ALTimerTaskStatus(Enum):
EXECUTED = "已执行" EXECUTED = "已执行"
ERROR = "执行失败" ERROR = "执行失败"
OUTDATED = "已过期" OUTDATED = "已过期"
UNKNOWN = "未知"
class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
@@ -43,8 +40,8 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
super().__init__(parent) super().__init__(parent)
self.setupUi(self) self.setupUi(self)
self.connectSignals()
self.modifyUi() self.modifyUi()
self.connectSignals()
def modifyUi( def modifyUi(
@@ -64,28 +61,28 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.TimerConfigLayout.addWidget(self.SpecificTimerWidget) self.TimerConfigLayout.addWidget(self.SpecificTimerWidget)
self.RelativeTimerWidget = QWidget() self.RelativeTimerWidget = QWidget()
self.RelativeTimerLayout = QGridLayout(self.RelativeTimerWidget) self.RelativeTimerLayout = QHBoxLayout(self.RelativeTimerWidget)
self.RelativeTimerLayout.addWidget(QLabel("相对时间:"), 0, 0) self.RelativeTimerLayout.addWidget(QLabel("相对时间:"))
self.RelativeDaySpinBox = QSpinBox() self.RelativeDaySpinBox = QSpinBox()
self.RelativeDaySpinBox.setMinimum(0) self.RelativeDaySpinBox.setMinimum(0)
self.RelativeDaySpinBox.setMaximum(365) self.RelativeDaySpinBox.setMaximum(364)
self.RelativeDaySpinBox.setSuffix("") self.RelativeDaySpinBox.setSuffix("")
self.RelativeTimerLayout.addWidget(self.RelativeDaySpinBox, 1, 0) self.RelativeTimerLayout.addWidget(self.RelativeDaySpinBox)
self.RelativeHourSpinBox = QSpinBox() self.RelativeHourSpinBox = QSpinBox()
self.RelativeHourSpinBox.setMinimum(0) self.RelativeHourSpinBox.setMinimum(0)
self.RelativeHourSpinBox.setMaximum(23) self.RelativeHourSpinBox.setMaximum(23)
self.RelativeHourSpinBox.setSuffix("") self.RelativeHourSpinBox.setSuffix("")
self.RelativeTimerLayout.addWidget(self.RelativeHourSpinBox, 1, 1) self.RelativeTimerLayout.addWidget(self.RelativeHourSpinBox)
self.RelativeMinuteSpinBox = QSpinBox() self.RelativeMinuteSpinBox = QSpinBox()
self.RelativeMinuteSpinBox.setMinimum(0) self.RelativeMinuteSpinBox.setMinimum(0)
self.RelativeMinuteSpinBox.setMaximum(59) self.RelativeMinuteSpinBox.setMaximum(59)
self.RelativeMinuteSpinBox.setSuffix("") self.RelativeMinuteSpinBox.setSuffix("")
self.RelativeTimerLayout.addWidget(self.RelativeMinuteSpinBox, 1, 2) self.RelativeTimerLayout.addWidget(self.RelativeMinuteSpinBox)
self.RelativeSecondSpinBox = QSpinBox() self.RelativeSecondSpinBox = QSpinBox()
self.RelativeSecondSpinBox.setMinimum(0) self.RelativeSecondSpinBox.setMinimum(0)
self.RelativeSecondSpinBox.setMaximum(59) self.RelativeSecondSpinBox.setMaximum(59)
self.RelativeSecondSpinBox.setSuffix("") self.RelativeSecondSpinBox.setSuffix("")
self.RelativeTimerLayout.addWidget(self.RelativeSecondSpinBox, 1, 3) self.RelativeTimerLayout.addWidget(self.RelativeSecondSpinBox)
self.TimerConfigLayout.addWidget(self.RelativeTimerWidget) self.TimerConfigLayout.addWidget(self.RelativeTimerWidget)
self.RelativeTimerWidget.setVisible(False) self.RelativeTimerWidget.setVisible(False)
@@ -97,6 +94,7 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.CancelButton.clicked.connect(self.reject) self.CancelButton.clicked.connect(self.reject)
self.ConfirmButton.clicked.connect(self.accept) self.ConfirmButton.clicked.connect(self.accept)
self.TimerTypeComboBox.currentIndexChanged.connect(self.onTimerTypeComboBoxIndexChanged) self.TimerTypeComboBox.currentIndexChanged.connect(self.onTimerTypeComboBoxIndexChanged)
self.RepeatCheckBox.toggled.connect(self.onRepeatCheckBoxToggled)
def getTimerTask( def getTimerTask(
@@ -121,7 +119,7 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
minutes = self.RelativeMinuteSpinBox.value(), minutes = self.RelativeMinuteSpinBox.value(),
seconds = self.RelativeSecondSpinBox.value() seconds = self.RelativeSecondSpinBox.value()
) )
return { task_data = {
"name": name, "name": name,
"task_uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}", "task_uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}",
"time_type": self.TimerTypeComboBox.currentText(), "time_type": self.TimerTypeComboBox.currentText(),
@@ -129,9 +127,39 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
"silent": silent, "silent": silent,
"add_time": added_time, "add_time": added_time,
"status": ALTimerTaskStatus.PENDING, "status": ALTimerTaskStatus.PENDING,
"executed": False "executed": False,
"repeat": self.RepeatCheckBox.isChecked(),
} }
if task_data["repeat"]:
repeat_days = []
if self.MonCheckBox.isChecked():
repeat_days.append(0)
if self.TueCheckBox.isChecked():
repeat_days.append(1)
if self.WedCheckBox.isChecked():
repeat_days.append(2)
if self.ThuCheckBox.isChecked():
repeat_days.append(3)
if self.FriCheckBox.isChecked():
repeat_days.append(4)
if self.SatCheckBox.isChecked():
repeat_days.append(5)
if self.SunCheckBox.isChecked():
repeat_days.append(6)
if not repeat_days:
repeat_days = [0, 1, 2, 3, 4, 5, 6]
task_data["repeat_days"] = repeat_days
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["repeat_days"],
task_data["repeat_hour"],
task_data["repeat_minute"],
task_data["repeat_second"]
)
return task_data
@Slot(int) @Slot(int)
def onTimerTypeComboBoxIndexChanged( def onTimerTypeComboBoxIndexChanged(
@@ -141,3 +169,17 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.SpecificTimerWidget.setVisible(index == 0) self.SpecificTimerWidget.setVisible(index == 0)
self.RelativeTimerWidget.setVisible(index == 1) self.RelativeTimerWidget.setVisible(index == 1)
@Slot(bool)
def onRepeatCheckBoxToggled(
self,
checked: bool
):
self.MonCheckBox.setEnabled(checked)
self.TueCheckBox.setEnabled(checked)
self.WedCheckBox.setEnabled(checked)
self.ThuCheckBox.setEnabled(checked)
self.FriCheckBox.setEnabled(checked)
self.SatCheckBox.setEnabled(checked)
self.SunCheckBox.setEnabled(checked)
+147
View File
@@ -0,0 +1,147 @@
# -*- 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 datetime
from PySide6.QtCore import Slot, Qt
from PySide6.QtWidgets import (
QDialog, QTableWidget, QTableWidgetItem,
QVBoxLayout, QHBoxLayout, QGridLayout,
QPushButton, QLabel, QHeaderView
)
from gui.ALTimerTaskAddDialog import ALTimerTaskStatus
class ALTimerTaskHistoryDialog(QDialog):
def __init__(
self,
parent = None,
task_data: dict = None
):
super().__init__(parent)
self.__task_data = task_data
self.__history = task_data.get("history", [])
self.modifyUi()
self.connectSignals()
def modifyUi(
self
):
self.setWindowTitle("定时任务执行历史 - AutoLibrary")
self.setMinimumSize(300, 300)
self.setMaximumSize(500, 400)
MainLayout = QVBoxLayout(self)
InfoLayout = QGridLayout()
TaskNameLabel = QLabel(f"任务: {self.__task_data.get('name', '未命名')}")
TaskNameLabel.setStyleSheet("font-weight: bold; font-size: 14px;")
InfoLayout.addWidget(TaskNameLabel, 0, 0)
TaskUUIDLabel = QLabel(f"UUID: {self.__task_data.get('task_uuid', '未命名')}")
TaskUUIDLabel.setStyleSheet("font-size: 10px;")
InfoLayout.addWidget(TaskUUIDLabel, 1, 0)
InfoLayout.setColumnStretch(0, 1)
if self.__task_data.get("repeat", False):
RepeatLabel = QLabel("重复任务")
RepeatLabel.setStyleSheet("color: #2294FF; font-weight: bold; font-size: 12px;")
InfoLayout.addWidget(RepeatLabel, 0, 1)
MainLayout.addLayout(InfoLayout)
self.HistoryTableWidget = QTableWidget()
self.HistoryTableWidget.setColumnCount(3)
self.HistoryTableWidget.setHorizontalHeaderLabels(["执行时间", "结果", "耗时(秒/s"])
self.HistoryTableWidget.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
self.HistoryTableWidget.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
self.HistoryTableWidget.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
self.HistoryTableWidget.verticalHeader().setVisible(False)
self.HistoryTableWidget.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
self.HistoryTableWidget.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
self.loadHistory()
MainLayout.addWidget(self.HistoryTableWidget)
ButtonLayout = QHBoxLayout()
ButtonLayout.addStretch()
self.CloseButton = QPushButton("关闭")
self.CloseButton.setFixedSize(80, 25)
self.CloseButton.setDefault(True)
self.ClearHistoryButton = QPushButton("清空历史")
self.ClearHistoryButton.setFixedSize(80, 25)
self.ClearHistoryButton.setStyleSheet("color: #DC0000;")
ButtonLayout.addWidget(self.ClearHistoryButton)
ButtonLayout.addWidget(self.CloseButton)
MainLayout.addLayout(ButtonLayout)
def connectSignals(
self
):
self.CloseButton.clicked.connect(self.accept)
self.ClearHistoryButton.clicked.connect(self.onClearHistoryButtonClicked)
def loadHistory(
self
):
self.HistoryTableWidget.setRowCount(len(self.__history))
for row, record in enumerate(self.__history):
self.addHistoryRow(row, record)
def addHistoryRow(
self,
row: int,
record: dict
):
execute_time = record.get("execute_time", "")
result = record.get("result", ALTimerTaskStatus.UNKNOWN)
duration = record.get("duration", 0)
ExecuteTimeItem = QTableWidgetItem(execute_time)
ExecuteTimeItem.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.HistoryTableWidget.setItem(row, 0, ExecuteTimeItem)
ResultItem = QTableWidgetItem(result.value)
ResultItem.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
match result:
case ALTimerTaskStatus.EXECUTED:
ResultItem.setForeground(Qt.GlobalColor.green)
case ALTimerTaskStatus.ERROR:
ResultItem.setForeground(Qt.GlobalColor.red)
case ALTimerTaskStatus.OUTDATED:
ResultItem.setForeground(Qt.GlobalColor.red)
case _:
ResultItem.setForeground(Qt.GlobalColor.black)
self.HistoryTableWidget.setItem(row, 1, ResultItem)
DurationItem = QTableWidgetItem(f"{duration:.2f}")
DurationItem.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.HistoryTableWidget.setItem(row, 2, DurationItem)
self.HistoryTableWidget.setRowHeight(row, 25)
@Slot()
def onClearHistoryButtonClicked(
self
):
self.__history.clear()
self.HistoryTableWidget.setRowCount(0)
self.__task_data["history"] = self.__history
def getHistory(
self
) -> list:
return self.__history
+158 -14
View File
@@ -15,7 +15,7 @@ from enum import Enum
from datetime import datetime, timedelta from datetime import datetime, timedelta
from PySide6.QtCore import ( from PySide6.QtCore import (
Qt, Signal, Slot, QTimer, QFileInfo, QDir Qt, Signal, Slot, QTimer
) )
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QDialog, QWidget, QListWidgetItem, QMessageBox, QDialog, QWidget, QListWidgetItem, QMessageBox,
@@ -25,10 +25,12 @@ from PySide6.QtGui import (
QCloseEvent QCloseEvent
) )
from utils.ConfigManager import ConfigType, instance import utils.ConfigManager as ConfigManager
import utils.TimerUtils as TimerUtils
from gui.resources.ui.Ui_ALTimerTaskManageWidget import Ui_ALTimerTaskManageWidget 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
class ALTimerTaskItemWidget(QWidget): class ALTimerTaskItemWidget(QWidget):
@@ -61,13 +63,25 @@ class ALTimerTaskItemWidget(QWidget):
TaskNameLabel.setFont(TaskNameLabelFont) TaskNameLabel.setFont(TaskNameLabelFont)
TaskNameLabel.setFixedHeight(25) TaskNameLabel.setFixedHeight(25)
self.TaskInfoLayout.addWidget(TaskNameLabel) self.TaskInfoLayout.addWidget(TaskNameLabel)
ExecuteTimeStr = self.__timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S") ExecuteTimeStr = self.__timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S")
if self.__timer_task.get("repeat", False):
repeat_days = self.__timer_task.get("repeat_days", [])
repeat_hour = self.__timer_task.get("repeat_hour", 0)
repeat_minute = self.__timer_task.get("repeat_minute", 0)
repeat_second = self.__timer_task.get("repeat_second", 0)
if len(repeat_days) == 7:
time_str = f"{repeat_hour:02d}:{repeat_minute:02d}:{repeat_second:02d}"
ExecuteTimeLabel = QLabel(f"下次执行时间: {ExecuteTimeStr} (每日 {time_str})")
else:
day_names = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
selected_days = [day_names[d] for d in repeat_days]
time_str = f"{repeat_hour:02d}:{repeat_minute:02d}:{repeat_second:02d}"
ExecuteTimeLabel = QLabel(f"下次执行时间: {ExecuteTimeStr} (每{','.join(selected_days)} {time_str})")
else:
ExecuteTimeLabel = QLabel(f"执行时间: {ExecuteTimeStr}") ExecuteTimeLabel = QLabel(f"执行时间: {ExecuteTimeStr}")
ExecuteTimeLabel.setStyleSheet("color: #969696;") ExecuteTimeLabel.setStyleSheet("color: #969696;")
ExecuteTimeLabel.setFixedHeight(20) ExecuteTimeLabel.setFixedHeight(20)
self.TaskInfoLayout.addWidget(ExecuteTimeLabel) self.TaskInfoLayout.addWidget(ExecuteTimeLabel)
self.ItemWidgetLayout.addLayout(self.TaskInfoLayout) self.ItemWidgetLayout.addLayout(self.TaskInfoLayout)
self.ItemWidgetLayout.addStretch() self.ItemWidgetLayout.addStretch()
@@ -118,8 +132,13 @@ class ALTimerTaskItemWidget(QWidget):
TaskModeLabel.setFixedSize(60, 25) TaskModeLabel.setFixedSize(60, 25)
self.ItemWidgetLayout.addWidget(TaskModeLabel) self.ItemWidgetLayout.addWidget(TaskModeLabel)
if self.__timer_task.get("repeat", False):
self.HistoryButton = QPushButton("历史")
self.HistoryButton.setFixedSize(80, 25)
self.ItemWidgetLayout.addWidget(self.HistoryButton)
self.DeleteButton = QPushButton("删除") self.DeleteButton = QPushButton("删除")
self.DeleteButton.setFixedSize(80, 25) self.DeleteButton.setFixedSize(80, 25)
self.DeleteButton.setStyleSheet("color: #DC0000;")
self.ItemWidgetLayout.addWidget(self.DeleteButton) self.ItemWidgetLayout.addWidget(self.DeleteButton)
if self.__timer_task["status"] == ALTimerTaskStatus.READY\ if self.__timer_task["status"] == ALTimerTaskStatus.READY\
or self.__timer_task["status"] == ALTimerTaskStatus.RUNNING: or self.__timer_task["status"] == ALTimerTaskStatus.RUNNING:
@@ -145,7 +164,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
): ):
super().__init__(parent) super().__init__(parent)
self.__cfg_mgr = instance() self.__cfg_mgr = ConfigManager.instance()
self.__timer_tasks = [] self.__timer_tasks = []
self.__check_timer = None self.__check_timer = None
self.__sort_policy = self.SortPolicy.BY_EXECUTE_TIME self.__sort_policy = self.SortPolicy.BY_EXECUTE_TIME
@@ -199,12 +218,15 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
) -> list: ) -> list:
try: try:
timer_tasks = self.__cfg_mgr.get(ConfigType.TIMERTASK) timer_tasks = self.__cfg_mgr.get(ConfigManager.ConfigType.TIMERTASK)
if timer_tasks and "timer_tasks" in timer_tasks: if timer_tasks and "timer_tasks" in timer_tasks:
for task in timer_tasks["timer_tasks"]: for task in timer_tasks["timer_tasks"]:
task["add_time"] = datetime.strptime(task["add_time"], "%Y-%m-%d %H:%M:%S") task["add_time"] = datetime.strptime(task["add_time"], "%Y-%m-%d %H:%M:%S")
task["execute_time"] = datetime.strptime(task["execute_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"]) task["status"] = ALTimerTaskStatus(task["status"])
if "history" in task:
for item in task["history"]:
item["result"] = ALTimerTaskStatus(item["result"])
return timer_tasks["timer_tasks"] return timer_tasks["timer_tasks"]
raise Exception("定时任务配置文件格式错误") raise Exception("定时任务配置文件格式错误")
except Exception as e: except Exception as e:
@@ -226,7 +248,10 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
task["add_time"] = task["add_time"].strftime("%Y-%m-%d %H:%M:%S") 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["execute_time"] = task["execute_time"].strftime("%Y-%m-%d %H:%M:%S")
task["status"] = task["status"].value task["status"] = task["status"].value
self.__cfg_mgr.set(ConfigType.TIMERTASK, "", { "timer_tasks": timer_tasks }) if "history" in task:
for item in task["history"]:
item["result"] = item["result"].value
self.__cfg_mgr.set(ConfigManager.ConfigType.TIMERTASK, "", { "timer_tasks": timer_tasks })
return True return True
except Exception as e: except Exception as e:
QMessageBox.warning( QMessageBox.warning(
@@ -332,7 +357,11 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
item.setData(Qt.UserRole, timer_task) item.setData(Qt.UserRole, timer_task)
widget = ALTimerTaskItemWidget(self, timer_task) widget = ALTimerTaskItemWidget(self, timer_task)
widget.DeleteButton.clicked.connect( widget.DeleteButton.clicked.connect(
lambda _, uuid = timer_task["task_uuid"]: self.deleteTask(uuid) lambda _, task = timer_task: self.deleteTask(task)
)
if timer_task.get("repeat", False) and hasattr(widget, "HistoryButton"):
widget.HistoryButton.clicked.connect(
lambda _, task = timer_task: self.showTaskHistory(task)
) )
item.setSizeHint(widget.size()) item.setSizeHint(widget.size())
self.TimerTasksListWidget.addItem(item) self.TimerTasksListWidget.addItem(item)
@@ -350,11 +379,42 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self.timerTasksChanged.emit() self.timerTasksChanged.emit()
def deleteTask( @staticmethod
self, def getTimerTaskDetailMessage(
task_uuid: str timer_task: dict
): ):
return (
f"任务名称:{timer_task["name"]}\n"
f"添加时间:{timer_task["add_time"]}\n"
f"当前状态:{timer_task["status"].value}\n"
f"下次执行时间:{datetime.strftime(timer_task["execute_time"], "%Y-%m-%d %H:%M:%S")}\n"
f"已执行次数:{len(timer_task['history'] if 'history' in timer_task else 0)}"
)
def deleteTask(
self,
timer_task: dict
):
if timer_task["repeat"]: # when delete a repeat task
msgbox = QMessageBox(self)
msgbox.setIcon(QMessageBox.Icon.Question)
msgbox.setWindowTitle("警告 - AutoLibrary")
msgbox.setStandardButtons(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
msgbox.setText("删除可重复性任务将同时删除所有已执行的记录 !\n是否继续 ?")
msgbox.setDetailedText(
"以下可重复性任务将被删除:\n"\
"\n"
f"{self.getTimerTaskDetailMessage(timer_task)}"
)
result = msgbox.exec()
if result != QMessageBox.StandardButton.Yes:
return
task_uuid = timer_task["task_uuid"]
self.__timer_tasks = [ self.__timer_tasks = [
x for x in self.__timer_tasks x for x in self.__timer_tasks
if x["task_uuid"] != task_uuid if x["task_uuid"] != task_uuid
@@ -374,8 +434,9 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
"是否要清除所有定时任务 ?", "是否要清除所有定时任务 ?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
) )
if result is QMessageBox.StandardButton.No: if result == QMessageBox.StandardButton.No:
return return
# READY and RUNNING tasks cannot be cleared
in_queue_tasks = [ in_queue_tasks = [
x for x in self.__timer_tasks x for x in self.__timer_tasks
if x["status"] == ALTimerTaskStatus.READY if x["status"] == ALTimerTaskStatus.READY
@@ -386,9 +447,50 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
QMessageBox.warning( QMessageBox.warning(
self, self,
"警告 - AutoLibrary", "警告 - AutoLibrary",
"存在正在执行或已就绪的队列任务,无法清除所有定时任务 !" f"存在 {in_queue_count}正在执行或已就绪的队列任务,无法清除所有定时任务 !"
) )
self.__timer_tasks = in_queue_tasks return
# repeat tasks ask before clear
repeat_tasks = [
x for x in self.__timer_tasks
if x.get("repeat", False)
]
repeat_tasks_count = len(repeat_tasks)
if repeat_tasks_count > 0:
msgbox = QMessageBox(self)
msgbox.setIcon(QMessageBox.Icon.Question)
msgbox.setWindowTitle("警告 - AutoLibrary")
msgbox.setStandardButtons(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
msgbox.setText(
f"存在 {repeat_tasks_count} 个可重复性任务,\n"
"删除可重复性任务将同时删除所有已执行的记录 !\n"
"是否继续 ?"
)
delete_msgs = [
self.getTimerTaskDetailMessage(x) for x in repeat_tasks
]
msgbox.setDetailedText(
"以下可重复性任务将被删除:\n"\
"\n"
f"{"\n\n".join(delete_msgs)}"
)
result = msgbox.exec()
if result != QMessageBox.StandardButton.Yes:
return
# clear all tasks
self.__timer_tasks.clear()
self.timerTasksChanged.emit()
def showTaskHistory(
self,
task: dict
):
dialog = ALTimerTaskHistoryDialog(self, task)
if dialog.exec() == QDialog.DialogCode.Accepted:
self.timerTasksChanged.emit() self.timerTasksChanged.emit()
@@ -405,6 +507,9 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
if timer_task["status"] is not ALTimerTaskStatus.PENDING: if timer_task["status"] is not ALTimerTaskStatus.PENDING:
continue continue
if timer_task["execute_time"] <= now + timedelta(seconds = -5): if timer_task["execute_time"] <= now + timedelta(seconds = -5):
if timer_task.get("repeat", False):
self.onRepeatTimerTaskIs(ALTimerTaskStatus.OUTDATED, timer_task)
else:
timer_task["status"] = ALTimerTaskStatus.OUTDATED timer_task["status"] = ALTimerTaskStatus.OUTDATED
need_update = True need_update = True
else: else:
@@ -460,9 +565,40 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
for task in self.__timer_tasks: for task in self.__timer_tasks:
if task["task_uuid"] == timer_task["task_uuid"]: if task["task_uuid"] == timer_task["task_uuid"]:
task["status"] = ALTimerTaskStatus.RUNNING task["status"] = ALTimerTaskStatus.RUNNING
break
self.timerTasksChanged.emit() self.timerTasksChanged.emit()
def onRepeatTimerTaskIs(
self,
status: ALTimerTaskStatus,
timer_task: dict
) -> dict:
if "history" not in timer_task:
timer_task["history"] = []
executed_time = datetime.now()
duration = (executed_time - timer_task["execute_time"]).total_seconds()
timer_task["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,
"duration": duration if status is ALTimerTaskStatus.EXECUTED else 0,
"uuid": timer_task["task_uuid"]
})
next_time = TimerUtils.calculateNextRepeatTime(
timer_task["repeat_days"],
timer_task["repeat_hour"],
timer_task["repeat_minute"],
timer_task["repeat_second"]
)
if next_time:
timer_task["execute_time"] = next_time
timer_task["status"] = ALTimerTaskStatus.PENDING
timer_task["executed"] = False
else:
timer_task["status"] = status
@Slot(dict) @Slot(dict)
def onTimerTaskIsExecuted( def onTimerTaskIsExecuted(
self, self,
@@ -471,7 +607,11 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
for task in self.__timer_tasks: for task in self.__timer_tasks:
if task["task_uuid"] == timer_task["task_uuid"]: if task["task_uuid"] == timer_task["task_uuid"]:
if task.get("repeat", False):
self.onRepeatTimerTaskIs(ALTimerTaskStatus.EXECUTED, task)
else:
task["status"] = ALTimerTaskStatus.EXECUTED task["status"] = ALTimerTaskStatus.EXECUTED
break
self.timerTasksChanged.emit() self.timerTasksChanged.emit()
@Slot(dict) @Slot(dict)
@@ -482,5 +622,9 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
for task in self.__timer_tasks: for task in self.__timer_tasks:
if task["task_uuid"] == timer_task["task_uuid"]: if task["task_uuid"] == timer_task["task_uuid"]:
if task.get("repeat", False):
self.onRepeatTimerTaskIs(ALTimerTaskStatus.ERROR, task)
else:
task["status"] = ALTimerTaskStatus.ERROR task["status"] = ALTimerTaskStatus.ERROR
break
self.timerTasksChanged.emit() self.timerTasksChanged.emit()
+6 -1
View File
@@ -195,6 +195,11 @@
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #DC0000;
}</string>
</property>
<property name="text"> <property name="text">
<string>删除用户</string> <string>删除用户</string>
</property> </property>
@@ -1356,7 +1361,7 @@
</size> </size>
</property> </property>
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;详情请参阅 &lt;a href=&quot;https://www.autolibrary.top/manuals&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#69fcff;&quot;&gt;用户手册&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;详情请参阅 &lt;a href=&quot;https://www.autolibrary.kenanzhu.com/manuals&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#69fcff;&quot;&gt;用户手册&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
<property name="whatsThis"> <property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+255 -8
View File
@@ -6,20 +6,20 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>300</width> <width>350</width>
<height>300</height> <height>400</height>
</rect> </rect>
</property> </property>
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>0</width> <width>350</width>
<height>300</height> <height>400</height>
</size> </size>
</property> </property>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>500</width> <width>350</width>
<height>300</height> <height>500</height>
</size> </size>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@@ -149,8 +149,20 @@
<property name="spacing"> <property name="spacing">
<number>5</number> <number>5</number>
</property> </property>
<item row="0" column="0"> <item row="1" column="0">
<widget class="QRadioButton" name="SilentlyRunRadioButton"> <widget class="QRadioButton" name="SilentlyRunRadioButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="text"> <property name="text">
<string>静默运行</string> <string>静默运行</string>
</property> </property>
@@ -168,13 +180,248 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="2" column="0">
<widget class="QRadioButton" name="ShowBeforeRunRadioButton"> <widget class="QRadioButton" name="ShowBeforeRunRadioButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="text"> <property name="text">
<string>运行前提示</string> <string>运行前提示</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="0">
<widget class="QGroupBox" name="RepeatConfigGroupBox">
<property name="title">
<string>重复运行</string>
</property>
<layout class="QVBoxLayout" name="RepeatConfigLayout" stretch="1,1,1">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<widget class="QCheckBox" name="RepeatCheckBox">
<property name="minimumSize">
<size>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>启用重复执行</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="minimumSize">
<size>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>重复周期(全选或全不选都为每日运行):</string>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="RepeatCheckBoxLayout" rowstretch="10,10" columnstretch="0,0,0,0" rowminimumheight="25,25">
<property name="spacing">
<number>0</number>
</property>
<item row="0" column="3">
<widget class="QCheckBox" name="ThuCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>周四</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QCheckBox" name="WedCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>周三</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="MonCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>周一</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="TueCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>周二</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="FriCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>周五</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="SatCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>周六</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QCheckBox" name="SunCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>周日</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@@ -6,19 +6,19 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>400</width> <width>500</width>
<height>400</height> <height>400</height>
</rect> </rect>
</property> </property>
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>400</width> <width>500</width>
<height>400</height> <height>400</height>
</size> </size>
</property> </property>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>600</width> <width>800</width>
<height>400</height> <height>400</height>
</size> </size>
</property> </property>
@@ -306,6 +306,11 @@
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #DC0000;
}</string>
</property>
<property name="text"> <property name="text">
<string>清除全部</string> <string>清除全部</string>
</property> </property>
+2 -2
View File
@@ -241,7 +241,7 @@ class AutoLib(MsgBase):
self._showTrace(f"用户 {username} 无法预约,已跳过") self._showTrace(f"用户 {username} 无法预约,已跳过")
result = 2 result = 2
# checkin # checkin
if run_mode["auto_checkin"] and result == 2: if run_mode["auto_checkin"] and result != 1:
if self.__lib_checker.canCheckin(): if self.__lib_checker.canCheckin():
if self.__lib_checkin.checkin(username): if self.__lib_checkin.checkin(username):
result = 0 result = 0
@@ -251,7 +251,7 @@ class AutoLib(MsgBase):
self._showTrace(f"用户 {username} 无法签到,已跳过") self._showTrace(f"用户 {username} 无法签到,已跳过")
result = 2 result = 2
# renewal # renewal
if run_mode["auto_renewal"] and result == 2: if run_mode["auto_renewal"] and result != 1:
can_renew, record = self.__lib_checker.canRenew() can_renew, record = self.__lib_checker.canRenew()
if can_renew: if can_renew:
if self.__lib_renew.renew(username, record, reserve_info): if self.__lib_renew.renew(username, record, reserve_info):
+28 -1
View File
@@ -88,6 +88,31 @@ class LibCheckin(LibOperator):
return False return False
def __enableCheckinBtn(
self
) -> bool:
script = """
try {
var checkin_btn = document.getElementById('btnCheckIn');
if (checkin_btn) {
checkin_btn.classList.remove('disabled');
return true;
}
return false;
} catch (e) {
return false;
}
"""
result = self.__driver.execute_script(script)
time.sleep(0.1)
if result:
self._showTrace("签到按钮已启用")
else:
self._showTrace("签到按钮启用失败")
return result
def checkin( def checkin(
self, self,
username: str username: str
@@ -104,7 +129,9 @@ class LibCheckin(LibOperator):
self._showTrace(f"用户 {username} 签到界面加载失败 !") self._showTrace(f"用户 {username} 签到界面加载失败 !")
return False return False
if "disabled" in checkin_btn.get_attribute("class"): if "disabled" in checkin_btn.get_attribute("class"):
self._showTrace("签到按钮不可用, 可能不在场馆内, 请连接图书馆网络后重试") self._showTrace("签到按钮不可用, 可能不在场馆内, 正在尝试启用......")
if not self.__enableCheckinBtn():
self._showTrace(f"签到按钮启用失败 !")
return False return False
checkin_btn.click() checkin_btn.click()
if self._waitResponseLoad(): if self._waitResponseLoad():
+67 -76
View File
@@ -14,10 +14,10 @@ from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support import expected_conditions as EC
from base.LibOperator import LibOperator from base.LibTimeSelector import LibTimeSelector
class LibRenew(LibOperator): class LibRenew(LibTimeSelector):
def __init__( def __init__(
self, self,
@@ -38,22 +38,6 @@ class LibRenew(LibOperator):
self.__driver.refresh() self.__driver.refresh()
return True return True
@staticmethod
def __timeToMins(
time_str: str
) -> int:
hour, minute = map(int, time_str.split(":"))
return hour*60 + minute
@staticmethod
def __minsToTime(
mins: int
) -> str:
hour, minute = divmod(mins, 60)
return f"{hour:02d}:{minute:02d}"
def __waitRenewDialog( def __waitRenewDialog(
self self
@@ -94,85 +78,92 @@ class LibRenew(LibOperator):
return True return True
def __selectNearstTime( def __selectNearestTime(
self, self,
record: dict, record: dict,
reserve_info: dict reserve_info: dict
) -> bool: ) -> bool:
""" """
TODO : this function is too long and too ugly Select the nearest available renewal time.
we need to refactor it to make it more readable.
but may be it is not a good idea to refactor it. :) who knows...
""" """
end_time = record["time"]["end"] end_time = record["time"]["end"]
renew_info = reserve_info["renew_time"] renew_info = reserve_info["renew_time"]
max_diff = renew_info["max_diff"] max_diff = renew_info["max_diff"]
prefer_earlier = renew_info["prefer_early"] prefer_earlier = renew_info["prefer_early"]
target_renew_mins = self.__timeToMins(end_time) + renew_info["expect_duration"]*60 target_renew_mins = self._timeToMins(end_time) + renew_info["expect_duration"]*60
renew_ok_btn = self.__driver.find_element(
By.CSS_SELECTOR, "#extendDiv .btnOK"
)
try:
renew_time_opts = self.__driver.find_elements(
By.CSS_SELECTOR, "#extendDiv .renewal_List li"
)
free_times = []
best_time_diff = max_diff
best_actual_diff = None
best_time_opt = None
# Validate and adjust target renew time to library closing time
if not self.__validateAndAdjustRenewTime(end_time, target_renew_mins):
return False
renew_ok_btn = self.__driver.find_element(By.CSS_SELECTOR, "#extendDiv .btnOK")
renew_time_opts = self.__driver.find_elements(By.CSS_SELECTOR, "#extendDiv .renewal_List li")
if not renew_time_opts: if not renew_time_opts:
self._showTrace("当前未查询到可用续约时间 !") self._showTrace("当前未查询到可用续约时间 !")
return False return False
for time_opt in renew_time_opts:
time_attr = time_opt.get_attribute("id")
if time_attr and time_attr.isdigit():
time_val = int(time_attr)
free_times.append(time_opt.text.strip())
else:
continue
actual_diff = time_val - target_renew_mins
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)
)
):
best_time_diff = abs_diff
best_actual_diff = actual_diff
best_time_opt = time_opt
if best_time_opt is not None: # Find best renewal time option
best_time_opt.click() best_opt, best_text, actual_diff, free_times = self._findBestTimeOption(
abs_time_diff = abs(best_actual_diff) renew_time_opts, target_renew_mins, max_diff, prefer_earlier, is_reserve=False
if best_actual_diff < 0:
time_relation = f"早了 {abs_time_diff} 分钟"
elif best_actual_diff > 0:
time_relation = f"晚了 {abs_time_diff} 分钟"
else:
time_relation = f"正好等于续约时间"
self._showTrace(
f"选择距离期望续约时间最近的 {best_time_opt.text}, "\
f"与期望续约时间相比 {time_relation}"
) )
# update the actual renew end time if best_opt is not None:
record["time"]["end"] = best_time_opt.text.strip() return self.__confirmRenewal(best_opt, best_text, actual_diff, record, renew_ok_btn)
renew_ok_btn.click()
return True
self._showTrace( self._showTrace(
"无法选择最近的可用续约时间 !" \ "无法选择最近的可用续约时间 ! "
f"所有可选时间与目标时间相差都超过了 {max_diff} 分钟 !" f"所有可选时间与目标时间相差都超过了 {max_diff} 分钟 !"
) )
self._showTrace( self._showTrace(f"当前可供续约的时间有: {free_times}")
f"当前可供续约的时间有: {free_times}"
)
return False return False
def __validateAndAdjustRenewTime(
self,
end_time: str,
target_renew_mins: int
) -> bool:
"""
Validate and adjust renewal time to library closing time if needed.
"""
LIBRARY_CLOSE_TIME = 1410 # 23:30 in minutes
if target_renew_mins > LIBRARY_CLOSE_TIME:
actual_renew_duration = LIBRARY_CLOSE_TIME - self._timeToMins(end_time)
if actual_renew_duration <= 0:
self._showTrace(f"当前结束时间 {end_time} 已接近闭馆时间,无法续约 !")
return False
self._showTrace(
f"续约时间已调整至闭馆时间 {self._minsToTime(LIBRARY_CLOSE_TIME)}"
f"实际续约时长为 {actual_renew_duration//60} 小时 {actual_renew_duration%60} 分钟"
)
return True
return True
def __confirmRenewal(
self,
best_opt,
best_text: str,
actual_diff: int,
record: dict,
ok_btn
) -> bool:
"""
Confirm the selected renewal time.
"""
try:
best_opt.click()
abs_diff = abs(actual_diff)
time_relation = self._formatTimeRelation(abs_diff, actual_diff, "续约时间")
self._showTrace(
f"选择距离期望续约时间最近的 {best_text}, "
f"与期望续约时间相比 {time_relation}"
)
record["time"]["end"] = best_text.strip()
ok_btn.click()
return True
except: except:
self._showTrace("查询可用续约时间时发生未知错误 !") self._showTrace("确认续约时发生错误 !")
return False return False
@@ -204,7 +195,7 @@ class LibRenew(LibOperator):
# so we need to refresh the page for subsequent operations. # so we need to refresh the page for subsequent operations.
self.__driver.refresh() self.__driver.refresh()
return False return False
if not self.__selectNearstTime(record, reserve_info): if not self.__selectNearestTime(record, reserve_info):
self._showTrace(f"用户 {username} 续约失败 !") self._showTrace(f"用户 {username} 续约失败 !")
self.__driver.refresh() self.__driver.refresh()
return False return False
+61 -94
View File
@@ -10,16 +10,15 @@ See the LICENSE file for details.
import time import time
import queue import queue
from datetime import datetime
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.webdriver import WebDriver from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support import expected_conditions as EC
from base.LibOperator import LibOperator from base.LibTimeSelector import LibTimeSelector
class LibReserve(LibOperator): class LibReserve(LibTimeSelector):
def __init__( def __init__(
self, self,
@@ -100,22 +99,6 @@ class LibReserve(LibOperator):
self._showTrace(f"预约结果加载失败 !") self._showTrace(f"预约结果加载失败 !")
return False return False
@staticmethod
def __timeToMins(
time_str: str
) -> int:
hour, minute = map(int, time_str.split(":"))
return hour*60 + minute
@staticmethod
def __minsToTime(
mins: int
) -> str:
hour, minute = divmod(mins, 60)
return f"{hour:02d}:{minute:02d}"
def __containRequiredInfo( def __containRequiredInfo(
self, self,
@@ -207,10 +190,10 @@ class LibReserve(LibOperator):
if reserve_info.get("end_time") is None: if reserve_info.get("end_time") is None:
reserve_info["end_time"] = {} reserve_info["end_time"] = {}
if "time" not in reserve_info["end_time"]: if "time" not in reserve_info["end_time"]:
end_mins = self.__timeToMins(reserve_info["begin_time"]["time"]) end_mins = self._timeToMins(reserve_info["begin_time"]["time"])
end_mins = end_mins + int(reserve_info["expect_duration"]*60) end_mins = end_mins + int(reserve_info["expect_duration"]*60)
reserve_info["end_time"] = { reserve_info["end_time"] = {
"time": self.__minsToTime(end_mins), "time": self._minsToTime(end_mins),
"max_diff": 30, "max_diff": 30,
"prefer_early": False "prefer_early": False
} }
@@ -232,8 +215,8 @@ class LibReserve(LibOperator):
): ):
begin_time, end_time = reserve_info["begin_time"], reserve_info["end_time"] begin_time, end_time = reserve_info["begin_time"], reserve_info["end_time"]
begin_mins = self.__timeToMins(begin_time["time"]) begin_mins = self._timeToMins(begin_time["time"])
end_mins = self.__timeToMins(end_time["time"]) end_mins = self._timeToMins(end_time["time"])
# if end time is earlier than begin_time, exchange them # if end time is earlier than begin_time, exchange them
if end_mins < begin_mins: if end_mins < begin_mins:
self._showTrace( self._showTrace(
@@ -242,15 +225,15 @@ class LibReserve(LibOperator):
reserve_info["end_time"] = begin_time reserve_info["end_time"] = begin_time
reserve_info["begin_time"] = end_time reserve_info["begin_time"] = end_time
begin_time, end_time = reserve_info["begin_time"], reserve_info["end_time"] begin_time, end_time = reserve_info["begin_time"], reserve_info["end_time"]
begin_mins = self.__timeToMins(begin_time["time"]) begin_mins = self._timeToMins(begin_time["time"])
end_mins = self.__timeToMins(end_time["time"]) end_mins = self._timeToMins(end_time["time"])
# ensure the end time is not later than 23:30 # ensure the end time is not later than 23:30
if end_mins > self.__timeToMins("23:30"): if end_mins > self._timeToMins("23:30"):
self._showTrace( self._showTrace(
f"结束时间 {end_time['time']} 晚于 23:30, 自动设置为 23:30" f"结束时间 {end_time['time']} 晚于 23:30, 自动设置为 23:30"
) )
reserve_info["end_time"]["time"] = "23:30" reserve_info["end_time"]["time"] = "23:30"
end_mins = self.__timeToMins("23:30") end_mins = self._timeToMins("23:30")
# ensure the duration is not longer than 8 hours # ensure the duration is not longer than 8 hours
if reserve_info["satisfy_duration"]: if reserve_info["satisfy_duration"]:
if reserve_info["expect_duration"] > 8: if reserve_info["expect_duration"] > 8:
@@ -267,7 +250,7 @@ class LibReserve(LibOperator):
f"{float((end_mins - begin_mins)/60)} 小时 " f"{float((end_mins - begin_mins)/60)} 小时 "
f"超出最大时长 8 小时, 自动设置为 8 小时" f"超出最大时长 8 小时, 自动设置为 8 小时"
) )
reserve_info["end_time"]["time"] = self.__minsToTime(begin_mins + 8*60) reserve_info["end_time"]["time"] = self._minsToTime(begin_mins + 8*60)
return True return True
@@ -496,6 +479,10 @@ class LibReserve(LibOperator):
prefer_earlier: bool = True prefer_earlier: bool = True
) -> int: ) -> int:
"""
Select the nearest available time option.
"""
# Wait for time options to load
try: try:
WebDriverWait(self.__driver, 2).until( WebDriverWait(self.__driver, 2).until(
EC.presence_of_all_elements_located( EC.presence_of_all_elements_located(
@@ -505,67 +492,34 @@ class LibReserve(LibOperator):
except: except:
self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间") self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间")
return -1 return -1
try:
# Find best time option
all_time_opts = self.__driver.find_elements( all_time_opts = self.__driver.find_elements(
By.CSS_SELECTOR, By.CSS_SELECTOR,
f"#{time_id} ul li a" f"#{time_id} ul li a"
) )
free_times = []
best_time_diff = max_time_diff
best_actual_diff = None
best_time_opt = None
if not all_time_opts: if not all_time_opts:
self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间") self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间")
return -1 return -1
for time_opt in all_time_opts: best_opt, best_text, actual_diff, free_times = self._findBestTimeOption(
time_attr = time_opt.get_attribute("time") all_time_opts, target_time, max_time_diff, prefer_earlier, is_reserve=True
if time_attr == "now":
now = datetime.now()
time_val = int(now.hour*60 + now.minute)
elif time_attr and time_attr.isdigit():
time_val = int(time_attr)
else:
continue
free_times.append(self.__minsToTime(time_val))
actual_diff = time_val - target_time
abs_diff = abs(actual_diff)
if abs_diff < best_time_diff or (
abs_diff == best_time_diff and (
# prefer earlier time
(prefer_earlier and actual_diff <= 0) or
# prefer later time
(not prefer_earlier and actual_diff >= 0)
) )
): if best_opt is not None:
best_time_diff = abs_diff best_opt.click()
best_actual_diff = actual_diff abs_diff = abs(actual_diff)
best_time_opt = time_opt time_relation = self._formatTimeRelation(abs_diff, actual_diff, time_type)
target_time += actual_diff
if best_time_opt is not None:
best_time_opt.click()
abs_time_diff = abs(best_actual_diff)
if best_actual_diff < 0:
time_relation = f"早了 {abs_time_diff} 分钟"
elif best_actual_diff > 0:
time_relation = f"晚了 {abs_time_diff} 分钟"
else:
time_relation = f"正好等于 {time_type}"
target_time += best_actual_diff
self._showTrace( self._showTrace(
f"选择距离期望 {time_type} 最近的 {best_time_opt.text}, "\ f"选择距离期望 {time_type} 最近的 {best_text}, "
f"与期望 {time_type} 相比 {time_relation}" f"与期望 {time_type} 相比 {time_relation}"
) )
return target_time return target_time
self._showTrace( self._showTrace(
f"无法选择最近的 {time_type} {self.__minsToTime(target_time)}, "\ f"无法选择最近的 {time_type} {self._minsToTime(target_time)}, "
f"所有可选时间与目标时间相差都超过 {max_time_diff} 分钟" f"所有可选时间与目标时间相差都超过 {max_time_diff} 分钟"
) )
self._showTrace(f"当前可供预约的 {time_type} 有: {free_times}") self._showTrace(f"当前可供预约的 {time_type} 有: {free_times}")
return -1 return -1
except:
self._showTrace(f"{time_type} {self.__minsToTime(target_time)} 选择失败 !")
return -1
def __selectSeatTime( def __selectSeatTime(
@@ -576,40 +530,35 @@ class LibReserve(LibOperator):
satisfy_duration: bool = True satisfy_duration: bool = True
) -> bool: ) -> bool:
"""Select seat begin and end time."""
expect_begin_time = actual_begin_time = begin_time["time"] expect_begin_time = actual_begin_time = begin_time["time"]
expect_end_time = actual_end_time = end_time["time"] expect_end_time = actual_end_time = end_time["time"]
expect_begin_mins = self.__timeToMins(expect_begin_time) expect_begin_mins = self._timeToMins(expect_begin_time)
actual_begin_mins = expect_begin_mins actual_begin_mins = expect_begin_mins
expect_end_mins = self.__timeToMins(expect_end_time) expect_end_mins = self._timeToMins(expect_end_time)
# select the begin time # Select begin time
if self.__selectNearestTime( if self.__selectNearestTime(
time_id="startTime", # dont change into begin, this is the element in the page time_id="startTime",
time_type="开始时间", time_type="开始时间",
target_time=expect_begin_mins, target_time=expect_begin_mins,
max_time_diff=begin_time["max_diff"], max_time_diff=begin_time["max_diff"],
prefer_earlier=begin_time["prefer_early"] prefer_earlier=begin_time["prefer_early"]
) == -1: ) == -1:
return False return False
else: actual_begin_time = self._minsToTime(expect_begin_mins)
actual_begin_time = self.__minsToTime(expect_begin_mins) actual_begin_mins = self._timeToMins(actual_begin_time)
actual_begin_mins = self.__timeToMins(actual_begin_time)
# if 'satisfy_duration' is True. # If 'satisfy_duration' is True, select end time based on actual begin time
# select the end time based on the begin time
# (because it may be changed under the 'max time diff' strategy) and expect duration.
if satisfy_duration: if satisfy_duration:
expect_end_mins = int(actual_begin_mins + expct_duration*60) expect_end_mins = self.validateAndAdjustEndTime(actual_begin_mins, expct_duration)
if expect_end_mins > self.__timeToMins("23:30"): expect_end_time = self._minsToTime(expect_end_mins)
expect_end_mins = self.__timeToMins("23:30")
self._showTrace( self._showTrace(
f"预约持续时间 {expct_duration} 小时, 超过最大预约时间 23:30, 自动调整为 23:30" f"需要满足期望预约持续时间: {expct_duration} 小时, "
f"根据开始时间 {actual_begin_time} 计算结束时间: {expect_end_time}"
) )
expect_end_time = self.__minsToTime(expect_end_mins)
self._showTrace( # Select end time
f"需要满足期望预约持续时间: {expct_duration} 小时, "\
f"根据开始时间 {actual_begin_time} 计算结束时间: {self.__minsToTime(expect_end_mins)}"
)
# select the end time
if self.__selectNearestTime( if self.__selectNearestTime(
time_id="endTime", time_id="endTime",
time_type="结束时间", time_type="结束时间",
@@ -618,8 +567,7 @@ class LibReserve(LibOperator):
prefer_earlier=end_time["prefer_early"] prefer_earlier=end_time["prefer_early"]
) == -1: ) == -1:
return False return False
else: actual_end_time = self._minsToTime(expect_end_mins)
actual_end_time = self.__minsToTime(expect_end_mins)
self._showTrace( self._showTrace(
f"期望预约时间段: {expect_begin_time} - {expect_end_time}, " f"期望预约时间段: {expect_begin_time} - {expect_end_time}, "
f"实际预约时间段: {actual_begin_time} - {actual_end_time}" f"实际预约时间段: {actual_begin_time} - {actual_end_time}"
@@ -627,6 +575,25 @@ class LibReserve(LibOperator):
return True return True
def validateAndAdjustEndTime(
self,
begin_mins: int,
duration: int
) -> int:
"""
Validate and adjust reserve end time to library closing time if needed.
"""
LIBRARY_CLOSE_TIME = self._timeToMins("23:30")
expect_end_mins = begin_mins + duration * 60
if expect_end_mins > LIBRARY_CLOSE_TIME:
expect_end_mins = LIBRARY_CLOSE_TIME
self._showTrace(
f"预约持续时间 {duration} 小时, 超过最大预约时间 23:30, 自动调整为 23:30"
)
return expect_end_mins
def reserve( def reserve(
self, self,
username: str, username: str,
+16 -5
View File
@@ -168,16 +168,19 @@ class ConfigManager:
JSONWriter(config_path, self.__config_data[config_type.value]) JSONWriter(config_path, self.__config_data[config_type.value])
def appDir( def configDir(
self self
) -> str: ) -> str:
return self.__config_dir return self.__config_dir
# ConfigManager singleton instance.
_config_manager_instance = None _config_manager_instance = None
# Utility function to get config data (thread-safe and validated) from ConfigManager instance. # Utility functions.
#
# Utility function to get validated automation config paths.
def getValidateAutomationConfigPaths( def getValidateAutomationConfigPaths(
) -> dict: ) -> dict:
""" """
@@ -193,7 +196,7 @@ def getValidateAutomationConfigPaths(
paths = auto_config.get(f"{cfg_type}_path", {}).get("paths", []) paths = auto_config.get(f"{cfg_type}_path", {}).get("paths", [])
index = auto_config.get(f"{cfg_type}_path", {}).get("current", 0) index = auto_config.get(f"{cfg_type}_path", {}).get("current", 0)
if paths == []: if paths == []:
paths.append(os.path.join(_config_manager_instance.appDir(), f"{cfg_type}.json")) paths.append(os.path.join(_config_manager_instance.configDir(), f"{cfg_type}.json"))
if index < 0: if index < 0:
index = 0 index = 0
if index >= len(paths): if index >= len(paths):
@@ -204,10 +207,18 @@ def getValidateAutomationConfigPaths(
_config_manager_instance.set(ConfigType.GLOBAL, "automation", auto_config) _config_manager_instance.set(ConfigType.GLOBAL, "automation", auto_config)
return config_paths return config_paths
# Utility function to get base config directory.
def getBaseConfigDir( def getBaseConfigDir(
) -> str: ) -> str:
"""
Get base config directory, on Windows, it is usually at :
'C:\\Users\\<username>\\AppData\\Local\\AutoLibrary\\config'.
return _config_manager_instance.appDir() Returns:
str: Base config directory.
"""
return _config_manager_instance.configDir()
# Singleton instance of ConfigManager. # Singleton instance of ConfigManager.
_instance_lock = threading.Lock() _instance_lock = threading.Lock()
@@ -227,7 +238,7 @@ def instance(
else: else:
if config_dir == "": if config_dir == "":
return _config_manager_instance return _config_manager_instance
if _config_manager_instance.appDir() != config_dir: if getBaseConfigDir() != config_dir:
raise ValueError( raise ValueError(
"ConfigManager 的实例已初始化,不能使用不同的配置目录。") "ConfigManager 的实例已初始化,不能使用不同的配置目录。")
return _config_manager_instance return _config_manager_instance
+50
View File
@@ -0,0 +1,50 @@
# -*- 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.
"""
from datetime import datetime, timedelta
def calculateNextRepeatTime(
repeat_days: list,
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): 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