mirror of
https://github.com/KenanZhu/AutoLibrary.git
synced 2026-06-18 23:43:02 +08:00
Compare commits
47 Commits
v1.0.5
..
1cd39ec84c
| Author | SHA1 | Date | |
|---|---|---|---|
| 1cd39ec84c | |||
| 73aab7b957 | |||
| 0a94c344d5 | |||
| 68e002ba8e | |||
| 94dc22819f | |||
| d55d2075cb | |||
| 82744e3a2d | |||
| 67493349dd | |||
| 0aea9b1540 | |||
| c02c6ddbe3 | |||
| c679a1c79e | |||
| b73242be00 | |||
| 9accf5ddc1 | |||
| 883859d1f9 | |||
| f37bcf836b | |||
| b0d1c0e99e | |||
| 5af6120be8 | |||
| 60e055f6bb | |||
| 01e8100774 | |||
| cf8493565e | |||
| 24bb76d039 | |||
| 7111411115 | |||
| 7df6a9157d | |||
| ebe3910df5 | |||
| 84367e4abe | |||
| 3a50991860 | |||
| e4482b01da | |||
| c06e0e05da | |||
| ff083884b6 | |||
| 9ae89b61a4 | |||
| 2152cc46a3 | |||
| 95a3ae2a24 | |||
| 896242a1e3 | |||
| fd96fc235e | |||
| 25aab588a8 | |||
| 6e1b8e6b10 | |||
| 5f2327cf61 | |||
| 96e7adabb0 | |||
| 42afbbe694 | |||
| 3777970332 | |||
| 9fb28e1368 | |||
| 4aeca08ce8 | |||
| a1ff85256a | |||
| 169de92d5b | |||
| 5ca4a14a14 | |||
| 155b3fe3ca | |||
| 99d454a566 |
@@ -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
|
||||||
+64
-32
@@ -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'
|
||||||
|
|
||||||
@@ -164,10 +176,7 @@ jobs:
|
|||||||
"exe = EXE("
|
"exe = EXE("
|
||||||
" pyz,"
|
" pyz,"
|
||||||
" a.scripts,"
|
" a.scripts,"
|
||||||
" a.binaries,"
|
" name='AutoLibrary',"
|
||||||
" a.datas,"
|
|
||||||
" [],"
|
|
||||||
" name='$exeName',"
|
|
||||||
" debug=False,"
|
" debug=False,"
|
||||||
" bootloader_ignore_signals=False,"
|
" bootloader_ignore_signals=False,"
|
||||||
" strip=False,"
|
" strip=False,"
|
||||||
@@ -182,10 +191,20 @@ jobs:
|
|||||||
" entitlements_file=None,"
|
" entitlements_file=None,"
|
||||||
" icon=['src\\gui\\resources\\icons\\AutoLibrary_32x32.ico'],"
|
" 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
|
$specLines | Out-File -FilePath "Main.spec" -Encoding UTF8
|
||||||
|
|
||||||
Write-Host "✓ Main.spec generated successfully"
|
Write-Host "✓ Main.spec (non-single file) generated successfully"
|
||||||
Write-Host "`nGenerated Main.spec ============"
|
Write-Host "`nGenerated Main.spec ============"
|
||||||
Get-Content "Main.spec" | Write-Host
|
Get-Content "Main.spec" | Write-Host
|
||||||
Write-Host "==================================`n"
|
Write-Host "==================================`n"
|
||||||
@@ -200,17 +219,17 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
$tagName = "${{ steps.get_version.outputs.TAG_NAME }}"
|
$tagName = "${{ steps.get_version.outputs.TAG_NAME }}"
|
||||||
$version = "${{ steps.get_version.outputs.VERSION }}"
|
$version = "${{ steps.get_version.outputs.VERSION }}"
|
||||||
$exeName = "AutoLibrary-$version.exe"
|
$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 executable: dist/$exeName"
|
Write-Host "Looking for distribution directory: $distDir"
|
||||||
if (Test-Path "dist/$exeName") {
|
if (Test-Path $distDir) {
|
||||||
Compress-Archive -Path "dist/$exeName" -DestinationPath $zipName
|
Compress-Archive -Path "$distDir/*" -DestinationPath $zipName
|
||||||
Write-Host "✓ Created release archive: $zipName"
|
Write-Host "✓ Created release archive (directory mode): $zipName"
|
||||||
} else {
|
} else {
|
||||||
Write-Error "✗ Executable not found: dist/$exeName"
|
Write-Error "✗ Distribution directory not found: $distDir"
|
||||||
Write-Host "Files in dist directory:"
|
Write-Host "Files in dist directory:"
|
||||||
Get-ChildItem "dist" | ForEach-Object { Write-Host " - $($_.Name)" }
|
Get-ChildItem "dist" | ForEach-Object { Write-Host " - $($_.Name)" }
|
||||||
exit 1
|
exit 1
|
||||||
@@ -218,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
|
||||||
|
|||||||
@@ -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
-33
@@ -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,41 +175,30 @@ 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: |
|
||||||
### 下载获取
|
|
||||||
- **Windows x86_64**: `AutoLibrary.${{ needs.build.outputs.tag_name }}-windows-x86_64.zip`
|
|
||||||
|
|
||||||
### 如何使用
|
|
||||||
1. 下载 `AutoLibrary.${{ needs.build.outputs.tag_name }}-windows-x86_64.zip` 文件
|
|
||||||
2. 解压到任意目录
|
|
||||||
3. 下载对应浏览器的驱动文件
|
|
||||||
4. 运行 `AutoLibrary-${{ needs.build.outputs.version }}.exe` (首次运行会初始化配置文件)
|
|
||||||
5. 按照提示操作即可
|
|
||||||
|
|
||||||
更多详情请访问 [AutoLibrary 网站](http://www.autolibrary.top) 和查看 [帮助手册](https://www.autolibrary.top/manuals)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
**完整更新日志见下方自动生成的 Release Notes**
|
**完整更新日志见下方自动生成的 Release Notes**
|
||||||
env:
|
env:
|
||||||
@@ -154,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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -5,12 +5,13 @@
|
|||||||

|

|
||||||
|
|
||||||
[](https://github.com/KenanZhu/AutoLibrary)
|
[](https://github.com/KenanZhu/AutoLibrary)
|
||||||

|

|
||||||
[](https://github.com/KenanZhu/AutoLibrary/actions/workflows/release.yml)
|
[](https://github.com/KenanZhu/AutoLibrary/actions/workflows/build-test.yml)
|
||||||
[](https://github.com/KenanZhu/AutoLibrary/releases)
|
[](https://github.com/KenanZhu/AutoLibrary/actions/workflows/release.yml)
|
||||||

|
[](https://github.com/KenanZhu/AutoLibrary/releases/latest)
|
||||||
|

|
||||||
|
|
||||||
了解更多请访问 [_AutoLibrary 网站_](http://www.autolibrary.top)
|
了解更多请访问 [_AutoLibrary 网站_](http://www.autolibrary.kenanzhu.com)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -22,17 +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)。
|
1. 下载最新版本的 [AutoLibrary 压缩包](https://github.com/KenanZhu/AutoLibrary/releases/latest)。
|
||||||
2. 解压下载的文件到任意目录。
|
2. 解压下载的文件到任意目录。
|
||||||
3. 下载对应浏览器的驱动文件,并在配置界面的运行配置选项卡对应位置选择你下载好的浏览器驱动
|
3. 下载对应浏览器类型和版本(具体操作请参考适用软件版本的 [帮助手册](https://www.autolibrary.kenanzhu.com/manuals))的驱动文件,并在配置界面的运行配置选项卡对应位置选择你下载好的浏览器驱动。
|
||||||
4. 运行 `AutoLibrary.exe` 文件。
|
4. 运行 `AutoLibrary-[主版本号].[次版本号].[修订版本号].Z.exe` 文件 (如 `AutoLibrary-1.0.0.exe`)。
|
||||||
5. 按照提示操作即可。
|
5. 点击 [配置] 按钮,在配置界面填写好预约信息和运行配置后,点击 [确认] 按钮。
|
||||||
|
6. 点击 [启动脚本] 按钮,即可开始自动预约、续约、签到等操作。
|
||||||
|
|
||||||
*注意 1*: 关于浏览器驱动的下载和其它相关问题,请参考我们的 [帮助手册](https://www.autolibrary.top/manuals) 中对应软件版本的内容。
|
*注意 1*: 关于浏览器驱动的下载和其它相关问题,请参考我们的 [帮助手册](https://www.autolibrary.kenanzhu.com/manuals) 中对应软件版本的内容。
|
||||||
|
|
||||||
#### 平台支持 & 编译步骤
|
#### 平台支持 & 编译步骤
|
||||||
|
|
||||||
@@ -98,11 +100,11 @@ def classification(self, img: bytes):
|
|||||||
|
|
||||||
#### 后续会有哪些功能?
|
#### 后续会有哪些功能?
|
||||||
|
|
||||||
当前版本的功能对于正常使用已经足够,不过后续会着重考虑完善 2-4 人预约时的使用体验,暂时有以下构想:
|
当前版本的功能对于正常使用已经足够,不过后续会着重完善预约时的使用体验,暂时有以下构想:
|
||||||
|
|
||||||
1. 2-4 人一起预约时,往往会偏向于预约并排或对面的整个空座位,这时候工具会按照一定策略查询搜索符合条件的座位,并预约并排或对面的整个座位,而不是各自独立预约。
|
- 引入交互预约面板功能,预约时直接在座位分布图中选择可用座位,并按用户分配,无需事先配置预约信息。
|
||||||
2. 预约时会考虑到组内用户的预约时间是否冲突,若冲突则会提示用户是否继续预约,若用户选择继续预约,则会按需要调整预约时间,再进行预约。
|
- ~~优化定时任务管理功能,用户可以在定时任务管理界面设置重复运行的定时任务,如每日预约、每周预约等。~~ (已完成)
|
||||||
3. 对于比较固定的用户,会考虑在定时任务管理中添加如 ‘每日任务’ ‘每周任务’ 等选项,用户可以根据需要设置定时任务重复的日期范围,自动完成预约,签到,续约等操作。
|
- 软件的自动更新以及公告栏功能,用户可以自动更新最新版本并获取最新公告事项。
|
||||||
|
|
||||||
不过由于本人的时间和能力有限,也需要考虑到图书馆的正常运行,所以后续功能会有所取舍,但也许会进行一些小的功能验证。
|
不过由于本人的时间和能力有限,也需要考虑到图书馆的正常运行,所以后续功能会有所取舍,但也许会进行一些小的功能验证。
|
||||||
|
|
||||||
|
|||||||
+14
-1
@@ -7,14 +7,25 @@ This software is provided "as is", without any warranty of any kind.
|
|||||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
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 sys
|
import sys
|
||||||
|
|
||||||
from PySide6.QtCore import QTranslator
|
from PySide6.QtCore import QTranslator, QStandardPaths, QDir
|
||||||
from PySide6.QtWidgets import QApplication
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
from gui.ALMainWindow import ALMainWindow
|
from gui.ALMainWindow import ALMainWindow
|
||||||
from gui.resources import ALResource
|
from gui.resources import ALResource
|
||||||
|
|
||||||
|
from utils.ConfigManager import instance
|
||||||
|
|
||||||
|
|
||||||
|
def initializeConfigManager():
|
||||||
|
|
||||||
|
app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
|
||||||
|
config_dir = os.path.join(app_dir, "config")
|
||||||
|
if not QDir(config_dir).exists():
|
||||||
|
QDir().mkpath(config_dir)
|
||||||
|
instance(config_dir)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
||||||
@@ -23,6 +34,8 @@ def main():
|
|||||||
if translator.load(":/res/trans/translators/qtbase_zh_CN.ts"):
|
if translator.load(":/res/trans/translators/qtbase_zh_CN.ts"):
|
||||||
app.installTranslator(translator)
|
app.installTranslator(translator)
|
||||||
app.setStyle('Fusion')
|
app.setStyle('Fusion')
|
||||||
|
app.setApplicationName("AutoLibrary")
|
||||||
|
initializeConfigManager()
|
||||||
window = ALMainWindow()
|
window = ALMainWindow()
|
||||||
window.show()
|
window.show()
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -23,7 +23,6 @@ from gui.ALVersionInfo import (
|
|||||||
AL_VERSION, AL_COMMIT_SHA, AL_COMMIT_DATE, AL_BUILD_DATE
|
AL_VERSION, AL_COMMIT_SHA, AL_COMMIT_DATE, AL_BUILD_DATE
|
||||||
)
|
)
|
||||||
from gui.resources.ui.Ui_ALAboutDialog import Ui_ALAboutDialog
|
from gui.resources.ui.Ui_ALAboutDialog import Ui_ALAboutDialog
|
||||||
|
|
||||||
from gui.resources import ALResource
|
from gui.resources import ALResource
|
||||||
|
|
||||||
|
|
||||||
@@ -83,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>
|
||||||
|
|||||||
+169
-132
@@ -21,38 +21,35 @@ from PySide6.QtGui import (
|
|||||||
QCloseEvent, QAction
|
QCloseEvent, QAction
|
||||||
)
|
)
|
||||||
|
|
||||||
|
import utils.ConfigManager as ConfigManager
|
||||||
|
|
||||||
|
from utils.JSONReader import JSONReader
|
||||||
|
from utils.JSONWriter import JSONWriter
|
||||||
|
|
||||||
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
|
||||||
from gui.ALSeatMapTable import ALSeatMapTable
|
from gui.ALSeatMapTable import ALSeatMapTable
|
||||||
from gui.ALUserTreeWidget import ALUserTreeWidget, ALUserTreeItemType
|
from gui.ALUserTreeWidget import ALUserTreeWidget, ALUserTreeItemType
|
||||||
|
|
||||||
from utils.ConfigReader import ConfigReader
|
|
||||||
from utils.ConfigWriter import ConfigWriter
|
|
||||||
|
|
||||||
|
|
||||||
class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||||
|
|
||||||
configWidgetIsClosed = Signal(dict)
|
configWidgetIsClosed = Signal()
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
parent = None,
|
parent = None,
|
||||||
config_paths = {
|
|
||||||
"run": "",
|
|
||||||
"user": ""
|
|
||||||
}
|
|
||||||
):
|
):
|
||||||
|
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.__config_paths = config_paths
|
self.__cfg_mgr = ConfigManager.instance()
|
||||||
|
self.__config_paths = ConfigManager.getValidateAutomationConfigPaths()
|
||||||
self.__config_data = {"run": {}, "user": {}}
|
self.__config_data = {"run": {}, "user": {}}
|
||||||
|
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
self.modifyUi()
|
self.modifyUi()
|
||||||
self.connectSignals()
|
self.connectSignals()
|
||||||
self.initlizeFloorRoomMap()
|
if not self.initializeConfigs():
|
||||||
self.initlizeDefaultConfigPaths()
|
|
||||||
if not self.initlizeConfigs():
|
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
@@ -68,8 +65,8 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
self.UserListLayout.insertWidget(0, self.UserTreeWidget)
|
self.UserListLayout.insertWidget(0, self.UserTreeWidget)
|
||||||
self.UserTreeWidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
self.UserTreeWidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||||
self.UserTreeWidget.customContextMenuRequested.connect(self.onUserTreeWidgetContextMenu)
|
self.UserTreeWidget.customContextMenuRequested.connect(self.onUserTreeWidgetContextMenu)
|
||||||
self.initlizeFloorRoomMap()
|
self.initializeFloorRoomMap()
|
||||||
self.initilizeUserInfoWidget()
|
self.initializeUserInfoWidget()
|
||||||
|
|
||||||
|
|
||||||
def connectSignals(
|
def connectSignals(
|
||||||
@@ -124,11 +121,11 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
event: QCloseEvent
|
event: QCloseEvent
|
||||||
):
|
):
|
||||||
|
|
||||||
self.configWidgetIsClosed.emit(self.__config_paths)
|
self.configWidgetIsClosed.emit()
|
||||||
super().closeEvent(event)
|
super().closeEvent(event)
|
||||||
|
|
||||||
|
|
||||||
def initlizeFloorRoomMap(
|
def initializeFloorRoomMap(
|
||||||
self
|
self
|
||||||
):
|
):
|
||||||
|
|
||||||
@@ -162,19 +159,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def initlizeDefaultConfigPaths(
|
def initializeConfigToWidget(
|
||||||
self
|
|
||||||
):
|
|
||||||
|
|
||||||
script_path = sys.executable
|
|
||||||
script_dir = QFileInfo(script_path).absoluteDir()
|
|
||||||
self.__default_config_paths = {
|
|
||||||
"user": QDir.toNativeSeparators(script_dir.absoluteFilePath("user.json")),
|
|
||||||
"run": QDir.toNativeSeparators(script_dir.absoluteFilePath("run.json"))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def initlizeConfigToWidget(
|
|
||||||
self,
|
self,
|
||||||
which: str,
|
which: str,
|
||||||
config_data: dict
|
config_data: dict
|
||||||
@@ -184,12 +169,12 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
self.setRunConfigToWidget(config_data)
|
self.setRunConfigToWidget(config_data)
|
||||||
self.CurrentRunConfigEdit.setText(self.__config_paths["run"])
|
self.CurrentRunConfigEdit.setText(self.__config_paths["run"])
|
||||||
elif which == "user":
|
elif which == "user":
|
||||||
self.initilizeUserInfoWidget()
|
self.initializeUserInfoWidget()
|
||||||
self.fillUserTree(config_data)
|
self.setUsersToTreeWidget(config_data)
|
||||||
self.CurrentUserConfigEdit.setText(self.__config_paths["user"])
|
self.CurrentUserConfigEdit.setText(self.__config_paths["user"])
|
||||||
|
|
||||||
|
|
||||||
def initlizeConfig(
|
def initializeConfig(
|
||||||
self,
|
self,
|
||||||
which: str
|
which: str
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@@ -200,7 +185,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
run_config_path = self.__config_paths[which]
|
run_config_path = self.__config_paths[which]
|
||||||
if not os.path.exists(run_config_path):
|
if not os.path.exists(run_config_path):
|
||||||
self.__config_data[which] = self.defaultRunConfig()
|
self.__config_data[which] = self.defaultRunConfig()
|
||||||
self.__config_paths[which] = self.__default_config_paths[which]
|
|
||||||
if self.saveRunConfig(self.__config_paths[which], self.__config_data[which]):
|
if self.saveRunConfig(self.__config_paths[which], self.__config_data[which]):
|
||||||
msg += f"运行配置文件已初始化, 文件路径: \n{self.__config_paths[which]}\n"
|
msg += f"运行配置文件已初始化, 文件路径: \n{self.__config_paths[which]}\n"
|
||||||
else:
|
else:
|
||||||
@@ -213,7 +197,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
user_config_path = self.__config_paths[which]
|
user_config_path = self.__config_paths[which]
|
||||||
if not os.path.exists(user_config_path):
|
if not os.path.exists(user_config_path):
|
||||||
self.__config_data[which] = self.defaultUserConfig()
|
self.__config_data[which] = self.defaultUserConfig()
|
||||||
self.__config_paths[which] = self.__default_config_paths[which]
|
|
||||||
if self.saveUserConfig(self.__config_paths[which], self.__config_data[which]):
|
if self.saveUserConfig(self.__config_paths[which], self.__config_data[which]):
|
||||||
msg += f"用户配置文件已初始化, 文件路径: \n{self.__config_paths[which]}\n"
|
msg += f"用户配置文件已初始化, 文件路径: \n{self.__config_paths[which]}\n"
|
||||||
else:
|
else:
|
||||||
@@ -225,18 +208,16 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
return is_success
|
return is_success
|
||||||
|
|
||||||
|
|
||||||
def initlizeConfigs(
|
def initializeConfigs(
|
||||||
self
|
self
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|
||||||
is_success = True
|
is_success = True
|
||||||
for which in ["run", "user"]:
|
for which in ["run", "user"]:
|
||||||
if not self.__config_paths[which]:
|
if not self.initializeConfig(which):
|
||||||
self.__config_paths[which] = self.__default_config_paths[which]
|
|
||||||
if not self.initlizeConfig(which):
|
|
||||||
is_success = False
|
is_success = False
|
||||||
break
|
break
|
||||||
self.initlizeConfigToWidget(which, self.__config_data[which])
|
self.initializeConfigToWidget(which, self.__config_data[which])
|
||||||
return is_success
|
return is_success
|
||||||
|
|
||||||
|
|
||||||
@@ -269,27 +250,8 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
) -> dict:
|
) -> dict:
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"groups": []
|
"groups": [
|
||||||
}
|
]
|
||||||
|
|
||||||
|
|
||||||
def defaultGroup(
|
|
||||||
self
|
|
||||||
) -> dict:
|
|
||||||
|
|
||||||
return {
|
|
||||||
"name": "默认分组",
|
|
||||||
"enabled": True,
|
|
||||||
"users": []
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def defaultUsers(
|
|
||||||
self
|
|
||||||
) -> dict:
|
|
||||||
|
|
||||||
return {
|
|
||||||
"users": []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -320,6 +282,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
run_config: dict
|
run_config: dict
|
||||||
):
|
):
|
||||||
|
|
||||||
|
try:
|
||||||
self.HostUrlEdit.setText(run_config["library"]["host_url"])
|
self.HostUrlEdit.setText(run_config["library"]["host_url"])
|
||||||
self.LoginUrlEdit.setText(run_config["library"]["login_url"])
|
self.LoginUrlEdit.setText(run_config["library"]["login_url"])
|
||||||
self.AutoCaptchaCheckBox.setChecked(run_config["login"]["auto_captcha"])
|
self.AutoCaptchaCheckBox.setChecked(run_config["login"]["auto_captcha"])
|
||||||
@@ -335,9 +298,25 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
self.AutoReserveCheckBox.setChecked(run_mode&0x01)
|
self.AutoReserveCheckBox.setChecked(run_mode&0x01)
|
||||||
self.AutoCheckinCheckBox.setChecked(run_mode&0x02)
|
self.AutoCheckinCheckBox.setChecked(run_mode&0x02)
|
||||||
self.AutoRenewalCheckBox.setChecked(run_mode&0x04)
|
self.AutoRenewalCheckBox.setChecked(run_mode&0x04)
|
||||||
|
except KeyError as e:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"警告 - AutoLibrary",
|
||||||
|
f"运行配置文件读取键 '{e}' 时发生错误 ! :\n"
|
||||||
|
f"文件路径: {self.__config_paths['run']}\n"
|
||||||
|
"文件可能被意外修改或已经损坏\n"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"警告 - AutoLibrary",
|
||||||
|
f"运行配置文件读取键 '{e}' 时发生未知错误 ! :\n"
|
||||||
|
f"文件路径: {self.__config_paths['run']}\n"
|
||||||
|
"文件可能被意外修改或已经损坏\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def initilizeUserInfoWidget(
|
def initializeUserInfoWidget(
|
||||||
self
|
self
|
||||||
):
|
):
|
||||||
|
|
||||||
@@ -362,7 +341,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
self.PreferLateRenewTimeCheckBox.setChecked(False)
|
self.PreferLateRenewTimeCheckBox.setChecked(False)
|
||||||
|
|
||||||
|
|
||||||
def collectUserFromUserInfoWidget(
|
def collectUserFromWidget(
|
||||||
self
|
self
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
|
||||||
@@ -395,7 +374,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
def collectUserConfigFromUserTreeWidget(
|
def collectUsersFromTreeWidget(
|
||||||
self
|
self
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
|
||||||
@@ -442,13 +421,64 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
self.ExpectRenewDurationSpinBox.setValue(user["reserve_info"]["renew_time"]["expect_duration"])
|
self.ExpectRenewDurationSpinBox.setValue(user["reserve_info"]["renew_time"]["expect_duration"])
|
||||||
self.MaxRenewTimeDiffSpinBox.setValue(user["reserve_info"]["renew_time"]["max_diff"])
|
self.MaxRenewTimeDiffSpinBox.setValue(user["reserve_info"]["renew_time"]["max_diff"])
|
||||||
self.PreferLateRenewTimeCheckBox.setChecked(not user["reserve_info"]["renew_time"]["prefer_early"])
|
self.PreferLateRenewTimeCheckBox.setChecked(not user["reserve_info"]["renew_time"]["prefer_early"])
|
||||||
except:
|
except KeyError as e:
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self,
|
self,
|
||||||
"警告 - AutoLibrary",
|
"警告 - AutoLibrary",
|
||||||
"用户配置文件读取发生错误 !\n"\
|
f"用户配置文件读取键 '{e}' 时发生错误 ! :\n"
|
||||||
f"用户: {user['username']} 配置文件可能已损坏"
|
f"文件路径: {self.__config_paths['user']}\n"
|
||||||
|
"文件可能被意外修改或已经损坏\n"
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"警告 - AutoLibrary",
|
||||||
|
f"用户配置文件读取键 '{e}' 时发生未知错误 ! :\n"
|
||||||
|
f"文件路径: {self.__config_paths['user']}\n"
|
||||||
|
"文件可能被意外修改或已经损坏\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def setUsersToTreeWidget(
|
||||||
|
self,
|
||||||
|
users: dict
|
||||||
|
):
|
||||||
|
|
||||||
|
self.UserTreeWidget.clear()
|
||||||
|
self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged)
|
||||||
|
try:
|
||||||
|
if "groups" in users:
|
||||||
|
for group_config in users["groups"]:
|
||||||
|
group_item = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value)
|
||||||
|
group_item.setText(0, group_config["name"])
|
||||||
|
group_item.setFlags(group_item.flags() | Qt.ItemIsEditable)
|
||||||
|
group_item.setCheckState(1, Qt.Checked if group_config.get("enabled", True) else Qt.Unchecked)
|
||||||
|
for user_config in group_config["users"]:
|
||||||
|
user_item = QTreeWidgetItem(group_item, ALUserTreeItemType.USER.value)
|
||||||
|
user_item.setText(0, user_config["username"])
|
||||||
|
user_item.setText(1, "" if user_config.get("enabled", True) else "跳过")
|
||||||
|
user_item.setData(0, Qt.UserRole, user_config)
|
||||||
|
user_item.setCheckState(1, Qt.Checked if user_config.get("enabled", True) else Qt.Unchecked)
|
||||||
|
user_item.setDisabled(not group_config.get("enabled", True))
|
||||||
|
group_item.setExpanded(True)
|
||||||
|
except KeyError as e:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"警告 - AutoLibrary",
|
||||||
|
f"用户配置文件读取键 '{e}' 时发生错误 ! :\n"
|
||||||
|
f"文件路径: {self.__config_paths['user']}\n"
|
||||||
|
"文件可能被意外修改或已经损坏\n"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"警告 - AutoLibrary",
|
||||||
|
f"用户配置文件读取键 '{e}' 时发生未知错误 ! :\n"
|
||||||
|
f"文件路径: {self.__config_paths['user']}\n"
|
||||||
|
"文件可能被意外修改或已经损坏\n"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged)
|
||||||
|
|
||||||
|
|
||||||
def loadRunConfig(
|
def loadRunConfig(
|
||||||
@@ -459,18 +489,18 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
try:
|
try:
|
||||||
if not run_config_path or not os.path.exists(run_config_path):
|
if not run_config_path or not os.path.exists(run_config_path):
|
||||||
raise Exception("文件路径不存在")
|
raise Exception("文件路径不存在")
|
||||||
run_config = ConfigReader(run_config_path).getConfigs()
|
run_config = JSONReader(run_config_path).data()
|
||||||
if run_config and "library" in run_config\
|
if run_config and "library" in run_config\
|
||||||
and "web_driver" in run_config\
|
and "web_driver" in run_config\
|
||||||
and "login" in run_config:
|
and "login" in run_config:
|
||||||
return run_config
|
return run_config
|
||||||
|
else:
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self,
|
self,
|
||||||
"警告 - AutoLibrary",
|
"警告 - AutoLibrary",
|
||||||
f"运行配置文件读取发生错误 ! : {e}\n"\
|
f"运行配置文件读取发生错误 ! :\n{e}"
|
||||||
f"文件路径: {run_config_path}"
|
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -486,14 +516,13 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
raise Exception("文件路径为空")
|
raise Exception("文件路径为空")
|
||||||
if not run_config_data or not isinstance(run_config_data, dict):
|
if not run_config_data or not isinstance(run_config_data, dict):
|
||||||
raise Exception("运行配置数据为空或类型错误")
|
raise Exception("运行配置数据为空或类型错误")
|
||||||
ConfigWriter(run_config_path, run_config_data)
|
JSONWriter(run_config_path, run_config_data)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self,
|
self,
|
||||||
"警告 - AutoLibrary",
|
"警告 - AutoLibrary",
|
||||||
f"配置文件写入发生错误 ! : {e}\n"\
|
f"配置文件写入发生错误 ! : \n{e}"
|
||||||
f"文件路径: {run_config_path}"
|
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -506,11 +535,11 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
try:
|
try:
|
||||||
if not user_config_path or not os.path.exists(user_config_path):
|
if not user_config_path or not os.path.exists(user_config_path):
|
||||||
raise Exception("文件路径不存在")
|
raise Exception("文件路径不存在")
|
||||||
user_config = ConfigReader(user_config_path).getConfigs()
|
user_config = JSONReader(user_config_path).data()
|
||||||
if user_config and "groups" in user_config:
|
if user_config and "groups" in user_config:
|
||||||
return user_config
|
return user_config
|
||||||
# compatibility with old version config format
|
# compatibility with old version config format
|
||||||
if user_config and "users" in user_config:
|
elif user_config and "users" in user_config:
|
||||||
user_config = {
|
user_config = {
|
||||||
"groups": [
|
"groups": [
|
||||||
{
|
{
|
||||||
@@ -521,13 +550,13 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
return user_config
|
return user_config
|
||||||
|
else:
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self,
|
self,
|
||||||
"警告 - AutoLibrary",
|
"警告 - AutoLibrary",
|
||||||
f"用户配置文件读取发生错误 ! : {e}\n"\
|
f"用户配置文件读取发生错误 ! :\n{e}"
|
||||||
f"文件路径: {user_config_path}"
|
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -543,14 +572,13 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
raise Exception("文件路径为空")
|
raise Exception("文件路径为空")
|
||||||
if not user_config_data or not isinstance(user_config_data, dict):
|
if not user_config_data or not isinstance(user_config_data, dict):
|
||||||
raise Exception("用户配置数据为空或类型错误")
|
raise Exception("用户配置数据为空或类型错误")
|
||||||
ConfigWriter(user_config_path, user_config_data)
|
JSONWriter(user_config_path, user_config_data)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self,
|
self,
|
||||||
"警告 - AutoLibrary",
|
"警告 - AutoLibrary",
|
||||||
f"用户配置文件写入发生错误 ! : {e}\n"\
|
f"用户配置文件写入发生错误 ! :\n{e}"
|
||||||
f"文件路径: \n{user_config_path}"
|
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -562,7 +590,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
) -> bool:
|
) -> bool:
|
||||||
|
|
||||||
if user_config_path:
|
if user_config_path:
|
||||||
self.__config_data["user"] = self.collectUserConfigFromUserTreeWidget()
|
self.__config_data["user"] = self.collectUsersFromTreeWidget()
|
||||||
if not self.saveUserConfig(
|
if not self.saveUserConfig(
|
||||||
user_config_path,
|
user_config_path,
|
||||||
self.__config_data["user"]
|
self.__config_data["user"]
|
||||||
@@ -601,38 +629,12 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
return True
|
return True
|
||||||
if user_config is not None:
|
if user_config is not None:
|
||||||
self.__config_data["user"].update(user_config)
|
self.__config_data["user"].update(user_config)
|
||||||
self.fillUserTree(self.__config_data["user"])
|
self.setUsersToTreeWidget(self.__config_data["user"])
|
||||||
return True
|
return True
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def fillUserTree(
|
|
||||||
self,
|
|
||||||
user_config_data: dict
|
|
||||||
):
|
|
||||||
|
|
||||||
self.UserTreeWidget.clear()
|
|
||||||
self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged)
|
|
||||||
try:
|
|
||||||
if "groups" in user_config_data:
|
|
||||||
for group_config in user_config_data["groups"]:
|
|
||||||
group_item = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value)
|
|
||||||
group_item.setText(0, group_config["name"])
|
|
||||||
group_item.setFlags(group_item.flags() | Qt.ItemIsEditable)
|
|
||||||
group_item.setCheckState(1, Qt.Checked if group_config.get("enabled", True) else Qt.Unchecked)
|
|
||||||
for user_config in group_config["users"]:
|
|
||||||
user_item = QTreeWidgetItem(group_item, ALUserTreeItemType.USER.value)
|
|
||||||
user_item.setText(0, user_config["username"])
|
|
||||||
user_item.setText(1, "" if user_config.get("enabled", True) else "跳过")
|
|
||||||
user_item.setData(0, Qt.UserRole, user_config)
|
|
||||||
user_item.setCheckState(1, Qt.Checked if user_config.get("enabled", True) else Qt.Unchecked)
|
|
||||||
user_item.setDisabled(not group_config.get("enabled", True))
|
|
||||||
group_item.setExpanded(True)
|
|
||||||
finally:
|
|
||||||
self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged)
|
|
||||||
|
|
||||||
|
|
||||||
def addGroup(
|
def addGroup(
|
||||||
self,
|
self,
|
||||||
group_name: str = ""
|
group_name: str = ""
|
||||||
@@ -650,6 +652,19 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
return group_item
|
return group_item
|
||||||
|
|
||||||
|
|
||||||
|
def delGroup(
|
||||||
|
self,
|
||||||
|
group_item: QTreeWidgetItem = None
|
||||||
|
):
|
||||||
|
|
||||||
|
if group_item is None:
|
||||||
|
return
|
||||||
|
if group_item.type() != ALUserTreeItemType.GROUP.value:
|
||||||
|
return
|
||||||
|
index = self.UserTreeWidget.indexOfTopLevelItem(group_item)
|
||||||
|
self.UserTreeWidget.takeTopLevelItem(index)
|
||||||
|
|
||||||
|
|
||||||
def addUser(
|
def addUser(
|
||||||
self,
|
self,
|
||||||
group_item: QTreeWidgetItem = None
|
group_item: QTreeWidgetItem = None
|
||||||
@@ -721,19 +736,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
self.UserTreeWidget.setCurrentItem(None)
|
self.UserTreeWidget.setCurrentItem(None)
|
||||||
|
|
||||||
|
|
||||||
def delGroup(
|
|
||||||
self,
|
|
||||||
group_item: QTreeWidgetItem = None
|
|
||||||
):
|
|
||||||
|
|
||||||
if group_item is None:
|
|
||||||
return
|
|
||||||
if group_item.type() != ALUserTreeItemType.GROUP.value:
|
|
||||||
return
|
|
||||||
index = self.UserTreeWidget.indexOfTopLevelItem(group_item)
|
|
||||||
self.UserTreeWidget.takeTopLevelItem(index)
|
|
||||||
|
|
||||||
|
|
||||||
def renameItem(
|
def renameItem(
|
||||||
self,
|
self,
|
||||||
item: QTreeWidgetItem,
|
item: QTreeWidgetItem,
|
||||||
@@ -762,7 +764,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
item.setData(0, Qt.UserRole, user)
|
item.setData(0, Qt.UserRole, user)
|
||||||
self.setUserToWidget(user)
|
self.setUserToWidget(user)
|
||||||
|
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def onShowPasswordCheckBoxChecked(
|
def onShowPasswordCheckBoxChecked(
|
||||||
self,
|
self,
|
||||||
@@ -818,7 +819,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
# possiblity of frequency edit. we just let the QListWidget
|
# possiblity of frequency edit. we just let the QListWidget
|
||||||
# help us.
|
# help us.
|
||||||
if previous and previous.type() == ALUserTreeItemType.USER.value:
|
if previous and previous.type() == ALUserTreeItemType.USER.value:
|
||||||
user = self.collectUserFromUserInfoWidget()
|
user = self.collectUserFromWidget()
|
||||||
if user:
|
if user:
|
||||||
self.UsernameEdit.textEdited.disconnect()
|
self.UsernameEdit.textEdited.disconnect()
|
||||||
user["enabled"] = previous.checkState(1) == Qt.Checked
|
user["enabled"] = previous.checkState(1) == Qt.Checked
|
||||||
@@ -826,7 +827,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
previous.setText(1, "" if user.get("enabled", True) else "跳过")
|
previous.setText(1, "" if user.get("enabled", True) else "跳过")
|
||||||
previous.setData(0, Qt.UserRole, user)
|
previous.setData(0, Qt.UserRole, user)
|
||||||
if current is None:
|
if current is None:
|
||||||
self.initilizeUserInfoWidget()
|
self.initializeUserInfoWidget()
|
||||||
return
|
return
|
||||||
if current.type() == ALUserTreeItemType.USER.value:
|
if current.type() == ALUserTreeItemType.USER.value:
|
||||||
user = current.data(0, Qt.UserRole)
|
user = current.data(0, Qt.UserRole)
|
||||||
@@ -834,7 +835,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
self.setUserToWidget(user)
|
self.setUserToWidget(user)
|
||||||
self.UsernameEdit.textEdited.connect(lambda text: current.setText(0, text))
|
self.UsernameEdit.textEdited.connect(lambda text: current.setText(0, text))
|
||||||
else:
|
else:
|
||||||
self.initilizeUserInfoWidget()
|
self.initializeUserInfoWidget()
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def onUserTreeWidgetItemChanged(
|
def onUserTreeWidgetItemChanged(
|
||||||
@@ -961,9 +962,27 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
)[0]
|
)[0]
|
||||||
if run_config_path:
|
if run_config_path:
|
||||||
run_config_path = QDir.toNativeSeparators(run_config_path)
|
run_config_path = QDir.toNativeSeparators(run_config_path)
|
||||||
if self.loadConfig(run_config_path):
|
data = self.loadRunConfig(run_config_path)
|
||||||
|
if data is not None:
|
||||||
|
self.__config_data["run"].update(data)
|
||||||
|
self.setRunConfigToWidget(data)
|
||||||
self.__config_paths["run"] = run_config_path
|
self.__config_paths["run"] = run_config_path
|
||||||
self.CurrentRunConfigEdit.setText(run_config_path)
|
self.CurrentRunConfigEdit.setText(run_config_path)
|
||||||
|
paths = self.__cfg_mgr.get(ConfigManager.ConfigType.GLOBAL, "automation.run_path.paths", [])
|
||||||
|
if run_config_path not in paths:
|
||||||
|
paths.append(run_config_path)
|
||||||
|
index = len(paths) - 1
|
||||||
|
else:
|
||||||
|
index = paths.index(run_config_path)
|
||||||
|
self.__cfg_mgr.set(ConfigManager.ConfigType.GLOBAL, "automation.run_path", {"current": index, "paths": paths})
|
||||||
|
else:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"警告 - AutoLibrary",
|
||||||
|
"运行配置文件读取发生错误 ! :\n"\
|
||||||
|
"无法从选择的运行配置文件中加载数据 ! :\n"\
|
||||||
|
"可能选择了错误的配置文件类型"
|
||||||
|
)
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def onBrowseCurrentUserConfigButtonClicked(
|
def onBrowseCurrentUserConfigButtonClicked(
|
||||||
@@ -978,9 +997,27 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
)[0]
|
)[0]
|
||||||
if user_config_path:
|
if user_config_path:
|
||||||
user_config_path = QDir.toNativeSeparators(user_config_path)
|
user_config_path = QDir.toNativeSeparators(user_config_path)
|
||||||
if self.loadConfig(user_config_path):
|
data = self.loadUserConfig(user_config_path)
|
||||||
|
if data is not None:
|
||||||
|
self.__config_data["user"].update(data)
|
||||||
|
self.setUsersToTreeWidget(data)
|
||||||
self.__config_paths["user"] = user_config_path
|
self.__config_paths["user"] = user_config_path
|
||||||
self.CurrentUserConfigEdit.setText(user_config_path)
|
self.CurrentUserConfigEdit.setText(user_config_path)
|
||||||
|
paths = self.__cfg_mgr.get(ConfigManager.ConfigType.GLOBAL, "automation.user_path.paths", [])
|
||||||
|
if user_config_path not in paths:
|
||||||
|
paths.append(user_config_path)
|
||||||
|
index = len(paths) - 1
|
||||||
|
else:
|
||||||
|
index = paths.index(user_config_path)
|
||||||
|
self.__cfg_mgr.set(ConfigManager.ConfigType.GLOBAL, "automation.user_path", {"current": index, "paths": paths})
|
||||||
|
else:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"警告 - AutoLibrary",
|
||||||
|
"用户配置文件读取发生错误 ! :\n"\
|
||||||
|
"无法从选择的用户配置文件中加载数据 ! :\n"\
|
||||||
|
"可能选择了错误的配置文件类型"
|
||||||
|
)
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def onBrowseExportRunConfigButtonClicked(
|
def onBrowseExportRunConfigButtonClicked(
|
||||||
@@ -1067,9 +1104,9 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
if run_exists or user_exists:
|
if run_exists or user_exists:
|
||||||
exist_files = []
|
exist_files = []
|
||||||
if run_exists:
|
if run_exists:
|
||||||
exist_files.append(run_config_path)
|
exist_files.append(f"运行配置文件: \n{run_config_path}")
|
||||||
if user_exists:
|
if user_exists:
|
||||||
exist_files.append(user_config_path)
|
exist_files.append(f"用户配置文件: \n{user_config_path}")
|
||||||
reply = QMessageBox.information(
|
reply = QMessageBox.information(
|
||||||
self,
|
self,
|
||||||
"提示 - AutoLibrary",
|
"提示 - AutoLibrary",
|
||||||
@@ -1085,8 +1122,8 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
"run": run_config_path,
|
"run": run_config_path,
|
||||||
"user": user_config_path
|
"user": user_config_path
|
||||||
}
|
}
|
||||||
self.initlizeConfigToWidget("run", self.__config_data["run"])
|
self.initializeConfigToWidget("run", self.__config_data["run"])
|
||||||
self.initlizeConfigToWidget("user", self.__config_data["user"])
|
self.initializeConfigToWidget("user", self.__config_data["user"])
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def onConfirmButtonClicked(
|
def onConfirmButtonClicked(
|
||||||
@@ -1103,7 +1140,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
QMessageBox.information(
|
QMessageBox.information(
|
||||||
self,
|
self,
|
||||||
"提示 - AutoLibrary",
|
"提示 - AutoLibrary",
|
||||||
"配置文件保存成功 !\n"
|
"配置文件保存成功 ! :\n"
|
||||||
f"运行配置文件路径: \n{self.__config_paths['run']}\n"\
|
f"运行配置文件路径: \n{self.__config_paths['run']}\n"\
|
||||||
f"用户配置文件路径: \n{self.__config_paths['user']}"
|
f"用户配置文件路径: \n{self.__config_paths['user']}"
|
||||||
)
|
)
|
||||||
@@ -1111,7 +1148,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
|||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self,
|
self,
|
||||||
"警告 - AutoLibrary",
|
"警告 - AutoLibrary",
|
||||||
"配置文件保存失败, 请检查文件路径权限"
|
"配置文件保存失败 !\n"
|
||||||
)
|
)
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
|||||||
+55
-53
@@ -7,33 +7,33 @@ 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 sys
|
|
||||||
import time
|
|
||||||
import queue
|
import queue
|
||||||
|
|
||||||
from PySide6.QtCore import (
|
from PySide6.QtCore import (
|
||||||
Qt, Signal, Slot, QDir, QFileInfo, QTimer, QUrl,
|
Qt, Signal, Slot, QTimer, QUrl,
|
||||||
)
|
)
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QMainWindow, QMenu, QSystemTrayIcon
|
QMainWindow, QMenu, QSystemTrayIcon, QMessageBox
|
||||||
)
|
)
|
||||||
from PySide6.QtGui import (
|
from PySide6.QtGui import (
|
||||||
QTextCursor, QCloseEvent, QFont, QIcon, QDesktopServices
|
QTextCursor, QCloseEvent, QFont, QIcon, QDesktopServices
|
||||||
)
|
)
|
||||||
|
|
||||||
|
import utils.ConfigManager as ConfigManager
|
||||||
|
|
||||||
from base.MsgBase import MsgBase
|
from base.MsgBase import MsgBase
|
||||||
|
|
||||||
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.ALConfigWidget import ALConfigWidget
|
from gui.ALConfigWidget import ALConfigWidget
|
||||||
from gui.ALTimerTaskManageWidget import ALTimerTaskManageWidget
|
from gui.ALTimerTaskManageWidget import ALTimerTaskManageWidget
|
||||||
from gui.ALAboutDialog import ALAboutDialog
|
from gui.ALAboutDialog import ALAboutDialog
|
||||||
from gui.ALMainWorkers import TimerTaskWorker, AutoLibWorker
|
from gui.ALMainWorkers import TimerTaskWorker, AutoLibWorker
|
||||||
|
|
||||||
from gui.resources import ALResource
|
|
||||||
|
|
||||||
|
|
||||||
class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
||||||
|
|
||||||
|
# signal : timer task
|
||||||
timerTaskIsRunning = Signal(dict)
|
timerTaskIsRunning = Signal(dict)
|
||||||
timerTaskIsExecuted = Signal(dict)
|
timerTaskIsExecuted = Signal(dict)
|
||||||
timerTaskIsError = Signal(dict)
|
timerTaskIsError = Signal(dict)
|
||||||
@@ -44,15 +44,10 @@ 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 = ConfigManager.instance()
|
||||||
self.__timer_task_queue = queue.Queue()
|
self.__timer_task_queue = queue.Queue()
|
||||||
script_path = sys.executable
|
self.__config_paths = ConfigManager.getValidateAutomationConfigPaths()
|
||||||
script_dir = QFileInfo(script_path).absoluteDir()
|
self.__alTimerTaskManageWidget = None
|
||||||
self.__config_paths = {
|
|
||||||
"run": QDir.toNativeSeparators(script_dir.absoluteFilePath("run.json")),
|
|
||||||
"user": QDir.toNativeSeparators(script_dir.absoluteFilePath("user.json")),
|
|
||||||
"timer_task": QDir.toNativeSeparators(script_dir.absoluteFilePath("timer_task.json")),
|
|
||||||
}
|
|
||||||
self.__alTimerTaskWidget = None
|
|
||||||
self.__alConfigWidget = None
|
self.__alConfigWidget = None
|
||||||
self.__auto_lib_thread = None
|
self.__auto_lib_thread = None
|
||||||
self.__current_timer_task_thread = None
|
self.__current_timer_task_thread = None
|
||||||
@@ -77,13 +72,24 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
|||||||
self.AboutAction.triggered.connect(self.onAboutActionTriggered)
|
self.AboutAction.triggered.connect(self.onAboutActionTriggered)
|
||||||
|
|
||||||
# initialize timer task widget, but not show it
|
# initialize timer task widget, but not show it
|
||||||
self.__alTimerTaskWidget = ALTimerTaskManageWidget(self, self.__config_paths["timer_task"])
|
try:
|
||||||
self.timerTaskIsRunning.connect(self.__alTimerTaskWidget.onTimerTaskIsRunning)
|
self.__alTimerTaskManageWidget = ALTimerTaskManageWidget(self)
|
||||||
self.timerTaskIsExecuted.connect(self.__alTimerTaskWidget.onTimerTaskIsExecuted)
|
except Exception as e:
|
||||||
self.timerTaskIsError.connect(self.__alTimerTaskWidget.onTimerTaskIsError)
|
QMessageBox.critical(
|
||||||
self.__alTimerTaskWidget.timerTaskIsReady.connect(self.onTimerTaskIsReady)
|
self,
|
||||||
self.__alTimerTaskWidget.timerTaskManageWidgetClosed.connect(self.onTimerTaskWidgetClosed)
|
"错误 - AutoLibrary",
|
||||||
self.__alTimerTaskWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint)
|
f"初始化定时任务功能失败: \n{e}"
|
||||||
|
)
|
||||||
|
self.__alTimerTaskManageWidget = None
|
||||||
|
self.TimerTaskManageWidgetButton.setEnabled(False)
|
||||||
|
self.TimerTaskManageWidgetButton.setToolTip("定时任务功能初始化失败, 请检查配置文件。")
|
||||||
|
return
|
||||||
|
self.timerTaskIsRunning.connect(self.__alTimerTaskManageWidget.onTimerTaskIsRunning)
|
||||||
|
self.timerTaskIsExecuted.connect(self.__alTimerTaskManageWidget.onTimerTaskIsExecuted)
|
||||||
|
self.timerTaskIsError.connect(self.__alTimerTaskManageWidget.onTimerTaskIsError)
|
||||||
|
self.__alTimerTaskManageWidget.timerTaskIsReady.connect(self.onTimerTaskIsReady)
|
||||||
|
self.__alTimerTaskManageWidget.timerTaskManageWidgetIsClosed.connect(self.onTimerTaskManageWidgetClosed)
|
||||||
|
self.__alTimerTaskManageWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint)
|
||||||
|
|
||||||
|
|
||||||
def onAboutActionTriggered(
|
def onAboutActionTriggered(
|
||||||
@@ -98,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)
|
||||||
|
|
||||||
|
|
||||||
@@ -107,22 +113,19 @@ 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")
|
||||||
|
|
||||||
self.TrayMenu = QMenu()
|
self.TrayMenu = QMenu()
|
||||||
self.TrayMenu.addAction("显示主窗口", self.showNormal)
|
self.TrayMenu.addAction("显示主窗口", self.showNormal)
|
||||||
self.TrayMenu.addAction("显示定时窗口", self.onTimerTaskWidgetButtonClicked)
|
self.TrayMenu.addAction("显示定时窗口", self.onTimerTaskManageWidgetButtonClicked)
|
||||||
self.TrayMenu.addAction("最小化到托盘", self.hideToTray)
|
self.TrayMenu.addAction("最小化到托盘", self.hideToTray)
|
||||||
self.TrayMenu.addSeparator()
|
self.TrayMenu.addSeparator()
|
||||||
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()
|
||||||
|
|
||||||
@@ -154,7 +157,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
|||||||
):
|
):
|
||||||
|
|
||||||
self.ConfigButton.clicked.connect(self.onConfigButtonClicked)
|
self.ConfigButton.clicked.connect(self.onConfigButtonClicked)
|
||||||
self.TimerTaskWidgetButton.clicked.connect(self.onTimerTaskWidgetButtonClicked)
|
self.TimerTaskManageWidgetButton.clicked.connect(self.onTimerTaskManageWidgetButtonClicked)
|
||||||
self.StartButton.clicked.connect(self.onStartButtonClicked)
|
self.StartButton.clicked.connect(self.onStartButtonClicked)
|
||||||
self.StopButton.clicked.connect(self.onStopButtonClicked)
|
self.StopButton.clicked.connect(self.onStopButtonClicked)
|
||||||
self.SendButton.clicked.connect(self.onSendButtonClicked)
|
self.SendButton.clicked.connect(self.onSendButtonClicked)
|
||||||
@@ -166,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():
|
||||||
@@ -173,13 +180,13 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
|||||||
if self.__is_running_timer_task:
|
if self.__is_running_timer_task:
|
||||||
self.__current_timer_task_thread.wait(2000)
|
self.__current_timer_task_thread.wait(2000)
|
||||||
self.__current_timer_task_thread.deleteLater()
|
self.__current_timer_task_thread.deleteLater()
|
||||||
if self.__alTimerTaskWidget:
|
if self.__alTimerTaskManageWidget:
|
||||||
self.__alTimerTaskWidget.close()
|
self.__alTimerTaskManageWidget.close()
|
||||||
self.__alTimerTaskWidget.deleteLater()
|
self.__alTimerTaskManageWidget.deleteLater()
|
||||||
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(
|
||||||
@@ -241,7 +248,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
|||||||
self._output_queue,
|
self._output_queue,
|
||||||
self.__config_paths
|
self.__config_paths
|
||||||
)
|
)
|
||||||
self.__current_timer_task_thread.TimerTaskWorkerIsFinished.connect(self.onTimerTaskFinished)
|
self.__current_timer_task_thread.timerTaskWorkerIsFinished.connect(self.onTimerTaskFinished)
|
||||||
self.__current_timer_task_thread.start()
|
self.__current_timer_task_thread.start()
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
self.__is_running_timer_task = False
|
self.__is_running_timer_task = False
|
||||||
@@ -276,16 +283,15 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def onTimerTaskWidgetClosed(
|
def onTimerTaskManageWidgetClosed(
|
||||||
self
|
self
|
||||||
):
|
):
|
||||||
|
|
||||||
self.TimerTaskWidgetButton.setEnabled(True)
|
self.TimerTaskManageWidgetButton.setEnabled(True)
|
||||||
|
|
||||||
@Slot(dict)
|
@Slot(dict)
|
||||||
def onConfigWidgetClosed(
|
def onConfigWidgetClosed(
|
||||||
self,
|
self
|
||||||
config_paths: dict
|
|
||||||
):
|
):
|
||||||
|
|
||||||
if self.__alConfigWidget:
|
if self.__alConfigWidget:
|
||||||
@@ -293,7 +299,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
|||||||
self.__alConfigWidget.deleteLater()
|
self.__alConfigWidget.deleteLater()
|
||||||
self.__alConfigWidget = None
|
self.__alConfigWidget = None
|
||||||
self.setControlButtons(True, None, None)
|
self.setControlButtons(True, None, None)
|
||||||
self.__config_paths = config_paths
|
|
||||||
|
|
||||||
@Slot(dict)
|
@Slot(dict)
|
||||||
def onTimerTaskIsReady(
|
def onTimerTaskIsReady(
|
||||||
@@ -311,7 +316,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
|||||||
):
|
):
|
||||||
|
|
||||||
self.__current_timer_task_thread.wait(1000)
|
self.__current_timer_task_thread.wait(1000)
|
||||||
self.__current_timer_task_thread.TimerTaskWorkerIsFinished.disconnect(self.onTimerTaskFinished)
|
self.__current_timer_task_thread.timerTaskWorkerIsFinished.disconnect(self.onTimerTaskFinished)
|
||||||
self.__current_timer_task_thread.deleteLater()
|
self.__current_timer_task_thread.deleteLater()
|
||||||
self.__current_timer_task_thread = None
|
self.__current_timer_task_thread = None
|
||||||
self.setControlButtons(None, False, True)
|
self.setControlButtons(None, False, True)
|
||||||
@@ -321,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(
|
||||||
@@ -333,14 +338,14 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
|||||||
self.timerTaskIsError.emit(timer_task)
|
self.timerTaskIsError.emit(timer_task)
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def onTimerTaskWidgetButtonClicked(
|
def onTimerTaskManageWidgetButtonClicked(
|
||||||
self
|
self
|
||||||
):
|
):
|
||||||
|
|
||||||
self.__alTimerTaskWidget.show()
|
self.__alTimerTaskManageWidget.show()
|
||||||
self.__alTimerTaskWidget.raise_()
|
self.__alTimerTaskManageWidget.raise_()
|
||||||
self.__alTimerTaskWidget.activateWindow()
|
self.__alTimerTaskManageWidget.activateWindow()
|
||||||
self.TimerTaskWidgetButton.setEnabled(False)
|
self.TimerTaskManageWidgetButton.setEnabled(False)
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def onConfigButtonClicked(
|
def onConfigButtonClicked(
|
||||||
@@ -348,10 +353,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
|||||||
):
|
):
|
||||||
|
|
||||||
if self.__alConfigWidget is None:
|
if self.__alConfigWidget is None:
|
||||||
self.__alConfigWidget = ALConfigWidget(
|
self.__alConfigWidget = ALConfigWidget(self)
|
||||||
self,
|
|
||||||
self.__config_paths
|
|
||||||
)
|
|
||||||
self.__alConfigWidget.configWidgetIsClosed.connect(self.onConfigWidgetClosed)
|
self.__alConfigWidget.configWidgetIsClosed.connect(self.onConfigWidgetClosed)
|
||||||
self.__alConfigWidget.show()
|
self.__alConfigWidget.show()
|
||||||
self.__alConfigWidget.raise_()
|
self.__alConfigWidget.raise_()
|
||||||
@@ -370,8 +372,8 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
|||||||
self._output_queue,
|
self._output_queue,
|
||||||
self.__config_paths
|
self.__config_paths
|
||||||
)
|
)
|
||||||
self.__auto_lib_thread.AutoLibWorkerIsFinished.connect(self.onStopButtonClicked)
|
self.__auto_lib_thread.autoLibWorkerIsFinished.connect(self.onStopButtonClicked)
|
||||||
self.__auto_lib_thread.AutoLibWorkerFinishedWithError.connect(self.onStopButtonClicked)
|
self.__auto_lib_thread.autoLibWorkerFinishedWithError.connect(self.onStopButtonClicked)
|
||||||
self.__auto_lib_thread.start()
|
self.__auto_lib_thread.start()
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
@@ -383,8 +385,8 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
|||||||
self._showTrace("正在停止操作......")
|
self._showTrace("正在停止操作......")
|
||||||
self.__auto_lib_thread.wait(2000)
|
self.__auto_lib_thread.wait(2000)
|
||||||
self._showTrace("操作已停止")
|
self._showTrace("操作已停止")
|
||||||
self.__auto_lib_thread.AutoLibWorkerIsFinished.disconnect(self.onStopButtonClicked)
|
self.__auto_lib_thread.autoLibWorkerIsFinished.disconnect(self.onStopButtonClicked)
|
||||||
self.__auto_lib_thread.AutoLibWorkerFinishedWithError.disconnect(self.onStopButtonClicked)
|
self.__auto_lib_thread.autoLibWorkerFinishedWithError.disconnect(self.onStopButtonClicked)
|
||||||
self.__auto_lib_thread.deleteLater()
|
self.__auto_lib_thread.deleteLater()
|
||||||
self.__auto_lib_thread = None
|
self.__auto_lib_thread = None
|
||||||
self.setControlButtons(None, False, True)
|
self.setControlButtons(None, False, True)
|
||||||
|
|||||||
+14
-14
@@ -17,13 +17,13 @@ from PySide6.QtCore import (
|
|||||||
|
|
||||||
from base.MsgBase import MsgBase
|
from base.MsgBase import MsgBase
|
||||||
from operators.AutoLib import AutoLib
|
from operators.AutoLib import AutoLib
|
||||||
from utils.ConfigReader import ConfigReader
|
from utils.JSONReader import JSONReader
|
||||||
|
|
||||||
|
|
||||||
class AutoLibWorker(MsgBase, QThread):
|
class AutoLibWorker(MsgBase, QThread):
|
||||||
|
|
||||||
AutoLibWorkerIsFinished = Signal()
|
autoLibWorkerIsFinished = Signal()
|
||||||
AutoLibWorkerFinishedWithError = Signal()
|
autoLibWorkerFinishedWithError = Signal()
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -69,11 +69,11 @@ class AutoLibWorker(MsgBase, QThread):
|
|||||||
self._showTrace(
|
self._showTrace(
|
||||||
f"正在加载配置文件, 运行配置文件路径: {self.__config_paths["run"]}"
|
f"正在加载配置文件, 运行配置文件路径: {self.__config_paths["run"]}"
|
||||||
)
|
)
|
||||||
self.__run_config = ConfigReader(self.__config_paths["run"]).getConfigs()
|
self.__run_config = JSONReader(self.__config_paths["run"]).data()
|
||||||
self._showTrace(
|
self._showTrace(
|
||||||
f"正在加载配置文件, 用户配置文件路径: {self.__config_paths["user"]}"
|
f"正在加载配置文件, 用户配置文件路径: {self.__config_paths["user"]}"
|
||||||
)
|
)
|
||||||
self.__user_config = ConfigReader(self.__config_paths["user"]).getConfigs()
|
self.__user_config = JSONReader(self.__config_paths["user"]).data()
|
||||||
if self.__run_config is None or self.__user_config is None:
|
if self.__run_config is None or self.__user_config is None:
|
||||||
self._showTrace("配置文件加载失败, 请检查配置文件是否正确")
|
self._showTrace("配置文件加载失败, 请检查配置文件是否正确")
|
||||||
self._showTrace("配置文件加载失败, 请检查配置文件是否正确")
|
self._showTrace("配置文件加载失败, 请检查配置文件是否正确")
|
||||||
@@ -116,17 +116,17 @@ class AutoLibWorker(MsgBase, QThread):
|
|||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._showTrace(f"AutoLibrary 运行时发生异常 : {e}")
|
self._showTrace(f"AutoLibrary 运行时发生异常 : {e}")
|
||||||
self.AutoLibWorkerFinishedWithError.emit()
|
self.autoLibWorkerFinishedWithError.emit()
|
||||||
return
|
return
|
||||||
if auto_lib:
|
if auto_lib:
|
||||||
auto_lib.close()
|
auto_lib.close()
|
||||||
self._showTrace("AutoLibrary 运行结束")
|
self._showTrace("AutoLibrary 运行结束")
|
||||||
self.AutoLibWorkerIsFinished.emit()
|
self.autoLibWorkerIsFinished.emit()
|
||||||
|
|
||||||
|
|
||||||
class TimerTaskWorker(AutoLibWorker):
|
class TimerTaskWorker(AutoLibWorker):
|
||||||
|
|
||||||
TimerTaskWorkerIsFinished = Signal(bool, dict)
|
timerTaskWorkerIsFinished = Signal(bool, dict)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -137,10 +137,10 @@ class TimerTaskWorker(AutoLibWorker):
|
|||||||
):
|
):
|
||||||
|
|
||||||
super().__init__(input_queue, output_queue, config_paths)
|
super().__init__(input_queue, output_queue, config_paths)
|
||||||
|
|
||||||
self.__timer_task = timer_task
|
self.__timer_task = timer_task
|
||||||
self.AutoLibWorkerIsFinished.connect(self.onTimerTaskIsFinished)
|
|
||||||
self.AutoLibWorkerFinishedWithError.connect(self.onTimerTaskIsError)
|
self.autoLibWorkerIsFinished.connect(self.onTimerTaskIsFinished)
|
||||||
|
self.autoLibWorkerFinishedWithError.connect(self.onTimerTaskFinishedWithError)
|
||||||
|
|
||||||
def run(
|
def run(
|
||||||
self
|
self
|
||||||
@@ -150,12 +150,12 @@ class TimerTaskWorker(AutoLibWorker):
|
|||||||
super().run()
|
super().run()
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def onTimerTaskIsError(
|
def onTimerTaskFinishedWithError(
|
||||||
self
|
self
|
||||||
):
|
):
|
||||||
|
|
||||||
self._showTrace(f"定时任务 {self.__timer_task['name']} 运行时发生异常")
|
self._showTrace(f"定时任务 {self.__timer_task['name']} 运行时发生异常")
|
||||||
self.TimerTaskWorkerIsFinished.emit(True, self.__timer_task)
|
self.timerTaskWorkerIsFinished.emit(True, self.__timer_task)
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def onTimerTaskIsFinished(
|
def onTimerTaskIsFinished(
|
||||||
@@ -163,4 +163,4 @@ class TimerTaskWorker(AutoLibWorker):
|
|||||||
):
|
):
|
||||||
|
|
||||||
self._showTrace(f"定时任务 {self.__timer_task['name']} 运行结束")
|
self._showTrace(f"定时任务 {self.__timer_task['name']} 运行结束")
|
||||||
self.TimerTaskWorkerIsFinished.emit(False, self.__timer_task)
|
self.timerTaskWorkerIsFinished.emit(False, self.__timer_task)
|
||||||
|
|||||||
@@ -17,12 +17,13 @@ from PySide6.QtWidgets import (
|
|||||||
from PySide6.QtGui import (
|
from PySide6.QtGui import (
|
||||||
QCloseEvent
|
QCloseEvent
|
||||||
)
|
)
|
||||||
|
|
||||||
from gui.ALSeatMapView import ALSeatMapView
|
from gui.ALSeatMapView import ALSeatMapView
|
||||||
|
|
||||||
|
|
||||||
class ALSeatMapSelectDialog(QDialog):
|
class ALSeatMapSelectDialog(QDialog):
|
||||||
|
|
||||||
seatMapSelectDialogClosed = Signal(list)
|
seatMapSelectDialogIsClosed = Signal(list)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -127,7 +128,7 @@ class ALSeatMapSelectDialog(QDialog):
|
|||||||
self.reject()
|
self.reject()
|
||||||
else:
|
else:
|
||||||
self.accept()
|
self.accept()
|
||||||
self.seatMapSelectDialogClosed.emit(self.getSelectedSeats())
|
self.seatMapSelectDialogIsClosed.emit(self.getSelectedSeats())
|
||||||
super().closeEvent(event)
|
super().closeEvent(event)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from PySide6.QtWidgets import (
|
|||||||
from PySide6.QtGui import (
|
from PySide6.QtGui import (
|
||||||
QPainter, QWheelEvent
|
QPainter, QWheelEvent
|
||||||
)
|
)
|
||||||
|
|
||||||
from gui.ALSeatFrame import ALSeatFrame
|
from gui.ALSeatFrame import ALSeatFrame
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -8,6 +8,7 @@ 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 os
|
||||||
|
import sys
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
@@ -24,11 +25,12 @@ from PySide6.QtGui import (
|
|||||||
QCloseEvent
|
QCloseEvent
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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
|
||||||
from utils.ConfigReader import ConfigReader
|
|
||||||
from utils.ConfigWriter import ConfigWriter
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
@@ -137,20 +156,19 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
|
|||||||
|
|
||||||
timerTaskIsReady = Signal(dict)
|
timerTaskIsReady = Signal(dict)
|
||||||
timerTasksChanged = Signal()
|
timerTasksChanged = Signal()
|
||||||
timerTaskManageWidgetClosed = Signal()
|
timerTaskManageWidgetIsClosed = Signal()
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
parent = None,
|
parent = None
|
||||||
timer_tasks_config_path: str = ""
|
|
||||||
):
|
):
|
||||||
|
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
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
|
||||||
self.__sort_order = Qt.SortOrder.AscendingOrder
|
self.__sort_order = Qt.SortOrder.AscendingOrder
|
||||||
self.__timer_tasks_config_path = timer_tasks_config_path
|
|
||||||
|
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
self.connectSignals()
|
self.connectSignals()
|
||||||
@@ -183,63 +201,63 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
|
|||||||
self
|
self
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|
||||||
timer_tasks = self.loadTimerTasks(self.__timer_tasks_config_path)
|
timer_tasks = self.getTimerTasks()
|
||||||
if timer_tasks is not None:
|
if timer_tasks is not None:
|
||||||
self.__timer_tasks = timer_tasks
|
self.__timer_tasks = timer_tasks
|
||||||
self.timerTasksChanged.emit()
|
self.timerTasksChanged.emit()
|
||||||
return True
|
return True
|
||||||
timer_tasks = []
|
timer_tasks = []
|
||||||
if self.saveTimerTasks(self.__timer_tasks_config_path, copy.deepcopy(timer_tasks)):
|
if self.setTimerTasks(copy.deepcopy(timer_tasks)):
|
||||||
self.__timer_tasks = timer_tasks
|
self.__timer_tasks = timer_tasks
|
||||||
self.updateTimerTaskList()
|
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def loadTimerTasks(
|
def getTimerTasks(
|
||||||
self,
|
self
|
||||||
timer_tasks_config_path: str
|
|
||||||
) -> list:
|
) -> list:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not timer_tasks_config_path or not os.path.exists(timer_tasks_config_path):
|
timer_tasks = self.__cfg_mgr.get(ConfigManager.ConfigType.TIMERTASK)
|
||||||
raise Exception("定时任务配置文件不存在")
|
|
||||||
timer_tasks = ConfigReader(timer_tasks_config_path).getConfigs()
|
|
||||||
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:
|
except Exception as e:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"警告 - AutoLibrary",
|
||||||
|
f"加载定时任务配置发生错误 ! : \n{e}"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def saveTimerTasks(
|
def setTimerTasks(
|
||||||
self,
|
self,
|
||||||
timer_tasks_config_path: str,
|
|
||||||
timer_tasks: list
|
timer_tasks: list
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not timer_tasks_config_path:
|
|
||||||
raise Exception("配置文件路径为空")
|
|
||||||
for task in timer_tasks:
|
for task in timer_tasks:
|
||||||
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
|
||||||
ConfigWriter(
|
if "history" in task:
|
||||||
timer_tasks_config_path,
|
for item in task["history"]:
|
||||||
{ "timer_tasks": timer_tasks }
|
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(
|
||||||
self,
|
self,
|
||||||
"警告 - AutoLibrary",
|
"警告 - AutoLibrary",
|
||||||
f"保存定时任务配置发生错误 ! : {e}\n"\
|
f"保存定时任务配置发生错误 ! : \n{e}"
|
||||||
f"文件路径: {timer_tasks_config_path}"
|
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -274,7 +292,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
|
|||||||
):
|
):
|
||||||
|
|
||||||
self.hide()
|
self.hide()
|
||||||
self.timerTaskManageWidgetClosed.emit()
|
self.timerTaskManageWidgetIsClosed.emit()
|
||||||
event.ignore()
|
event.ignore()
|
||||||
|
|
||||||
|
|
||||||
@@ -339,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)
|
||||||
@@ -357,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
|
||||||
@@ -381,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
|
||||||
@@ -393,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()
|
||||||
|
|
||||||
|
|
||||||
@@ -412,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:
|
||||||
@@ -453,7 +551,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
|
|||||||
self
|
self
|
||||||
):
|
):
|
||||||
|
|
||||||
self.saveTimerTasks(self.__timer_tasks_config_path, copy.deepcopy(self.__timer_tasks))
|
self.setTimerTasks(copy.deepcopy(self.__timer_tasks))
|
||||||
self.updateTimerTaskList()
|
self.updateTimerTaskList()
|
||||||
self.updateStat()
|
self.updateStat()
|
||||||
|
|
||||||
@@ -467,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,
|
||||||
@@ -478,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)
|
||||||
@@ -489,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()
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
workflow process. Do not edit manually.
|
workflow process. Do not edit manually.
|
||||||
|
|
||||||
This file is auto-generated during the workflow process.
|
This file is auto-generated during the workflow process.
|
||||||
Last updated: 2026-02-16 07:04:48 UTC
|
Last updated: 2026-02-26 15:04:28 UTC
|
||||||
"""
|
"""
|
||||||
|
|
||||||
AL_VERSION = "1.0.5"
|
AL_VERSION = "1.1.0"
|
||||||
AL_TAG = "v1.0.5"
|
AL_TAG = "v1.1.0"
|
||||||
AL_COMMIT_SHA = "local"
|
AL_COMMIT_SHA = "local"
|
||||||
AL_COMMIT_DATE = "null" # time zone : UTC
|
AL_COMMIT_DATE = "null" # time zone : UTC
|
||||||
AL_BUILD_DATE = "null" # time zone : UTC
|
AL_BUILD_DATE = "null" # time zone : UTC
|
||||||
|
|||||||
@@ -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><html><head/><body><p>详情请参阅 <a href="https://www.autolibrary.top/manuals"><span style=" text-decoration: underline; color:#69fcff;">用户手册</span></a></p></body></html></string>
|
<string><html><head/><body><p>详情请参阅 <a href="https://www.autolibrary.kenanzhu.com/manuals"><span style=" text-decoration: underline; color:#69fcff;">用户手册</span></a></p></body></html></string>
|
||||||
</property>
|
</property>
|
||||||
<property name="whatsThis">
|
<property name="whatsThis">
|
||||||
<string><html><head/><body><p><br/></p></body></html></string>
|
<string><html><head/><body><p><br/></p></body></html></string>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
<number>5</number>
|
<number>5</number>
|
||||||
</property>
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="TimerTaskWidgetButton">
|
<widget class="QPushButton" name="TimerTaskManageWidgetButton">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>25</width>
|
<width>25</width>
|
||||||
|
|||||||
@@ -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,26 +6,26 @@
|
|||||||
<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>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
<string>定时任务管理 - AutoLibrary</string>
|
<string>定时任务管理 - AutoLibrary</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="ALTimerTaskWidgetLayout">
|
<layout class="QVBoxLayout" name="ALTimerTaskManageWidgetLayout">
|
||||||
<property name="spacing">
|
<property name="spacing">
|
||||||
<number>5</number>
|
<number>5</number>
|
||||||
</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>
|
||||||
|
|||||||
@@ -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,8 +251,9 @@ 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:
|
||||||
if record := self.__lib_checker.canRenew():
|
can_renew, record = self.__lib_checker.canRenew()
|
||||||
|
if can_renew:
|
||||||
if self.__lib_renew.renew(username, record, reserve_info):
|
if self.__lib_renew.renew(username, record, reserve_info):
|
||||||
if self.__lib_checker.postRenewCheck(record):
|
if self.__lib_checker.postRenewCheck(record):
|
||||||
result = 0
|
result = 0
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ class LibChecker(LibOperator):
|
|||||||
|
|
||||||
def canRenew(
|
def canRenew(
|
||||||
self
|
self
|
||||||
):
|
) -> tuple[bool, dict]:
|
||||||
|
|
||||||
# only check the current date
|
# only check the current date
|
||||||
date = time.strftime("%Y-%m-%d", time.localtime())
|
date = time.strftime("%Y-%m-%d", time.localtime())
|
||||||
@@ -326,12 +326,13 @@ class LibChecker(LibOperator):
|
|||||||
)
|
)
|
||||||
if abs(time_diff_seconds) < 120*60:
|
if abs(time_diff_seconds) < 120*60:
|
||||||
self._showTrace(f"{trace_msg}, 可以续约")
|
self._showTrace(f"{trace_msg}, 可以续约")
|
||||||
return record
|
return True, record
|
||||||
else:
|
else:
|
||||||
self._showTrace(f"{trace_msg}, 无法续约")
|
self._showTrace(f"{trace_msg}, 无法续约")
|
||||||
return None
|
return False, None # we do not need to return the record, because if current
|
||||||
|
# time is not available for renewal, the record is not required
|
||||||
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法续约")
|
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法续约")
|
||||||
return None
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
def postRenewCheck(
|
def postRenewCheck(
|
||||||
|
|||||||
@@ -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
-78
@@ -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,87 +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
|
||||||
|
|
||||||
|
|
||||||
@@ -206,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
@@ -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,
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (c) 2026 KenanZhu.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This software is provided "as is", without any warranty of any kind.
|
||||||
|
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||||
|
See the LICENSE file for details.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from utils.JSONReader import JSONReader
|
||||||
|
from utils.JSONWriter import JSONWriter
|
||||||
|
|
||||||
|
|
||||||
|
# This config manager class only responsible for global and other
|
||||||
|
# unconfigurable config files.
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigType(Enum):
|
||||||
|
"""
|
||||||
|
Config type class. Values represent the default filename.
|
||||||
|
"""
|
||||||
|
GLOBAL = "autolibrary.json" # Global config file.
|
||||||
|
BULLETIN = "bulletin.json" # Bulletin board config file.
|
||||||
|
TIMERTASK = "timer_task.json" # Timer task config file.
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigTemplate:
|
||||||
|
"""
|
||||||
|
Config template class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
config_type: ConfigType
|
||||||
|
):
|
||||||
|
|
||||||
|
self.__config_type = config_type
|
||||||
|
|
||||||
|
|
||||||
|
def template(
|
||||||
|
self
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Get config template.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Config template.
|
||||||
|
"""
|
||||||
|
match self.__config_type:
|
||||||
|
case ConfigType.GLOBAL:
|
||||||
|
return {
|
||||||
|
"automation": {
|
||||||
|
"run_path": {
|
||||||
|
"current": 0,
|
||||||
|
"paths": []
|
||||||
|
},
|
||||||
|
"user_path": {
|
||||||
|
"current": 0,
|
||||||
|
"paths": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ConfigType.BULLETIN:
|
||||||
|
return {
|
||||||
|
"bulletin": [],
|
||||||
|
"last_sync_time": None
|
||||||
|
}
|
||||||
|
case ConfigType.TIMERTASK:
|
||||||
|
return {
|
||||||
|
"timer_tasks": []
|
||||||
|
}
|
||||||
|
case _:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigManager:
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
config_dir: str
|
||||||
|
):
|
||||||
|
|
||||||
|
self.__config_dir = os.path.abspath(config_dir)
|
||||||
|
self.__config_lock = threading.Lock()
|
||||||
|
self.__config_data = {}
|
||||||
|
|
||||||
|
self.initialize()
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
for config_type in ConfigType:
|
||||||
|
self.load(config_type)
|
||||||
|
|
||||||
|
|
||||||
|
def load(
|
||||||
|
self,
|
||||||
|
config_type: ConfigType
|
||||||
|
):
|
||||||
|
|
||||||
|
config_path = os.path.join(self.__config_dir, config_type.value)
|
||||||
|
if os.path.exists(config_path):
|
||||||
|
try:
|
||||||
|
config_data = JSONReader(config_path).data()
|
||||||
|
self.__config_data[config_type.value] = config_data
|
||||||
|
return
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.__config_data[config_type.value] = ConfigTemplate(config_type).template()
|
||||||
|
JSONWriter(config_path, self.__config_data[config_type.value])
|
||||||
|
|
||||||
|
|
||||||
|
def get(
|
||||||
|
self,
|
||||||
|
config_type: ConfigType,
|
||||||
|
key: str = "",
|
||||||
|
default: Optional[Any] = None
|
||||||
|
) -> Any:
|
||||||
|
|
||||||
|
with self.__config_lock:
|
||||||
|
config_data = self.__config_data[config_type.value]
|
||||||
|
if key == "":
|
||||||
|
return config_data
|
||||||
|
keys = key.split('.')
|
||||||
|
for k in keys[:-1]:
|
||||||
|
config_data = config_data.get(k, None)
|
||||||
|
if config_data is None:
|
||||||
|
return default
|
||||||
|
return config_data.get(keys[-1], default)
|
||||||
|
|
||||||
|
|
||||||
|
def set(
|
||||||
|
self,
|
||||||
|
config_type: ConfigType,
|
||||||
|
key: str = "",
|
||||||
|
value: Any = None
|
||||||
|
):
|
||||||
|
|
||||||
|
with self.__config_lock:
|
||||||
|
root_data = self.__config_data[config_type.value]
|
||||||
|
if key == "":
|
||||||
|
self.__config_data[config_type.value] = value
|
||||||
|
else:
|
||||||
|
keys = key.split('.')
|
||||||
|
config_data = root_data
|
||||||
|
for k in keys[:-1]:
|
||||||
|
if k not in config_data:
|
||||||
|
config_data[k] = {}
|
||||||
|
config_data = config_data[k]
|
||||||
|
config_data[keys[-1]] = value
|
||||||
|
self.save(config_type)
|
||||||
|
|
||||||
|
|
||||||
|
def save(
|
||||||
|
self,
|
||||||
|
config_type: ConfigType
|
||||||
|
):
|
||||||
|
|
||||||
|
config_path = os.path.join(self.__config_dir, config_type.value)
|
||||||
|
JSONWriter(config_path, self.__config_data[config_type.value])
|
||||||
|
|
||||||
|
|
||||||
|
def configDir(
|
||||||
|
self
|
||||||
|
) -> str:
|
||||||
|
|
||||||
|
return self.__config_dir
|
||||||
|
|
||||||
|
|
||||||
|
# ConfigManager singleton instance.
|
||||||
|
_config_manager_instance = None
|
||||||
|
|
||||||
|
# Utility functions.
|
||||||
|
#
|
||||||
|
# Utility function to get validated automation config paths.
|
||||||
|
def getValidateAutomationConfigPaths(
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Get validated automation config paths from ConfigManager instance.
|
||||||
|
These function will validate the config paths and return the validated paths in a dict.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Validated automation config paths.
|
||||||
|
"""
|
||||||
|
config_paths = {"run": "", "user": ""}
|
||||||
|
auto_config = _config_manager_instance.get(ConfigType.GLOBAL, "automation", {})
|
||||||
|
for cfg_type in ["run", "user"]:
|
||||||
|
paths = auto_config.get(f"{cfg_type}_path", {}).get("paths", [])
|
||||||
|
index = auto_config.get(f"{cfg_type}_path", {}).get("current", 0)
|
||||||
|
if paths == []:
|
||||||
|
paths.append(os.path.join(_config_manager_instance.configDir(), f"{cfg_type}.json"))
|
||||||
|
if index < 0:
|
||||||
|
index = 0
|
||||||
|
if index >= len(paths):
|
||||||
|
index = len(paths) - 1
|
||||||
|
config_paths[cfg_type] = paths[index]
|
||||||
|
data = {"current": index, "paths": paths}
|
||||||
|
auto_config[f"{cfg_type}_path"] = data
|
||||||
|
_config_manager_instance.set(ConfigType.GLOBAL, "automation", auto_config)
|
||||||
|
return config_paths
|
||||||
|
|
||||||
|
# Utility function to get base config directory.
|
||||||
|
def getBaseConfigDir(
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Get base config directory, on Windows, it is usually at :
|
||||||
|
'C:\\Users\\<username>\\AppData\\Local\\AutoLibrary\\config'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Base config directory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return _config_manager_instance.configDir()
|
||||||
|
|
||||||
|
# Singleton instance of ConfigManager.
|
||||||
|
_instance_lock = threading.Lock()
|
||||||
|
def instance(
|
||||||
|
config_dir: str = ""
|
||||||
|
) -> ConfigManager:
|
||||||
|
"""
|
||||||
|
Initialize ConfigManager singleton instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_dir (str): Config directory.
|
||||||
|
"""
|
||||||
|
global _config_manager_instance
|
||||||
|
with _instance_lock:
|
||||||
|
if _config_manager_instance is None:
|
||||||
|
_config_manager_instance = ConfigManager(config_dir)
|
||||||
|
else:
|
||||||
|
if config_dir == "":
|
||||||
|
return _config_manager_instance
|
||||||
|
if getBaseConfigDir() != config_dir:
|
||||||
|
raise ValueError(
|
||||||
|
"ConfigManager 的实例已初始化,不能使用不同的配置目录。")
|
||||||
|
return _config_manager_instance
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
Copyright (c) 2025 - 2026 KenanZhu.
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
This software is provided "as is", without any warranty of any kind.
|
|
||||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
|
||||||
See the LICENSE file for details.
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
import copy
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigReader:
|
|
||||||
"""
|
|
||||||
Config reader class.
|
|
||||||
|
|
||||||
This class is used to read config file in JSON format.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_path (str): The path of config file.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
>>> print(open("config.json", "r", encoding="utf-8").read())
|
|
||||||
{
|
|
||||||
"key1": {
|
|
||||||
"key2": "value1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>>> config_reader = ConfigReader("config.json")
|
|
||||||
>>> config_reader.get("key1/key2")
|
|
||||||
"value1"
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
config_path: str
|
|
||||||
):
|
|
||||||
|
|
||||||
self.__config_path = config_path
|
|
||||||
self.__config_data = None
|
|
||||||
self.__readConfig()
|
|
||||||
|
|
||||||
|
|
||||||
def __readConfig(
|
|
||||||
self
|
|
||||||
):
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(self.__config_path, 'r', encoding='utf-8') as file:
|
|
||||||
self.__config_data = json.load(file)
|
|
||||||
except FileNotFoundError as e:
|
|
||||||
raise Exception(f"配置文件不存在: {self.__config_path}") from e
|
|
||||||
except PermissionError as e:
|
|
||||||
raise Exception(f"没有足够的权限读取配置文件: {self.__config_path}") from e
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
raise Exception(f"JSON 解析错误: {self.__config_path}") from e
|
|
||||||
except Exception as e:
|
|
||||||
raise Exception(f"读取配置文件时未知错误: {e}") from e
|
|
||||||
|
|
||||||
|
|
||||||
def getConfigs(
|
|
||||||
self
|
|
||||||
) -> dict:
|
|
||||||
|
|
||||||
return self.__config_data.copy()
|
|
||||||
|
|
||||||
|
|
||||||
def getConfig(
|
|
||||||
self,
|
|
||||||
key: str
|
|
||||||
) -> Any:
|
|
||||||
|
|
||||||
config = self.__config_data.get(key, {})
|
|
||||||
return copy.deepcopy(config)
|
|
||||||
|
|
||||||
|
|
||||||
def get(
|
|
||||||
self,
|
|
||||||
key: str,
|
|
||||||
default: Any = None
|
|
||||||
) -> Any:
|
|
||||||
|
|
||||||
keys = key.split('/')
|
|
||||||
current = self.__config_data
|
|
||||||
for k in keys:
|
|
||||||
if isinstance(current, dict) and k in current:
|
|
||||||
current = current[k]
|
|
||||||
else:
|
|
||||||
return default
|
|
||||||
return copy.deepcopy(current)
|
|
||||||
|
|
||||||
|
|
||||||
def hasConfig(
|
|
||||||
self,
|
|
||||||
key: str
|
|
||||||
) -> bool:
|
|
||||||
|
|
||||||
return self.getConfig(key) != {}
|
|
||||||
|
|
||||||
|
|
||||||
def reReadConfig(
|
|
||||||
self
|
|
||||||
) -> bool:
|
|
||||||
|
|
||||||
return self.__readConfig()
|
|
||||||
|
|
||||||
|
|
||||||
def configPath(
|
|
||||||
self
|
|
||||||
) -> str:
|
|
||||||
|
|
||||||
return self.__config_path
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
Copyright (c) 2025 - 2026 KenanZhu.
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
This software is provided "as is", without any warranty of any kind.
|
|
||||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
|
||||||
See the LICENSE file for details.
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigWriter:
|
|
||||||
"""
|
|
||||||
Config writer class.
|
|
||||||
|
|
||||||
This class is used to write config file in JSON format.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_path (str): The path of config file.
|
|
||||||
config_data (dict): The config data to be written.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
>>> config_data = {
|
|
||||||
... "key1": {
|
|
||||||
... "key2": "value1"
|
|
||||||
... }
|
|
||||||
... }
|
|
||||||
>>> config_writer = ConfigWriter("config.json", config_data)
|
|
||||||
>>> config_writer.set("key1/key2", "value1")
|
|
||||||
True
|
|
||||||
>>> print(open("config.json", "r", encoding="utf-8").read())
|
|
||||||
{
|
|
||||||
"key1": {
|
|
||||||
"key2": "value1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
config_path: str,
|
|
||||||
config_data: dict
|
|
||||||
):
|
|
||||||
|
|
||||||
self.__config_path = config_path
|
|
||||||
self.__config_data = config_data.copy() if config_data is not None else {}
|
|
||||||
self.__writeConfig()
|
|
||||||
|
|
||||||
|
|
||||||
def __writeConfig(
|
|
||||||
self
|
|
||||||
):
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(self.__config_path, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(self.__config_data, f, indent=4, sort_keys=False)
|
|
||||||
except PermissionError as e:
|
|
||||||
raise Exception(f"没有足够的权限写入配置文件: {self.__config_path}") from e
|
|
||||||
except IOError as e:
|
|
||||||
raise Exception(f"写入配置文件时发生 IO 错误: {self.__config_path}") from e
|
|
||||||
except TypeError as e:
|
|
||||||
raise Exception(f"配置数据包含无法 JSON 序列化的类型: {e}") from e
|
|
||||||
except Exception as e:
|
|
||||||
raise Exception(f"写入配置文件时未知错误: {e}") from e
|
|
||||||
|
|
||||||
|
|
||||||
def setConfigs(
|
|
||||||
self,
|
|
||||||
configs: dict
|
|
||||||
) -> bool:
|
|
||||||
|
|
||||||
self.__config_data = configs
|
|
||||||
return self.__writeConfig()
|
|
||||||
|
|
||||||
|
|
||||||
def setConfig(
|
|
||||||
self,
|
|
||||||
key: str,
|
|
||||||
value: dict
|
|
||||||
) -> bool:
|
|
||||||
|
|
||||||
self.__config_data[key] = value
|
|
||||||
return self.__writeConfig()
|
|
||||||
|
|
||||||
|
|
||||||
def set(
|
|
||||||
self,
|
|
||||||
key: str,
|
|
||||||
value: Any
|
|
||||||
) -> bool:
|
|
||||||
|
|
||||||
keys = key.replace("\\", "/").split("/")
|
|
||||||
current = self.__config_data
|
|
||||||
for k in keys[:-1]:
|
|
||||||
if k not in current or not isinstance(current[k], dict):
|
|
||||||
current[k] = {}
|
|
||||||
current = current[k]
|
|
||||||
current[keys[-1]] = value
|
|
||||||
return self.__writeConfig()
|
|
||||||
|
|
||||||
|
|
||||||
def reWriteConfig(
|
|
||||||
self
|
|
||||||
) -> bool:
|
|
||||||
|
|
||||||
return self.__writeConfig()
|
|
||||||
|
|
||||||
|
|
||||||
def configPath(
|
|
||||||
self
|
|
||||||
) -> str:
|
|
||||||
|
|
||||||
return self.__config_path
|
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (c) 2026 KenanZhu.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This software is provided "as is", without any warranty of any kind.
|
||||||
|
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||||
|
See the LICENSE file for details.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class JSONReader:
|
||||||
|
"""
|
||||||
|
JSON reader class.
|
||||||
|
|
||||||
|
This class is used to read JSON file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
json_path (str): The path of JSON file.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> print(open("config.json", "r", encoding="utf-8").read())
|
||||||
|
{
|
||||||
|
"key1": {
|
||||||
|
"key2": "value1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>>> json_reader = JSONReader("config.json")
|
||||||
|
>>> data = json_reader.data()
|
||||||
|
>>> data["key1"]["key2"]
|
||||||
|
"value1"
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
json_path: str
|
||||||
|
):
|
||||||
|
|
||||||
|
self.__json_path = os.path.abspath(json_path)
|
||||||
|
self.__json_data = None
|
||||||
|
self.__read()
|
||||||
|
|
||||||
|
|
||||||
|
def __read(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.__json_path, 'r', encoding='utf-8') as file:
|
||||||
|
self.__json_data = json.load(file)
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
raise Exception(f"文件不存在: {self.__json_path}") from e
|
||||||
|
except PermissionError as e:
|
||||||
|
raise Exception(f"没有足够的权限读取文件: {self.__json_path}") from e
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise Exception(f"JSON 解析错误: {self.__json_path}") from e
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(f"读取文件时发生未知错误: {e}") from e
|
||||||
|
|
||||||
|
|
||||||
|
def read(
|
||||||
|
self
|
||||||
|
) -> bool:
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.__read()
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def data(
|
||||||
|
self
|
||||||
|
) -> dict:
|
||||||
|
|
||||||
|
return self.__json_data.copy()
|
||||||
|
|
||||||
|
|
||||||
|
def path(
|
||||||
|
self
|
||||||
|
) -> str:
|
||||||
|
|
||||||
|
return self.__json_path
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (c) 2026 KenanZhu.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This software is provided "as is", without any warranty of any kind.
|
||||||
|
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||||
|
See the LICENSE file for details.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class JSONWriter:
|
||||||
|
"""
|
||||||
|
JSON writer class.
|
||||||
|
|
||||||
|
This class is used to write JSON file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
json_path (str): The path of JSON file.
|
||||||
|
json_data (dict): The JSON data to be written.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> json_data = {
|
||||||
|
... "key1": {
|
||||||
|
... "key2": "value1"
|
||||||
|
... }
|
||||||
|
... }
|
||||||
|
>>> json_writer = JSONWriter("config.json", json_data)
|
||||||
|
>>> print(open("config.json", "r", encoding="utf-8").read())
|
||||||
|
{
|
||||||
|
"key1": {
|
||||||
|
"key2": "value1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
json_path: str,
|
||||||
|
json_data: dict
|
||||||
|
):
|
||||||
|
|
||||||
|
self.__json_path = os.path.abspath(json_path)
|
||||||
|
self.__json_data = json_data.copy() if json_data is not None else {}
|
||||||
|
self.__write()
|
||||||
|
|
||||||
|
|
||||||
|
def __write(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.__json_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(self.__json_data, f, indent=4, sort_keys=False)
|
||||||
|
except PermissionError as e:
|
||||||
|
raise Exception(f"没有足够的权限写入文件: {self.__json_path}") from e
|
||||||
|
except IOError as e:
|
||||||
|
raise Exception(f"写入文件时发生 IO 错误: {self.__json_path}") from e
|
||||||
|
except TypeError as e:
|
||||||
|
raise Exception(f"JSON 数据包含无法 JSON 序列化的类型: {e}") from e
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(f"写入文件时发生未知错误: {e}") from e
|
||||||
|
|
||||||
|
|
||||||
|
def write(
|
||||||
|
self
|
||||||
|
) -> bool:
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.__write()
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def path(
|
||||||
|
self
|
||||||
|
) -> str:
|
||||||
|
|
||||||
|
return self.__json_path
|
||||||
@@ -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
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
Utils module for the AutoLibrary project.
|
Utils module for the AutoLibrary project.
|
||||||
|
|
||||||
Here are the classes and modules in this package:
|
Here are the classes and modules in this package:
|
||||||
- ConfigReader: Configuration reader class for the AutoLibrary project.
|
- ConfigManager: Configuration manager class for the AutoLibrary project.
|
||||||
- ConfigWriter: Configuration writer class for the AutoLibrary project.
|
- JSONReader: JSON reader class for the AutoLibrary project.
|
||||||
|
- JSONWriter: JSON writer class for the AutoLibrary project.
|
||||||
"""
|
"""
|
||||||
Reference in New Issue
Block a user