Compare commits
237 Commits
v1.0.3
...
ed8dec70a6
| Author | SHA1 | Date | |
|---|---|---|---|
| ed8dec70a6 | |||
| dfa9ae1a0c | |||
| 06740776a9 | |||
| c8f9ae752f | |||
| 89c1e33257 | |||
| 862f86827a | |||
| ed98623cda | |||
| ab51c72a11 | |||
| 2fe4584129 | |||
| 92c5fa0dfb | |||
| 05ad9ba0b3 | |||
| 0069d5274b | |||
| 6a9c662287 | |||
| d7fff16991 | |||
| db345f9efc | |||
| 133433f2d8 | |||
| 6f32923865 | |||
| a716e5b945 | |||
| 588718f9c5 | |||
| 0ba76babc6 | |||
| dc7e5d8cd8 | |||
| 818acd4efc | |||
| 308a1dfcf3 | |||
| c250fa4a6e | |||
| 8f8e3e4ba7 | |||
| 88a74a7a47 | |||
| 5552af1345 | |||
| 6d05d4c7cb | |||
| 01f4ccaa0e | |||
| f9175371dc | |||
| 8e1b28f3fe | |||
| 57f1cfb3f2 | |||
| 007b4dc2ef | |||
| 0bad34d7a8 | |||
| 61d1b44402 | |||
| 72301c63fd | |||
| 609850ab60 | |||
| 8e14d45b71 | |||
| f6ef9af39d | |||
| 72dce83c55 | |||
| 67f297b434 | |||
| 86f0761eed | |||
| 79e5b43498 | |||
| b56d2c203e | |||
| 44dbde3355 | |||
| 62f8ec3d91 | |||
| 2d77cbec79 | |||
| 10d731518a | |||
| 9fdb6f7652 | |||
| ef903ee817 | |||
| d6e8eef8c8 | |||
| e893752c25 | |||
| 1cfd7382be | |||
| 1d9e41ab86 | |||
| 645f07b4d2 | |||
| 732f104c5c | |||
| a2bc1881bc | |||
| c1004ed2bc | |||
| 38489191f5 | |||
| 35253dadbb | |||
| c0b6e0899c | |||
| 9c1772b186 | |||
| 05b93799d4 | |||
| c337904010 | |||
| 779aad13b8 | |||
| f3360423e5 | |||
| bea12d5f0c | |||
| b24f39456e | |||
| bb63ee6f03 | |||
| 3ebebe015f | |||
| 02b3a62868 | |||
| d7e19dcd52 | |||
| 59c06b3a19 | |||
| b78fd2d1e4 | |||
| 2aace40a26 | |||
| df7ad92f7f | |||
| 910e3e3224 | |||
| f7167c13f4 | |||
| eb8da498a2 | |||
| b279b51b42 | |||
| 43336f98d2 | |||
| e77c561685 | |||
| 345cb95b98 | |||
| caa563e770 | |||
| 280028259f | |||
| a6bc103c73 | |||
| 2226e8ac90 | |||
| 106463b9e5 | |||
| 5e898180c7 | |||
| a03ab38279 | |||
| 4761cade26 | |||
| 531b05651e | |||
| 3cea7df736 | |||
| a0fd03f12f | |||
| 9b47886e5b | |||
| 82738be99a | |||
| e097b5afc9 | |||
| fe7453fe02 | |||
| 1d4b03d162 | |||
| 4642916fd5 | |||
| 5800437ba2 | |||
| 23467c1d3d | |||
| b8c0a29c59 | |||
| 87787ad3dc | |||
| e800f6ece1 | |||
| 600a304ab8 | |||
| c038c8005d | |||
| 6cf182c8c8 | |||
| 33c0f4414c | |||
| 2843300cf9 | |||
| 9bdc9a3de9 | |||
| 500ddd41c5 | |||
| 14c6db3384 | |||
| bbd97970a6 | |||
| 22d3c3462c | |||
| dc287f3aa5 | |||
| 7886379875 | |||
| 967ede4b04 | |||
| 27250dba2f | |||
| 46b3447d1e | |||
| 4d0d7a952c | |||
| e11f696b76 | |||
| ffae43d5bd | |||
| baa4f23136 | |||
| 1c88d3db7b | |||
| 3880f90916 | |||
| d3d146b1b3 | |||
| 0f74a3b0ec | |||
| 9305c559cd | |||
| f56945f29e | |||
| 37132de4fc | |||
| ac5385bcfe | |||
| f984217bda | |||
| 4e7780fe70 | |||
| 7149cb2b7d | |||
| 2c90008fcd | |||
| 5c393595d7 | |||
| 4924f4b031 | |||
| 62c1ecdb07 | |||
| aef28b6d5e | |||
| afa1d39051 | |||
| 84cff6acc3 | |||
| e40c7f4f3e | |||
| c8e202dc8c | |||
| 9a3abc365c | |||
| 6b2bf4863e | |||
| 95aa2bb518 | |||
| 571af554d2 | |||
| 706fc889f9 | |||
| bf93cc2cbc | |||
| 1cfe261324 | |||
| e5dea7bcc5 | |||
| 30b36b68dd | |||
| 595f43d852 | |||
| 02463f087e | |||
| e481824344 | |||
| 160d6a2428 | |||
| ec683cf154 | |||
| 2d0782c368 | |||
| 824b9b8869 | |||
| c26f19b6b3 | |||
| 1d99ca92f2 | |||
| 50ebeb0fab | |||
| faa26b489a | |||
| c03eed1d51 | |||
| 2f5680c547 | |||
| 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 | |||
| 3963b3f2e6 | |||
| f2a05809bd | |||
| b55a0c06a5 | |||
| 2496c4e367 | |||
| de30559af1 | |||
| e1c2efc8c0 | |||
| 26a70cdceb | |||
| ce14be2555 | |||
| eda16f01f1 | |||
| 22f806bfb0 | |||
| d26852eaaf | |||
| 2ffe620532 | |||
| fe42d3cd98 | |||
| 0795939aa3 | |||
| 8b6baf9b6a | |||
| 7098d7075f | |||
| be3942ea2f | |||
| 7e3a089e21 | |||
| f3d68c40cb | |||
| 0ceff677e4 | |||
| 6f6b415bff | |||
| 735f31830d | |||
| 7be5afeae1 | |||
| 3d6978c9c2 |
@@ -0,0 +1,494 @@
|
||||
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
|
||||
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'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: requirements.txt
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.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_Logo_64.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_Logo_64.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 "`n========================================"
|
||||
Write-Host "Generated Main.spec"
|
||||
Write-Host "========================================"
|
||||
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"
|
||||
|
||||
"ZIP_NAME=$zipName" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
|
||||
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: |
|
||||
"## Build Test Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
|
||||
"" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||
"========================================" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||
"✓ Pull request build test completed successfully!" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||
"- Version: ${{ steps.get_version.outputs.VERSION }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||
"- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||
"- Pull Request #${{ github.event.pull_request.number || 'N/A' }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||
"- Branch: ${{ github.event.pull_request.head.ref || github.ref }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||
"- Event: ${{ github.event_name }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||
shell: pwsh
|
||||
|
||||
#
|
||||
# Build macOS
|
||||
#
|
||||
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Get version info
|
||||
id: get_version
|
||||
run: |
|
||||
version="pr-test"
|
||||
tag_name="pr-test"
|
||||
echo "✓ Mode: Pull Request Test Build"
|
||||
echo "✓ Tag: $tag_name"
|
||||
echo "✓ Version: $version"
|
||||
echo "VERSION=$version" >> $GITHUB_OUTPUT
|
||||
echo "TAG_NAME=$tag_name" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: requirements.txt
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Solve ddddocr compatibility and copy model files
|
||||
run: |
|
||||
ddddocr_path=$(python -c "import ddddocr, os; print(os.path.dirname(ddddocr.__file__))")
|
||||
echo "ddddocr package location: $ddddocr_path"
|
||||
|
||||
init_file="$ddddocr_path/__init__.py"
|
||||
if [ -f "$init_file" ]; then
|
||||
echo "Fixing ddddocr compatibility in: $init_file"
|
||||
# macOS 使用 BSD sed,要求 -i 后跟备份后缀名(空字符串 = 不备份)
|
||||
sed -i '' 's/Image\.ANTIALIAS/Image.Resampling.LANCZOS/g' "$init_file"
|
||||
echo "✓ Fixed: Image.ANTIALIAS -> Image.Resampling.LANCZOS"
|
||||
else
|
||||
echo "✗ ddddocr __init__.py not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p models
|
||||
echo "✓ Created models directory"
|
||||
|
||||
# 多路径 fallback 搜索 ONNX 模型,提升 ddddocr 版本兼容性
|
||||
onnx_source=""
|
||||
onnx_dest="models/common.onnx"
|
||||
candidate_paths=(
|
||||
"$ddddocr_path/common.onnx"
|
||||
"$ddddocr_path/models/common.onnx"
|
||||
"$ddddocr_path/ddddocr/common.onnx"
|
||||
)
|
||||
for candidate in "${candidate_paths[@]}"; do
|
||||
if [ -f "$candidate" ]; then
|
||||
onnx_source="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
# 若以上候选均未命中,回退到全包搜索
|
||||
if [ -z "$onnx_source" ]; then
|
||||
echo "⚠ 未在已知路径找到 ONNX 模型,回退到全包搜索..."
|
||||
onnx_source=$(find "$ddddocr_path" -name "*.onnx" -type f 2>/dev/null | head -1)
|
||||
fi
|
||||
|
||||
if [ -n "$onnx_source" ] && [ -f "$onnx_source" ]; then
|
||||
cp "$onnx_source" "$onnx_dest"
|
||||
echo "✓ ONNX model copied: $onnx_source -> $onnx_dest"
|
||||
else
|
||||
echo "✗ ONNX model not found in ddddocr package"
|
||||
echo " Searched directory: $ddddocr_path"
|
||||
ls -la "$ddddocr_path/" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "$onnx_dest" ]; then
|
||||
file_size=$(du -h "$onnx_dest" | cut -f1)
|
||||
echo "✓ Model file verified: $onnx_dest (Size: $file_size)"
|
||||
else
|
||||
echo "✗ Failed to copy model file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Compile Qt Resource files
|
||||
run: |
|
||||
cd batchs
|
||||
bash compile_rc.sh
|
||||
|
||||
- name: Compile Qt UI files
|
||||
run: |
|
||||
cd batchs
|
||||
bash compile_ui.sh
|
||||
|
||||
- name: Generate 'Main.spec'
|
||||
env:
|
||||
APP_VERSION: ${{ steps.get_version.outputs.VERSION }}
|
||||
run: |
|
||||
version="$APP_VERSION"
|
||||
exe_name="AutoLibrary-$version"
|
||||
echo "Generating Main.spec for version: $version"
|
||||
echo "App name: $exe_name"
|
||||
|
||||
printf '%s\n' \
|
||||
'# -*- mode: python ; coding: utf-8 -*-' \
|
||||
'' \
|
||||
'a = Analysis(' \
|
||||
" ['src/Main.py']," \
|
||||
' pathex=[],' \
|
||||
' binaries=[],' \
|
||||
' datas=[' \
|
||||
" ('models/common.onnx', 'ddddocr')," \
|
||||
" ('src/gui/resources/icons/AutoLibrary_Logo_64.ico', 'gui/resources/icons')," \
|
||||
' ],' \
|
||||
' hiddenimports=[],' \
|
||||
' hookspath=[],' \
|
||||
' hooksconfig={},' \
|
||||
' runtime_hooks=[],' \
|
||||
' excludes=[],' \
|
||||
' noarchive=False,' \
|
||||
' optimize=0,' \
|
||||
')' \
|
||||
'pyz = PYZ(a.pure)' \
|
||||
'' \
|
||||
'exe = EXE(' \
|
||||
' pyz,' \
|
||||
' a.scripts,' \
|
||||
' [],' \
|
||||
' exclude_binaries=True,' \
|
||||
" 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_Logo_64.ico']," \
|
||||
')' \
|
||||
'' \
|
||||
'coll = COLLECT(' \
|
||||
' exe,' \
|
||||
' a.binaries,' \
|
||||
' a.zipfiles,' \
|
||||
' a.datas,' \
|
||||
' strip=False,' \
|
||||
' upx=True,' \
|
||||
' upx_exclude=[],' \
|
||||
" name='${exe_name}'," \
|
||||
')' \
|
||||
'' \
|
||||
'app = BUNDLE(' \
|
||||
' coll,' \
|
||||
" name='AutoLibrary.app'," \
|
||||
" icon='src/gui/resources/icons/AutoLibrary_Logo.icns'," \
|
||||
" bundle_identifier='com.kenanzhu.autolibrary'," \
|
||||
' info_plist={' \
|
||||
" 'NSHighResolutionCapable': 'True'," \
|
||||
" 'CFBundleName': 'AutoLibrary'," \
|
||||
" 'CFBundleDisplayName': 'AutoLibrary'," \
|
||||
" 'CFBundleShortVersionString': '${version}'," \
|
||||
" 'CFBundleVersion': '${version}'," \
|
||||
" 'CFBundleExecutable': 'AutoLibrary'," \
|
||||
" 'NSPrincipalClass': 'NSApplication'," \
|
||||
" 'LSMinimumSystemVersion': '11.0'," \
|
||||
" 'NSRequiresAquaSystemAppearance': 'False'," \
|
||||
" 'NSHumanReadableCopyright': 'Copyright 2025 - 2026 KenanZhu. All rights reserved.'," \
|
||||
' },' \
|
||||
')' \
|
||||
> Main.spec
|
||||
|
||||
echo "✓ Main.spec generated successfully"
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "Generated Main.spec"
|
||||
echo "========================================"
|
||||
cat Main.spec
|
||||
echo "========================================"
|
||||
|
||||
- name: Build with PyInstaller
|
||||
run: |
|
||||
python -m PyInstaller Main.spec
|
||||
|
||||
- name: Create DMG
|
||||
run: |
|
||||
tag_name="${{ steps.get_version.outputs.TAG_NAME }}"
|
||||
dmg_name="AutoLibrary.$tag_name-macos-arm64.dmg"
|
||||
app_path="dist/AutoLibrary.app"
|
||||
|
||||
if [ ! -d "$app_path" ]; then
|
||||
echo "✗ App bundle not found: $app_path"
|
||||
echo "Files in dist directory:"
|
||||
ls -la dist/
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Creating DMG from: $app_path"
|
||||
xattr -cr "$app_path" 2>/dev/null || true
|
||||
|
||||
tmp_dmg_dir=$(mktemp -d)
|
||||
cp -R "$app_path" "$tmp_dmg_dir/"
|
||||
ln -s /Applications "$tmp_dmg_dir/Applications"
|
||||
|
||||
hdiutil create \
|
||||
-volname "AutoLibrary $tag_name" \
|
||||
-srcfolder "$tmp_dmg_dir" \
|
||||
-ov \
|
||||
-format UDZO \
|
||||
"$dmg_name"
|
||||
|
||||
rm -rf "$tmp_dmg_dir"
|
||||
|
||||
if [ -f "$dmg_name" ]; then
|
||||
dmg_size=$(du -h "$dmg_name" | cut -f1)
|
||||
echo "✓ DMG created: $dmg_name (Size: $dmg_size)"
|
||||
else
|
||||
echo "✗ Failed to create DMG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Artifacts ready:"
|
||||
echo " - $dmg_name"
|
||||
echo " - $app_path"
|
||||
|
||||
- name: Archive artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: AutoLibrary.${{ steps.get_version.outputs.TAG_NAME }}-macos-arm64
|
||||
path: |
|
||||
AutoLibrary.${{ steps.get_version.outputs.TAG_NAME }}-macos-arm64.dmg
|
||||
dist/AutoLibrary.app/**
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload build summary
|
||||
run: |
|
||||
tag_name="${{ steps.get_version.outputs.TAG_NAME }}"
|
||||
version="${{ steps.get_version.outputs.VERSION }}"
|
||||
dmg_name="AutoLibrary.$tag_name-macos-arm64.dmg"
|
||||
|
||||
{
|
||||
echo "## Build Test Summary (macOS)"
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "✓ Pull request build test completed successfully!"
|
||||
echo "- Version: $version"
|
||||
echo "- Tag: $tag_name"
|
||||
echo "- Pull Request #${{ github.event.pull_request.number || 'N/A' }}"
|
||||
echo "- Branch: ${{ github.event.pull_request.head.ref || github.ref }}"
|
||||
echo "- Event: ${{ github.event_name }}"
|
||||
echo "- DMG: $dmg_name"
|
||||
echo ""
|
||||
echo "### How to test on macOS"
|
||||
echo '1. Download the DMG artifact'
|
||||
echo '2. Open the DMG and drag AutoLibrary.app to /Applications'
|
||||
echo '3. Right-click → Open the app (to bypass Gatekeeper)'
|
||||
} >> $GITHUB_STEP_SUMMARY
|
||||
@@ -1,28 +1,29 @@
|
||||
name: Build
|
||||
|
||||
# This workflow compiles the application for Windows platform using PyInstaller, and
|
||||
# archives the built artifacts as 'AutoLibrary.<tag_name>-windows-x86_64.zip'.
|
||||
# This workflow compiles the application for Windows and macOS platforms using
|
||||
# PyInstaller, and archives the built artifacts.
|
||||
#
|
||||
# - Windows: AutoLibrary.<tag_name>-windows-x86_64.zip
|
||||
# - macOS: AutoLibrary.<tag_name>-macos-arm64.dmg
|
||||
#
|
||||
# It is triggered when called by the release workflow.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version number'
|
||||
required: true
|
||||
type: string
|
||||
tag_name:
|
||||
description: 'Tag name'
|
||||
required: true
|
||||
required: false
|
||||
type: string
|
||||
outputs:
|
||||
version:
|
||||
description: 'The version number'
|
||||
value: ${{ jobs.build-windows.outputs.version }}
|
||||
tag_name:
|
||||
description: 'The tag name'
|
||||
value: ${{ jobs.build-windows.outputs.tag_name }}
|
||||
description: 'Version number'
|
||||
required: false
|
||||
type: string
|
||||
is_test:
|
||||
description: 'Whether this is a test build (not a release)'
|
||||
required: false
|
||||
type: string
|
||||
default: 'true'
|
||||
|
||||
#
|
||||
# Build Windows
|
||||
@@ -32,18 +33,18 @@ jobs:
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
outputs:
|
||||
version: ${{ steps.get_version.outputs.VERSION }}
|
||||
tag_name: ${{ steps.get_version.outputs.TAG_NAME }}
|
||||
version: ${{ steps.get_version.outputs.VERSION }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: main
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
# here we download the build version of ALVersionInfo.py from artifacts
|
||||
# and replace the committed version
|
||||
- name: Download build version of ALVersionInfo.py
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: updated-version-info-for-build
|
||||
path: src/gui/
|
||||
@@ -51,33 +52,49 @@ jobs:
|
||||
- name: Get version info
|
||||
id: get_version
|
||||
run: |
|
||||
$version = "${{ inputs.version }}"
|
||||
$tagName = "${{ inputs.tag_name }}"
|
||||
$isTest = "${{ inputs.is_test }}"
|
||||
if ($isTest -eq "true") {
|
||||
$version = "test"
|
||||
$tagName = "test"
|
||||
Write-Host "✓ Mode: Test Build"
|
||||
} else {
|
||||
$version = "${{ inputs.version }}"
|
||||
$tagName = "${{ inputs.tag_name }}"
|
||||
|
||||
if ([string]::IsNullOrEmpty($version)) {
|
||||
$version = "test"
|
||||
$tagName = "test"
|
||||
Write-Host "✓ Mode: Independent Build (No inputs provided)"
|
||||
}
|
||||
}
|
||||
|
||||
echo "TAG_NAME=$tagName" >> $env:GITHUB_OUTPUT
|
||||
echo "VERSION=$version" >> $env:GITHUB_OUTPUT
|
||||
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: Verify 'ALVersionInfo.py' was updated
|
||||
run: |
|
||||
$versionInfoFile = "src/gui/ALVersionInfo.py"
|
||||
Write-Host "Verifying $versionInfoFile content:"
|
||||
Write-Host "=================================="
|
||||
Write-Host "========================================"
|
||||
Get-Content $versionInfoFile | Write-Host
|
||||
Write-Host "=================================="
|
||||
Write-Host "========================================"
|
||||
shell: pwsh
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: requirements.txt
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirement.txt
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Solve ddddocr compatibility and copy model files
|
||||
run: |
|
||||
@@ -94,13 +111,13 @@ jobs:
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not (Test-Path "model")) {
|
||||
New-Item -ItemType Directory -Path "model" | Out-Null
|
||||
Write-Host "✓ Created model directory"
|
||||
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 = "model/common.onnx"
|
||||
$onnxDest = "models/common.onnx"
|
||||
if (Test-Path $onnxSource) {
|
||||
Copy-Item $onnxSource $onnxDest -Force
|
||||
Write-Host "✓ Copied ONNX model from: $onnxSource"
|
||||
@@ -119,18 +136,18 @@ jobs:
|
||||
}
|
||||
shell: pwsh
|
||||
|
||||
- name: Compile Qt UI files
|
||||
run: |
|
||||
cd src/gui/batchs
|
||||
./compile_ui.bat
|
||||
shell: cmd
|
||||
|
||||
- name: Compile Qt Resource files
|
||||
run: |
|
||||
cd src/gui/batchs
|
||||
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 }}"
|
||||
@@ -148,8 +165,8 @@ jobs:
|
||||
" pathex=[],"
|
||||
" binaries=[],"
|
||||
" datas=["
|
||||
" ('model\\common.onnx', 'ddddocr'),"
|
||||
" ('src\\gui\\icons\\AutoLibrary_32x32.ico', 'gui\\icons'),"
|
||||
" ('models\\common.onnx', 'ddddocr'),"
|
||||
" ('src\\gui\\resources\\icons\\AutoLibrary_Logo_64.ico', 'gui\\resources\\icons'),"
|
||||
" ],"
|
||||
" hiddenimports=[],"
|
||||
" hookspath=[],"
|
||||
@@ -164,10 +181,7 @@ jobs:
|
||||
"exe = EXE("
|
||||
" pyz,"
|
||||
" a.scripts,"
|
||||
" a.binaries,"
|
||||
" a.datas,"
|
||||
" [],"
|
||||
" name='$exeName',"
|
||||
" name='AutoLibrary',"
|
||||
" debug=False,"
|
||||
" bootloader_ignore_signals=False,"
|
||||
" strip=False,"
|
||||
@@ -180,15 +194,27 @@ jobs:
|
||||
" target_arch=None,"
|
||||
" codesign_identity=None,"
|
||||
" entitlements_file=None,"
|
||||
" icon=['src\\gui\\icons\\AutoLibrary_32x32.ico'],"
|
||||
" icon=['src\\gui\\resources\\icons\\AutoLibrary_Logo_64.ico'],"
|
||||
")"
|
||||
""
|
||||
"coll = COLLECT("
|
||||
" exe,"
|
||||
" a.binaries,"
|
||||
" a.datas,"
|
||||
" strip=False,"
|
||||
" upx=True,"
|
||||
" upx_exclude=[],"
|
||||
" name='$exeName'"
|
||||
")"
|
||||
)
|
||||
$specLines | Out-File -FilePath "Main.spec" -Encoding UTF8
|
||||
|
||||
Write-Host "✓ Main.spec generated successfully"
|
||||
Write-Host "`nGenerated Main.spec ============"
|
||||
Write-Host "✓ Main.spec (non-single file) generated successfully"
|
||||
Write-Host "`n========================================"
|
||||
Write-Host "Generated Main.spec"
|
||||
Write-Host "========================================"
|
||||
Get-Content "Main.spec" | Write-Host
|
||||
Write-Host "==================================`n"
|
||||
Write-Host "========================================`n"
|
||||
shell: pwsh
|
||||
|
||||
- name: Build with PyInstaller
|
||||
@@ -200,17 +226,17 @@ jobs:
|
||||
run: |
|
||||
$tagName = "${{ steps.get_version.outputs.TAG_NAME }}"
|
||||
$version = "${{ steps.get_version.outputs.VERSION }}"
|
||||
$exeName = "AutoLibrary-$version.exe"
|
||||
$distDir = "dist/AutoLibrary-$version"
|
||||
$zipName = "AutoLibrary.$tagName-windows-x86_64.zip"
|
||||
|
||||
echo "ZIP_PATH=$zipName" >> $env:GITHUB_OUTPUT
|
||||
"ZIP_NAME=$zipName" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
|
||||
Write-Host "Looking for executable: dist/$exeName"
|
||||
if (Test-Path "dist/$exeName") {
|
||||
Compress-Archive -Path "dist/$exeName" -DestinationPath $zipName
|
||||
Write-Host "✓ Created release archive: $zipName"
|
||||
Write-Host "Looking for distribution directory: $distDir"
|
||||
if (Test-Path $distDir) {
|
||||
Compress-Archive -Path "$distDir/*" -DestinationPath $zipName
|
||||
Write-Host "✓ Created release archive (directory mode): $zipName"
|
||||
} else {
|
||||
Write-Error "✗ Executable not found: dist/$exeName"
|
||||
Write-Error "✗ Distribution directory not found: $distDir"
|
||||
Write-Host "Files in dist directory:"
|
||||
Get-ChildItem "dist" | ForEach-Object { Write-Host " - $($_.Name)" }
|
||||
exit 1
|
||||
@@ -218,8 +244,320 @@ jobs:
|
||||
shell: pwsh
|
||||
|
||||
- name: Archive artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: AutoLibrary.${{ steps.get_version.outputs.TAG_NAME }}-windows-x86_64
|
||||
path: |
|
||||
${{ steps.zip_release.outputs.ZIP_PATH }}
|
||||
${{ steps.zip_release.outputs.ZIP_NAME }}
|
||||
retention-days: ${{ inputs.is_test == 'true' && 7 || 90 }}
|
||||
|
||||
- name: Upload build summary
|
||||
if: ${{ inputs.is_test == 'true' }}
|
||||
run: |
|
||||
"## Build Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
|
||||
"" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||
"========================================" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||
"✓ Build test completed successfully!" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||
"- Version: ${{ steps.get_version.outputs.VERSION }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||
"- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||
"- Event: ${{ github.event_name }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||
"- Ref: ${{ github.ref }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||
shell: pwsh
|
||||
|
||||
#
|
||||
# Build macOS
|
||||
#
|
||||
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Download build version of ALVersionInfo.py
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: updated-version-info-for-build
|
||||
path: src/gui/
|
||||
|
||||
- name: Get version info
|
||||
id: get_version
|
||||
run: |
|
||||
is_test="${{ inputs.is_test }}"
|
||||
if [ "$is_test" = "true" ]; then
|
||||
version="test"
|
||||
tag_name="test"
|
||||
echo "✓ Mode: Test Build"
|
||||
else
|
||||
version="${{ inputs.version }}"
|
||||
tag_name="${{ inputs.tag_name }}"
|
||||
|
||||
if [ -z "$version" ]; then
|
||||
version="test"
|
||||
tag_name="test"
|
||||
echo "✓ Mode: Independent Build (No inputs provided)"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "✓ Tag: $tag_name"
|
||||
echo "✓ Version: $version"
|
||||
|
||||
echo "VERSION=$version" >> $GITHUB_OUTPUT
|
||||
echo "TAG_NAME=$tag_name" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Verify 'ALVersionInfo.py' was updated
|
||||
run: |
|
||||
version_info_file="src/gui/ALVersionInfo.py"
|
||||
echo "Verifying $version_info_file content:"
|
||||
echo "========================================"
|
||||
cat "$version_info_file"
|
||||
echo "========================================"
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: requirements.txt
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Solve ddddocr compatibility and copy model files
|
||||
run: |
|
||||
ddddocr_path=$(python -c "import ddddocr, os; print(os.path.dirname(ddddocr.__file__))")
|
||||
echo "ddddocr package location: $ddddocr_path"
|
||||
|
||||
init_file="$ddddocr_path/__init__.py"
|
||||
if [ -f "$init_file" ]; then
|
||||
echo "Fixing ddddocr compatibility in: $init_file"
|
||||
# macOS 使用 BSD sed,要求 -i 后跟备份后缀名(空字符串 = 不备份)
|
||||
sed -i '' 's/Image\.ANTIALIAS/Image.Resampling.LANCZOS/g' "$init_file"
|
||||
echo "✓ Fixed: Image.ANTIALIAS -> Image.Resampling.LANCZOS"
|
||||
else
|
||||
echo "✗ ddddocr __init__.py not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p models
|
||||
echo "✓ Created models directory"
|
||||
|
||||
# 多路径 fallback 搜索 ONNX 模型,提升 ddddocr 版本兼容性
|
||||
onnx_source=""
|
||||
onnx_dest="models/common.onnx"
|
||||
candidate_paths=(
|
||||
"$ddddocr_path/common.onnx"
|
||||
"$ddddocr_path/models/common.onnx"
|
||||
"$ddddocr_path/ddddocr/common.onnx"
|
||||
)
|
||||
for candidate in "${candidate_paths[@]}"; do
|
||||
if [ -f "$candidate" ]; then
|
||||
onnx_source="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
# 若以上候选均未命中,回退到全包搜索
|
||||
if [ -z "$onnx_source" ]; then
|
||||
echo "⚠ 未在已知路径找到 ONNX 模型,回退到全包搜索..."
|
||||
onnx_source=$(find "$ddddocr_path" -name "*.onnx" -type f 2>/dev/null | head -1)
|
||||
fi
|
||||
|
||||
if [ -n "$onnx_source" ] && [ -f "$onnx_source" ]; then
|
||||
cp "$onnx_source" "$onnx_dest"
|
||||
echo "✓ ONNX model copied: $onnx_source -> $onnx_dest"
|
||||
else
|
||||
echo "✗ ONNX model not found in ddddocr package"
|
||||
echo " Searched directory: $ddddocr_path"
|
||||
ls -la "$ddddocr_path/" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "$onnx_dest" ]; then
|
||||
file_size=$(du -h "$onnx_dest" | cut -f1)
|
||||
echo "✓ Model file verified: $onnx_dest (Size: $file_size)"
|
||||
else
|
||||
echo "✗ Failed to copy model file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Compile Qt Resource files
|
||||
run: |
|
||||
cd batchs
|
||||
bash compile_rc.sh
|
||||
|
||||
- name: Compile Qt UI files
|
||||
run: |
|
||||
cd batchs
|
||||
bash compile_ui.sh
|
||||
|
||||
- name: Generate 'Main.spec'
|
||||
env:
|
||||
APP_VERSION: ${{ steps.get_version.outputs.VERSION }}
|
||||
run: |
|
||||
version="$APP_VERSION"
|
||||
exe_name="AutoLibrary-$version"
|
||||
echo "Generating Main.spec for version: $version"
|
||||
echo "App name: $exe_name"
|
||||
|
||||
printf '%s\n' \
|
||||
'# -*- mode: python ; coding: utf-8 -*-' \
|
||||
'' \
|
||||
'a = Analysis(' \
|
||||
" ['src/Main.py']," \
|
||||
' pathex=[],' \
|
||||
' binaries=[],' \
|
||||
' datas=[' \
|
||||
" ('models/common.onnx', 'ddddocr')," \
|
||||
" ('src/gui/resources/icons/AutoLibrary_Logo_64.ico', 'gui/resources/icons')," \
|
||||
' ],' \
|
||||
' hiddenimports=[],' \
|
||||
' hookspath=[],' \
|
||||
' hooksconfig={},' \
|
||||
' runtime_hooks=[],' \
|
||||
' excludes=[],' \
|
||||
' noarchive=False,' \
|
||||
' optimize=0,' \
|
||||
')' \
|
||||
'pyz = PYZ(a.pure)' \
|
||||
'' \
|
||||
'exe = EXE(' \
|
||||
' pyz,' \
|
||||
' a.scripts,' \
|
||||
' [],' \
|
||||
' exclude_binaries=True,' \
|
||||
" 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_Logo_64.ico']," \
|
||||
')' \
|
||||
'' \
|
||||
'coll = COLLECT(' \
|
||||
' exe,' \
|
||||
' a.binaries,' \
|
||||
' a.zipfiles,' \
|
||||
' a.datas,' \
|
||||
' strip=False,' \
|
||||
' upx=True,' \
|
||||
' upx_exclude=[],' \
|
||||
" name='${exe_name}'," \
|
||||
')' \
|
||||
'' \
|
||||
'app = BUNDLE(' \
|
||||
' coll,' \
|
||||
" name='AutoLibrary.app'," \
|
||||
" icon='src/gui/resources/icons/AutoLibrary_Logo.icns'," \
|
||||
" bundle_identifier='com.kenanzhu.autolibrary'," \
|
||||
' info_plist={' \
|
||||
" 'NSHighResolutionCapable': 'True'," \
|
||||
" 'CFBundleName': 'AutoLibrary'," \
|
||||
" 'CFBundleDisplayName': 'AutoLibrary'," \
|
||||
" 'CFBundleShortVersionString': '${version}'," \
|
||||
" 'CFBundleVersion': '${version}'," \
|
||||
" 'CFBundleExecutable': 'AutoLibrary'," \
|
||||
" 'NSPrincipalClass': 'NSApplication'," \
|
||||
" 'LSMinimumSystemVersion': '11.0'," \
|
||||
" 'NSRequiresAquaSystemAppearance': 'False'," \
|
||||
" 'NSHumanReadableCopyright': 'Copyright 2025 - 2026 KenanZhu. All rights reserved.'," \
|
||||
' },' \
|
||||
')' \
|
||||
> Main.spec
|
||||
|
||||
echo "✓ Main.spec generated successfully"
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "Generated Main.spec"
|
||||
echo "========================================"
|
||||
cat Main.spec
|
||||
echo "========================================"
|
||||
|
||||
- name: Build with PyInstaller
|
||||
run: |
|
||||
python -m PyInstaller Main.spec
|
||||
|
||||
- name: Create DMG
|
||||
run: |
|
||||
tag_name="${{ steps.get_version.outputs.TAG_NAME }}"
|
||||
dmg_name="AutoLibrary.$tag_name-macos-arm64.dmg"
|
||||
app_path="dist/AutoLibrary.app"
|
||||
|
||||
if [ ! -d "$app_path" ]; then
|
||||
echo "✗ App bundle not found: $app_path"
|
||||
echo "Files in dist directory:"
|
||||
ls -la dist/
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Creating DMG from: $app_path"
|
||||
xattr -cr "$app_path" 2>/dev/null || true
|
||||
|
||||
tmp_dmg_dir=$(mktemp -d)
|
||||
cp -R "$app_path" "$tmp_dmg_dir/"
|
||||
ln -s /Applications "$tmp_dmg_dir/Applications"
|
||||
|
||||
hdiutil create \
|
||||
-volname "AutoLibrary $tag_name" \
|
||||
-srcfolder "$tmp_dmg_dir" \
|
||||
-ov \
|
||||
-format UDZO \
|
||||
"$dmg_name"
|
||||
|
||||
rm -rf "$tmp_dmg_dir"
|
||||
|
||||
if [ -f "$dmg_name" ]; then
|
||||
dmg_size=$(du -h "$dmg_name" | cut -f1)
|
||||
echo "✓ DMG created: $dmg_name (Size: $dmg_size)"
|
||||
else
|
||||
echo "✗ Failed to create DMG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Artifacts ready:"
|
||||
echo " - $dmg_name"
|
||||
echo " - $app_path"
|
||||
|
||||
- name: Archive artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: AutoLibrary.${{ steps.get_version.outputs.TAG_NAME }}-macos-arm64
|
||||
path: |
|
||||
AutoLibrary.${{ steps.get_version.outputs.TAG_NAME }}-macos-arm64.dmg
|
||||
dist/AutoLibrary.app/**
|
||||
retention-days: ${{ inputs.is_test == 'true' && 7 || 90 }}
|
||||
|
||||
- name: Upload build summary
|
||||
if: ${{ inputs.is_test == 'true' }}
|
||||
run: |
|
||||
tag_name="${{ steps.get_version.outputs.TAG_NAME }}"
|
||||
version="${{ steps.get_version.outputs.VERSION }}"
|
||||
dmg_name="AutoLibrary.$tag_name-macos-arm64.dmg"
|
||||
|
||||
{
|
||||
echo "## Build Summary (macOS)"
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "✓ Build test completed successfully!"
|
||||
echo "- Version: $version"
|
||||
echo "- Tag: $tag_name"
|
||||
echo "- Event: ${{ github.event_name }}"
|
||||
echo "- Ref: ${{ github.ref }}"
|
||||
echo "- DMG: $dmg_name"
|
||||
echo ""
|
||||
echo "### How to test on macOS"
|
||||
echo '1. Download the DMG artifact'
|
||||
echo '2. Open the DMG and drag AutoLibrary.app to /Applications'
|
||||
echo '3. Right-click → Open the app (to bypass Gatekeeper)'
|
||||
} >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Commit Release
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
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
|
||||
type: string
|
||||
version:
|
||||
@@ -20,10 +20,33 @@ on:
|
||||
description: 'File path to commit'
|
||||
required: true
|
||||
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:
|
||||
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:
|
||||
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 }}
|
||||
branch_name:
|
||||
description: 'The branch name where the commit was made'
|
||||
value: ${{ jobs.commit-release.outputs.branch_name }}
|
||||
|
||||
jobs:
|
||||
commit-release:
|
||||
@@ -32,18 +55,19 @@ jobs:
|
||||
contents: write
|
||||
outputs:
|
||||
new_commit_sha: ${{ steps.commit_info.outputs.commit_sha }}
|
||||
branch_name: ${{ steps.push_release.outputs.branch_name }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: main
|
||||
ref: ${{ inputs.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
# here we download the commit version of ALVersionInfo.py from artifacts
|
||||
# and replace the original file with it.
|
||||
- name: Download commit version of ALVersionInfo.py
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: updated-version-info-for-commit
|
||||
path: downloaded-file/
|
||||
@@ -59,7 +83,9 @@ jobs:
|
||||
|
||||
echo "✓ File replaced: $FILE_PATH"
|
||||
echo ""
|
||||
echo "Updated file content ==================="
|
||||
echo "========================================"
|
||||
echo "Updated file content"
|
||||
echo "========================================"
|
||||
cat "$FILE_PATH"
|
||||
echo "========================================"
|
||||
|
||||
@@ -81,18 +107,38 @@ jobs:
|
||||
git commit -m "chore(release): v${VERSION} [auto release commit]"
|
||||
echo "✓ Changes committed"
|
||||
|
||||
- name: Push to main branch
|
||||
- name: Push to release branch
|
||||
id: push_release
|
||||
run: |
|
||||
MAIN_BRANCH=$(git remote show origin | grep 'HEAD branch' | cut -d' ' -f5)
|
||||
if [ -z "$MAIN_BRANCH" ]; then
|
||||
MAIN_BRANCH="main"
|
||||
# Get the release branch name from the input ref
|
||||
BRANCH_NAME=$(echo "${{ inputs.ref }}" | sed 's|refs/heads/||')
|
||||
|
||||
if [ -z "$BRANCH_NAME" ]; then
|
||||
echo "✗ Error: Could not determine branch name from ref: ${{ inputs.ref }}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Pushing to branch: ${MAIN_BRANCH}"
|
||||
git push origin HEAD:${MAIN_BRANCH}
|
||||
echo "✓ Changes pushed to ${MAIN_BRANCH}"
|
||||
echo "Pushing to branch: ${BRANCH_NAME}"
|
||||
git push origin HEAD:${BRANCH_NAME}
|
||||
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
|
||||
if: ${{ inputs.create_tag != 'true' }}
|
||||
run: |
|
||||
TAG_NAME="${{ inputs.tag_name }}"
|
||||
|
||||
@@ -107,3 +153,9 @@ jobs:
|
||||
COMMIT_SHA=$(git rev-parse --short HEAD)
|
||||
echo "commit_sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
|
||||
echo "✓ New commit SHA: $COMMIT_SHA"
|
||||
echo "## Commit Release Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "========================================" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Version: ${{ inputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Tag: ${{ inputs.tag_name }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Commit SHA: $COMMIT_SHA" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
@@ -1,41 +1,55 @@
|
||||
name: Release
|
||||
|
||||
# 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:
|
||||
# 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
|
||||
# the updated version file as an artifact.
|
||||
#
|
||||
# for more information, please refer to the comment in the workflow 'update-version.yml'
|
||||
|
||||
# 2. Commit Release:
|
||||
# Commits version changes and moves the release tag to this new release commit.
|
||||
# 3. Commit Release:
|
||||
# Commits version changes to release branch and creates the release tag.
|
||||
|
||||
# 3. Build:
|
||||
# Compiles the application for Windows platform using PyInstaller, and
|
||||
# archives the built artifacts as 'AutoLibrary.<tag_name>-windows-x86_64.zip'.
|
||||
# 4. Build:
|
||||
# Compiles the application for Windows and macOS platforms using PyInstaller, and
|
||||
# archives the built artifacts.
|
||||
# - Windows: AutoLibrary.<tag_name>-windows-x86_64.zip
|
||||
# - macOS: AutoLibrary.<tag_name>-macos-arm64.dmg
|
||||
|
||||
# 4. Release:
|
||||
# 5. Release:
|
||||
# Creates GitHub release with generated artifacts and release notes
|
||||
|
||||
# < 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
|
||||
# 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:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
branches:
|
||||
- 'release/v*'
|
||||
|
||||
jobs:
|
||||
#
|
||||
# Start :
|
||||
# virtual job that indacates the start of the release process
|
||||
# virtual job that indicates the start of the release process
|
||||
#
|
||||
|
||||
start:
|
||||
@@ -44,6 +58,62 @@ jobs:
|
||||
- name: Start release
|
||||
run: |
|
||||
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 :
|
||||
@@ -52,31 +122,35 @@ jobs:
|
||||
|
||||
update-version:
|
||||
needs:
|
||||
- start
|
||||
- extract-version
|
||||
uses: ./.github/workflows/update-version.yml
|
||||
permissions:
|
||||
contents: write
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
tag_name: ${{ needs.extract-version.outputs.tag_name }}
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
#
|
||||
# Commit release :
|
||||
# this job commits the updated version file and move the release
|
||||
# tag to this new commit
|
||||
# this job commits the updated version file to main and creates
|
||||
# the release tag (not moving an existing tag)
|
||||
#
|
||||
|
||||
commit-release:
|
||||
needs:
|
||||
- extract-version
|
||||
- update-version
|
||||
if: ${{ needs.update-version.outputs.has_changes == 'true' }}
|
||||
uses: ./.github/workflows/commit-release.yml
|
||||
permissions:
|
||||
contents: write
|
||||
with:
|
||||
tag_name: ${{ needs.update-version.outputs.tag_name }}
|
||||
version: ${{ needs.update-version.outputs.version }}
|
||||
tag_name: ${{ needs.extract-version.outputs.tag_name }}
|
||||
version: ${{ needs.extract-version.outputs.version }}
|
||||
file_path: src/gui/ALVersionInfo.py
|
||||
create_tag: 'true'
|
||||
is_rc: ${{ needs.extract-version.outputs.is_rc }}
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
#
|
||||
# Build :
|
||||
@@ -86,13 +160,14 @@ jobs:
|
||||
needs:
|
||||
- update-version
|
||||
- commit-release
|
||||
if: always() && needs.update-version.result == 'success' && needs.commit-release.result == 'success'
|
||||
if: always() && needs.update-version.result == 'success' && (needs.commit-release.result == 'success' || needs.commit-release.result == 'skipped')
|
||||
uses: ./.github/workflows/build.yml
|
||||
permissions:
|
||||
contents: write
|
||||
with:
|
||||
version: ${{ needs.update-version.outputs.version }}
|
||||
tag_name: ${{ needs.update-version.outputs.tag_name }}
|
||||
version: ${{ needs.update-version.outputs.version }}
|
||||
is_test: 'false'
|
||||
|
||||
#
|
||||
# Release :
|
||||
@@ -102,48 +177,44 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build
|
||||
- extract-version
|
||||
if: always() && needs.build.result == 'success'
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
- name: Download Windows artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: AutoLibrary.${{ needs.build.outputs.tag_name }}-windows-x86_64
|
||||
name: AutoLibrary.${{ needs.extract-version.outputs.tag_name }}-windows-x86_64
|
||||
path: artifacts/
|
||||
|
||||
- name: Download macOS artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: AutoLibrary.${{ needs.extract-version.outputs.tag_name }}-macos-arm64
|
||||
path: artifacts/
|
||||
|
||||
- name: Create release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ needs.build.outputs.tag_name }}
|
||||
name: AutoLibrary ${{ needs.build.outputs.tag_name }}
|
||||
tag_name: ${{ needs.extract-version.outputs.tag_name }}
|
||||
name: AutoLibrary ${{ needs.extract-version.outputs.tag_name }}
|
||||
files: |
|
||||
artifacts/AutoLibrary.${{ needs.build.outputs.tag_name }}-windows-x86_64.zip
|
||||
artifacts/AutoLibrary.${{ needs.extract-version.outputs.tag_name }}-windows-x86_64.zip
|
||||
artifacts/AutoLibrary.${{ needs.extract-version.outputs.tag_name }}-macos-arm64.dmg
|
||||
draft: false
|
||||
prerelease: false
|
||||
prerelease: ${{ needs.extract-version.outputs.is_rc == 'true' }}
|
||||
generate_release_notes: true
|
||||
body: |
|
||||
### 下载获取
|
||||
- **Windows x86_64**: `AutoLibrary.${{ needs.build.outputs.tag_name }}-windows-x86_64.zip`
|
||||
|
||||
### 如何使用
|
||||
1. 下载 `AutoLibrary.${{ needs.build.outputs.tag_name }}-windows-x86_64.zip` 文件
|
||||
2. 解压到任意目录
|
||||
3. 下载对应浏览器的驱动文件
|
||||
4. 运行 `AutoLibrary-${{ needs.build.outputs.version }}.exe` (首次运行会初始化配置文件)
|
||||
5. 按照提示操作即可
|
||||
|
||||
更多详情请访问 [AutoLibrary 网站](http://autolibrary.cv) 和查看 [帮助手册](https://autolibrary.cv/docs/manual_lists.html)
|
||||
|
||||
---
|
||||
**完整更新日志见下方自动生成的 Release Notes**
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# End :
|
||||
# virtual job that indacates the end of the release process
|
||||
# virtual job that indicates the end of the release process
|
||||
#
|
||||
|
||||
end:
|
||||
@@ -154,3 +225,114 @@ jobs:
|
||||
- name: End release
|
||||
run: |
|
||||
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' || needs.commit-release.result == 'skipped') }}
|
||||
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 "========================================"
|
||||
echo "Branch status before merge"
|
||||
echo "========================================"
|
||||
git log --oneline --graph --all -5
|
||||
echo "========================================"
|
||||
echo "Diff: ${MAIN_BRANCH} vs origin/${BRANCH_NAME}"
|
||||
echo "========================================"
|
||||
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 "========================================"
|
||||
echo "Merge result"
|
||||
echo "========================================"
|
||||
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 "========================================" >> $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:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
fetch-depth: 0
|
||||
@@ -128,10 +128,15 @@ jobs:
|
||||
echo "Build version file location: $VER_INFO_BUILDFILE"
|
||||
echo "Commit version file location: $VER_INFO_COMMITFILE"
|
||||
echo ""
|
||||
echo "Build version ALVersionInfo.py content ="
|
||||
echo "========================================"
|
||||
echo "Build version ALVersionInfo.py"
|
||||
echo "========================================"
|
||||
cat "$VER_INFO_BUILDFILE"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
echo "Commit version ALVersionInfo.py content "
|
||||
echo "========================================"
|
||||
echo "Commit version ALVersionInfo.py"
|
||||
echo "========================================"
|
||||
cat "$VER_INFO_COMMITFILE"
|
||||
echo "========================================"
|
||||
|
||||
@@ -140,15 +145,20 @@ jobs:
|
||||
run: |
|
||||
if git diff --quiet src/gui/ALVersionInfo.py; then
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
echo "! No changes detected in ALVersionInfo.py"
|
||||
echo "⚠ No changes detected in ALVersionInfo.py"
|
||||
else
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
echo "✓ ALVersionInfo.py has been modified"
|
||||
fi
|
||||
echo "## Update Version Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "========================================" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Version: ${{ steps.get_version.outputs.VERSION }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Upload modified ALVersionInfo.py ready for build
|
||||
if: steps.check_changes.outputs.has_changes == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: updated-version-info-for-build
|
||||
path: src/gui/temp/ALVersionInfo.py
|
||||
@@ -156,7 +166,7 @@ jobs:
|
||||
|
||||
- name: Upload modified ALVersionInfo.py ready for commit
|
||||
if: steps.check_changes.outputs.has_changes == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: updated-version-info-for-commit
|
||||
path: src/gui/ALVersionInfo.py
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
__pycache__/
|
||||
build/
|
||||
dist/
|
||||
model/*.onnx
|
||||
driver/*.exe
|
||||
src/gui/configs/*.json
|
||||
src/gui/translators/qtbase_zh_CN.qm
|
||||
src/gui/AutoLibraryResources.py
|
||||
src/gui/AutoLibraryResource.py
|
||||
src/gui/Ui_ALMainWindow.py
|
||||
src/gui/Ui_ALConfigWidget.py
|
||||
src/gui/Ui_ALTimerTaskWidget.py
|
||||
src/gui/Ui_ALAddTimerTaskDialog.py
|
||||
src/gui/Ui_ALAboutDialog.py
|
||||
|
||||
Main.spec
|
||||
models/*.*
|
||||
drivers/*.*
|
||||
!models/*.md
|
||||
!drivers/*.md
|
||||
!templates/*.md
|
||||
!templates/configs/*.md
|
||||
|
||||
src/gui/resources/ui/Ui_*.py
|
||||
src/gui/resources/translators/qtbase_zh_CN.qm
|
||||
src/gui/resources/ALResource.py
|
||||
|
||||
Main.spec
|
||||
@@ -1,15 +0,0 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
ddddocr = "*"
|
||||
selenium = "*"
|
||||
pyinstaller = "*"
|
||||
pyside6 = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[requires]
|
||||
python_version = "3.13"
|
||||
@@ -1,630 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "26dffc26812d5328611959b95713a7ed65e20c08c60089b54283b0f406dd08e4"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.13"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"altgraph": {
|
||||
"hashes": [
|
||||
"sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406",
|
||||
"sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"
|
||||
],
|
||||
"version": "==0.17.4"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11",
|
||||
"sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==25.4.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de",
|
||||
"sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2025.10.5"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
"sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb",
|
||||
"sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b",
|
||||
"sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f",
|
||||
"sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9",
|
||||
"sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44",
|
||||
"sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2",
|
||||
"sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c",
|
||||
"sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75",
|
||||
"sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65",
|
||||
"sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e",
|
||||
"sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a",
|
||||
"sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e",
|
||||
"sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25",
|
||||
"sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a",
|
||||
"sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe",
|
||||
"sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b",
|
||||
"sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91",
|
||||
"sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592",
|
||||
"sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187",
|
||||
"sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c",
|
||||
"sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1",
|
||||
"sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94",
|
||||
"sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba",
|
||||
"sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb",
|
||||
"sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165",
|
||||
"sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529",
|
||||
"sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca",
|
||||
"sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c",
|
||||
"sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6",
|
||||
"sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c",
|
||||
"sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0",
|
||||
"sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743",
|
||||
"sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63",
|
||||
"sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5",
|
||||
"sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5",
|
||||
"sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4",
|
||||
"sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d",
|
||||
"sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b",
|
||||
"sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93",
|
||||
"sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205",
|
||||
"sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27",
|
||||
"sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512",
|
||||
"sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d",
|
||||
"sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c",
|
||||
"sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037",
|
||||
"sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26",
|
||||
"sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322",
|
||||
"sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb",
|
||||
"sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c",
|
||||
"sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8",
|
||||
"sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4",
|
||||
"sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414",
|
||||
"sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9",
|
||||
"sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664",
|
||||
"sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9",
|
||||
"sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775",
|
||||
"sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739",
|
||||
"sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc",
|
||||
"sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062",
|
||||
"sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe",
|
||||
"sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9",
|
||||
"sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92",
|
||||
"sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5",
|
||||
"sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13",
|
||||
"sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d",
|
||||
"sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26",
|
||||
"sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f",
|
||||
"sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495",
|
||||
"sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b",
|
||||
"sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6",
|
||||
"sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c",
|
||||
"sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef",
|
||||
"sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5",
|
||||
"sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18",
|
||||
"sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad",
|
||||
"sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3",
|
||||
"sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7",
|
||||
"sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5",
|
||||
"sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534",
|
||||
"sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49",
|
||||
"sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2",
|
||||
"sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5",
|
||||
"sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453",
|
||||
"sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"coloredlogs": {
|
||||
"hashes": [
|
||||
"sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934",
|
||||
"sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==15.0.1"
|
||||
},
|
||||
"ddddocr": {
|
||||
"hashes": [
|
||||
"sha256:5991594d481d33ba0b136022e910f578d6d5b0ca536b44886591359622ab0c70",
|
||||
"sha256:7c44b58ba7d7566d785c65b8526ec5b78efacd121e993dea4fda5f7966897428"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.0.6"
|
||||
},
|
||||
"flatbuffers": {
|
||||
"hashes": [
|
||||
"sha256:255538574d6cb6d0a79a17ec8bc0d30985913b87513a01cce8bcdb6b4c44d0e2",
|
||||
"sha256:676f9fa62750bb50cf531b42a0a2a118ad8f7f797a511eda12881c016f093b12"
|
||||
],
|
||||
"version": "==25.9.23"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
"sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1",
|
||||
"sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.16.0"
|
||||
},
|
||||
"humanfriendly": {
|
||||
"hashes": [
|
||||
"sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477",
|
||||
"sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==10.0"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea",
|
||||
"sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.11"
|
||||
},
|
||||
"mpmath": {
|
||||
"hashes": [
|
||||
"sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f",
|
||||
"sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"
|
||||
],
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"numpy": {
|
||||
"hashes": [
|
||||
"sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64",
|
||||
"sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e",
|
||||
"sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0",
|
||||
"sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365",
|
||||
"sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d",
|
||||
"sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c",
|
||||
"sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52",
|
||||
"sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36",
|
||||
"sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec",
|
||||
"sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f",
|
||||
"sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197",
|
||||
"sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7",
|
||||
"sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9",
|
||||
"sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37",
|
||||
"sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a",
|
||||
"sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db",
|
||||
"sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c",
|
||||
"sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7",
|
||||
"sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d",
|
||||
"sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e",
|
||||
"sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f",
|
||||
"sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a",
|
||||
"sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16",
|
||||
"sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e",
|
||||
"sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868",
|
||||
"sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05",
|
||||
"sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e",
|
||||
"sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff",
|
||||
"sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f",
|
||||
"sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7",
|
||||
"sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f",
|
||||
"sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e",
|
||||
"sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562",
|
||||
"sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6",
|
||||
"sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0",
|
||||
"sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26",
|
||||
"sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0",
|
||||
"sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d",
|
||||
"sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879",
|
||||
"sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef",
|
||||
"sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29",
|
||||
"sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252",
|
||||
"sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847",
|
||||
"sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6",
|
||||
"sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32",
|
||||
"sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0",
|
||||
"sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3",
|
||||
"sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b",
|
||||
"sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3",
|
||||
"sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc",
|
||||
"sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc",
|
||||
"sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda",
|
||||
"sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a",
|
||||
"sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40",
|
||||
"sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032",
|
||||
"sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7",
|
||||
"sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966",
|
||||
"sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9",
|
||||
"sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346",
|
||||
"sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2",
|
||||
"sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a",
|
||||
"sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786",
|
||||
"sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f",
|
||||
"sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc",
|
||||
"sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb",
|
||||
"sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646",
|
||||
"sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd",
|
||||
"sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1",
|
||||
"sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11",
|
||||
"sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667",
|
||||
"sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996",
|
||||
"sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953",
|
||||
"sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b",
|
||||
"sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb"
|
||||
],
|
||||
"markers": "python_version >= '3.11'",
|
||||
"version": "==2.3.4"
|
||||
},
|
||||
"onnxruntime": {
|
||||
"hashes": [
|
||||
"sha256:0be6a37a45e6719db5120e9986fcd30ea205ac8103fd1fb74b6c33348327a0cc",
|
||||
"sha256:0f9b4ae77f8e3c9bee50c27bc1beede83f786fe1d52e99ac85aa8d65a01e9b77",
|
||||
"sha256:162f4ca894ec3de1a6fd53589e511e06ecdc3ff646849b62a9da7489dee9ce95",
|
||||
"sha256:1f9cc0a55349c584f083c1c076e611a7c35d5b867d5d6e6d6c823bf821978088",
|
||||
"sha256:218295a8acae83905f6f1aed8cacb8e3eb3bd7513a13fe4ba3b2664a19fc4a6b",
|
||||
"sha256:25de5214923ce941a3523739d34a520aac30f21e631de53bba9174dc9c004435",
|
||||
"sha256:2ff531ad8496281b4297f32b83b01cdd719617e2351ffe0dba5684fb283afa1f",
|
||||
"sha256:45d127d6e1e9b99d1ebeae9bcd8f98617a812f53f46699eafeb976275744826b",
|
||||
"sha256:4ca88747e708e5c67337b0f65eed4b7d0dd70d22ac332038c9fc4635760018f7",
|
||||
"sha256:6f91d2c9b0965e86827a5ba01531d5b669770b01775b23199565d6c1f136616c",
|
||||
"sha256:76ff670550dc23e58ea9bc53b5149b99a44e63b34b524f7b8547469aaa0dcb8c",
|
||||
"sha256:87d8b6eaf0fbeb6835a60a4265fde7a3b60157cf1b2764773ac47237b4d48612",
|
||||
"sha256:8bace4e0d46480fbeeb7bbe1ffe1f080e6663a42d1086ff95c1551f2d39e7872",
|
||||
"sha256:8f7d1fe034090a1e371b7f3ca9d3ccae2fabae8c1d8844fb7371d1ea38e8e8d2",
|
||||
"sha256:902c756d8b633ce0dedd889b7c08459433fbcf35e9c38d1c03ddc020f0648c6e",
|
||||
"sha256:9d2385e774f46ac38f02b3a91a91e30263d41b2f1f4f26ae34805b2a9ddef466",
|
||||
"sha256:a7730122afe186a784660f6ec5807138bf9d792fa1df76556b27307ea9ebcbe3",
|
||||
"sha256:b28740f4ecef1738ea8f807461dd541b8287d5650b5be33bca7b474e3cbd1f36",
|
||||
"sha256:b8f029a6b98d3cf5be564d52802bb50a8489ab73409fa9db0bf583eabb7c2321",
|
||||
"sha256:bbfd2fca76c855317568c1b36a885ddea2272c13cb0e395002c402f2360429a6",
|
||||
"sha256:da44b99206e77734c5819aa2142c69e64f3b46edc3bd314f6a45a932defc0b3e",
|
||||
"sha256:e2b9233c4947907fd1818d0e581c049c41ccc39b2856cc942ff6d26317cee145"
|
||||
],
|
||||
"markers": "python_version >= '3.10'",
|
||||
"version": "==1.23.2"
|
||||
},
|
||||
"outcome": {
|
||||
"hashes": [
|
||||
"sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8",
|
||||
"sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.3.0.post0"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484",
|
||||
"sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==25.0"
|
||||
},
|
||||
"pefile": {
|
||||
"hashes": [
|
||||
"sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc",
|
||||
"sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.0'",
|
||||
"version": "==2023.2.7"
|
||||
},
|
||||
"pillow": {
|
||||
"hashes": [
|
||||
"sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643",
|
||||
"sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e",
|
||||
"sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e",
|
||||
"sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc",
|
||||
"sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642",
|
||||
"sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6",
|
||||
"sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1",
|
||||
"sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b",
|
||||
"sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399",
|
||||
"sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba",
|
||||
"sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad",
|
||||
"sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47",
|
||||
"sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739",
|
||||
"sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b",
|
||||
"sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f",
|
||||
"sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10",
|
||||
"sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52",
|
||||
"sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d",
|
||||
"sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b",
|
||||
"sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a",
|
||||
"sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9",
|
||||
"sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d",
|
||||
"sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098",
|
||||
"sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905",
|
||||
"sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b",
|
||||
"sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3",
|
||||
"sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371",
|
||||
"sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953",
|
||||
"sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01",
|
||||
"sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca",
|
||||
"sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e",
|
||||
"sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7",
|
||||
"sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27",
|
||||
"sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082",
|
||||
"sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e",
|
||||
"sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d",
|
||||
"sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8",
|
||||
"sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a",
|
||||
"sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad",
|
||||
"sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3",
|
||||
"sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a",
|
||||
"sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d",
|
||||
"sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353",
|
||||
"sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee",
|
||||
"sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b",
|
||||
"sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b",
|
||||
"sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a",
|
||||
"sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7",
|
||||
"sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef",
|
||||
"sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a",
|
||||
"sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a",
|
||||
"sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257",
|
||||
"sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07",
|
||||
"sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4",
|
||||
"sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c",
|
||||
"sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c",
|
||||
"sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4",
|
||||
"sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe",
|
||||
"sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8",
|
||||
"sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5",
|
||||
"sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6",
|
||||
"sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e",
|
||||
"sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8",
|
||||
"sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e",
|
||||
"sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275",
|
||||
"sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3",
|
||||
"sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76",
|
||||
"sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227",
|
||||
"sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9",
|
||||
"sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5",
|
||||
"sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79",
|
||||
"sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca",
|
||||
"sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa",
|
||||
"sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b",
|
||||
"sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e",
|
||||
"sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197",
|
||||
"sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab",
|
||||
"sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79",
|
||||
"sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2",
|
||||
"sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363",
|
||||
"sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0",
|
||||
"sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e",
|
||||
"sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782",
|
||||
"sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925",
|
||||
"sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0",
|
||||
"sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b",
|
||||
"sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced",
|
||||
"sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c",
|
||||
"sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344",
|
||||
"sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9",
|
||||
"sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1"
|
||||
],
|
||||
"markers": "python_version >= '3.10'",
|
||||
"version": "==12.0.0"
|
||||
},
|
||||
"protobuf": {
|
||||
"hashes": [
|
||||
"sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954",
|
||||
"sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995",
|
||||
"sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef",
|
||||
"sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455",
|
||||
"sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee",
|
||||
"sha256:c963e86c3655af3a917962c9619e1a6b9670540351d7af9439d06064e3317cc9",
|
||||
"sha256:cd33a8e38ea3e39df66e1bbc462b076d6e5ba3a4ebbde58219d777223a7873d3",
|
||||
"sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035",
|
||||
"sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90",
|
||||
"sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==6.33.0"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [
|
||||
"sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2",
|
||||
"sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.23"
|
||||
},
|
||||
"pyinstaller": {
|
||||
"hashes": [
|
||||
"sha256:0a48f55b85ff60f83169e10050f2759019cf1d06773ad1c4da3a411cd8751058",
|
||||
"sha256:53559fe1e041a234f2b4dcc3288ea8bdd57f7cad8a6644e422c27bb407f3edef",
|
||||
"sha256:6d5f8617f3650ff9ef893e2ab4ddbf3c0d23d0c602ef74b5df8fbef4607840c8",
|
||||
"sha256:73ba72e04fcece92e32518bbb1e1fb5ac2892677943dfdff38e01a06e8742851",
|
||||
"sha256:7fd1c785219a87ca747c21fa92f561b0d2926a7edc06d0a0fe37f3736e00bd7a",
|
||||
"sha256:b1752488248f7899281b17ca3238eefb5410521291371a686a4f5830f29f52b3",
|
||||
"sha256:b756ddb9007b8141c5476b553351f9d97559b8af5d07f9460869bfae02be26b0",
|
||||
"sha256:ba618a61627ee674d6d68e5de084ba17c707b59a4f2a856084b3999bdffbd3f0",
|
||||
"sha256:bc10eb1a787f99fea613509f55b902fbd2d8b73ff5f51ff245ea29a481d97d41",
|
||||
"sha256:c8b7ef536711617e12fef4673806198872033fa06fa92326ad7fd1d84a9fa454",
|
||||
"sha256:d0af8a401de792c233c32c44b16d065ca9ab8262ee0c906835c12bdebc992a64",
|
||||
"sha256:d1ebf84d02c51fed19b82a8abb4df536923abd55bb684d694e1356e4ae2a0ce5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version < '3.15' and python_version >= '3.8'",
|
||||
"version": "==6.16.0"
|
||||
},
|
||||
"pyinstaller-hooks-contrib": {
|
||||
"hashes": [
|
||||
"sha256:56e972bdaad4e9af767ed47d132362d162112260cbe488c9da7fee01f228a5a6",
|
||||
"sha256:ccbfaa49399ef6b18486a165810155e5a8d4c59b41f20dc5da81af7482aaf038"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2025.9"
|
||||
},
|
||||
"pyreadline3": {
|
||||
"hashes": [
|
||||
"sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7",
|
||||
"sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.5.4"
|
||||
},
|
||||
"pyside6": {
|
||||
"hashes": [
|
||||
"sha256:4b709bdeeb89d386059343a5a706fc185cee37b517bda44c7d6b64d5fdaf3339",
|
||||
"sha256:70a8bcc73ea8d6baab70bba311eac77b9a1d31f658d0b418e15eb6ea36c97e6f",
|
||||
"sha256:9f402f883e640048fab246d36e298a5e16df9b18ba2e8c519877e472d3602820",
|
||||
"sha256:ae8c3c8339cd7c3c9faa7cc5c52670dcc8662ccf4b63a6fed61c6345b90c4c01",
|
||||
"sha256:c2cbc5dc2a164e3c7c51b3435e24203e90e5edd518c865466afccbd2e5872bb0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version < '3.14' and python_version >= '3.9'",
|
||||
"version": "==6.10.0"
|
||||
},
|
||||
"pyside6-addons": {
|
||||
"hashes": [
|
||||
"sha256:08d4ed46c4c9a353a9eb84134678f8fdd4ce17fb8cce2b3686172a7575025464",
|
||||
"sha256:15d32229d681be0bba1b936c4a300da43d01e1917ada5b57f9e03a387c245ab0",
|
||||
"sha256:88e61e21ee4643cdd9efb39ec52f4dc1ac74c0b45c5b7fa453d03c094f0a8a5c",
|
||||
"sha256:92536427413f3b6557cf53f1a515cd766725ee46a170aff57ad2ff1dfce0ffb1",
|
||||
"sha256:99d93a32c17c5f6d797c3b90dd58f2a8bae13abde81e85802c34ceafaee11859"
|
||||
],
|
||||
"markers": "python_version < '3.14' and python_version >= '3.9'",
|
||||
"version": "==6.10.0"
|
||||
},
|
||||
"pyside6-essentials": {
|
||||
"hashes": [
|
||||
"sha256:003e871effe1f3e5b876bde715c15a780d876682005a6e989d89f48b8b93e93a",
|
||||
"sha256:1d5e013a8698e37ab8ef360e6960794eb5ef20832a8d562e649b8c5a0574b2d8",
|
||||
"sha256:6dd0936394cb14da2fd8e869899f5e0925a738b1c8d74c2f22503720ea363fb1",
|
||||
"sha256:b1dd0864f0577a448fb44426b91cafff7ee7cccd1782ba66491e1c668033f998",
|
||||
"sha256:fc167eb211dd1580e20ba90d299e74898e7a5a1306d832421e879641fc03b6fe"
|
||||
],
|
||||
"markers": "python_version < '3.14' and python_version >= '3.9'",
|
||||
"version": "==6.10.0"
|
||||
},
|
||||
"pysocks": {
|
||||
"hashes": [
|
||||
"sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299",
|
||||
"sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5",
|
||||
"sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.7.1"
|
||||
},
|
||||
"pywin32-ctypes": {
|
||||
"hashes": [
|
||||
"sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8",
|
||||
"sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.2.3"
|
||||
},
|
||||
"selenium": {
|
||||
"hashes": [
|
||||
"sha256:c117af6727859d50f622d6d0785b945c5db3e28a45ec12ad85cee2e7cc84fc4c",
|
||||
"sha256:ed47563f188130a6fd486b327ca7ba48c5b11fb900e07d6457befdde320e35fd"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.10'",
|
||||
"version": "==4.38.0"
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922",
|
||||
"sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==80.9.0"
|
||||
},
|
||||
"shiboken6": {
|
||||
"hashes": [
|
||||
"sha256:0bc5631c1bf150cbef768a17f5f289aae1cb4db6c6b0c19b2421394e27783717",
|
||||
"sha256:7a5f5f400ebfb3a13616030815708289c2154e701a60b9db7833b843e0bee543",
|
||||
"sha256:b01377e68d14132360efb0f4b7233006d26aa8ae9bb50edf00960c2a5f52d148",
|
||||
"sha256:dfc4beab5fec7dbbebbb418f3bf99af865d6953aa0795435563d4cbb82093b61",
|
||||
"sha256:e612734da515d683696980107cdc0396a3ae0f07b059f0f422ec8a2333810234"
|
||||
],
|
||||
"markers": "python_version < '3.14' and python_version >= '3.9'",
|
||||
"version": "==6.10.0"
|
||||
},
|
||||
"sniffio": {
|
||||
"hashes": [
|
||||
"sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2",
|
||||
"sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.3.1"
|
||||
},
|
||||
"sortedcontainers": {
|
||||
"hashes": [
|
||||
"sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88",
|
||||
"sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"
|
||||
],
|
||||
"version": "==2.4.0"
|
||||
},
|
||||
"sympy": {
|
||||
"hashes": [
|
||||
"sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517",
|
||||
"sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==1.14.0"
|
||||
},
|
||||
"trio": {
|
||||
"hashes": [
|
||||
"sha256:150f29ec923bcd51231e1d4c71c7006e65247d68759dd1c19af4ea815a25806b",
|
||||
"sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5"
|
||||
],
|
||||
"markers": "python_version >= '3.10'",
|
||||
"version": "==0.32.0"
|
||||
},
|
||||
"trio-websocket": {
|
||||
"hashes": [
|
||||
"sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae",
|
||||
"sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.12.2"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466",
|
||||
"sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==4.15.0"
|
||||
},
|
||||
"urllib3": {
|
||||
"extras": [
|
||||
"socks"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760",
|
||||
"sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==2.5.0"
|
||||
},
|
||||
"websocket-client": {
|
||||
"hashes": [
|
||||
"sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98",
|
||||
"sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==1.9.0"
|
||||
},
|
||||
"wsproto": {
|
||||
"hashes": [
|
||||
"sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065",
|
||||
"sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"
|
||||
],
|
||||
"markers": "python_full_version >= '3.7.0'",
|
||||
"version": "==1.2.0"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
}
|
||||
@@ -2,67 +2,61 @@
|
||||
# AutoLibrary
|
||||
---
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||

|
||||

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

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

|
||||
|
||||
了解更多请访问 [_AutoLibrary 网站_](http://autolibrary.cv)
|
||||
了解更多请访问 [_AutoLibrary 网站_](http://www.autolibrary.kenanzhu.com)
|
||||
|
||||
---
|
||||
|
||||
### 功能
|
||||
|
||||
#### 核心特性
|
||||
|
||||
1. 自动预约 - 支持自动预约
|
||||
2. 自动续约 - 支持自动续约
|
||||
3. 自动签到 - 支持自动签到
|
||||
4. 批量操作 - 支持同时预约多个用户,可以指定当前需要跳过的用户,并将用户分成多个组
|
||||
5. 定时任务 - 使用内置定时任务管理,添加定时任务,指定时间后按当前预约信息自动运行
|
||||
4. 远程签到 - 支持远程签到,无需在图书馆网络环境下即可签到
|
||||
|
||||
*1,2,3 的具体操作方法和注意事项请访问我们的 [帮助手册](https://autolibrary.cv/docs/manual_lists.html)*
|
||||
#### 辅助特性
|
||||
|
||||
### 特点
|
||||
1. 批量操作 - 支持同时预约多个用户,可以指定当前需要跳过的用户,并将用户分成多个组
|
||||
2. 定时任务 - 使用内置定时任务管理,添加定时任务,指定时间后按当前预约信息自动运行,支持设置重复任务
|
||||
3. 驱动管理 - 内置浏览器驱动自动管理,支持自动检测浏览器版本并下载对应驱动,无需手动下载
|
||||
4. 自定义主题 - 支持深浅色主题切换,多款应用样式切换,可导入 .altheme 自定义主题一键换肤
|
||||
|
||||
#### 关于预约等操作的注意事项
|
||||
> [!TIP]
|
||||
> 前往 [主题市场](https://www.autolibrary.kenanzhu.com/marketplace) 获取和分享更多自定义主题。
|
||||
|
||||
工具会自动处理登录过程的验证码识别过程,正常情况下单次识别准确率在 90% 以上,如遇验证码识别错误,大概率是校园网网络环境不佳导致的。
|
||||
|
||||
只要确保处于校园网网络环境内,工具都是可以正常运行的。操作处理速度基本上取决于校园网的网络环境,一般情况下在 3-4 秒(不考虑硬件差异)左右即可完成一个用户的操作,完全满足正常使用需求。
|
||||
|
||||
> [!NOTE]
|
||||
> 工具仅作为正常的预约,签到和续约的图书馆辅助工具,请勿干扰图书馆的正常运行(如故意预约多个座位,或同时预约大量的用户等,对此影响图书馆正常运行本工具概不负责,请在善用工具方便自己的情况下尽量不用影响其他同学的使用)。
|
||||
|
||||
#### 关于批量操作的注意事项
|
||||
|
||||
批量操作时,建议将需要操作的用户分成多个组,每个组的用户数量不要超过 4 人(即一整张桌子的数量),否则会影响操作效率,大量用户同时预约会一定程度上增加图书馆服务器的压力,影响正常使用。根据需要在用户管理界面中可以勾选本次操作是否跳过该用户,以提高运行效率。
|
||||
|
||||
#### 关于定时任务的注意事项
|
||||
|
||||
定时任务会在指定的时间自动运行,运行时会根据当前预约信息进行操作。一般情况下不建议设置两个运行开始时间比较接近的定时任务,否则后一个任务会等待前一个任务完成后才会运行,按照队列的顺序执行。
|
||||
*具体操作方法和注意事项请访问我们的 [帮助手册](https://manuals.autolibrary.kenanzhu.com)*
|
||||
|
||||
### 如何使用
|
||||
|
||||
1. 下载最新版本的 [AutoLibrary 压缩包](https://github.com/KenanZhu/AutoLibrary/releases)。
|
||||
2. 解压下载的文件到任意目录。
|
||||
3. 下载对应浏览器的驱动文件,并在配置界面的运行配置选项卡对应位置选择你下载好的浏览器驱动
|
||||
4. 运行 `AutoLibrary.exe` 文件。
|
||||
5. 按照提示操作即可。
|
||||
1. 下载最新版本的 [AutoLibrary 安装程序](https://github.com/KenanZhu/AutoLibrary/releases/latest) 或 [压缩包](https://github.com/KenanZhu/AutoLibrary/releases/latest) 。
|
||||
2. 双击运行安装程序进行安装,或将压缩包解压到任意目录。
|
||||
3. 运行 `AutoLibrary`,即可打开主界面。
|
||||
4. 点击 [配置] 按钮,在配置界面填写好预约信息和运行配置后,点击 [确认] 按钮。
|
||||
5. 点击 [启动脚本] 按钮,即可开始自动预约、续约、签到等操作。
|
||||
|
||||
*注意 1*: 关于浏览器驱动的下载和其它相关问题,请参考我们的 [帮助手册](https://autolibrary.cv/docs/manual_lists.html) 中对应软件版本的内容。
|
||||
*注意 1*: 工具内置浏览器驱动自动管理功能,会自动检测本地浏览器版本并下载对应的驱动文件。如果自动下载失败,也可以手动下载驱动文件并在配置界面的运行配置选项卡对应位置选择驱动文件路径。
|
||||
|
||||
#### 平台支持 & 编译步骤
|
||||
|
||||
本工具目前仅支持 Windows 平台,由于使用 PySide6 库开发,理论上是可以自行编译并在 Linux 和 macOS 上运行,这里提供简单的编译步骤:
|
||||
本工具目前支持 Windows 和 macOS(Apple Silicon)平台,由于使用 PySide6 库开发,理论上是可以自行编译并在 Linux 和旧架构 macOS 上运行,这里提供简单的编译步骤:
|
||||
|
||||
1. 确保系统安装了 Python 3.13 版本 (推荐,过低或高版本会导致兼容问题)。
|
||||
2. 安装 pyside6 selenium ddddocr 库,命令为 `pip install pyside6 selenium ddddocr`。
|
||||
3. 在 `src/gui/batchs` 目录下运行 `compile_ui.bat` (linux 和 macOS 系统使用 `compile_ui.sh`) 文件来编译 Qt 的 UI 文件。
|
||||
2. 安装所有依赖库,命令为 `pip install -r requirements.txt` (建议在虚拟环境下操作)。
|
||||
3. 在 `batchs` 目录下运行 `compile_ui.bat` (linux 和 macOS 系统使用 `compile_ui.sh`) 文件来编译 Qt 的 UI 文件。
|
||||
4. 在上一步相同目录内运行 `compile_rc.bat` (linux 和 macOS 系统使用 `compile_rc.sh`) 文件来编译 Qt 的资源文件。
|
||||
5. 待上述步骤完成后,运行 `src/Main.py` 文件即可。
|
||||
|
||||
*注意 1*:如果 python 使用的是虚拟环境,请在虚拟环境安装依赖后,在激活的虚拟环境终端中使用 `cd src/gui/batchs` 命令切换到 `batchs` 目录下,再运行编译脚本。否则会提示缺少必要的 Qt PySide 依赖库。
|
||||
*注意 1*:如果 python 使用的是虚拟环境,请在虚拟环境安装依赖后,在激活的虚拟环境终端中使用 `cd batchs` 命令切换到 `batchs` 目录下,再运行编译脚本。否则会提示缺少必要的 Qt PySide 依赖库。
|
||||
|
||||
*注意 2*:由于 ddddocr 的代码版本问题,其中 `__init__.py` 文件中的函数 `def classification(self, img: bytes):` 中的 `image.resize` 方法传入了不符合当前 pillow 版本的 `resample` 参数 `Image.ANTIALIAS`,该重采样常量已经在 10.0.0 版中删除 [1](@ref)。请将 `image.resize` 方法中的参数替换为 `resample=Image.Resampling.LANCZOS`,具体函数如下:
|
||||
```python
|
||||
@@ -77,6 +71,25 @@ def classification(self, img: bytes):
|
||||
|
||||
[1](@ref):[pillow 中已经删除或已经弃用的常量](https://pillow.ac.cn/en/stable/deprecations.html#constants)
|
||||
|
||||
### 注意事项
|
||||
|
||||
#### 关于预约等操作
|
||||
|
||||
工具会自动处理登录过程的验证码识别过程,正常情况下单次识别准确率在 90% 以上,如遇验证码识别错误,大概率是校园网网络环境不佳导致的。
|
||||
|
||||
只要确保处于校园网网络环境内,工具都是可以正常运行的。操作处理速度基本上取决于校园网的网络环境,一般情况下在 3-4 秒(不考虑硬件差异)左右即可完成一个用户的操作,完全满足正常使用需求。
|
||||
|
||||
> [!NOTE]
|
||||
> 工具仅作为正常的预约,签到和续约的图书馆辅助工具,请勿干扰图书馆的正常运行(如故意预约多个座位,或同时预约大量的用户等)由此影响图书馆正常运行的情况本工具概不负责,请在善用工具方便自己的情况下尽量不用影响其他同学的使用。
|
||||
|
||||
#### 关于批量操作
|
||||
|
||||
批量操作时,建议将需要操作的用户分成多个组,每个组的用户数量不要超过 4 人(即一整张桌子的数量),否则会影响操作效率,大量用户同时预约会一定程度上增加图书馆服务器的压力,影响正常使用。根据需要在用户管理界面中可以勾选本次操作是否跳过该用户,以提高运行效率。
|
||||
|
||||
#### 关于定时任务
|
||||
|
||||
定时任务会在指定的时间自动运行,运行时会根据当前预约信息进行操作。一般情况下不建议设置两个运行开始时间比较接近的定时任务,否则后一个任务会等待前一个任务完成后才会运行,按照队列的顺序执行。
|
||||
|
||||
### Q&A
|
||||
|
||||
#### 为什么开发这个工具?
|
||||
@@ -93,22 +106,22 @@ def classification(self, img: bytes):
|
||||
|
||||
#### 会有手机端的版本吗?
|
||||
|
||||
暂时没有考虑,而且也没有足够的时间和能力开发多平台的版本并测试维护,所以暂时只提供 Windows 版本。
|
||||
暂时没有考虑,而且也没有足够的时间和能力开发多平台的版本并测试维护,所以暂时只提供 Windows 和 macOS(Apple Silicon)版本。
|
||||
|
||||
#### 后续会有哪些功能?
|
||||
|
||||
当前 v1.0.0 版本的功能对于正常使用已经足够,不过后续会着重考虑完善 2-4 人预约时的使用体验,暂时有以下构想:
|
||||
当前版本的功能对于正常使用已经足够,不过后续会着重完善预约时的使用体验,暂时有以下构想:
|
||||
|
||||
1. 2-4 人一起预约时,往往会偏向于预约并排或对面的整个空座位,这时候工具会按照一定策略查询搜索符合条件的座位,并预约并排或对面的整个座位,而不是各自独立预约。
|
||||
2. 预约时会考虑到组内用户的预约时间是否冲突,若冲突则会提示用户是否继续预约,若用户选择继续预约,则会按需要调整预约时间,再进行预约。
|
||||
3. 对于比较固定的用户,会考虑在定时任务管理中添加如 ‘每日任务’ ‘每周任务’ 等选项,用户可以根据需要设置定时任务重复的日期范围,自动完成预约,签到,续约等操作。
|
||||
- ~~引入交互预约面板功能,预约时直接在座位分布图中选择可用座位,并按用户分配,无需事先配置预约信息。~~ (不计划)
|
||||
- ~~优化定时任务管理功能,用户可以在定时任务管理界面设置重复运行的定时任务,如每日预约、每周预约等。~~ (已完成)
|
||||
- 软件的自动更新以及公告栏功能,用户可以自动更新最新版本并获取最新公告事项。
|
||||
|
||||
不过由于本人的时间和能力有限,也需要考虑到图书馆的正常运行,所以后续功能会有所取舍,但也许会进行一些小的功能验证。
|
||||
|
||||
#### 其他功能建议?
|
||||
|
||||
如果你有其他功能建议,或者遇到了什么功能性,操作上的问题,欢迎提交 Issue 到本项目。
|
||||
如果你有足够的开发能力,欢迎为本项目提交 PR,也可以 Fork 本项目,根据自己的需求进行修改和完善。
|
||||
如果你有其他功能建议,或者遇到了什么功能性,操作上的问题,欢迎提交 [Issue](https://github.com/KenanZhu/AutoLibrary/issues) 到本项目。
|
||||
如果你有足够的开发能力,欢迎为本项目提交 [PR](https://github.com/KenanZhu/AutoLibrary/pulls) ,也可以 Fork 本项目,根据自己的需求进行修改和完善。
|
||||
|
||||
### 联系我
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
This folder is used to store the batch scripts.
|
||||
@@ -3,6 +3,7 @@ chcp 65001 >nul
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
cd /d "%~dp0.."
|
||||
cd src/gui/resources
|
||||
|
||||
echo [AutoLibrary compile] 检查翻译文件...
|
||||
if exist translators (
|
||||
@@ -18,12 +19,11 @@ if exist translators (
|
||||
|
||||
pyside6-lrelease "%%f"
|
||||
if !errorlevel! equ 0 (
|
||||
echo [AutoLibrary compile] 翻译文件 "%%f" ✓ 编译成功,输出文件: "!qm_filename!"
|
||||
echo [AutoLibrary compile] 翻译文件 "%%f" 编译成功,输出文件: "!qm_filename!"
|
||||
) else (
|
||||
echo [AutoLibrary compile] 翻译文件 "%%f" ✗ 编译失败
|
||||
echo [AutoLibrary compile] 翻译文件 "%%f" 编译失败
|
||||
)
|
||||
)
|
||||
echo.
|
||||
) else (
|
||||
echo [AutoLibrary compile] 未找到任何 .ts 翻译文件
|
||||
)
|
||||
@@ -52,11 +52,10 @@ for %%f in (*.qrc) do (
|
||||
|
||||
pyside6-rcc "%%f" -o "!output_file!"
|
||||
if !errorlevel! equ 0 (
|
||||
echo [AutoLibrary compile] 文件 "%%f" ✓ 编译成功,输出文件: "!output_file!"
|
||||
echo [AutoLibrary compile] 文件 "%%f" 编译成功,输出文件: "!output_file!"
|
||||
) else (
|
||||
echo [AutoLibrary compile] 文件 "%%f" ✗ 编译失败
|
||||
echo [AutoLibrary compile] 文件 "%%f" 编译失败
|
||||
)
|
||||
echo.
|
||||
)
|
||||
|
||||
echo [AutoLibrary compile] 所有操作完成。
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PARENT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
cd "$PARENT_DIR"
|
||||
PRJECT_DIR="$SCRIPT_DIR/.."
|
||||
|
||||
cd "$PRJECT_DIR/src/gui/resources"
|
||||
|
||||
echo "[AutoLibrary compile] 检查翻译文件..."
|
||||
if [ -d "translators" ]; then
|
||||
@@ -10,7 +11,6 @@ if [ -d "translators" ]; then
|
||||
ts_files=(*.ts)
|
||||
ts_count=${#ts_files[@]}
|
||||
|
||||
# 如果第一个元素是"*.ts"(表示没有匹配),则数量为0
|
||||
if [ "$ts_count" -eq 1 ] && [ "${ts_files[0]}" = "*.ts" ]; then
|
||||
ts_count=0
|
||||
fi
|
||||
@@ -23,12 +23,11 @@ if [ -d "translators" ]; then
|
||||
echo "[AutoLibrary compile] 正在编译翻译文件: \"$file\" -> \"$qm_file\""
|
||||
|
||||
if pyside6-lrelease "$file"; then
|
||||
echo "[AutoLibrary compile] 翻译文件 \"$file\" ✓ 编译成功,输出文件: \"$qm_file\""
|
||||
echo "[AutoLibrary compile] 翻译文件 \"$file\" 编译成功,输出文件: \"$qm_file\""
|
||||
else
|
||||
echo "[AutoLibrary compile] 翻译文件 \"$file\" ✗ 编译失败"
|
||||
echo "[AutoLibrary compile] 翻译文件 \"$file\" 编译失败"
|
||||
fi
|
||||
done
|
||||
echo
|
||||
else
|
||||
echo "[AutoLibrary compile] 未找到任何 .ts 翻译文件"
|
||||
fi
|
||||
@@ -36,7 +35,6 @@ if [ -d "translators" ]; then
|
||||
else
|
||||
echo "[AutoLibrary compile] 未找到 translators 目录"
|
||||
fi
|
||||
echo
|
||||
|
||||
file_count=$(ls *.qrc 2>/dev/null | wc -l)
|
||||
|
||||
@@ -46,7 +44,6 @@ if [ $file_count -eq 0 ]; then
|
||||
fi
|
||||
|
||||
echo "[AutoLibrary compile] 找到 $file_count 个 .qrc 文件,开始编译..."
|
||||
echo
|
||||
|
||||
for file in *.qrc; do
|
||||
base_name=$(basename "$file" .qrc)
|
||||
@@ -54,11 +51,10 @@ for file in *.qrc; do
|
||||
echo "[AutoLibrary compile] 正在编译: \"$file\" -> \"$output_file\""
|
||||
|
||||
if pyside6-rcc "$file" -o "$output_file"; then
|
||||
echo "[AutoLibrary compile] 文件 \"$file\" ✓ 编译成功,输出文件: \"$output_file\""
|
||||
echo "[AutoLibrary compile] 文件 \"$file\" 编译成功,输出文件: \"$output_file\""
|
||||
else
|
||||
echo "[AutoLibrary compile] 文件 \"$file\" ✗ 编译失败"
|
||||
echo "[AutoLibrary compile] 文件 \"$file\" 编译失败"
|
||||
fi
|
||||
echo
|
||||
done
|
||||
|
||||
echo "[AutoLibrary compile] 所有操作完成。"
|
||||
@@ -3,6 +3,7 @@ chcp 65001 >nul
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
cd /d "%~dp0.."
|
||||
cd src/gui/resources/ui
|
||||
|
||||
set count=0
|
||||
for %%f in (*.ui) do set /a count+=1
|
||||
@@ -23,11 +24,10 @@ for %%f in (*.ui) do (
|
||||
|
||||
pyside6-uic "%%f" -o "!output_file!"
|
||||
if !errorlevel! equ 0 (
|
||||
echo [AutoLibrary compile] 文件 "%%f" ✓ 编译成功,输出文件: "!output_file!"
|
||||
echo [AutoLibrary compile] 文件 "%%f" 编译成功,输出文件: "!output_file!"
|
||||
) else (
|
||||
echo [AutoLibrary compile] 文件 "%%f" ✗ 编译失败
|
||||
echo [AutoLibrary compile] 文件 "%%f" 编译失败
|
||||
)
|
||||
echo.
|
||||
)
|
||||
|
||||
echo [AutoLibrary compile] 所有操作完成。
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PARENT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
cd "$PARENT_DIR"
|
||||
PRJECT_DIR="$SCRIPT_DIR/.."
|
||||
|
||||
cd "$PRJECT_DIR/src/gui/resources/ui"
|
||||
|
||||
file_count=$(ls *.ui 2>/dev/null | wc -l)
|
||||
|
||||
@@ -12,7 +13,6 @@ if [ $file_count -eq 0 ]; then
|
||||
fi
|
||||
|
||||
echo "[AutoLibrary compile] 找到 $file_count 个 .ui 文件,开始编译..."
|
||||
echo
|
||||
|
||||
for file in *.ui; do
|
||||
base_name=$(basename "$file" .ui)
|
||||
@@ -20,11 +20,10 @@ for file in *.ui; do
|
||||
echo "[AutoLibrary compile] 正在编译: \"$file\" -> \"$output_file\""
|
||||
|
||||
if pyside6-uic "$file" -o "$output_file"; then
|
||||
echo "[AutoLibrary compile] 文件 \"$file\" ✓ 编译成功,输出文件: \"$output_file\""
|
||||
echo "[AutoLibrary compile] 文件 \"$file\" 编译成功,输出文件: \"$output_file\""
|
||||
else
|
||||
echo "[AutoLibrary compile] 文件 \"$file\" ✗ 编译失败"
|
||||
echo "[AutoLibrary compile] 文件 \"$file\" 编译失败"
|
||||
fi
|
||||
echo
|
||||
done
|
||||
|
||||
echo "[AutoLibrary compile] 所有操作完成。"
|
||||
@@ -1 +0,0 @@
|
||||
For more infomation, please visit our website: https://www.autolibrary.cv
|
||||
@@ -1 +0,0 @@
|
||||
This folder is used to store the browser driver using by selenium.
|
||||
@@ -0,0 +1 @@
|
||||
This folder is used to store the browser drivers using by selenium.
|
||||
@@ -0,0 +1,3 @@
|
||||
This folder is used to store the manuals.
|
||||
|
||||
Our manuals are available at https://manuals.autolibrary.kenanzhu.com
|
||||
@@ -1 +0,0 @@
|
||||
This folder is used to store the model using by ddddocr.
|
||||
@@ -0,0 +1 @@
|
||||
This folder is used to store the models using by ddddocr.
|
||||
@@ -0,0 +1,34 @@
|
||||
attrs==26.1.0
|
||||
certifi==2026.2.25
|
||||
cffi==2.0.0
|
||||
charset-normalizer==3.4.6
|
||||
ddddocr==1.0.6
|
||||
flatbuffers==25.12.19
|
||||
h11==0.16.0
|
||||
idna==3.11
|
||||
lupa==2.8
|
||||
numpy==2.4.3
|
||||
onnxruntime==1.24.4
|
||||
outcome==1.3.0.post0
|
||||
packaging==26.0
|
||||
pillow==12.1.1
|
||||
protobuf==7.34.0
|
||||
pybrowsers==1.3.2
|
||||
pycparser==3.0
|
||||
pyinstaller==6.19.0
|
||||
PySide6==6.10.2
|
||||
PySide6_Addons==6.10.2
|
||||
PySide6_Essentials==6.10.2
|
||||
PySocks==1.7.1
|
||||
QtAwesome==1.4.2
|
||||
QtPy==2.4.3
|
||||
requests==2.32.5
|
||||
selenium==4.38.0
|
||||
shiboken6==6.10.2
|
||||
sniffio==1.3.1
|
||||
sortedcontainers==2.4.0
|
||||
trio==0.33.0
|
||||
trio-websocket==0.12.2
|
||||
typing_extensions==4.15.0
|
||||
urllib3==2.6.3
|
||||
wsproto==1.3.2
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 - 2026 KenanZhu.
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
@@ -13,20 +13,23 @@ from PySide6.QtCore import QTranslator
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from gui.ALMainWindow import ALMainWindow
|
||||
from gui import AutoLibraryResource
|
||||
from gui.resources import ALResource
|
||||
|
||||
from boot.AppInitializer import initializeApp
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
translator = QTranslator()
|
||||
if translator.load(":/res/trans/translators/qtbase_zh_CN.ts"):
|
||||
if translator.load(":/res/translators/qtbase_zh_CN.ts"):
|
||||
app.installTranslator(translator)
|
||||
app.setStyle('Fusion')
|
||||
app.setApplicationName("AutoLibrary")
|
||||
if not initializeApp():
|
||||
sys.exit(-1)
|
||||
window = ALMainWindow()
|
||||
window.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2026 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
from datetime import (
|
||||
date,
|
||||
datetime,
|
||||
)
|
||||
|
||||
from lupa import LuaRuntime as _LuaRuntime
|
||||
|
||||
from autoscript._helpers import (
|
||||
_TYPE_DEFAULT_VAR,
|
||||
_assignPath,
|
||||
_checkDateFormat,
|
||||
_checkTimeFormat,
|
||||
_checkType,
|
||||
_cleanLuaError,
|
||||
_navigatePath,
|
||||
)
|
||||
|
||||
try:
|
||||
from lupa.lua55 import LuaError as _LuaError, LuaSyntaxError as _LuaSyntaxError
|
||||
except ImportError:
|
||||
try:
|
||||
from lupa.lua54 import LuaError as _LuaError, LuaSyntaxError as _LuaSyntaxError
|
||||
except ImportError:
|
||||
_LuaError = Exception
|
||||
_LuaSyntaxError = Exception
|
||||
|
||||
|
||||
__all__ = ["ASEngine"]
|
||||
|
||||
|
||||
class ASEngine:
|
||||
|
||||
@staticmethod
|
||||
def getCurrentDate(
|
||||
) -> str:
|
||||
|
||||
return date.today().isoformat()
|
||||
|
||||
@staticmethod
|
||||
def getCurrentTime(
|
||||
) -> str:
|
||||
|
||||
return datetime.now().strftime("%H:%M")
|
||||
|
||||
@staticmethod
|
||||
def _sandbox(
|
||||
lua,
|
||||
) -> None:
|
||||
|
||||
lua.execute("""
|
||||
io = nil
|
||||
require = nil
|
||||
dofile = nil
|
||||
loadfile = nil
|
||||
load = nil
|
||||
package = nil
|
||||
rawget = nil
|
||||
rawset = nil
|
||||
rawequal = nil
|
||||
getfenv = nil
|
||||
setfenv = nil
|
||||
debug = nil
|
||||
if os then
|
||||
os.execute = nil
|
||||
os.exit = nil
|
||||
os.getenv = nil
|
||||
os.remove = nil
|
||||
os.rename = nil
|
||||
os.tmpname = nil
|
||||
os.setlocale = nil
|
||||
end
|
||||
""")
|
||||
|
||||
@staticmethod
|
||||
def _registerHelpers(
|
||||
lua,
|
||||
) -> None:
|
||||
|
||||
lua.execute("""
|
||||
function date(y, m, d)
|
||||
return os.time({year = y, month = m, day = d})
|
||||
end
|
||||
|
||||
function time(h, m)
|
||||
return h * 60 + m
|
||||
end
|
||||
|
||||
function datenow()
|
||||
local now = os.date("*t")
|
||||
return os.time({year = now.year, month = now.month, day = now.day})
|
||||
end
|
||||
|
||||
function timenow()
|
||||
local now = os.date("*t")
|
||||
return now.hour * 60 + now.min
|
||||
end
|
||||
|
||||
function dateadd(date_val, n)
|
||||
return date_val + n * 86400
|
||||
end
|
||||
|
||||
function timeadd(time_val, n)
|
||||
return (time_val + n * 60) % 1440
|
||||
end
|
||||
|
||||
function strtodate(iso_str)
|
||||
local y, m, d = iso_str:match("(%d+)-(%d+)-(%d+)")
|
||||
return os.time({year = y, month = m, day = d})
|
||||
end
|
||||
|
||||
function strtotime(hm_str)
|
||||
local h, m = hm_str:match("(%d+):(%d+)")
|
||||
return h * 60 + m
|
||||
end
|
||||
|
||||
function datetostr(ts)
|
||||
return os.date("%Y-%m-%d", ts)
|
||||
end
|
||||
|
||||
function timetostr(m)
|
||||
return string.format("%02d:%02d", math.floor(m / 60), m % 60)
|
||||
end
|
||||
""")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
targetVars: list[tuple] = None,
|
||||
):
|
||||
|
||||
self._targetVars: dict[str, dict] = {}
|
||||
self._lua = None
|
||||
|
||||
if targetVars:
|
||||
for item in targetVars:
|
||||
name, varType, keyPath = item[0], item[1], item[2]
|
||||
self.addTargetVar(name, varType, keyPath)
|
||||
|
||||
def _getLua(
|
||||
self,
|
||||
):
|
||||
|
||||
if self._lua is None:
|
||||
self._lua = _LuaRuntime(unpack_returned_tuples=True)
|
||||
self._sandbox(self._lua)
|
||||
self._registerHelpers(self._lua)
|
||||
return self._lua
|
||||
|
||||
def _push(
|
||||
self,
|
||||
targetData: dict,
|
||||
) -> None:
|
||||
|
||||
lua = self._getLua()
|
||||
g = lua.globals()
|
||||
strToDate = g["strtodate"]
|
||||
strToTime = g["strtotime"]
|
||||
|
||||
for varName, info in self._targetVars.items():
|
||||
keyPath = info["keyPath"]
|
||||
vt = info["type"]
|
||||
raw = _navigatePath(targetData, keyPath)
|
||||
if vt == "Date":
|
||||
if not isinstance(raw, str) or not raw.strip():
|
||||
raise ValueError(
|
||||
f"Date 类型变量 '{varName}' 对应的数据为空或不是字符串类型,"
|
||||
f"请检查路径 {keyPath} 的值是否为合法的日期字符串 (YYYY-MM-DD)"
|
||||
)
|
||||
raw = raw.strip()
|
||||
_checkDateFormat(raw, varName)
|
||||
g[varName] = strToDate(raw)
|
||||
elif vt == "Time":
|
||||
if not isinstance(raw, str) or not raw.strip():
|
||||
raise ValueError(
|
||||
f"Time 类型变量 '{varName}' 对应的数据为空或不是字符串类型,"
|
||||
f"请检查路径 {keyPath} 的值是否为合法的时间字符串 (HH:MM)"
|
||||
)
|
||||
raw = raw.strip()
|
||||
_checkTimeFormat(raw, varName)
|
||||
g[varName] = strToTime(raw)
|
||||
else:
|
||||
if raw is None:
|
||||
raw = _TYPE_DEFAULT_VAR.get(vt, False)
|
||||
g[varName] = raw
|
||||
|
||||
def _pull(
|
||||
self,
|
||||
targetData: dict,
|
||||
) -> None:
|
||||
|
||||
lua = self._getLua()
|
||||
g = lua.globals()
|
||||
dateToStr = g["datetostr"]
|
||||
timeToStr = g["timetostr"]
|
||||
|
||||
for varName, info in self._targetVars.items():
|
||||
try:
|
||||
luaVal = g[varName]
|
||||
except KeyError:
|
||||
continue
|
||||
vt = info["type"]
|
||||
if vt == "Date":
|
||||
luaVal = dateToStr(luaVal)
|
||||
elif vt == "Time":
|
||||
luaVal = timeToStr(luaVal)
|
||||
elif vt == "Float" and isinstance(luaVal, int) and not isinstance(luaVal, bool):
|
||||
luaVal = float(luaVal)
|
||||
_checkType(varName, vt, luaVal)
|
||||
_assignPath(targetData, info["keyPath"], luaVal)
|
||||
|
||||
def addTargetVar(
|
||||
self,
|
||||
name: str,
|
||||
varType: str,
|
||||
keyPath: list,
|
||||
) -> None:
|
||||
|
||||
upperName = name.upper().strip()
|
||||
self._targetVars[upperName] = {
|
||||
"type": varType,
|
||||
"keyPath": keyPath,
|
||||
}
|
||||
|
||||
def execute(
|
||||
self,
|
||||
scriptText: str,
|
||||
targetData: dict,
|
||||
) -> None:
|
||||
|
||||
if not scriptText or not scriptText.strip():
|
||||
return
|
||||
try:
|
||||
self._push(targetData)
|
||||
self._getLua().execute(scriptText)
|
||||
self._pull(targetData)
|
||||
except _LuaSyntaxError as e:
|
||||
raise ValueError(
|
||||
f"AutoScript 语法错误: {_cleanLuaError(str(e))}"
|
||||
)
|
||||
except _LuaError as e:
|
||||
raise ValueError(
|
||||
f"AutoScript 运行时错误: {_cleanLuaError(str(e))}"
|
||||
)
|
||||
except ValueError as e:
|
||||
raise ValueError(f"AutoScript 数据错误: {e}")
|
||||
except Exception as e:
|
||||
raise ValueError(f"AutoScript 未知错误: {e}")
|
||||
|
||||
def reset(
|
||||
self,
|
||||
) -> None:
|
||||
|
||||
self._targetVars = {}
|
||||
self._lua = None
|
||||
@@ -0,0 +1,58 @@
|
||||
# -*- 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 .ASEngine import ASEngine
|
||||
|
||||
__version__ = "1.0.0" # autoscript version
|
||||
|
||||
_TARGET_VAR_DEFS = [
|
||||
("USERNAME", "String", ["username"], "用户名"),
|
||||
("USER_ENABLE", "Boolean", ["enabled"], "用户启用"),
|
||||
("RESERVE_DATE", "Date", ["reserve_info", "date"], "预约日期"),
|
||||
("RESERVE_BEGIN_TIME", "Time", ["reserve_info", "begin_time", "time"], "预约开始时间"),
|
||||
("RESERVE_END_TIME", "Time", ["reserve_info", "end_time", "time"], "预约结束时间"),
|
||||
]
|
||||
_MOCK_TYPE_VALUES = {
|
||||
"String": "__mock__",
|
||||
"Boolean": True,
|
||||
"Date": "2099-01-01",
|
||||
"Time": "00:00",
|
||||
"Int": 0,
|
||||
"Float": 0.0,
|
||||
}
|
||||
|
||||
|
||||
def createAllVariablesTable(
|
||||
) -> dict:
|
||||
|
||||
return {
|
||||
displayName: (name, varType)
|
||||
for name, varType, _, displayName in _TARGET_VAR_DEFS
|
||||
}
|
||||
|
||||
def createTargetVarDefs(
|
||||
) -> list:
|
||||
|
||||
return list(_TARGET_VAR_DEFS)
|
||||
|
||||
def createMockTargetData(
|
||||
) -> dict:
|
||||
|
||||
data = {}
|
||||
for _, varType, keyPath, _ in _TARGET_VAR_DEFS:
|
||||
d = data
|
||||
for key in keyPath[:-1]:
|
||||
d = d.setdefault(key, {})
|
||||
d[keyPath[-1]] = _MOCK_TYPE_VALUES.get(varType, "")
|
||||
return data
|
||||
|
||||
def createEngine(
|
||||
) -> ASEngine:
|
||||
|
||||
return ASEngine(_TARGET_VAR_DEFS)
|
||||
@@ -0,0 +1,153 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2026 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
from datetime import (
|
||||
date,
|
||||
datetime,
|
||||
)
|
||||
|
||||
|
||||
_TYPE_DEFAULT_VAR: dict[str, str | int | float | bool] = {
|
||||
"String": "",
|
||||
"Int": 0,
|
||||
"Float": 0.0,
|
||||
"Boolean": False,
|
||||
}
|
||||
|
||||
|
||||
def _navigatePath(
|
||||
data: dict,
|
||||
keyPath: list,
|
||||
default=None,
|
||||
):
|
||||
|
||||
d = data
|
||||
for key in keyPath[:-1]:
|
||||
d = d.get(key, {})
|
||||
if not isinstance(d, dict):
|
||||
return default
|
||||
return d.get(keyPath[-1], default)
|
||||
|
||||
def _assignPath(
|
||||
data: dict,
|
||||
keyPath: list,
|
||||
value,
|
||||
) -> None:
|
||||
|
||||
d = data
|
||||
for key in keyPath[:-1]:
|
||||
d = d.setdefault(key, {})
|
||||
d[keyPath[-1]] = value
|
||||
|
||||
def _checkDateFormat(
|
||||
dateStr: str,
|
||||
varName: str = "",
|
||||
) -> None:
|
||||
|
||||
prefix = f"Date 类型变量 '{varName}' 的" if varName else ""
|
||||
try:
|
||||
date.fromisoformat(dateStr)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f"{prefix}值 '{dateStr}' 不是合法的日期格式,"
|
||||
f"应为 YYYY-MM-DD"
|
||||
)
|
||||
|
||||
def _checkTimeFormat(
|
||||
timeStr: str,
|
||||
varName: str = "",
|
||||
) -> None:
|
||||
|
||||
prefix = f"Time 类型变量 '{varName}' 的" if varName else ""
|
||||
try:
|
||||
datetime.strptime(timeStr, "%H:%M")
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f"{prefix}值 '{timeStr}' 不是合法的时间格式,"
|
||||
f"应为 HH:MM"
|
||||
)
|
||||
|
||||
def _checkType(
|
||||
varName: str,
|
||||
varType: str,
|
||||
value,
|
||||
) -> None:
|
||||
|
||||
if varType == "Date":
|
||||
if not isinstance(value, str):
|
||||
raise ValueError(
|
||||
f"Date 类型变量 '{varName}' 只能接受日期字符串,"
|
||||
f"不能接受 {_pyTypeToASType(value)} 类型"
|
||||
)
|
||||
_checkDateFormat(value, varName)
|
||||
return
|
||||
if varType == "Time":
|
||||
if not isinstance(value, str):
|
||||
raise ValueError(
|
||||
f"Time 类型变量 '{varName}' 只能接受时间字符串,"
|
||||
f"不能接受 {_pyTypeToASType(value)} 类型"
|
||||
)
|
||||
_checkTimeFormat(value, varName)
|
||||
return
|
||||
if varType == "Int":
|
||||
if isinstance(value, bool):
|
||||
raise ValueError(
|
||||
f"Int 类型变量 '{varName}' 不能接受 Boolean 类型的值"
|
||||
)
|
||||
if not isinstance(value, int) and not (isinstance(value, float) and value == int(value)):
|
||||
raise ValueError(
|
||||
f"Int 类型变量 '{varName}' 不能接受 {_pyTypeToASType(value)} 类型的值"
|
||||
)
|
||||
return
|
||||
if varType == "Float":
|
||||
if isinstance(value, bool):
|
||||
raise ValueError(
|
||||
f"Float 类型变量 '{varName}' 不能接受 Boolean 类型的值"
|
||||
)
|
||||
if not isinstance(value, (int, float)):
|
||||
raise ValueError(
|
||||
f"Float 类型变量 '{varName}' 不能接受 {_pyTypeToASType(value)} 类型的值"
|
||||
)
|
||||
return
|
||||
if varType == "Boolean":
|
||||
if not isinstance(value, bool):
|
||||
raise ValueError(
|
||||
f"Boolean 类型变量 '{varName}' 不能接受 {_pyTypeToASType(value)} 类型的值"
|
||||
)
|
||||
return
|
||||
if varType == "String":
|
||||
if not isinstance(value, str):
|
||||
raise ValueError(
|
||||
f"String 类型变量 '{varName}' 不能接受 {_pyTypeToASType(value)} 类型的值"
|
||||
)
|
||||
return
|
||||
|
||||
def _pyTypeToASType(
|
||||
value,
|
||||
) -> str:
|
||||
|
||||
if isinstance(value, bool):
|
||||
return "Boolean"
|
||||
if isinstance(value, int):
|
||||
return "Int"
|
||||
if isinstance(value, float):
|
||||
return "Float"
|
||||
if isinstance(value, str):
|
||||
return "String"
|
||||
return "Unknown"
|
||||
|
||||
def _cleanLuaError(
|
||||
rawMsg: str,
|
||||
) -> str:
|
||||
|
||||
msg = rawMsg.replace('[string "<python>"]:', "").strip()
|
||||
stackIdx = msg.find("stack traceback:")
|
||||
if stackIdx != -1:
|
||||
msg = msg[:stackIdx].strip()
|
||||
return msg
|
||||
@@ -1,37 +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 queue
|
||||
|
||||
from base.MsgBase import MsgBase
|
||||
|
||||
|
||||
class LibOperator(MsgBase):
|
||||
"""
|
||||
Base abstract class for library operation.
|
||||
|
||||
This class provides the foundation for library-related operations, inheriting
|
||||
message handling and tracing abilities from MsgBase. It serves as an abstract
|
||||
base class that must be subclassed to implement specific library functionality.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
pass
|
||||
@@ -1,14 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 - 2026 KenanZhu.
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import time
|
||||
import logging
|
||||
import queue
|
||||
import datetime
|
||||
|
||||
from managers.log.LogManager import getLogger
|
||||
|
||||
|
||||
class MsgBase:
|
||||
@@ -29,6 +32,18 @@ class MsgBase:
|
||||
implement queue polling to retrieve and process messages.
|
||||
"""
|
||||
|
||||
class TraceLevel:
|
||||
"""
|
||||
Enum class for trace levels.
|
||||
|
||||
This class provides the trace levels for the logger.
|
||||
"""
|
||||
DEBUG = logging.DEBUG
|
||||
INFO = logging.INFO
|
||||
WARNING = logging.WARNING
|
||||
ERROR = logging.ERROR
|
||||
CRITICAL = logging.CRITICAL
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
@@ -38,7 +53,10 @@ class MsgBase:
|
||||
self._class_name = self.__class__.__name__
|
||||
self._input_queue = input_queue
|
||||
self._output_queue = output_queue
|
||||
|
||||
try:
|
||||
self._logger = getLogger(self._class_name)
|
||||
except RuntimeError:
|
||||
self._logger = None
|
||||
|
||||
def _showMsg(
|
||||
self,
|
||||
@@ -47,15 +65,26 @@ class MsgBase:
|
||||
|
||||
self._output_queue.put(f"[{self._class_name:<15}] >>> : {msg}")
|
||||
|
||||
|
||||
def _showTrace(
|
||||
self,
|
||||
msg: str
|
||||
msg: str,
|
||||
level: int = logging.INFO,
|
||||
no_log: bool = False
|
||||
):
|
||||
|
||||
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
||||
self._output_queue.put(f"{timestamp}-[{self._class_name:<15}] : {msg}")
|
||||
if self._logger and not no_log:
|
||||
self._logger.log(level, msg)
|
||||
|
||||
def _showLog(
|
||||
self,
|
||||
msg: str,
|
||||
level: int = logging.INFO
|
||||
):
|
||||
|
||||
if self._logger:
|
||||
self._logger.log(level, msg)
|
||||
|
||||
def _waitMsg(
|
||||
self,
|
||||
@@ -67,15 +96,3 @@ class MsgBase:
|
||||
return msg
|
||||
except queue.Empty:
|
||||
return None
|
||||
|
||||
|
||||
def _inputMsg(
|
||||
self,
|
||||
timeout: float = 1.0
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
self._input_queue.get(timeout=timeout)
|
||||
return True
|
||||
except queue.Empty:
|
||||
return False
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Base module for the AutoLibrary project.
|
||||
Copyright (c) 2026 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
Here are the classes and modules in this package:
|
||||
- MsgBase: Base class for messages.\
|
||||
- LibOperator: Base class for library operators.
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# -*- 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
|
||||
|
||||
from PySide6.QtCore import QStandardPaths, QDir
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from interfaces.ConfigProvider import CfgKey
|
||||
from managers.config.ConfigManager import instance as configInstance
|
||||
from managers.driver.WebDriverManager import instance as webdriverInstance
|
||||
from managers.log.LogManager import instance as logInstance
|
||||
from managers.theme.ThemeManager import(
|
||||
setActiveStyle,
|
||||
instance as themeInstance
|
||||
)
|
||||
|
||||
|
||||
def _initializeLogManager(
|
||||
) -> bool:
|
||||
|
||||
app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
|
||||
log_dir = os.path.join(app_dir, "logs")
|
||||
if not QDir(log_dir).exists():
|
||||
if not QDir().mkpath(log_dir):
|
||||
return False
|
||||
logInstance(log_dir)
|
||||
return True
|
||||
|
||||
def _initializeConfigManager(
|
||||
) -> bool:
|
||||
|
||||
logger = logInstance().getLogger("AppInitializer")
|
||||
|
||||
app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
|
||||
old_config_dir = os.path.join(app_dir, "config")
|
||||
new_config_dir = os.path.join(app_dir, "configs")
|
||||
if QDir(old_config_dir).exists(): # old config dir exists
|
||||
#we rename it to compatible with new version
|
||||
logger.info("存在旧配置目录 %s,将其重命名为 %s", old_config_dir, new_config_dir)
|
||||
if not QDir().rename(old_config_dir, new_config_dir):
|
||||
logger.error("重命名旧配置目录 %s 到 %s 失败", old_config_dir, new_config_dir)
|
||||
return False
|
||||
elif not QDir(new_config_dir).exists():
|
||||
logger.info("初始化配置目录 %s", new_config_dir)
|
||||
if not QDir().mkpath(new_config_dir):
|
||||
logger.error("创建配置目录 %s 失败", new_config_dir)
|
||||
return False
|
||||
configInstance(new_config_dir)
|
||||
return True
|
||||
|
||||
def _initializeWebDriverManager(
|
||||
) -> bool:
|
||||
|
||||
logger = logInstance().getLogger("AppInitializer")
|
||||
|
||||
app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
|
||||
driver_dir = os.path.join(app_dir, "drivers")
|
||||
if not QDir(driver_dir).exists():
|
||||
logger.info("初始化驱动目录 %s", driver_dir)
|
||||
if not QDir().mkpath(driver_dir):
|
||||
logger.error("创建驱动目录 %s 失败", driver_dir)
|
||||
return False
|
||||
webdriverInstance(driver_dir)
|
||||
return True
|
||||
|
||||
def _initializeAppearance(
|
||||
):
|
||||
|
||||
logger = logInstance().getLogger("AppInitializer")
|
||||
|
||||
app = QApplication.instance()
|
||||
if not app:
|
||||
return
|
||||
cfg = configInstance()
|
||||
saved_style = cfg.get(CfgKey.GLOBAL.APPEARANCE.STYLE, "Fusion")
|
||||
saved_theme = cfg.get(CfgKey.GLOBAL.APPEARANCE.THEME, "system")
|
||||
saved_custom_theme = cfg.get(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "")
|
||||
app.setStyle(saved_style)
|
||||
setActiveStyle(saved_style)
|
||||
if saved_custom_theme:
|
||||
try:
|
||||
themeInstance().applyTheme(saved_custom_theme)
|
||||
except Exception:
|
||||
logger.warning("无法应用自定义主题 '%s',回退到默认外观", saved_custom_theme)
|
||||
themeInstance().clearTheme(saved_theme)
|
||||
return
|
||||
themeInstance().clearTheme(saved_theme)
|
||||
|
||||
def initializeApp(
|
||||
) -> bool:
|
||||
"""
|
||||
Initialize the application components
|
||||
|
||||
Order:
|
||||
LogManager -> ConfigManager -> WebDriverManager -> Appearance
|
||||
"""
|
||||
|
||||
if not _initializeLogManager():
|
||||
return False
|
||||
if not _initializeConfigManager():
|
||||
return False
|
||||
if not _initializeWebDriverManager():
|
||||
return False
|
||||
_initializeAppearance()
|
||||
return True
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- 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.
|
||||
"""
|
||||
@@ -1,31 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 - 2026 KenanZhu.
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import sys
|
||||
import platform
|
||||
|
||||
from PySide6.QtGui import (
|
||||
QIcon
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QApplication
|
||||
)
|
||||
from PySide6.QtCore import (
|
||||
QTimer, Qt
|
||||
Qt,
|
||||
QTimer
|
||||
)
|
||||
from PySide6.QtGui import QIcon
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QDialog,
|
||||
QTabWidget,
|
||||
QTextBrowser
|
||||
)
|
||||
|
||||
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.Ui_ALAboutDialog import Ui_ALAboutDialog
|
||||
|
||||
from gui import AutoLibraryResource
|
||||
from gui.resources.ui.Ui_ALAboutDialog import Ui_ALAboutDialog
|
||||
from gui.resources import ALResource
|
||||
|
||||
|
||||
class ALAboutDialog(QDialog, Ui_ALAboutDialog):
|
||||
@@ -40,16 +43,28 @@ class ALAboutDialog(QDialog, Ui_ALAboutDialog):
|
||||
self.modifyUi()
|
||||
self.connectSignals()
|
||||
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.LogoIconLabel.setPixmap(QIcon(":/res/icon/icons/AutoLibrary_32x32.ico").pixmap(48, 48))
|
||||
info_text = self.generateAboutText()
|
||||
self.AboutInfoEdit.setHtml(info_text)
|
||||
self.AboutInfoEdit.setTextInteractionFlags(Qt.TextBrowserInteraction)
|
||||
|
||||
self.LogoIconLabel.setPixmap(QIcon(":/res/icons/AutoLibrary_Logo_64.svg").pixmap(48, 48))
|
||||
self.TabWidget = QTabWidget()
|
||||
self.TabWidget.setDocumentMode(True)
|
||||
AboutBrowser = QTextBrowser()
|
||||
AboutBrowser.setHtml(self.generateAboutText())
|
||||
AboutBrowser.setOpenExternalLinks(True)
|
||||
AboutBrowser.setLineWrapMode(QTextBrowser.LineWrapMode.NoWrap)
|
||||
AboutBrowser.setTextInteractionFlags(Qt.TextBrowserInteraction)
|
||||
browser_font = AboutBrowser.font()
|
||||
browser_font.setFamilies(["Courier New", "Consolas", "Menlo", "DejaVu Sans Mono", "monospace"])
|
||||
AboutBrowser.setFont(browser_font)
|
||||
self.TabWidget.addTab(AboutBrowser, "关于")
|
||||
LicenseBrowser = QTextBrowser()
|
||||
LicenseBrowser.setHtml(self.generateLicenseText())
|
||||
LicenseBrowser.setOpenExternalLinks(True)
|
||||
LicenseBrowser.setTextInteractionFlags(Qt.TextBrowserInteraction)
|
||||
self.TabWidget.addTab(LicenseBrowser, "许可证")
|
||||
self.AboutInfoLayout.addWidget(self.TabWidget)
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
@@ -57,39 +72,61 @@ class ALAboutDialog(QDialog, Ui_ALAboutDialog):
|
||||
|
||||
self.CopyButton.clicked.connect(self.copyAboutInfo)
|
||||
|
||||
|
||||
def generateAboutText(
|
||||
self
|
||||
):
|
||||
) -> str:
|
||||
|
||||
os_info = self.getOSInfo()
|
||||
run_on = f"{os_info['system']} {os_info['version']} {os_info['architecture']}"
|
||||
selenium_ver = self.getSeleniumVersion()
|
||||
about_text = f"""
|
||||
<h4>Version Information:</h4>
|
||||
Version: {AL_VERSION}<br>
|
||||
<b style="font-size:14px;">VERSION: {AL_VERSION}</b><br>
|
||||
Commit SHA: {AL_COMMIT_SHA}<br>
|
||||
Commit date: {AL_COMMIT_DATE}<br>
|
||||
Build date: {AL_BUILD_DATE}<br>
|
||||
Python version: {platform.python_version()}<br>
|
||||
Qt version: {self.getQtVersion()}<br>
|
||||
|
||||
<h4>System Information:</h4>
|
||||
<br>
|
||||
<b style="font-size:14px;">SYSTEM</b><br>
|
||||
Running on: {run_on}<br>
|
||||
Processor: {platform.processor()}<br>
|
||||
Operating system: {os_info['system']}<br>
|
||||
System version: {os_info['version']}<br>
|
||||
System architecture: {os_info['architecture']}<br>
|
||||
|
||||
<h4>Project Information:</h4>
|
||||
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 website: <a href="https://www.autolibrary.cv/" style="text-decoration: none;">https://www.autolibrary.cv/</a><br>
|
||||
|
||||
<h4>Author Information:</h4>
|
||||
Developer: KenanZhu<br>
|
||||
Contact: nanoki_zh@163.com<br>
|
||||
GitHub: <a href="https://www.github.com/KenanZhu" style="text-decoration: none;">https://www.github.com/KenanZhu</a><br>
|
||||
<br>
|
||||
<b style="font-size:14px;">DEPENDENCIES</b><br>
|
||||
Python: {platform.python_version()}<br>
|
||||
Qt(PySide6): {self.getQtVersion()}<br>
|
||||
Selenium: {selenium_ver}<br>
|
||||
<br>
|
||||
<b style="font-size:14px;">PROJECT</b><br>
|
||||
Website: <a href="https://www.autolibrary.kenanzhu.com" style="text-decoration:none;">https://www.autolibrary.kenanzhu.com</a><br>
|
||||
Repository: <a href="https://www.github.com/KenanZhu/AutoLibrary" style="text-decoration:none;">https://www.github.com/KenanZhu/AutoLibrary</a><br>
|
||||
<br>
|
||||
<b style="font-size:14px;">AUTHOR</b><br>
|
||||
Developer/Maintainer: KenanZhu<br>
|
||||
Contact: <a href="mailto:nanoki_zh@163.com">nanoki_zh@163.com</a><br>
|
||||
GitHub: <a href="https://www.github.com/KenanZhu" style="text-decoration:none;">https://www.github.com/KenanZhu</a><br>
|
||||
"""
|
||||
return about_text
|
||||
|
||||
def generateLicenseText(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
return """
|
||||
<b>MIT License</b>
|
||||
<p>Copyright © 2025 - 2026 KenanZhu</p>
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:</p>
|
||||
<p>The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.</p>
|
||||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.</p>"""
|
||||
|
||||
def getOSInfo(
|
||||
self
|
||||
@@ -98,7 +135,6 @@ GitHub: <a href="https://www.github.com/KenanZhu" style="text-decoration: none;"
|
||||
system = platform.system()
|
||||
version = platform.version()
|
||||
architecture = platform.architecture()[0]
|
||||
|
||||
if system == "Windows":
|
||||
try:
|
||||
version = platform.win32_ver()[1]
|
||||
@@ -115,14 +151,12 @@ GitHub: <a href="https://www.github.com/KenanZhu" style="text-decoration: none;"
|
||||
version = f"{distro.name()} {distro.version()}"
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return {
|
||||
'system': system,
|
||||
'version': version,
|
||||
'architecture': architecture
|
||||
}
|
||||
|
||||
|
||||
def getQtVersion(
|
||||
self
|
||||
):
|
||||
@@ -133,14 +167,23 @@ GitHub: <a href="https://www.github.com/KenanZhu" style="text-decoration: none;"
|
||||
except:
|
||||
return "Unknown"
|
||||
|
||||
def getSeleniumVersion(
|
||||
self
|
||||
):
|
||||
|
||||
try:
|
||||
import selenium
|
||||
return selenium.__version__
|
||||
except Exception:
|
||||
return "Unknown"
|
||||
|
||||
def copyAboutInfo(
|
||||
self
|
||||
):
|
||||
|
||||
about_text = self.AboutInfoEdit.toPlainText()
|
||||
clipboard = QApplication.clipboard()
|
||||
clipboard.setText(about_text)
|
||||
about_text = self.TabWidget.currentWidget().toPlainText()
|
||||
Clipboard = QApplication.clipboard()
|
||||
Clipboard.setText(about_text)
|
||||
original_text = self.CopyButton.text()
|
||||
self.CopyButton.setText("已复制")
|
||||
QTimer.singleShot(2000, lambda: self.CopyButton.setText(original_text))
|
||||
QTimer.singleShot(2000, lambda: self.CopyButton.setText(original_text))
|
||||
|
||||
@@ -1,143 +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 uuid
|
||||
|
||||
from enum import Enum
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Slot, QDateTime
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QLabel, QDialog, QWidget, QSpinBox,
|
||||
QHBoxLayout, QGridLayout, QDateTimeEdit
|
||||
)
|
||||
|
||||
from gui.Ui_ALAddTimerTaskDialog import Ui_ALAddTimerTaskDialog
|
||||
|
||||
|
||||
class TimerTaskStatus(Enum):
|
||||
|
||||
PENDING = "等待中"
|
||||
READY = "已就绪"
|
||||
RUNNING = "执行中"
|
||||
EXECUTED = "已执行"
|
||||
ERROR = "执行失败"
|
||||
OUTDATED = "已过期"
|
||||
|
||||
|
||||
class ALAddTimerTaskWidget(QDialog, Ui_ALAddTimerTaskDialog):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self.setupUi(self)
|
||||
self.connectSignals()
|
||||
self.modifyUi()
|
||||
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.TimerTypeComboBox.setCurrentIndex(0)
|
||||
self.SpecificTimerWidget = QWidget()
|
||||
self.SpecificTimerLayout = QHBoxLayout(self.SpecificTimerWidget)
|
||||
self.SpecificTimerLayout.addWidget(QLabel("定时时间:"))
|
||||
self.SpecificDateTimeEdit = QDateTimeEdit()
|
||||
self.SpecificDateTimeEdit.setCalendarPopup(True)
|
||||
self.SpecificDateTimeEdit.setDisplayFormat("yyyy-MM-dd HH:mm:ss")
|
||||
self.SpecificDateTimeEdit.setMinimumDateTime(QDateTime.currentDateTime())
|
||||
self.SpecificDateTimeEdit.setDateTime(QDateTime.currentDateTime().addSecs(60))
|
||||
self.SpecificTimerLayout.addWidget(self.SpecificDateTimeEdit)
|
||||
self.TimerConfigLayout.addWidget(self.SpecificTimerWidget)
|
||||
|
||||
self.RelativeTimerWidget = QWidget()
|
||||
self.RelativeTimerLayout = QGridLayout(self.RelativeTimerWidget)
|
||||
self.RelativeTimerLayout.addWidget(QLabel("相对时间:"), 0, 0)
|
||||
self.RelativeDaySpinBox = QSpinBox()
|
||||
self.RelativeDaySpinBox.setMinimum(0)
|
||||
self.RelativeDaySpinBox.setMaximum(365)
|
||||
self.RelativeDaySpinBox.setSuffix("天")
|
||||
self.RelativeTimerLayout.addWidget(self.RelativeDaySpinBox, 1, 0)
|
||||
self.RelativeHourSpinBox = QSpinBox()
|
||||
self.RelativeHourSpinBox.setMinimum(0)
|
||||
self.RelativeHourSpinBox.setMaximum(23)
|
||||
self.RelativeHourSpinBox.setSuffix("时")
|
||||
self.RelativeTimerLayout.addWidget(self.RelativeHourSpinBox, 1, 1)
|
||||
self.RelativeMinuteSpinBox = QSpinBox()
|
||||
self.RelativeMinuteSpinBox.setMinimum(0)
|
||||
self.RelativeMinuteSpinBox.setMaximum(59)
|
||||
self.RelativeMinuteSpinBox.setSuffix("分")
|
||||
self.RelativeTimerLayout.addWidget(self.RelativeMinuteSpinBox, 1, 2)
|
||||
self.RelativeSecondSpinBox = QSpinBox()
|
||||
self.RelativeSecondSpinBox.setMinimum(0)
|
||||
self.RelativeSecondSpinBox.setMaximum(59)
|
||||
self.RelativeSecondSpinBox.setSuffix("秒")
|
||||
self.RelativeTimerLayout.addWidget(self.RelativeSecondSpinBox, 1, 3)
|
||||
self.TimerConfigLayout.addWidget(self.RelativeTimerWidget)
|
||||
self.RelativeTimerWidget.setVisible(False)
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.CancelButton.clicked.connect(self.reject)
|
||||
self.ConfirmButton.clicked.connect(self.accept)
|
||||
self.TimerTypeComboBox.currentIndexChanged.connect(self.onTimerTypeComboBoxIndexChanged)
|
||||
|
||||
|
||||
def getTimerTask(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
added_time = datetime.now()
|
||||
if not self.TaskNameLineEdit.text():
|
||||
name = f"未命名任务-{added_time.strftime("%Y%m%d%H%M%S")}"
|
||||
else:
|
||||
name = self.TaskNameLineEdit.text()
|
||||
timer_type_index = self.TimerTypeComboBox.currentIndex()
|
||||
silent = not self.ShowBeforeRunRadioButton.isChecked()
|
||||
if timer_type_index == 0:
|
||||
execute_time = self.SpecificDateTimeEdit.dateTime()
|
||||
tmp_time_str = execute_time.toString("yyyy-MM-dd HH:mm:ss")
|
||||
execute_time = datetime.strptime(tmp_time_str, "%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
execute_time = datetime.now() + timedelta(
|
||||
days = self.RelativeDaySpinBox.value(),
|
||||
hours = self.RelativeHourSpinBox.value(),
|
||||
minutes = self.RelativeMinuteSpinBox.value(),
|
||||
seconds = self.RelativeSecondSpinBox.value()
|
||||
)
|
||||
return {
|
||||
"name": name,
|
||||
"task_uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}",
|
||||
"time_type": self.TimerTypeComboBox.currentText(),
|
||||
"execute_time": execute_time,
|
||||
"silent": silent,
|
||||
"add_time": added_time,
|
||||
"status": TimerTaskStatus.PENDING,
|
||||
"executed": False
|
||||
}
|
||||
|
||||
|
||||
@Slot(int)
|
||||
def onTimerTypeComboBoxIndexChanged(
|
||||
self,
|
||||
index: int
|
||||
):
|
||||
|
||||
self.SpecificTimerWidget.setVisible(index == 0)
|
||||
self.RelativeTimerWidget.setVisible(index == 1)
|
||||
@@ -1,249 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ALAddTimerTaskDialog</class>
|
||||
<widget class="QDialog" name="ALAddTimerTaskDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>300</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>300</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>500</width>
|
||||
<height>300</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>添加定时任务 - AutoLibrary</string>
|
||||
</property>
|
||||
<property name="sizeGripEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="ALAddTimerTaskLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="TaskNameLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="TaskNameLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>60</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="QLineEdit" name="TaskNameLineEdit"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="TimerConfigGroupBox">
|
||||
<property name="title">
|
||||
<string>定时设置</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="TimerConfigLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="TimerTypeSelectLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="TimerTypeLabel">
|
||||
<property name="text">
|
||||
<string>定时类型:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="TimerTypeComboBox">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>特定时间</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>相对时间</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="TaskConfigGroupBox">
|
||||
<property name="title">
|
||||
<string>运行设置</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="TaskConfigLayout">
|
||||
<property name="leftMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QRadioButton" name="SilentlyRunRadioButton">
|
||||
<property name="text">
|
||||
<string>静默运行</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="autoRepeat">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="autoExclusive">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QRadioButton" name="ShowBeforeRunRadioButton">
|
||||
<property name="text">
|
||||
<string>运行前提示</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="ControLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QFrame" name="ControlSpaceFrame">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Shadow::Plain</enum>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="CancelButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>取消</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="ConfirmButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>添加</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,684 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2026 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
from copy import deepcopy
|
||||
|
||||
import qtawesome as qta
|
||||
|
||||
from PySide6.QtCore import (
|
||||
QDate,
|
||||
QSize,
|
||||
Qt,
|
||||
QTime,
|
||||
QTimer,
|
||||
Slot
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QColor,
|
||||
QFont,
|
||||
QSyntaxHighlighter,
|
||||
QTextCharFormat,
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QComboBox,
|
||||
QDateEdit,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QDoubleSpinBox,
|
||||
QFormLayout,
|
||||
QFrame,
|
||||
QGridLayout,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QHeaderView,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QMessageBox,
|
||||
QPlainTextEdit,
|
||||
QPushButton,
|
||||
QSpinBox,
|
||||
QSplitter,
|
||||
QStyle,
|
||||
QTabWidget,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QTimeEdit,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from autoscript import (
|
||||
createAllVariablesTable,
|
||||
createMockTargetData,
|
||||
createTargetVarDefs,
|
||||
createEngine,
|
||||
)
|
||||
|
||||
|
||||
class ALScriptHighlighter(QSyntaxHighlighter):
|
||||
"""
|
||||
Syntax highlighter for Lua-based AutoScript.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self._rules = []
|
||||
|
||||
keyword_fmt = QTextCharFormat()
|
||||
keyword_fmt.setForeground(QColor("#569CD6"))
|
||||
keyword_fmt.setFontWeight(QFont.Weight.Bold)
|
||||
for kw in [
|
||||
"if", "elseif", "else", "end", "then",
|
||||
"and", "or", "not",
|
||||
"local", "function", "return", "nil",
|
||||
]:
|
||||
self._rules.append((r"\b" + kw + r"\b", keyword_fmt))
|
||||
bool_fmt = QTextCharFormat()
|
||||
bool_fmt.setForeground(QColor("#4FC1FF"))
|
||||
bool_fmt.setFontWeight(QFont.Weight.Bold)
|
||||
self._rules.append((r"\btrue\b", bool_fmt))
|
||||
self._rules.append((r"\bfalse\b", bool_fmt))
|
||||
cmp_fmt = QTextCharFormat()
|
||||
cmp_fmt.setForeground(QColor("#C586C0"))
|
||||
cmp_fmt.setFontWeight(QFont.Weight.Normal)
|
||||
for op in [r"==", r"~=", r">=", r"<=", r">", r"<"]:
|
||||
self._rules.append((op, cmp_fmt))
|
||||
arith_fmt = QTextCharFormat()
|
||||
arith_fmt.setForeground(QColor("#C586C0"))
|
||||
arith_fmt.setFontWeight(QFont.Weight.Normal)
|
||||
for op in [r"\+", r"-", r"\*", r"/", r"\.\."]:
|
||||
self._rules.append((op, arith_fmt))
|
||||
func_fmt = QTextCharFormat()
|
||||
func_fmt.setForeground(QColor("#DCDCAA"))
|
||||
func_fmt.setFontWeight(QFont.Weight.Normal)
|
||||
for fn in [ "time", "date", "datenow", "timenow", "dateadd", "timeadd"]:
|
||||
self._rules.append((r"\b" + fn + r"\b", func_fmt))
|
||||
var_fmt = QTextCharFormat()
|
||||
var_fmt.setForeground(QColor("#9CDCFE"))
|
||||
var_fmt.setFontWeight(QFont.Weight.Normal)
|
||||
var_names = [name for _, (name, _) in createAllVariablesTable().items()]
|
||||
for var in var_names:
|
||||
self._rules.append((r"\b" + var + r"\b", var_fmt))
|
||||
st_fmt = QTextCharFormat()
|
||||
st_fmt.setForeground(QColor("#CE9178"))
|
||||
st_fmt.setFontWeight(QFont.Weight.Normal)
|
||||
self._rules.append((r'"[^"]*"', st_fmt))
|
||||
self._rules.append((r"'[^']*'", st_fmt))
|
||||
num_fmt = QTextCharFormat()
|
||||
num_fmt.setForeground(QColor("#B5CEA8"))
|
||||
num_fmt.setFontWeight(QFont.Weight.Normal)
|
||||
self._rules.append((r"\b\d+(?:\.\d+)?\b", num_fmt))
|
||||
comment_fmt = QTextCharFormat()
|
||||
comment_fmt.setForeground(QColor("#6A9955"))
|
||||
comment_fmt.setFontItalic(True)
|
||||
self._rules.append((r"--[^\n]*", comment_fmt))
|
||||
|
||||
def highlightBlock(
|
||||
self,
|
||||
text
|
||||
):
|
||||
|
||||
import re
|
||||
for pattern, fmt in self._rules:
|
||||
for match in re.finditer(pattern, text, re.IGNORECASE):
|
||||
start = match.start()
|
||||
length = match.end() - match.start()
|
||||
self.setFormat(start, length, fmt)
|
||||
|
||||
|
||||
class _DebugResultDialog(QDialog):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
changes: list,
|
||||
parent = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("调试运行结果 - AutoLibrary")
|
||||
self.setMinimumSize(600, 200)
|
||||
DbgLayout = QVBoxLayout(self)
|
||||
DbgTable = QTableWidget(len(changes), 3)
|
||||
DbgTable.setHorizontalHeaderLabels(["目标变量", "原始数据", "运行后数据"])
|
||||
DbgTable.horizontalHeader().setStretchLastSection(True)
|
||||
DbgTable.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||
DbgTable.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
||||
for row, (display_name, name, var_type, before_val, after_val) in enumerate(changes):
|
||||
label = f"{display_name}: {name}({var_type})"
|
||||
DbgTable.setItem(row, 0, QTableWidgetItem(label))
|
||||
DbgTable.setItem(row, 1, QTableWidgetItem(str(before_val)))
|
||||
DbgTable.setItem(row, 2, QTableWidgetItem(str(after_val)))
|
||||
DbgLayout.addWidget(DbgTable)
|
||||
DbgBtnBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
|
||||
DbgBtnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
|
||||
DbgBtnBox.accepted.connect(self.accept)
|
||||
DbgLayout.addWidget(DbgBtnBox)
|
||||
|
||||
|
||||
class _TabToSpacesEditor(QPlainTextEdit):
|
||||
|
||||
def keyPressEvent(
|
||||
self,
|
||||
event
|
||||
):
|
||||
|
||||
if event.key() == Qt.Key.Key_Tab:
|
||||
self.insertPlainText(" ")
|
||||
return
|
||||
super().keyPressEvent(event)
|
||||
|
||||
|
||||
class ALAutoScriptEditDialog(QDialog):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None,
|
||||
script: str = "",
|
||||
mockData: dict = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self._font_size = 21
|
||||
self._mock_widgets = {}
|
||||
|
||||
self.setupUi()
|
||||
self.connectSignals()
|
||||
self.TextEdit.setPlainText(script)
|
||||
self._high_lighter = ALScriptHighlighter(
|
||||
self.TextEdit.document()
|
||||
)
|
||||
if mockData:
|
||||
self.setMockData(mockData)
|
||||
|
||||
def setupUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.setWindowTitle("AutoScript 编辑 - AutoLibrary")
|
||||
self.setMinimumSize(660, 600)
|
||||
Layout = QVBoxLayout(self)
|
||||
Layout.setSpacing(3)
|
||||
Layout.setContentsMargins(3, 3, 3, 3)
|
||||
ToolbarLayout = QHBoxLayout()
|
||||
self.ZoomInBtn = QPushButton("")
|
||||
self.ZoomInBtn.setIcon(qta.icon("fa6s.plus", color=self._iconColor()))
|
||||
self.ZoomInBtn.setIconSize(QSize(14, 14))
|
||||
self.ZoomInBtn.setFixedSize(25, 25)
|
||||
self.ZoomOutBtn = QPushButton("")
|
||||
self.ZoomOutBtn.setIcon(qta.icon("fa6s.minus", color=self._iconColor()))
|
||||
self.ZoomOutBtn.setIconSize(QSize(14, 14))
|
||||
self.ZoomOutBtn.setFixedSize(25, 25)
|
||||
self.ZoomResetBtn = QPushButton("")
|
||||
self.ZoomResetBtn.setIcon(qta.icon("fa6s.rotate-left", color=self._iconColor()))
|
||||
self.ZoomResetBtn.setIconSize(QSize(14, 14))
|
||||
self.ZoomResetBtn.setFixedSize(25, 25)
|
||||
self.ZoomResetBtn.setToolTip("重置缩放")
|
||||
self.ZoomLabel = QLabel(f"{self._font_size}px")
|
||||
self.ZoomLabel.setFixedHeight(25)
|
||||
self.OrchBtn = QPushButton("编排")
|
||||
self.OrchBtn.setFixedSize(80, 25)
|
||||
self.OrchBtn.setToolTip("可视化生成 AutoScript 代码并插入到光标位置")
|
||||
ToolbarLayout.addWidget(self.OrchBtn)
|
||||
self.DebugBtn = QPushButton("▶ 调试运行")
|
||||
self.DebugBtn.setFixedSize(80, 25)
|
||||
self.DebugBtn.setToolTip("使用右侧模拟数据执行脚本,查看目标变量变化")
|
||||
ToolbarLayout.addWidget(self.DebugBtn)
|
||||
Sep = QFrame()
|
||||
Sep.setFrameShape(QFrame.Shape.VLine)
|
||||
Sep.setFrameShadow(QFrame.Shadow.Sunken)
|
||||
Sep.setFixedWidth(1)
|
||||
ToolbarLayout.addWidget(Sep)
|
||||
ToolbarLayout.addWidget(self.ZoomInBtn)
|
||||
ToolbarLayout.addWidget(self.ZoomOutBtn)
|
||||
ToolbarLayout.addWidget(self.ZoomResetBtn)
|
||||
ToolbarLayout.addWidget(self.ZoomLabel)
|
||||
ToolbarLayout.addStretch()
|
||||
self.CopyBtn = QPushButton("")
|
||||
self.CopyBtn.setIcon(qta.icon("fa6s.copy", color=self._iconColor()))
|
||||
self.CopyBtn.setIconSize(QSize(14, 14))
|
||||
self.CopyBtn.setFixedSize(25, 25)
|
||||
self.CopyBtn.setToolTip("复制脚本")
|
||||
ToolbarLayout.addWidget(self.CopyBtn)
|
||||
Layout.addLayout(ToolbarLayout)
|
||||
self.TextEdit = _TabToSpacesEditor(self)
|
||||
self.TextEdit.setTabStopDistance(40)
|
||||
self.TextEdit.setLineWrapMode(
|
||||
QPlainTextEdit.LineWrapMode.NoWrap
|
||||
)
|
||||
self.TextEdit.setStyleSheet(
|
||||
"QPlainTextEdit {"
|
||||
" font-family: 'Courier New', 'Consolas', monospace;"
|
||||
f" font-size: {self._font_size}px;"
|
||||
"}"
|
||||
)
|
||||
Layout.addWidget(self.TextEdit)
|
||||
self.createButtonPanel(Layout)
|
||||
self.BtnBox = QDialogButtonBox(
|
||||
QDialogButtonBox.StandardButton.Ok |
|
||||
QDialogButtonBox.StandardButton.Cancel
|
||||
)
|
||||
self.BtnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
|
||||
self.BtnBox.button(QDialogButtonBox.StandardButton.Ok).setFixedSize(80, 25)
|
||||
self.BtnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
|
||||
self.BtnBox.button(QDialogButtonBox.StandardButton.Cancel).setFixedSize(80, 25)
|
||||
Layout.addWidget(self.BtnBox)
|
||||
|
||||
def createButtonPanel(
|
||||
self,
|
||||
ParentLayout
|
||||
):
|
||||
|
||||
Splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||
TabWidget = QTabWidget()
|
||||
TabWidget.setMaximumHeight(150)
|
||||
BasicWidget = QWidget()
|
||||
BasicLayout = QGridLayout(BasicWidget)
|
||||
BasicLayout.setSpacing(4)
|
||||
BasicLayout.setContentsMargins(4, 4, 4, 4)
|
||||
BasicLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||
control_buttons = [
|
||||
("如果 (if...)", "if then\n \nend"),
|
||||
("再如果 (elseif...)", "elseif then\n "),
|
||||
("否则 (else)", "else"),
|
||||
("结束 (end)", "end"),
|
||||
("跳过 (pass)", "-- pass"),
|
||||
]
|
||||
self.addButtonsToGrid(BasicLayout, control_buttons, 0, 0, 3)
|
||||
assign_buttons = [
|
||||
("赋值 (=)", " = "),
|
||||
]
|
||||
self.addButtonsToGrid(BasicLayout, assign_buttons, 1, 2, 3)
|
||||
TabWidget.addTab(BasicWidget, "基本语法")
|
||||
OperatorWidget = QWidget()
|
||||
OperatorLayout = QGridLayout(OperatorWidget)
|
||||
OperatorLayout.setSpacing(4)
|
||||
OperatorLayout.setContentsMargins(4, 4, 4, 4)
|
||||
OperatorLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||
arithmetic_buttons = [
|
||||
("加 (+)", " + "),
|
||||
("减 (-)", " - "),
|
||||
]
|
||||
self.addButtonsToGrid(OperatorLayout, arithmetic_buttons, 0, 0, 3)
|
||||
compare_buttons = [
|
||||
("等于 (==)", " == "),
|
||||
("不等于 (~=)", " ~= "),
|
||||
("大于 (>)", " > "),
|
||||
("小于 (<)", " < "),
|
||||
("大于等于 (>=)", " >= "),
|
||||
("小于等于 (<=)", " <= "),
|
||||
]
|
||||
self.addButtonsToGrid(OperatorLayout, compare_buttons, 1, 0, 3)
|
||||
logic_buttons = [
|
||||
("且 (and)", " and "),
|
||||
("或 (or)", " or "),
|
||||
]
|
||||
self.addButtonsToGrid(OperatorLayout, logic_buttons, 2, 0, 3)
|
||||
TabWidget.addTab(OperatorWidget, "运算符")
|
||||
LiteralWidget = QWidget()
|
||||
LiteralLayout = QGridLayout(LiteralWidget)
|
||||
LiteralLayout.setSpacing(4)
|
||||
LiteralLayout.setContentsMargins(4, 4, 4, 4)
|
||||
LiteralLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||
bool_buttons = [
|
||||
("真 (true)", "true"),
|
||||
("假 (false)", "false"),
|
||||
]
|
||||
self.addButtonsToGrid(LiteralLayout, bool_buttons, 0, 0, 3)
|
||||
date_time_buttons = [
|
||||
("日期", 'date(2026, 1, 1)'),
|
||||
("时间", 'time(0, 0)'),
|
||||
]
|
||||
self.addButtonsToGrid(LiteralLayout, date_time_buttons, 1, 0, 3)
|
||||
hint_buttons = [
|
||||
("字符串", '"请输入文本"'),
|
||||
("数字", "123"),
|
||||
("注释", "-- 请输入注释"),
|
||||
]
|
||||
self.addButtonsToGrid(LiteralLayout, hint_buttons, 2, 0, 3)
|
||||
TabWidget.addTab(LiteralWidget, "字面量")
|
||||
VarWidget = QWidget()
|
||||
VarLayout = QGridLayout(VarWidget)
|
||||
VarLayout.setSpacing(4)
|
||||
VarLayout.setContentsMargins(4, 4, 4, 4)
|
||||
VarLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||
var_buttons = [
|
||||
(display_name, name) for display_name, (name, _) in createAllVariablesTable().items()
|
||||
]
|
||||
self.addButtonsToGrid(VarLayout, var_buttons, 0, 0, 3)
|
||||
TabWidget.addTab(VarWidget, "变量")
|
||||
FuncWidget = QWidget()
|
||||
FuncLayout = QGridLayout(FuncWidget)
|
||||
FuncLayout.setSpacing(4)
|
||||
FuncLayout.setContentsMargins(4, 4, 4, 4)
|
||||
FuncLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||
func_buttons = [
|
||||
("datenow()", "datenow()", "返回当前日期的 Unix 时间戳"),
|
||||
("timenow()", "timenow()", "返回当前时间在一天中的分钟数"),
|
||||
("dateadd(day, n)", "dateadd(, )", "日期偏移: dateadd(日期时间戳, 天数)"),
|
||||
("timeadd(time, n)", "timeadd(, )", "时间偏移: timeadd(分钟数, 分钟数)"),
|
||||
]
|
||||
for i, (text, template, tooltip) in enumerate(func_buttons):
|
||||
Btn = QPushButton(text)
|
||||
Btn.setProperty("template", template)
|
||||
Btn.clicked.connect(self.insertTemplate)
|
||||
Btn.setFixedWidth(100)
|
||||
Btn.setFixedHeight(25)
|
||||
Btn.setToolTip(tooltip)
|
||||
FuncLayout.addWidget(Btn, i // 2, i % 2)
|
||||
TabWidget.addTab(FuncWidget, "工具函数")
|
||||
MockPanel = self.createMockPanel()
|
||||
MockPanel.setMinimumWidth(260)
|
||||
Splitter.addWidget(TabWidget)
|
||||
Splitter.addWidget(MockPanel)
|
||||
Splitter.setStretchFactor(0, 1)
|
||||
Splitter.setStretchFactor(1, 1)
|
||||
Splitter.setSizes([530, 530])
|
||||
ParentLayout.addWidget(Splitter)
|
||||
|
||||
def addButtonsToGrid(
|
||||
self,
|
||||
grid_layout,
|
||||
buttons,
|
||||
start_row,
|
||||
start_col,
|
||||
max_columns
|
||||
):
|
||||
|
||||
col = start_col
|
||||
row = start_row
|
||||
|
||||
for btn_text, template in buttons:
|
||||
Btn = QPushButton(btn_text)
|
||||
Btn.setProperty("template", template)
|
||||
Btn.clicked.connect(self.insertTemplate)
|
||||
Btn.setFixedWidth(100)
|
||||
Btn.setFixedHeight(25)
|
||||
Btn.setToolTip(f"插入: {template}")
|
||||
grid_layout.addWidget(Btn, row, col)
|
||||
col += 1
|
||||
if col >= start_col + max_columns:
|
||||
col = start_col
|
||||
row += 1
|
||||
|
||||
def createMockPanel(
|
||||
self
|
||||
) -> QGroupBox:
|
||||
|
||||
Group = QGroupBox("模拟目标数据")
|
||||
Form = QFormLayout(Group)
|
||||
Form.setSpacing(4)
|
||||
Form.setContentsMargins(5, 10, 5, 5)
|
||||
self._mock_widgets = {}
|
||||
mock_data = createMockTargetData()
|
||||
for name, var_type, key_path, display_name in createTargetVarDefs():
|
||||
d = mock_data
|
||||
for key in key_path:
|
||||
d = d[key]
|
||||
default = d
|
||||
Widget = self.makeMockInput(var_type, default)
|
||||
Label = QLabel(f"{display_name}: {name}({var_type})")
|
||||
Form.addRow(Label, Widget)
|
||||
self._mock_widgets[name] = (Widget, var_type, key_path)
|
||||
return Group
|
||||
|
||||
def makeMockInput(
|
||||
self,
|
||||
var_type: str,
|
||||
default
|
||||
) -> QWidget:
|
||||
|
||||
if var_type == "String":
|
||||
widget = QLineEdit()
|
||||
widget.setText(str(default))
|
||||
return widget
|
||||
if var_type == "Boolean":
|
||||
widget = QComboBox()
|
||||
widget.addItems(["是", "否"])
|
||||
widget.setCurrentIndex(0 if default else 1)
|
||||
return widget
|
||||
if var_type == "Date":
|
||||
widget = QDateEdit()
|
||||
widget.setCalendarPopup(True)
|
||||
widget.setDisplayFormat("yyyy-MM-dd")
|
||||
widget.setDate(QDate.fromString(str(default), "yyyy-MM-dd"))
|
||||
return widget
|
||||
if var_type == "Time":
|
||||
widget = QTimeEdit()
|
||||
widget.setDisplayFormat("HH:mm")
|
||||
widget.setTime(QTime.fromString(str(default), "HH:mm"))
|
||||
return widget
|
||||
if var_type == "Int":
|
||||
widget = QSpinBox()
|
||||
widget.setMinimum(-999999)
|
||||
widget.setMaximum(999999)
|
||||
widget.setValue(int(default) if default else 0)
|
||||
return widget
|
||||
if var_type == "Float":
|
||||
widget = QDoubleSpinBox()
|
||||
widget.setMinimum(-999999.0)
|
||||
widget.setMaximum(999999.0)
|
||||
widget.setDecimals(2)
|
||||
widget.setValue(float(default) if default else 0.0)
|
||||
return widget
|
||||
widget = QLineEdit()
|
||||
widget.setText(str(default))
|
||||
return widget
|
||||
|
||||
def getMockData(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
data = {}
|
||||
for name, var_type, key_path, _ in createTargetVarDefs():
|
||||
widget, _, _ = self._mock_widgets[name]
|
||||
value = self.getMockValue(widget, var_type)
|
||||
d = data
|
||||
for key in key_path[:-1]:
|
||||
d = d.setdefault(key, {})
|
||||
d[key_path[-1]] = value
|
||||
return data
|
||||
|
||||
def setMockData(
|
||||
self,
|
||||
data: dict
|
||||
):
|
||||
|
||||
if not data:
|
||||
return
|
||||
for name, var_type, key_path, _ in createTargetVarDefs():
|
||||
d = data
|
||||
try:
|
||||
for key in key_path:
|
||||
d = d[key]
|
||||
except (KeyError, TypeError):
|
||||
continue
|
||||
widget, _, _ = self._mock_widgets[name]
|
||||
self.setMockValue(widget, var_type, d)
|
||||
|
||||
def getMockValue(
|
||||
self,
|
||||
widget: QWidget,
|
||||
var_type: str
|
||||
):
|
||||
|
||||
if var_type == "Boolean":
|
||||
return widget.currentIndex() == 0
|
||||
if var_type == "Date":
|
||||
return widget.date().toString("yyyy-MM-dd")
|
||||
if var_type == "Time":
|
||||
return widget.time().toString("HH:mm")
|
||||
if var_type == "Int":
|
||||
return widget.value()
|
||||
if var_type == "Float":
|
||||
return widget.value()
|
||||
return widget.text()
|
||||
|
||||
def setMockValue(
|
||||
self,
|
||||
widget: QWidget,
|
||||
var_type: str,
|
||||
value
|
||||
):
|
||||
|
||||
if var_type == "Boolean":
|
||||
widget.setCurrentIndex(0 if value else 1)
|
||||
elif var_type == "Date":
|
||||
widget.setDate(QDate.fromString(str(value), "yyyy-MM-dd"))
|
||||
elif var_type == "Time":
|
||||
widget.setTime(QTime.fromString(str(value), "HH:mm"))
|
||||
elif var_type == "Int":
|
||||
widget.setValue(int(value))
|
||||
elif var_type == "Float":
|
||||
widget.setValue(float(value))
|
||||
else:
|
||||
widget.setText(str(value))
|
||||
|
||||
def _iconColor(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
return QApplication.instance().palette().color(
|
||||
QApplication.instance().palette().ColorRole.WindowText
|
||||
).name()
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.BtnBox.accepted.connect(self.accept)
|
||||
self.BtnBox.rejected.connect(self.reject)
|
||||
self.OrchBtn.clicked.connect(self.onOpenOrchDialog)
|
||||
self.DebugBtn.clicked.connect(self.onDebugRun)
|
||||
self.ZoomInBtn.clicked.connect(self.onZoomIn)
|
||||
self.ZoomOutBtn.clicked.connect(self.onZoomOut)
|
||||
self.ZoomResetBtn.clicked.connect(self.onZoomReset)
|
||||
self.CopyBtn.clicked.connect(self.onCopy)
|
||||
|
||||
def getScript(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
return self.TextEdit.toPlainText()
|
||||
|
||||
def updateFontSize(
|
||||
self
|
||||
):
|
||||
|
||||
self.TextEdit.setStyleSheet(
|
||||
"QPlainTextEdit {"
|
||||
" font-family: 'Courier New', 'Consolas', monospace;"
|
||||
f" font-size: {self._font_size}px;"
|
||||
"}"
|
||||
)
|
||||
self.ZoomLabel.setText(f"{self._font_size}px")
|
||||
|
||||
@Slot()
|
||||
def insertTemplate(
|
||||
self
|
||||
):
|
||||
|
||||
Btn = self.sender()
|
||||
if not isinstance(Btn, QPushButton):
|
||||
return
|
||||
template = Btn.property("template")
|
||||
if not template:
|
||||
return
|
||||
cursor = self.TextEdit.textCursor()
|
||||
cursor.insertText(template)
|
||||
|
||||
@Slot()
|
||||
def onZoomIn(
|
||||
self
|
||||
):
|
||||
|
||||
self._font_size = min(self._font_size + 2, 40)
|
||||
self.updateFontSize()
|
||||
|
||||
@Slot()
|
||||
def onZoomOut(
|
||||
self
|
||||
):
|
||||
|
||||
self._font_size = max(self._font_size - 2, 8)
|
||||
self.updateFontSize()
|
||||
|
||||
@Slot()
|
||||
def onZoomReset(
|
||||
self
|
||||
):
|
||||
|
||||
self._font_size = 21
|
||||
self.updateFontSize()
|
||||
|
||||
@Slot()
|
||||
def onCopy(
|
||||
self
|
||||
):
|
||||
|
||||
Clipboard = QApplication.clipboard()
|
||||
Clipboard.setText(self.TextEdit.toPlainText())
|
||||
self.CopyBtn.setEnabled(False)
|
||||
QTimer.singleShot(2000, lambda: (
|
||||
self.CopyBtn.setEnabled(True)
|
||||
))
|
||||
|
||||
@Slot()
|
||||
def onOpenOrchDialog(
|
||||
self
|
||||
):
|
||||
|
||||
from gui.ALAutoScriptOrchDialog import ALAutoScriptOrchDialog
|
||||
Dialpg = ALAutoScriptOrchDialog(self)
|
||||
if Dialpg.exec() == QDialog.DialogCode.Accepted:
|
||||
script = Dialpg.getScript()
|
||||
if script:
|
||||
cursor = self.TextEdit.textCursor()
|
||||
cursor.insertText(script)
|
||||
Dialpg.deleteLater()
|
||||
|
||||
@Slot()
|
||||
def onDebugRun(
|
||||
self
|
||||
):
|
||||
|
||||
script = self.TextEdit.toPlainText().strip()
|
||||
if not script:
|
||||
QMessageBox.warning(self, "提示", "脚本内容为空。")
|
||||
return
|
||||
target_data = self.getMockData()
|
||||
before = deepcopy(target_data)
|
||||
try:
|
||||
engine = createEngine()
|
||||
engine.execute(script, target_data)
|
||||
except ValueError as e:
|
||||
QMessageBox.warning(self, "运行错误", str(e))
|
||||
return
|
||||
changes = []
|
||||
for name, var_type, key_path, display_name in createTargetVarDefs():
|
||||
before_val = before
|
||||
after_val = target_data
|
||||
try:
|
||||
for key in key_path:
|
||||
before_val = before_val[key]
|
||||
after_val = after_val[key]
|
||||
except (KeyError, TypeError):
|
||||
continue
|
||||
if before_val != after_val:
|
||||
changes.append((display_name, name, var_type, before_val, after_val))
|
||||
if not changes:
|
||||
QMessageBox.information(self, "调试运行", "目标变量未发生变化。")
|
||||
return
|
||||
Dialpg = _DebugResultDialog(changes, self)
|
||||
Dialpg.exec()
|
||||
Dialpg.deleteLater()
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- 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 ._dialog import ALAutoScriptOrchDialog
|
||||
@@ -0,0 +1,269 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2026 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
"""
|
||||
Conditional block widget for the AutoScript orchestration dialog.
|
||||
"""
|
||||
from PySide6.QtCore import Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from gui.ALAutoScriptOrchDialog._widgets import (
|
||||
ActionStepFrame,
|
||||
ConditionRowFrame,
|
||||
)
|
||||
|
||||
|
||||
class ConditionalBlock(QGroupBox):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
block_index: int,
|
||||
var_mgr = None,
|
||||
parent = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self._block_index = block_index
|
||||
self._var_mgr = var_mgr
|
||||
self._action_widgets = []
|
||||
self._condition_rows = []
|
||||
|
||||
self.setupUi()
|
||||
self.connectSignals()
|
||||
self.addInitialConditionRow()
|
||||
|
||||
def setupUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.setUpdatesEnabled(False)
|
||||
self.setStyleSheet(
|
||||
"QGroupBox { "\
|
||||
"font-weight: bold;"\
|
||||
"border: 1px solid #ccc;"\
|
||||
"margin-top: 5px;"\
|
||||
"padding-top: 5px; "\
|
||||
"}"
|
||||
)
|
||||
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||||
MainLayout = QVBoxLayout(self)
|
||||
MainLayout.setSpacing(6)
|
||||
MainLayout.setContentsMargins(8, 8, 8, 8)
|
||||
HeaderLayout = QHBoxLayout()
|
||||
HeaderLayout.setSpacing(8)
|
||||
self.TypeCombo = QComboBox(self)
|
||||
self.TypeCombo.addItem("IF", "IF")
|
||||
self.TypeCombo.addItem("ELSE IF", "ELSE IF")
|
||||
self.TypeCombo.addItem("ELSE", "ELSE")
|
||||
self.TypeCombo.setFixedHeight(25)
|
||||
if self._block_index == 0:
|
||||
self.TypeCombo.setEnabled(False)
|
||||
HeaderLayout.addWidget(QLabel("类型:", self))
|
||||
HeaderLayout.addWidget(self.TypeCombo)
|
||||
HeaderLayout.addStretch()
|
||||
self.DeleteBlockBtn = QPushButton("删除此块", self)
|
||||
self.DeleteBlockBtn.setStyleSheet("color: red;")
|
||||
self.DeleteBlockBtn.setFixedHeight(25)
|
||||
HeaderLayout.addWidget(self.DeleteBlockBtn)
|
||||
MainLayout.addLayout(HeaderLayout)
|
||||
self.ConditionWidget = QWidget(self)
|
||||
self.ConditionWidget.setSizePolicy(
|
||||
QSizePolicy.Preferred, QSizePolicy.Preferred
|
||||
)
|
||||
CondLayout = QVBoxLayout(self.ConditionWidget)
|
||||
CondLayout.setContentsMargins(4, 4, 4, 4)
|
||||
CondLayout.setSpacing(6)
|
||||
self.CondRowsLayout = QVBoxLayout()
|
||||
self.CondRowsLayout.setSpacing(4)
|
||||
CondLayout.addLayout(self.CondRowsLayout)
|
||||
self.AddCondBtn = QPushButton("+ 添加条件", self.ConditionWidget)
|
||||
self.AddCondBtn.setFixedHeight(25)
|
||||
CondLayout.addWidget(self.AddCondBtn)
|
||||
MainLayout.addWidget(self.ConditionWidget)
|
||||
self.ActionLabel = QLabel("执行步骤:", self)
|
||||
self.ActionLabel.setFixedHeight(25)
|
||||
MainLayout.addWidget(self.ActionLabel)
|
||||
self.ActionsLayout = QVBoxLayout()
|
||||
self.ActionsLayout.setSpacing(4)
|
||||
MainLayout.addLayout(self.ActionsLayout)
|
||||
self.AddActionBtn = QPushButton("+ 添加执行步骤", self)
|
||||
self.AddActionBtn.setFixedHeight(25)
|
||||
MainLayout.addWidget(self.AddActionBtn)
|
||||
self.setUpdatesEnabled(True)
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.TypeCombo.currentIndexChanged.connect(self.onTypeChanged)
|
||||
self.AddCondBtn.clicked.connect(self.addConditionRow)
|
||||
self.AddActionBtn.clicked.connect(self.addActionStep)
|
||||
|
||||
def addInitialConditionRow(
|
||||
self
|
||||
):
|
||||
|
||||
Row = ConditionRowFrame(
|
||||
self._var_mgr, self._block_index,
|
||||
is_first=True, parent=self
|
||||
)
|
||||
self._condition_rows.append(Row)
|
||||
self.CondRowsLayout.addWidget(Row)
|
||||
|
||||
def addConditionRow(
|
||||
self
|
||||
):
|
||||
|
||||
Row = ConditionRowFrame(
|
||||
self._var_mgr, self._block_index,
|
||||
is_first=False, parent=self
|
||||
)
|
||||
Row.DeleteBtn.clicked.connect(lambda: self.removeConditionRow(Row))
|
||||
self._condition_rows.append(Row)
|
||||
self.CondRowsLayout.addWidget(Row)
|
||||
|
||||
def removeConditionRow(
|
||||
self,
|
||||
row: ConditionRowFrame
|
||||
):
|
||||
|
||||
if row in self._condition_rows and len(self._condition_rows) > 1:
|
||||
self._condition_rows.remove(row)
|
||||
self.CondRowsLayout.removeWidget(row)
|
||||
row.hide()
|
||||
row.deleteLater()
|
||||
|
||||
def addActionStep(
|
||||
self
|
||||
):
|
||||
|
||||
Step = ActionStepFrame(self._var_mgr, self._block_index, parent=self)
|
||||
Step.DeleteBtn.clicked.connect(lambda: self.removeActionStep(Step))
|
||||
self._action_widgets.append(Step)
|
||||
self.ActionsLayout.addWidget(Step)
|
||||
|
||||
def removeActionStep(
|
||||
self,
|
||||
step: ActionStepFrame
|
||||
):
|
||||
|
||||
if step in self._action_widgets:
|
||||
self._action_widgets.remove(step)
|
||||
self.ActionsLayout.removeWidget(step)
|
||||
step.hide()
|
||||
step.deleteLater()
|
||||
|
||||
def getBlockType(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
return self.TypeCombo.currentData()
|
||||
|
||||
def getConditionRows(
|
||||
self
|
||||
):
|
||||
|
||||
return list(self._condition_rows)
|
||||
|
||||
def getActionSteps(
|
||||
self
|
||||
):
|
||||
|
||||
return list(self._action_widgets)
|
||||
|
||||
def countActionSteps(
|
||||
self
|
||||
) -> int:
|
||||
|
||||
return len(self._action_widgets)
|
||||
|
||||
def toScript(
|
||||
self
|
||||
) -> list:
|
||||
"""
|
||||
Generate Lua script lines for this conditional block.
|
||||
"""
|
||||
|
||||
block_type = self.getBlockType()
|
||||
lines = []
|
||||
if block_type in ("IF", "ELSE IF"):
|
||||
cond_texts = [
|
||||
r.toScript() for r in self._condition_rows if r.toScript()
|
||||
]
|
||||
if not cond_texts:
|
||||
cond_texts = ["true"]
|
||||
if len(cond_texts) == 1:
|
||||
combined = cond_texts[0]
|
||||
else:
|
||||
parts = []
|
||||
for i, ct in enumerate(cond_texts):
|
||||
if i > 0:
|
||||
logic = self._condition_rows[i].getLogic() or "and"
|
||||
parts.append(f" {logic} ")
|
||||
parts.append(f"({ct})")
|
||||
combined = "".join(parts)
|
||||
if block_type == "IF":
|
||||
lines.append(f"if {combined} then")
|
||||
else:
|
||||
lines.append(f"elseif {combined} then")
|
||||
else:
|
||||
lines.append("else")
|
||||
for step in self._action_widgets:
|
||||
script_line = step.toScript()
|
||||
if script_line:
|
||||
lines.append(script_line)
|
||||
return lines
|
||||
|
||||
def refreshVarCombos(
|
||||
self
|
||||
):
|
||||
|
||||
for row in self._condition_rows:
|
||||
row.refreshVarCombos()
|
||||
for step in self._action_widgets:
|
||||
step.refreshVarCombos()
|
||||
|
||||
def setPrevBlockType(
|
||||
self,
|
||||
prev_type: str | None
|
||||
):
|
||||
|
||||
model = self.TypeCombo.model()
|
||||
if model is None:
|
||||
return
|
||||
for data in ("ELSE IF", "ELSE"):
|
||||
idx = self.TypeCombo.findData(data)
|
||||
if idx < 0:
|
||||
continue
|
||||
item = model.item(idx)
|
||||
should_enable = prev_type != "ELSE"
|
||||
item.setEnabled(should_enable)
|
||||
if prev_type == "ELSE" and self.TypeCombo.currentData() in ("ELSE IF", "ELSE"):
|
||||
self.TypeCombo.setCurrentIndex(0)
|
||||
|
||||
@Slot(int)
|
||||
def onTypeChanged(
|
||||
self,
|
||||
idx: int
|
||||
):
|
||||
|
||||
is_cond = self.TypeCombo.currentData() in ("IF", "ELSE IF")
|
||||
self.ConditionWidget.setVisible(is_cond)
|
||||
self.ActionLabel.setText(
|
||||
"执行步骤:" if is_cond else "ELSE 执行步骤:"
|
||||
)
|
||||
@@ -0,0 +1,164 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2026 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
"""
|
||||
Orchestration dialog for visually composing AutoScript scripts.
|
||||
"""
|
||||
from PySide6.QtCore import Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QFrame,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from gui.ALAutoScriptOrchDialog._helpers import VariableManager
|
||||
from gui.ALAutoScriptOrchDialog._blocks import ConditionalBlock
|
||||
|
||||
|
||||
class ALAutoScriptOrchDialog(QDialog):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self._blocks = []
|
||||
self._var_mgr = VariableManager(self)
|
||||
|
||||
self.setupUi()
|
||||
self.connectSignals()
|
||||
self.addBlock()
|
||||
self.ScrollLayout.addStretch()
|
||||
|
||||
def setupUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.setWindowTitle("AutoScript 指令编排 - AutoLibrary")
|
||||
self.setMinimumSize(640, 600)
|
||||
self.setModal(True)
|
||||
MainLayout = QVBoxLayout(self)
|
||||
Scroll = QScrollArea()
|
||||
Scroll.setWidgetResizable(True)
|
||||
Scroll.setFrameShape(QFrame.NoFrame)
|
||||
ScrollContent = QWidget()
|
||||
self.ScrollLayout = QVBoxLayout(ScrollContent)
|
||||
self.ScrollLayout.setSpacing(5)
|
||||
Scroll.setWidget(ScrollContent)
|
||||
MainLayout.addWidget(Scroll)
|
||||
self.AddBlockBtn = QPushButton("+ 添加判断块")
|
||||
self.AddBlockBtn.setFixedHeight(25)
|
||||
MainLayout.addWidget(self.AddBlockBtn)
|
||||
self.BtnBox = QDialogButtonBox(
|
||||
QDialogButtonBox.StandardButton.Ok |
|
||||
QDialogButtonBox.StandardButton.Cancel
|
||||
)
|
||||
self.BtnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
|
||||
self.BtnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
|
||||
MainLayout.addWidget(self.BtnBox)
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.BtnBox.accepted.connect(self.onAccept)
|
||||
self.BtnBox.rejected.connect(self.reject)
|
||||
self.AddBlockBtn.clicked.connect(self.addBlock)
|
||||
|
||||
def updateBlockTypeRestrictions(
|
||||
self
|
||||
):
|
||||
|
||||
prevType = None
|
||||
for block in self._blocks:
|
||||
block.setPrevBlockType(prevType)
|
||||
prevType = block.getBlockType()
|
||||
|
||||
def addBlock(
|
||||
self
|
||||
):
|
||||
|
||||
Block = ConditionalBlock(
|
||||
len(self._blocks), self._var_mgr, parent=self
|
||||
)
|
||||
Block.DeleteBlockBtn.clicked.connect(lambda: self.removeBlock(Block))
|
||||
Block.TypeCombo.currentIndexChanged.connect(self.updateBlockTypeRestrictions)
|
||||
Block.addActionStep()
|
||||
self._blocks.append(Block)
|
||||
self.updateBlockTypeRestrictions()
|
||||
if self.ScrollLayout.count() > 0:
|
||||
lastItem = self.ScrollLayout.itemAt(
|
||||
self.ScrollLayout.count() - 1
|
||||
)
|
||||
if lastItem and lastItem.spacerItem():
|
||||
self.ScrollLayout.insertWidget(
|
||||
self.ScrollLayout.count() - 1, Block
|
||||
)
|
||||
return
|
||||
self.ScrollLayout.addWidget(Block)
|
||||
|
||||
def removeBlock(
|
||||
self,
|
||||
block: ConditionalBlock
|
||||
):
|
||||
|
||||
if len(self._blocks) <= 1:
|
||||
QMessageBox.information(self, "提示", "至少保留一个判断块。")
|
||||
return
|
||||
if block in self._blocks:
|
||||
self._blocks.remove(block)
|
||||
self.ScrollLayout.removeWidget(block)
|
||||
block.hide()
|
||||
block.deleteLater()
|
||||
for i, blk in enumerate(self._blocks):
|
||||
blk.blockIndex = i
|
||||
if i == 0:
|
||||
blk.TypeCombo.setEnabled(False)
|
||||
blk.TypeCombo.setCurrentIndex(0)
|
||||
else:
|
||||
blk.TypeCombo.setEnabled(True)
|
||||
blk.refreshVarCombos()
|
||||
self.updateBlockTypeRestrictions()
|
||||
|
||||
def getScript(
|
||||
self
|
||||
) -> str:
|
||||
"""
|
||||
Generate the complete Lua script from all blocks.
|
||||
"""
|
||||
|
||||
parts = []
|
||||
prevType = None
|
||||
for block in self._blocks:
|
||||
blockType = block.getBlockType()
|
||||
if blockType == "IF" and prevType is not None:
|
||||
parts.append("end")
|
||||
lines = block.toScript()
|
||||
parts.extend(lines)
|
||||
prevType = blockType
|
||||
if self._blocks and self._blocks[0].getBlockType() == "IF":
|
||||
parts.append("end")
|
||||
return "\n".join(parts)
|
||||
|
||||
@Slot()
|
||||
def onAccept(
|
||||
self
|
||||
):
|
||||
|
||||
script = self.getScript().strip()
|
||||
if not script:
|
||||
QMessageBox.warning(self, "提示", "脚本内容为空,请添加至少一个操作步骤。")
|
||||
return
|
||||
self.accept()
|
||||
@@ -0,0 +1,516 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2026 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
"""
|
||||
Helper utilities and constants for the AutoScript orchestration dialog.
|
||||
"""
|
||||
import re
|
||||
|
||||
from PySide6.QtCore import QObject
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
QDateEdit,
|
||||
QDoubleSpinBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QSizePolicy,
|
||||
QSpinBox,
|
||||
QStackedWidget,
|
||||
QTimeEdit,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from autoscript import createAllVariablesTable
|
||||
|
||||
VARTYPE_INFOS = [
|
||||
# varType, isArithType
|
||||
("String", False),
|
||||
("Int", True),
|
||||
("Float", True),
|
||||
("Boolean", False),
|
||||
("Date", True),
|
||||
("Time", True),
|
||||
]
|
||||
|
||||
|
||||
def getTypeOrder(
|
||||
) -> list:
|
||||
|
||||
return [t for t, _ in VARTYPE_INFOS]
|
||||
|
||||
def getArithType(
|
||||
var_type: str
|
||||
) -> bool:
|
||||
|
||||
for t, a in VARTYPE_INFOS:
|
||||
if t == var_type:
|
||||
return a
|
||||
|
||||
def getPresetVars(
|
||||
) -> list:
|
||||
|
||||
return [
|
||||
{"name": name.upper(), "type": vtype, "display": display}
|
||||
for display, (name, vtype) in createAllVariablesTable().items()
|
||||
]
|
||||
|
||||
|
||||
COMPARE_OPTIONS = [
|
||||
("等于", "=="),
|
||||
("不等于", "~="),
|
||||
("大于", ">"),
|
||||
("小于", "<"),
|
||||
("大于等于", ">="),
|
||||
("小于等于", "<="),
|
||||
]
|
||||
LOGIC_OPTIONS = [
|
||||
("并且 (and)", "and"),
|
||||
("或者 (or)", "or"),
|
||||
]
|
||||
ACTION_OPTIONS = [
|
||||
("设置为", "set"),
|
||||
("增加", "add"),
|
||||
("减少", "sub"),
|
||||
]
|
||||
DATE_OPTIONS = [
|
||||
("前天", "day_before_yesterday"),
|
||||
("昨天", "yesterday"),
|
||||
("今天", "today"),
|
||||
("明天", "tomorrow"),
|
||||
("后天", "day_after_tomorrow")
|
||||
]
|
||||
DATE_OFFSET_OPTIONS = [
|
||||
("天", "days"),
|
||||
("周", "weeks"),
|
||||
# NOTE: "月" and "年" use fixed day counts (30 / 365), not calendar months/years,
|
||||
# because dateadd() works with second-level offsets (n * 86400).
|
||||
("月", "months"),
|
||||
("年", "years"),
|
||||
]
|
||||
|
||||
|
||||
class _DateInputContainer(QWidget):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi()
|
||||
|
||||
def setupUi(
|
||||
self
|
||||
):
|
||||
|
||||
Layout = QHBoxLayout(self)
|
||||
Layout.setContentsMargins(0, 0, 0, 0)
|
||||
Layout.setSpacing(4)
|
||||
self.ModeCombo = QComboBox(self)
|
||||
self.ModeCombo.addItem("相对日期", "relative")
|
||||
self.ModeCombo.addItem("绝对日期", "absolute")
|
||||
self.ModeCombo.setFixedHeight(25)
|
||||
self.Stack = QStackedWidget(self)
|
||||
self.RelCombo = QComboBox(self)
|
||||
for display, data in DATE_OPTIONS:
|
||||
self.RelCombo.addItem(display, data)
|
||||
self.RelCombo.setFixedHeight(25)
|
||||
self.Stack.addWidget(self.RelCombo)
|
||||
self.DateEdit = QDateEdit(self)
|
||||
self.DateEdit.setDisplayFormat("yyyy-MM-dd")
|
||||
self.DateEdit.setCalendarPopup(True)
|
||||
self.DateEdit.setFixedHeight(25)
|
||||
self.Stack.addWidget(self.DateEdit)
|
||||
self.ModeCombo.currentIndexChanged.connect(
|
||||
lambda i: self.Stack.setCurrentIndex(i)
|
||||
)
|
||||
Layout.addWidget(self.ModeCombo)
|
||||
Layout.addWidget(self.Stack)
|
||||
Layout.addStretch()
|
||||
|
||||
def getValue(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
mode = self.ModeCombo.currentData()
|
||||
if mode == "relative":
|
||||
return self.RelCombo.currentText()
|
||||
return self.DateEdit.date().toString("yyyy-MM-dd")
|
||||
|
||||
|
||||
class _TimeInputContainer(QWidget):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self.TimeEdit = QTimeEdit(self)
|
||||
self.TimeEdit.setDisplayFormat("HH:mm")
|
||||
self.TimeEdit.setFixedHeight(25)
|
||||
|
||||
Layout = QHBoxLayout(self)
|
||||
Layout.setContentsMargins(0, 0, 0, 0)
|
||||
Layout.addWidget(self.TimeEdit)
|
||||
|
||||
def getValue(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
return self.TimeEdit.time().toString("HH:mm")
|
||||
|
||||
|
||||
class _DateOffsetContainer(QWidget):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self.SpinBox = QSpinBox(self)
|
||||
self.SpinBox.setRange(0, 99999)
|
||||
self.SpinBox.setFixedHeight(25)
|
||||
self.UnitCombo = QComboBox(self)
|
||||
for display, data in DATE_OFFSET_OPTIONS:
|
||||
self.UnitCombo.addItem(display, data)
|
||||
self.UnitCombo.setFixedHeight(25)
|
||||
|
||||
Layout = QHBoxLayout(self)
|
||||
Layout.setContentsMargins(0, 0, 0, 0)
|
||||
Layout.setSpacing(4)
|
||||
Layout.addWidget(self.SpinBox)
|
||||
Layout.addWidget(self.UnitCombo)
|
||||
Layout.addStretch()
|
||||
|
||||
def getValue(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
return str(self.getOffsetDays())
|
||||
|
||||
def getOffsetDays(
|
||||
self
|
||||
) -> int:
|
||||
|
||||
val = self.SpinBox.value()
|
||||
unit = self.UnitCombo.currentData()
|
||||
if unit == "weeks":
|
||||
return val*7
|
||||
if unit == "months":
|
||||
return val*30
|
||||
if unit == "years":
|
||||
return val*365
|
||||
return val
|
||||
|
||||
|
||||
class _TimeOffsetContainer(QWidget):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self.SpinBox = QSpinBox(self)
|
||||
self.SpinBox.setRange(0, 99999)
|
||||
self.SpinBox.setSuffix(" 小时")
|
||||
self.SpinBox.setFixedHeight(25)
|
||||
|
||||
Layout = QHBoxLayout(self)
|
||||
Layout.setContentsMargins(0, 0, 0, 0)
|
||||
Layout.addWidget(self.SpinBox)
|
||||
|
||||
def getValue(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
return str(self.getOffsetHours())
|
||||
|
||||
def getOffsetHours(
|
||||
self
|
||||
) -> int:
|
||||
|
||||
return self.SpinBox.value()
|
||||
|
||||
|
||||
class VariableManager(QObject):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self._vars = []
|
||||
self._name_map = {}
|
||||
|
||||
self.initPresetVars()
|
||||
|
||||
def initPresetVars(
|
||||
self
|
||||
):
|
||||
|
||||
for p in getPresetVars():
|
||||
entry = {"name": p["name"], "type": p["type"], "display": p["display"]}
|
||||
self._vars.append(entry)
|
||||
self._name_map[p["name"]] = entry
|
||||
|
||||
def getInfoByName(
|
||||
self,
|
||||
name: str
|
||||
):
|
||||
|
||||
return self._name_map.get(name.upper().strip())
|
||||
|
||||
def populateCombo(
|
||||
self,
|
||||
combo: QComboBox
|
||||
):
|
||||
|
||||
current_data = combo.currentData()
|
||||
combo.blockSignals(True)
|
||||
combo.clear()
|
||||
for entry in self._vars:
|
||||
combo.addItem(
|
||||
entry["display"],
|
||||
(entry["name"], entry["type"])
|
||||
)
|
||||
if current_data:
|
||||
for i in range(combo.count()):
|
||||
d = combo.itemData(i)
|
||||
if d and d[0] == current_data[0]:
|
||||
combo.setCurrentIndex(i)
|
||||
break
|
||||
combo.blockSignals(False)
|
||||
|
||||
|
||||
def makeValueWidget(
|
||||
var_type: str,
|
||||
parent: QWidget = None
|
||||
) -> QWidget:
|
||||
|
||||
if var_type == "Int":
|
||||
Widget = QSpinBox(parent)
|
||||
Widget.setRange(-999999, 999999)
|
||||
Widget.setFixedHeight(25)
|
||||
Widget.setMinimumWidth(100)
|
||||
return Widget
|
||||
if var_type == "Float":
|
||||
Widget = QDoubleSpinBox(parent)
|
||||
Widget.setRange(-999999.0, 999999.0)
|
||||
Widget.setDecimals(2)
|
||||
Widget.setFixedHeight(25)
|
||||
Widget.setMinimumWidth(100)
|
||||
return Widget
|
||||
if var_type == "String":
|
||||
Widget = QLineEdit(parent)
|
||||
Widget.setPlaceholderText("输入值")
|
||||
Widget.setFixedHeight(25)
|
||||
Widget.setMinimumWidth(120)
|
||||
return Widget
|
||||
if var_type == "Boolean":
|
||||
Widget = QComboBox(parent)
|
||||
Widget.addItem("是 (true)", "true")
|
||||
Widget.addItem("否 (false)", "false")
|
||||
Widget.setFixedHeight(25)
|
||||
Widget.setMinimumWidth(100)
|
||||
return Widget
|
||||
if var_type == "Date":
|
||||
return _DateInputContainer(parent)
|
||||
if var_type == "Time":
|
||||
return _TimeInputContainer(parent)
|
||||
Widget = QLineEdit(parent)
|
||||
Widget.setPlaceholderText("输入值")
|
||||
Widget.setFixedHeight(25)
|
||||
Widget.setMinimumWidth(120)
|
||||
return Widget
|
||||
|
||||
def makeOffsetWidget(
|
||||
var_type: str,
|
||||
parent: QWidget = None
|
||||
) -> QWidget:
|
||||
|
||||
if var_type == "Int":
|
||||
Widget = QSpinBox(parent)
|
||||
Widget.setRange(-999999, 999999)
|
||||
Widget.setFixedHeight(25)
|
||||
Widget.setMinimumWidth(100)
|
||||
return Widget
|
||||
if var_type == "Float":
|
||||
Widget = QDoubleSpinBox(parent)
|
||||
Widget.setRange(-999999.0, 999999.0)
|
||||
Widget.setDecimals(2)
|
||||
Widget.setFixedHeight(25)
|
||||
Widget.setMinimumWidth(100)
|
||||
return Widget
|
||||
if var_type == "Date":
|
||||
return _DateOffsetContainer(parent)
|
||||
if var_type == "Time":
|
||||
return _TimeOffsetContainer(parent)
|
||||
Widget = QLabel("(不支持该操作)", parent)
|
||||
Widget.setFixedHeight(25)
|
||||
return Widget
|
||||
|
||||
def makeVarRefCombo(
|
||||
parent: QWidget = None
|
||||
) -> QComboBox:
|
||||
|
||||
ComboBox = QComboBox(parent)
|
||||
ComboBox.setFixedHeight(25)
|
||||
ComboBox.setMinimumWidth(120)
|
||||
ComboBox.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
return ComboBox
|
||||
|
||||
def makeComboWidget(
|
||||
items,
|
||||
min_width: int = 80,
|
||||
parent: QWidget = None
|
||||
) -> QComboBox:
|
||||
|
||||
ComboBox = QComboBox(parent)
|
||||
for display, data in items:
|
||||
ComboBox.addItem(display, data)
|
||||
ComboBox.setFixedHeight(25)
|
||||
ComboBox.setMinimumWidth(min_width)
|
||||
return ComboBox
|
||||
|
||||
def makeLabel(
|
||||
text: str,
|
||||
parent: QWidget = None,
|
||||
width: int = None
|
||||
) -> QLabel:
|
||||
|
||||
Label = QLabel(text, parent)
|
||||
Label.setFixedHeight(25)
|
||||
if width:
|
||||
Label.setFixedWidth(width)
|
||||
return Label
|
||||
|
||||
def getValueFromWidget(
|
||||
widget: QWidget
|
||||
) -> str:
|
||||
|
||||
if hasattr(widget, "getValue"):
|
||||
return widget.getValue()
|
||||
if isinstance(widget, QTimeEdit):
|
||||
return widget.time().toString("HH:mm")
|
||||
if isinstance(widget, QDateEdit):
|
||||
return widget.date().toString("yyyy-MM-dd")
|
||||
if isinstance(widget, QComboBox):
|
||||
return widget.currentData() or widget.currentText()
|
||||
if isinstance(widget, QSpinBox):
|
||||
return str(widget.value())
|
||||
if isinstance(widget, QDoubleSpinBox):
|
||||
return str(widget.value())
|
||||
if isinstance(widget, QLineEdit):
|
||||
return widget.text()
|
||||
return ""
|
||||
|
||||
def encodeValueStr(
|
||||
raw_value: str,
|
||||
var_type: str
|
||||
) -> str:
|
||||
"""
|
||||
Encode a raw widget value as a Lua expression.
|
||||
|
||||
Arithmetic expressions (A + B) are passed through for numeric types;
|
||||
Date/Time arithmetic is translated to ``dateadd()`` / ``timeadd()`` calls.
|
||||
"""
|
||||
|
||||
if var_type in ("Date", "Time"):
|
||||
return encodeDateOrTime(str(raw_value), var_type)
|
||||
if isinstance(raw_value, bool):
|
||||
return "true" if raw_value else "false"
|
||||
s = str(raw_value)
|
||||
if isArithExpr(s):
|
||||
return s
|
||||
if var_type == "Boolean":
|
||||
up = s.upper().strip()
|
||||
if up in ("TRUE", "FALSE"):
|
||||
return up.lower()
|
||||
return "true" if raw_value else "false"
|
||||
if var_type == "String":
|
||||
escaped = s.replace("\\", "\\\\").replace('"', '\\"')
|
||||
return f'"{escaped}"'
|
||||
return s
|
||||
|
||||
def encodeDateOrTime(
|
||||
raw_value: str,
|
||||
var_type: str
|
||||
) -> str:
|
||||
"""
|
||||
Translate a date/time widget value into a Lua expression.
|
||||
"""
|
||||
|
||||
s = raw_value.strip()
|
||||
up = s.upper()
|
||||
# Input comes from widget values — single binary expressions only (e.g. "A + 3",
|
||||
# "CURRENT_DATE + 5"). Multi-operator expressions are not produced by the UI.
|
||||
m_arith_spaced = re.match(r'^(.+?)\s+([+-])\s+(.+)$', s)
|
||||
m_arith_nospace = re.match(r'^([A-Za-z_]\w*)([+-])(\d+|[A-Za-z_]\w*)$', s)
|
||||
m_arith = m_arith_spaced or m_arith_nospace
|
||||
if m_arith:
|
||||
left = m_arith.group(1).strip().upper()
|
||||
sign = m_arith.group(2)
|
||||
right = m_arith.group(3).strip()
|
||||
operand = right if sign == "+" else f"-{right}"
|
||||
if left == "CURRENT_DATE":
|
||||
return f"dateadd(datenow(), {operand})"
|
||||
if left == "CURRENT_TIME":
|
||||
return f"timeadd(timenow(), {operand})"
|
||||
if var_type == "Date":
|
||||
return f"dateadd({left}, {operand})"
|
||||
if var_type == "Time":
|
||||
return f"timeadd({left}, {operand})"
|
||||
return f"{left} {sign} {right}"
|
||||
if up == "CURRENT_DATE":
|
||||
return "datenow()"
|
||||
if up == "CURRENT_TIME":
|
||||
return "timenow()"
|
||||
_REL_MAP = {
|
||||
"前天": "dateadd(datenow(), -2)",
|
||||
"昨天": "dateadd(datenow(), -1)",
|
||||
"今天": "datenow()",
|
||||
"明天": "dateadd(datenow(), 1)",
|
||||
"后天": "dateadd(datenow(), 2)",
|
||||
}
|
||||
if s in _REL_MAP:
|
||||
return _REL_MAP[s]
|
||||
if var_type == "Date":
|
||||
m_date = re.match(r"^(\d{4})-(\d{2})-(\d{2})$", s)
|
||||
if m_date:
|
||||
y, m, d = int(m_date.group(1)), int(m_date.group(2)), int(m_date.group(3))
|
||||
return f"date({y}, {m}, {d})"
|
||||
if var_type == "Time":
|
||||
m_time = re.match(r"^(\d{1,2}):(\d{2})$", s)
|
||||
if m_time:
|
||||
h, m = int(m_time.group(1)), int(m_time.group(2))
|
||||
return f"time({h}, {m})"
|
||||
if re.match(r"^[+-]?\d+$", s):
|
||||
return s
|
||||
if re.match(r"^[A-Za-z_]\w*$", s):
|
||||
return s
|
||||
return f'"{s}"'
|
||||
|
||||
# Pre-compiled patterns for detecting arithmetic expressions (A + B / A - B)
|
||||
_RE_ARITH_SPACED = re.compile(r'^(.+?)\s+([+-])\s+(.+)$')
|
||||
_RE_ARITH_NOSPACE = re.compile(r'^([A-Za-z_]\w*)([+-])(\d+|[A-Za-z_]\w*)$')
|
||||
|
||||
def isArithExpr(
|
||||
expr: str
|
||||
) -> bool:
|
||||
"""
|
||||
Return True if expr looks like a two-operand arithmetic expression (A ± B).
|
||||
"""
|
||||
|
||||
s = expr.strip()
|
||||
return bool(_RE_ARITH_SPACED.match(s) or _RE_ARITH_NOSPACE.match(s))
|
||||
@@ -0,0 +1,468 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2026 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
"""
|
||||
Widget components for the AutoScript orchestration dialog.
|
||||
"""
|
||||
from PySide6.QtCore import Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QStackedWidget
|
||||
)
|
||||
|
||||
from gui.ALAutoScriptOrchDialog._helpers import (
|
||||
ACTION_OPTIONS,
|
||||
COMPARE_OPTIONS,
|
||||
LOGIC_OPTIONS,
|
||||
encodeValueStr,
|
||||
getPresetVars,
|
||||
getTypeOrder,
|
||||
getValueFromWidget,
|
||||
getArithType,
|
||||
makeComboWidget,
|
||||
makeLabel,
|
||||
makeOffsetWidget,
|
||||
makeValueWidget,
|
||||
makeVarRefCombo,
|
||||
)
|
||||
|
||||
|
||||
class ConditionRowFrame(QFrame):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
var_mgr,
|
||||
parent_block_index: int = 0,
|
||||
is_first: bool = False,
|
||||
parent = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self._var_mgr = var_mgr
|
||||
self._block_index = parent_block_index
|
||||
self._is_first = is_first
|
||||
self._is_bool_mode = False
|
||||
self._raw_rhs_expr = ""
|
||||
|
||||
self.setupUi()
|
||||
self.connectSignals()
|
||||
|
||||
def setupUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.setUpdatesEnabled(False)
|
||||
self.setFrameShape(QFrame.StyledPanel)
|
||||
self.setFrameShadow(QFrame.Raised)
|
||||
self.setFixedHeight(32)
|
||||
Layout = QHBoxLayout(self)
|
||||
Layout.setContentsMargins(2, 2, 2, 2)
|
||||
Layout.setSpacing(4)
|
||||
if self._is_first:
|
||||
self.LogicCombo = None
|
||||
else:
|
||||
self.LogicCombo = makeComboWidget(LOGIC_OPTIONS, min_width=110, parent=self)
|
||||
Layout.addWidget(self.LogicCombo)
|
||||
self.LeftVarCombo = QComboBox(self)
|
||||
self.LeftVarCombo.setFixedHeight(25)
|
||||
self.LeftVarCombo.setMinimumWidth(120)
|
||||
self.LeftVarCombo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.populateLeftVarCombo()
|
||||
Layout.addWidget(self.LeftVarCombo)
|
||||
self.OpCombo = makeComboWidget(COMPARE_OPTIONS, min_width=80, parent=self)
|
||||
Layout.addWidget(self.OpCombo)
|
||||
self.CompTypeCombo = makeComboWidget([
|
||||
("特定值", "literal"),
|
||||
("变量", "variable"),
|
||||
], min_width=70, parent=self)
|
||||
Layout.addWidget(self.CompTypeCombo)
|
||||
self.RhsStack = QStackedWidget(self)
|
||||
self.RhsStack.setFixedHeight(25)
|
||||
self.initLiteralStack()
|
||||
self.RhsVarCombo = makeVarRefCombo(self)
|
||||
self.RhsStack.addWidget(self.RhsVarCombo)
|
||||
self.RhsStack.setCurrentIndex(0)
|
||||
Layout.addWidget(self.RhsStack)
|
||||
if not self._is_first:
|
||||
self.DeleteBtn = QPushButton("×", self)
|
||||
self.DeleteBtn.setFixedSize(25, 25)
|
||||
self.DeleteBtn.setStyleSheet("color: red; font-weight: bold;")
|
||||
Layout.addWidget(self.DeleteBtn)
|
||||
else:
|
||||
self.DeleteBtn = None
|
||||
Layout.addStretch()
|
||||
self.setUpdatesEnabled(True)
|
||||
|
||||
def populateLeftVarCombo(
|
||||
self
|
||||
):
|
||||
|
||||
was_bool = self._is_bool_mode
|
||||
bool_name = None
|
||||
if was_bool:
|
||||
data = self.LeftVarCombo.currentData()
|
||||
if data:
|
||||
bool_name = data[0]
|
||||
self._var_mgr.populateCombo(self.LeftVarCombo)
|
||||
# Append boolean literal sentinels at the end
|
||||
self.LeftVarCombo.insertSeparator(self.LeftVarCombo.count())
|
||||
self.LeftVarCombo.addItem("true", ("true", "Boolean"))
|
||||
self.LeftVarCombo.addItem("false", ("false", "Boolean"))
|
||||
if was_bool and bool_name:
|
||||
for ci in range(self.LeftVarCombo.count()):
|
||||
d = self.LeftVarCombo.itemData(ci)
|
||||
if d and d[0] == bool_name:
|
||||
self.LeftVarCombo.setCurrentIndex(ci)
|
||||
break
|
||||
|
||||
def populateRHSVarCombo(
|
||||
self
|
||||
):
|
||||
|
||||
self._var_mgr.populateCombo(self.RhsVarCombo)
|
||||
|
||||
def initLiteralStack(
|
||||
self
|
||||
):
|
||||
|
||||
self.LiteralStack = QStackedWidget(self)
|
||||
self.LiteralStack.setFixedHeight(25)
|
||||
self.literal_widgets = {}
|
||||
for vt in getTypeOrder():
|
||||
W = makeValueWidget(vt, self.LiteralStack)
|
||||
self.literal_widgets[vt] = W
|
||||
self.LiteralStack.addWidget(W)
|
||||
self.LiteralStack.setCurrentWidget(self.literal_widgets.get("String"))
|
||||
self.RhsStack.addWidget(self.LiteralStack)
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.LeftVarCombo.currentIndexChanged.connect(self.onLeftVarChanged)
|
||||
self.CompTypeCombo.currentIndexChanged.connect(self.onCompTypeChanged)
|
||||
|
||||
def getLogic(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
return self.LogicCombo.currentData() if self.LogicCombo else ""
|
||||
|
||||
def updateRHSLiteralWidget(
|
||||
self,
|
||||
vartype: str
|
||||
):
|
||||
|
||||
if vartype not in self.literal_widgets:
|
||||
vartype = "String"
|
||||
self.LiteralStack.setCurrentWidget(self.literal_widgets[vartype])
|
||||
|
||||
def toScript(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
data = self.LeftVarCombo.currentData()
|
||||
if self._is_bool_mode and data:
|
||||
return data[0]
|
||||
if not data:
|
||||
return ""
|
||||
name, vartype = data
|
||||
# CURRENT_DATE / CURRENT_TIME map to datenow() / timenow()
|
||||
if name == "CURRENT_DATE":
|
||||
name = "datenow()"
|
||||
elif name == "CURRENT_TIME":
|
||||
name = "timenow()"
|
||||
op_sym = self.OpCombo.currentData()
|
||||
if self._raw_rhs_expr:
|
||||
return f"{name} {op_sym} {self._raw_rhs_expr}"
|
||||
is_var_ref = (self.CompTypeCombo.currentData() == "variable")
|
||||
if is_var_ref:
|
||||
rd = self.RhsVarCombo.currentData()
|
||||
if rd:
|
||||
rhs_name = rd[0]
|
||||
if rhs_name == "CURRENT_DATE":
|
||||
rhs_name = "datenow()"
|
||||
elif rhs_name == "CURRENT_TIME":
|
||||
rhs_name = "timenow()"
|
||||
return f"{name} {op_sym} {rhs_name}"
|
||||
rhs_text = self.RhsVarCombo.currentText().strip()
|
||||
if rhs_text:
|
||||
return f"{name} {op_sym} {rhs_text}"
|
||||
return ""
|
||||
w = self.literal_widgets.get(vartype)
|
||||
if w:
|
||||
raw_val = getValueFromWidget(w)
|
||||
encoded = encodeValueStr(raw_val, vartype)
|
||||
return f"{name} {op_sym} {encoded}"
|
||||
return ""
|
||||
|
||||
def refreshVarCombos(
|
||||
self
|
||||
):
|
||||
|
||||
self.populateLeftVarCombo()
|
||||
self.populateRHSVarCombo()
|
||||
|
||||
@Slot(int)
|
||||
def onLeftVarChanged(
|
||||
self,
|
||||
idx
|
||||
):
|
||||
|
||||
self._raw_rhs_expr = ""
|
||||
if idx < 0:
|
||||
return
|
||||
data = self.LeftVarCombo.itemData(idx)
|
||||
if not data:
|
||||
return
|
||||
name, vartype = data
|
||||
is_bool = name in ("true", "false")
|
||||
self._is_bool_mode = is_bool
|
||||
self.OpCombo.setVisible(not is_bool)
|
||||
self.CompTypeCombo.setVisible(not is_bool)
|
||||
self.RhsStack.setVisible(not is_bool)
|
||||
if not is_bool:
|
||||
self.updateRHSLiteralWidget(vartype)
|
||||
|
||||
@Slot(int)
|
||||
def onCompTypeChanged(
|
||||
self,
|
||||
idx
|
||||
):
|
||||
|
||||
self._raw_rhs_expr = ""
|
||||
isVar = (self.CompTypeCombo.currentData() == "variable")
|
||||
self.RhsStack.setCurrentIndex(1 if isVar else 0)
|
||||
if isVar:
|
||||
self.populateRHSVarCombo()
|
||||
|
||||
|
||||
class ActionStepFrame(QFrame):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
var_mgr,
|
||||
parent_block_index: int = 0,
|
||||
parent = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self._var_mgr = var_mgr
|
||||
self._block_index = parent_block_index
|
||||
self._current_target_type = "String"
|
||||
|
||||
self.setupUi()
|
||||
self.connectSignals()
|
||||
|
||||
def setupUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.setUpdatesEnabled(False)
|
||||
self.setFrameShape(QFrame.StyledPanel)
|
||||
self.setFrameShadow(QFrame.Raised)
|
||||
self.setFixedHeight(35)
|
||||
Layout = QHBoxLayout(self)
|
||||
Layout.setContentsMargins(2, 2, 2, 2)
|
||||
Layout.setSpacing(4)
|
||||
self.OpTypeCombo = makeComboWidget(ACTION_OPTIONS, min_width=70, parent=self)
|
||||
Layout.addWidget(self.OpTypeCombo)
|
||||
Layout.addWidget(makeLabel("设置", self))
|
||||
self.TargetCombo = QComboBox(self)
|
||||
self.TargetCombo.setFixedHeight(25)
|
||||
self.TargetCombo.setMinimumWidth(120)
|
||||
self.populateTargetCombo()
|
||||
Layout.addWidget(self.TargetCombo)
|
||||
Layout.addWidget(makeLabel("为", self))
|
||||
self.ValueSrcCombo = makeComboWidget([
|
||||
("特定值", "literal"),
|
||||
("变量", "variable"),
|
||||
], min_width=70, parent=self)
|
||||
Layout.addWidget(self.ValueSrcCombo)
|
||||
self.ValueStack = QStackedWidget(self)
|
||||
self.ValueStack.setFixedHeight(25)
|
||||
self.initValueStacks()
|
||||
Layout.addWidget(self.ValueStack)
|
||||
self.ExistingVarCombo = makeVarRefCombo(self)
|
||||
self.ExistingVarCombo.setVisible(False)
|
||||
Layout.addWidget(self.ExistingVarCombo)
|
||||
self.DeleteBtn = QPushButton("×", self)
|
||||
self.DeleteBtn.setFixedSize(25, 25)
|
||||
self.DeleteBtn.setStyleSheet("color: red; font-weight: bold;")
|
||||
Layout.addWidget(self.DeleteBtn)
|
||||
self.setUpdatesEnabled(True)
|
||||
|
||||
def populateTargetCombo(
|
||||
self
|
||||
):
|
||||
|
||||
self.TargetCombo.blockSignals(True)
|
||||
self.TargetCombo.clear()
|
||||
for p in getPresetVars():
|
||||
if p["name"] in ("CURRENT_TIME", "CURRENT_DATE"):
|
||||
continue
|
||||
info = self._var_mgr.getInfoByName(p["name"])
|
||||
if info:
|
||||
self.TargetCombo.addItem(
|
||||
info["display"],
|
||||
(info["name"], info["type"])
|
||||
)
|
||||
self.TargetCombo.blockSignals(False)
|
||||
|
||||
def initValueStacks(
|
||||
self
|
||||
):
|
||||
|
||||
self.literal_widgets = {}
|
||||
self.offset_widgets = {}
|
||||
for vt in getTypeOrder():
|
||||
self.literal_widgets[vt] = makeValueWidget(vt, self.ValueStack)
|
||||
self.ValueStack.addWidget(self.literal_widgets[vt])
|
||||
if getArithType(vt):
|
||||
self.offset_widgets[vt] = makeOffsetWidget(vt, self.ValueStack)
|
||||
self.ValueStack.addWidget(self.offset_widgets[vt])
|
||||
else:
|
||||
Lbl = QLabel("(不支持该操作)", self.ValueStack)
|
||||
Lbl.setFixedHeight(25)
|
||||
self.offset_widgets[vt] = Lbl
|
||||
self.ValueStack.addWidget(Lbl)
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.OpTypeCombo.currentIndexChanged.connect(self.onOpTypeChanged)
|
||||
self.TargetCombo.currentIndexChanged.connect(self.onTargetChanged)
|
||||
self.ValueSrcCombo.currentIndexChanged.connect(self.onValueSrcChanged)
|
||||
|
||||
def getTargetName(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
data = self.TargetCombo.currentData()
|
||||
return data[0] if data else ""
|
||||
|
||||
def updateValueWidget(
|
||||
self
|
||||
):
|
||||
|
||||
op = self.OpTypeCombo.currentData()
|
||||
is_arith = (op in ("add", "sub"))
|
||||
actual_type = self._current_target_type
|
||||
if is_arith and actual_type in self.offset_widgets:
|
||||
self.ValueStack.setCurrentWidget(self.offset_widgets[actual_type])
|
||||
elif actual_type in self.literal_widgets:
|
||||
self.ValueStack.setCurrentWidget(self.literal_widgets[actual_type])
|
||||
else:
|
||||
self.ValueStack.setCurrentWidget(self.literal_widgets.get("String"))
|
||||
|
||||
def toScript(
|
||||
self
|
||||
) -> str:
|
||||
"""
|
||||
Generate a single line of Lua script from the current widget state.
|
||||
"""
|
||||
|
||||
target = self.getTargetName()
|
||||
op = self.OpTypeCombo.currentData()
|
||||
if op == "pass":
|
||||
return " -- pass"
|
||||
if not target:
|
||||
return ""
|
||||
raw_val = self.getValueRaw()
|
||||
vartype = self._current_target_type
|
||||
if op == "set":
|
||||
encoded = encodeValueStr(raw_val, vartype)
|
||||
return f" {target} = {encoded}"
|
||||
elif op == "add":
|
||||
if vartype == "Date" and hasattr(self.ValueStack.currentWidget(), "getOffsetDays"):
|
||||
days = self.ValueStack.currentWidget().getOffsetDays()
|
||||
return f" {target} = dateadd({target}, {days})"
|
||||
if vartype == "Time" and hasattr(self.ValueStack.currentWidget(), "getOffsetHours"):
|
||||
hours = self.ValueStack.currentWidget().getOffsetHours()
|
||||
return f" {target} = timeadd({target}, {hours})"
|
||||
return f" {target} = {target} + {raw_val}"
|
||||
elif op == "sub":
|
||||
if vartype == "Date" and hasattr(self.ValueStack.currentWidget(), "getOffsetDays"):
|
||||
days = self.ValueStack.currentWidget().getOffsetDays()
|
||||
return f" {target} = dateadd({target}, -{days})"
|
||||
if vartype == "Time" and hasattr(self.ValueStack.currentWidget(), "getOffsetHours"):
|
||||
hours = self.ValueStack.currentWidget().getOffsetHours()
|
||||
return f" {target} = timeadd({target}, -{hours})"
|
||||
return f" {target} = {target} - {raw_val}"
|
||||
return ""
|
||||
|
||||
def getValueRaw(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
if self.ValueSrcCombo.currentData() == "variable":
|
||||
data = self.ExistingVarCombo.currentData()
|
||||
return data[0] if data else ""
|
||||
w = self.ValueStack.currentWidget()
|
||||
if w:
|
||||
return getValueFromWidget(w)
|
||||
return ""
|
||||
|
||||
def refreshVarCombos(
|
||||
self
|
||||
):
|
||||
|
||||
current_data = self.TargetCombo.currentData()
|
||||
self.populateTargetCombo()
|
||||
if current_data:
|
||||
for i in range(self.TargetCombo.count()):
|
||||
d = self.TargetCombo.itemData(i)
|
||||
if d and d[0] == current_data[0]:
|
||||
self.TargetCombo.setCurrentIndex(i)
|
||||
break
|
||||
self._var_mgr.populateCombo(self.ExistingVarCombo)
|
||||
|
||||
@Slot(int)
|
||||
def onTargetChanged(
|
||||
self,
|
||||
idx
|
||||
):
|
||||
|
||||
if idx < 0:
|
||||
return
|
||||
data = self.TargetCombo.itemData(idx)
|
||||
if not data:
|
||||
return
|
||||
_, vartype = data
|
||||
self._current_target_type = vartype
|
||||
self.updateValueWidget()
|
||||
self.onValueSrcChanged(self.ValueSrcCombo.currentIndex())
|
||||
|
||||
@Slot(int)
|
||||
def onOpTypeChanged(
|
||||
self,
|
||||
idx
|
||||
):
|
||||
|
||||
self.updateValueWidget()
|
||||
|
||||
@Slot(int)
|
||||
def onValueSrcChanged(
|
||||
self,
|
||||
idx
|
||||
):
|
||||
|
||||
is_var = (self.ValueSrcCombo.currentData() == "variable")
|
||||
self.ValueStack.setVisible(not is_var)
|
||||
self.ExistingVarCombo.setVisible(is_var)
|
||||
if is_var:
|
||||
self._var_mgr.populateCombo(self.ExistingVarCombo)
|
||||
else:
|
||||
self.updateValueWidget()
|
||||
@@ -0,0 +1,352 @@
|
||||
# -*- 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, timedelta
|
||||
from typing import Any
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Qt,
|
||||
Signal,
|
||||
Slot,
|
||||
QTimer
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QFont
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QListWidgetItem,
|
||||
QMessageBox,
|
||||
QVBoxLayout,
|
||||
QWidget
|
||||
)
|
||||
|
||||
from gui.ALBulletinWorker import ALBulletinFetchWorker
|
||||
from gui.ALStatusLabel import ALStatusLabel
|
||||
from gui.resources.ui.Ui_ALBulletinDialog import Ui_ALBulletinDialog
|
||||
from managers.bulletin.BulletinManager import instance as bulletinInstance
|
||||
|
||||
|
||||
class ALBulletinItemWidget(QWidget):
|
||||
"""
|
||||
Single bulletin item widget for the bulletin list.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
bulletin: dict[str, Any] = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self.__bulletin = bulletin.copy() if bulletin else {}
|
||||
self.modifyUi()
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.ItemWidgetLayout = QVBoxLayout()
|
||||
self.ItemWidgetLayout.setSpacing(10)
|
||||
self.ItemWidgetLayout.setContentsMargins(10, 5, 10, 5)
|
||||
self.BulletinTitleLayout = QHBoxLayout()
|
||||
self.BulletinTitleLabel = QLabel(self.__bulletin.get("title", "无标题"))
|
||||
title_font = QFont()
|
||||
title_font.setBold(True)
|
||||
self.BulletinTitleLabel.setFont(title_font)
|
||||
self.BulletinTitleLayout.addWidget(self.BulletinTitleLabel)
|
||||
if self.__bulletin.get("isNew", False):
|
||||
self.NewIndicatorLabel = QLabel("新 !")
|
||||
self.NewIndicatorLabel.setStyleSheet(
|
||||
"color: #DC0000;"\
|
||||
"font-size: 10px;"\
|
||||
"font-weight: bold;"\
|
||||
"font-style: italic;"
|
||||
)
|
||||
self.NewIndicatorLabel.setFixedSize(25, 25)
|
||||
self.BulletinTitleLayout.addWidget(self.NewIndicatorLabel)
|
||||
else:
|
||||
self.NewIndicatorLabel = None
|
||||
if self.__bulletin.get("isEdited", False):
|
||||
self.BulletinIsEditedLabel = QLabel("(已编辑)")
|
||||
self.BulletinIsEditedLabel.setStyleSheet(
|
||||
"color: #FF9800;"\
|
||||
"font-size: 10px;"
|
||||
)
|
||||
self.BulletinTitleLayout.addWidget(self.BulletinIsEditedLabel)
|
||||
self.BulletinTitleLayout.addStretch()
|
||||
self.ItemWidgetLayout.addLayout(self.BulletinTitleLayout)
|
||||
self.BulletinInfoLayout = QHBoxLayout()
|
||||
self.BulletinDateLabel = QLabel()
|
||||
try:
|
||||
raw_dt = self.__bulletin.get("dateTime", "")
|
||||
date_time = datetime.fromisoformat(raw_dt) if raw_dt else datetime.now()
|
||||
except (ValueError, TypeError):
|
||||
date_time = datetime.now()
|
||||
self.BulletinDateLabel.setText(date_time.strftime("%Y-%m-%d %H:%M:%S"))
|
||||
self.BulletinDateLabel.setStyleSheet("color: #969696; font-size: 11px;")
|
||||
self.BulletinInfoLayout.addWidget(self.BulletinDateLabel)
|
||||
self.BulletinAuthorLabel = QLabel(self.__bulletin.get("author", "未知"))
|
||||
self.BulletinAuthorLabel.setStyleSheet("color: #969696; font-size: 11px;")
|
||||
self.BulletinInfoLayout.addWidget(self.BulletinAuthorLabel)
|
||||
self.BulletinInfoLayout.addStretch()
|
||||
self.ItemWidgetLayout.addLayout(self.BulletinInfoLayout)
|
||||
self.setLayout(self.ItemWidgetLayout)
|
||||
self.__bulletin = None
|
||||
|
||||
def markAsRead(
|
||||
self
|
||||
):
|
||||
|
||||
if self.NewIndicatorLabel:
|
||||
self.NewIndicatorLabel.hide()
|
||||
|
||||
|
||||
class ALBulletinDialog(QDialog, Ui_ALBulletinDialog):
|
||||
"""
|
||||
Bulletin viewer dialog.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.__sync_timer: QTimer | None = None
|
||||
self.__fetch_worker: ALBulletinFetchWorker | None = None
|
||||
self.__bulletin_mgr = bulletinInstance()
|
||||
|
||||
self.setupUi(self)
|
||||
self.modifyUi()
|
||||
self.connectSignals()
|
||||
self.setupTimer()
|
||||
if self.shouldAutoSync():
|
||||
self.syncBulletin()
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.ALSyncStatusLabel = ALStatusLabel(self)
|
||||
self.ALSyncStatusLabel.setFixedSize(30, 30)
|
||||
self.SyncLayout.replaceWidget(
|
||||
self.SyncStatusPlaceholder, self.ALSyncStatusLabel
|
||||
)
|
||||
title_font = QFont()
|
||||
title_font.setBold(True)
|
||||
title_font.setPointSize(15)
|
||||
self.BulletinTitleLabel.setFont(title_font)
|
||||
self.BulletinDateLabel.setStyleSheet("color: #969696;")
|
||||
self.BulletinAuthorLabel.setStyleSheet("color: #969696;")
|
||||
self.BulletinIsEditedLabel.setStyleSheet("color: #FF9800;")
|
||||
self.BulletinIsEditedLabel.hide()
|
||||
last_time = self.__bulletin_mgr.lastSyncTime()
|
||||
if last_time:
|
||||
self.setLastSyncTime(last_time)
|
||||
self.updateBulletinList(self.__bulletin_mgr.bulletins())
|
||||
|
||||
def shouldAutoSync(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
last = self.__bulletin_mgr.lastSyncTime()
|
||||
if last is None:
|
||||
return True
|
||||
try:
|
||||
last_dt = datetime.fromisoformat(last)
|
||||
interval_min = max(self.__bulletin_mgr.syncInterval()//2, 1)
|
||||
threshold = timedelta(minutes=interval_min)
|
||||
return (datetime.now().astimezone() - last_dt) > threshold
|
||||
except (ValueError, TypeError):
|
||||
return True
|
||||
|
||||
def setLastSyncTime(
|
||||
self,
|
||||
iso_str: str
|
||||
):
|
||||
"""
|
||||
Set last sync time to update the UI status.
|
||||
"""
|
||||
|
||||
try:
|
||||
dt = datetime.fromisoformat(iso_str)
|
||||
self.LastSyncDateTimeEdit.setDateTime(dt)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.BulletinListWidget.itemClicked.connect(self.onBulletinListWidgetItemClicked)
|
||||
self.SyncButton.clicked.connect(self.syncBulletin)
|
||||
|
||||
def setupTimer(
|
||||
self
|
||||
):
|
||||
|
||||
self.__sync_timer = QTimer(self)
|
||||
self.__sync_timer.timeout.connect(self.syncBulletin)
|
||||
self.__sync_timer.start(self.__bulletin_mgr.syncInterval()*60*1000)
|
||||
|
||||
def updateBulletinList(
|
||||
self,
|
||||
bulletins: list[dict]
|
||||
):
|
||||
|
||||
self.BulletinListWidget.clear()
|
||||
sorted_list = sorted(
|
||||
bulletins, key=lambda x: x.get("dateTime", ""), reverse=True
|
||||
)
|
||||
for bulletin in sorted_list:
|
||||
item = QListWidgetItem()
|
||||
item.setData(Qt.UserRole, bulletin)
|
||||
widget = ALBulletinItemWidget(self, bulletin)
|
||||
item.setSizeHint(widget.sizeHint())
|
||||
self.BulletinListWidget.addItem(item)
|
||||
self.BulletinListWidget.setItemWidget(item, widget)
|
||||
|
||||
def showBulletin(
|
||||
self,
|
||||
bulletin: dict
|
||||
):
|
||||
|
||||
self.BulletinTitleLabel.setText(bulletin.get("title", "无标题"))
|
||||
try:
|
||||
raw_dt = bulletin.get("dateTime", "")
|
||||
date_time = datetime.fromisoformat(raw_dt) if raw_dt else datetime.now()
|
||||
except (ValueError, TypeError):
|
||||
date_time = datetime.now()
|
||||
self.BulletinDateLabel.setText(date_time.strftime("%Y-%m-%d %H:%M:%S"))
|
||||
self.BulletinAuthorLabel.setText(bulletin.get("author", "未知"))
|
||||
if bulletin.get("isEdited", False):
|
||||
self.BulletinIsEditedLabel.show()
|
||||
else:
|
||||
self.BulletinIsEditedLabel.hide()
|
||||
self.BulletinContentTextBrowser.setHtml(
|
||||
"<div style='font-size: 14px;'>{content}</div>".format(
|
||||
content=bulletin.get("content", "无内容")
|
||||
)
|
||||
)
|
||||
|
||||
def clearBulletin(
|
||||
self
|
||||
):
|
||||
|
||||
self.BulletinTitleLabel.setText("")
|
||||
self.BulletinDateLabel.setText("")
|
||||
self.BulletinAuthorLabel.setText("")
|
||||
self.BulletinContentTextBrowser.setText("")
|
||||
self.BulletinIsEditedLabel.hide()
|
||||
|
||||
def syncBulletin(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__fetch_worker is not None:
|
||||
return
|
||||
self.SyncButton.setEnabled(False)
|
||||
self.SyncButton.setText("同步中...")
|
||||
self.ALSyncStatusLabel.status = ALStatusLabel.Status.RUNNING
|
||||
self.SyncStatusDetailLabel.setText("")
|
||||
self.SyncStatusDetailLabel.setStyleSheet("")
|
||||
params = self.__bulletin_mgr.getSyncDateTimeAndRange()
|
||||
self.__fetch_worker = ALBulletinFetchWorker(
|
||||
self, self.__bulletin_mgr.apiUrl(), params
|
||||
)
|
||||
self.__fetch_worker.fetchWorkerIsFinished.connect(self.onBulletinsFetched)
|
||||
self.__fetch_worker.fetchWorkerFinishedWithError.connect(self.onBulletinsFetchError)
|
||||
self.__fetch_worker.start()
|
||||
|
||||
@Slot(dict)
|
||||
def onBulletinsFetched(
|
||||
self,
|
||||
data: dict
|
||||
):
|
||||
|
||||
worker = self.sender()
|
||||
if worker is not self.__fetch_worker:
|
||||
return
|
||||
worker.fetchWorkerIsFinished.disconnect(self.onBulletinsFetched)
|
||||
worker.fetchWorkerFinishedWithError.disconnect(self.onBulletinsFetchError)
|
||||
worker.wait(2000)
|
||||
worker.deleteLater()
|
||||
self.__fetch_worker = None
|
||||
bulletins = data.get("bulletins", [])
|
||||
delete_ids = data.get("delete_ids", [])
|
||||
merged = self.__bulletin_mgr.updateAndMergeBulletins(bulletins, delete_ids)
|
||||
self.__bulletin_mgr.setLastSyncTime(datetime.now().astimezone().isoformat())
|
||||
self.SyncButton.setEnabled(True)
|
||||
self.SyncButton.setText("同步")
|
||||
self.ALSyncStatusLabel.status = ALStatusLabel.Status.SUCCESS
|
||||
self.SyncStatusDetailLabel.setText("同步成功")
|
||||
self.SyncStatusDetailLabel.setStyleSheet("color: green;")
|
||||
QTimer.singleShot(3000, self, self.clearSyncStatus)
|
||||
|
||||
self.setLastSyncTime(self.__bulletin_mgr.lastSyncTime())
|
||||
self.updateBulletinList(merged)
|
||||
self.clearBulletin()
|
||||
interval_ms = self.__bulletin_mgr.syncInterval()*60*1000
|
||||
if self.__sync_timer:
|
||||
self.__sync_timer.start(interval_ms)
|
||||
|
||||
@Slot(str)
|
||||
def onBulletinsFetchError(
|
||||
self,
|
||||
error_message: str
|
||||
):
|
||||
|
||||
worker = self.sender()
|
||||
if worker is not self.__fetch_worker:
|
||||
return
|
||||
worker.fetchWorkerIsFinished.disconnect(self.onBulletinsFetched)
|
||||
worker.fetchWorkerFinishedWithError.disconnect(self.onBulletinsFetchError)
|
||||
worker.wait(2000)
|
||||
worker.deleteLater()
|
||||
self.__fetch_worker = None
|
||||
self.SyncButton.setEnabled(True)
|
||||
self.SyncButton.setText("重试")
|
||||
self.ALSyncStatusLabel.status = ALStatusLabel.Status.FAILURE
|
||||
self.SyncStatusDetailLabel.setText("同步失败,请检查网络连接")
|
||||
self.SyncStatusDetailLabel.setStyleSheet("color: red;")
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"同步失败:{error_message}"
|
||||
)
|
||||
interval_ms = self.__bulletin_mgr.syncInterval()*60*1000
|
||||
if self.__sync_timer:
|
||||
self.__sync_timer.start(interval_ms)
|
||||
|
||||
def clearSyncStatus(
|
||||
self
|
||||
):
|
||||
|
||||
self.ALSyncStatusLabel.status = ALStatusLabel.Status.WAITING
|
||||
self.SyncStatusDetailLabel.setText("")
|
||||
self.SyncStatusDetailLabel.setStyleSheet("")
|
||||
|
||||
@Slot(QListWidgetItem)
|
||||
def onBulletinListWidgetItemClicked(
|
||||
self,
|
||||
item: QListWidgetItem
|
||||
):
|
||||
|
||||
if item is None or item.data(Qt.UserRole) is None:
|
||||
return
|
||||
bulletin = item.data(Qt.UserRole)
|
||||
if bulletin.get("isNew", False):
|
||||
self.__bulletin_mgr.markBulletinAsRead(bulletin["id"])
|
||||
widget = self.BulletinListWidget.itemWidget(item)
|
||||
if widget:
|
||||
widget.markAsRead()
|
||||
item.setData(Qt.UserRole, bulletin)
|
||||
self.showBulletin(bulletin)
|
||||
@@ -0,0 +1,188 @@
|
||||
# -*- 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 (
|
||||
QObject,
|
||||
QTimer,
|
||||
Signal,
|
||||
Slot
|
||||
)
|
||||
|
||||
from gui.ALBulletinWorker import ALBulletinFetchWorker
|
||||
from managers.bulletin.BulletinManager import instance as bulletinInstance
|
||||
|
||||
|
||||
class ALBulletinPoller(QObject):
|
||||
"""
|
||||
Background bulletin poller.
|
||||
|
||||
Owns the periodic poll timer and the ALBulletinFetchWorker
|
||||
lifecycle. Emits signals when new bulletins are detected so
|
||||
the owner can show tray notifications without knowing the
|
||||
fetch / merge details.
|
||||
"""
|
||||
|
||||
newBulletinsDetected = Signal(int)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self.__timer = QTimer(self)
|
||||
self.__timer.timeout.connect(self.__poll)
|
||||
self.__worker = None
|
||||
self.__dialog_open = False
|
||||
self.__stopped = False
|
||||
self.__mgr = bulletinInstance()
|
||||
|
||||
def start(
|
||||
self
|
||||
):
|
||||
|
||||
self.__stopped = False
|
||||
interval_ms = self.__mgr.syncInterval()*60*1000
|
||||
self.__timer.start(interval_ms)
|
||||
|
||||
def stop(
|
||||
self
|
||||
):
|
||||
|
||||
self.__stopped = True
|
||||
self.__timer.stop()
|
||||
self.__cleanupWorker()
|
||||
|
||||
def restart(
|
||||
self
|
||||
):
|
||||
|
||||
self.stop()
|
||||
self.start()
|
||||
|
||||
def fetchNow(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__worker is not None:
|
||||
return
|
||||
if self.__dialog_open:
|
||||
return
|
||||
self.__doFetch()
|
||||
|
||||
def isPolling(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
return self.__timer.isActive()
|
||||
|
||||
def setDialogOpen(
|
||||
self,
|
||||
open: bool
|
||||
):
|
||||
|
||||
self.__dialog_open = open
|
||||
|
||||
def __disconnectWorker(
|
||||
self,
|
||||
worker: ALBulletinFetchWorker
|
||||
):
|
||||
|
||||
try:
|
||||
worker.fetchWorkerIsFinished.disconnect(self.__onFetched)
|
||||
except (TypeError, RuntimeError):
|
||||
pass
|
||||
try:
|
||||
worker.fetchWorkerFinishedWithError.disconnect(self.__onError)
|
||||
except (TypeError, RuntimeError):
|
||||
pass
|
||||
|
||||
def __cleanupWorker(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__worker is None:
|
||||
return
|
||||
self.__disconnectWorker(self.__worker)
|
||||
self.__worker.wait(500)
|
||||
self.__worker.deleteLater()
|
||||
self.__worker = None
|
||||
|
||||
def __doFetch(
|
||||
self
|
||||
):
|
||||
|
||||
params = self.__mgr.getSyncDateTimeAndRange()
|
||||
self.__worker = ALBulletinFetchWorker(
|
||||
self, self.__mgr.apiUrl(), params
|
||||
)
|
||||
self.__worker.fetchWorkerIsFinished.connect(self.__onFetched)
|
||||
self.__worker.fetchWorkerFinishedWithError.connect(self.__onError)
|
||||
self.__worker.start()
|
||||
|
||||
@Slot()
|
||||
def __poll(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__worker is not None:
|
||||
return
|
||||
if self.__dialog_open:
|
||||
return
|
||||
self.__doFetch()
|
||||
|
||||
@Slot(dict)
|
||||
def __onFetched(
|
||||
self,
|
||||
data: dict
|
||||
):
|
||||
|
||||
worker = self.sender()
|
||||
if worker is not self.__worker:
|
||||
return
|
||||
self.__disconnectWorker(worker)
|
||||
worker.wait(500)
|
||||
worker.deleteLater()
|
||||
self.__worker = None
|
||||
old_ids = {str(b.get("id", "")) for b in self.__mgr.bulletins()}
|
||||
bulletins = data.get("bulletins", [])
|
||||
delete_ids = data.get("delete_ids", [])
|
||||
self.__mgr.updateAndMergeBulletins(bulletins, delete_ids)
|
||||
self.__mgr.setLastSyncTime(datetime.now().astimezone().isoformat())
|
||||
delete_id_set = {str(d) for d in delete_ids}
|
||||
new_ids = {
|
||||
str(b.get("id", ""))
|
||||
for b in bulletins
|
||||
if str(b.get("id", "")) and str(b.get("id", "")) not in old_ids
|
||||
}
|
||||
new_ids -= delete_id_set
|
||||
if new_ids and not self.__dialog_open:
|
||||
self.newBulletinsDetected.emit(len(new_ids))
|
||||
if not self.__stopped:
|
||||
interval_ms = self.__mgr.syncInterval()*60*1000
|
||||
self.__timer.start(interval_ms)
|
||||
|
||||
@Slot(str)
|
||||
def __onError(
|
||||
self,
|
||||
error_message: str
|
||||
):
|
||||
|
||||
worker = self.sender()
|
||||
if worker is not self.__worker:
|
||||
return
|
||||
self.__disconnectWorker(worker)
|
||||
worker.wait(500)
|
||||
worker.deleteLater()
|
||||
self.__worker = None
|
||||
if not self.__stopped:
|
||||
interval_ms = self.__mgr.syncInterval()*60*1000
|
||||
self.__timer.start(interval_ms)
|
||||
@@ -0,0 +1,57 @@
|
||||
# -*- 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 requests
|
||||
|
||||
from PySide6.QtCore import (
|
||||
QThread,
|
||||
Signal
|
||||
)
|
||||
|
||||
|
||||
class ALBulletinFetchWorker(QThread):
|
||||
"""
|
||||
Worker thread for fetching bulletins from the server.
|
||||
"""
|
||||
|
||||
fetchWorkerIsFinished = Signal(dict)
|
||||
fetchWorkerFinishedWithError = Signal(str)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
request_url: str = "",
|
||||
params: dict = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self.__request_url = request_url.rstrip("/") if request_url else ""
|
||||
self.__params = params or {}
|
||||
|
||||
def run(
|
||||
self
|
||||
):
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
self.__request_url, params=self.__params, timeout=5
|
||||
)
|
||||
response.raise_for_status()
|
||||
r = response.json()
|
||||
if r.get("code") == 200:
|
||||
data = r.get("data", {})
|
||||
self.fetchWorkerIsFinished.emit(data)
|
||||
else:
|
||||
raise Exception(
|
||||
f"服务器返回错误: [{r.get('code', '未知代码')}] {r.get('msg', '未知错误')}"
|
||||
)
|
||||
except requests.RequestException as e:
|
||||
self.fetchWorkerFinishedWithError.emit(f"获取公告数据时发生网络错误: \n{e}")
|
||||
except Exception as e:
|
||||
self.fetchWorkerFinishedWithError.emit(f"获取公告数据时发生未知错误: \n{e}")
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 - 2026 KenanZhu.
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
@@ -8,56 +8,71 @@ You may use, modify, and distribute this file under the terms of the MIT License
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Qt, Signal, Slot, QTime, QDate, QDir, QFileInfo
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QLineEdit, QMessageBox, QFileDialog,
|
||||
QTreeWidgetItem, QMenu, QInputDialog
|
||||
QDate,
|
||||
QDir,
|
||||
QFileInfo,
|
||||
Qt,
|
||||
QTime,
|
||||
Signal,
|
||||
Slot
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QCloseEvent, QAction
|
||||
QAction,
|
||||
QCloseEvent
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QFileDialog,
|
||||
QInputDialog,
|
||||
QLineEdit,
|
||||
QMenu,
|
||||
QMessageBox,
|
||||
QTreeWidgetItem,
|
||||
QWidget
|
||||
)
|
||||
|
||||
from gui.Ui_ALConfigWidget import Ui_ALConfigWidget
|
||||
from gui.ALSeatMapWidget import ALSeatMapWidget
|
||||
from gui.ALSeatMapTable import seats_maps
|
||||
from gui.ALUserTreeWidget import TreeItemType
|
||||
from gui.ALUserTreeWidget import ALUserTreeWidget
|
||||
import managers.config.ConfigManager as ConfigManager
|
||||
|
||||
from utils.ConfigReader import ConfigReader
|
||||
from utils.ConfigWriter import ConfigWriter
|
||||
from gui.ALSeatMapSelectDialog import ALSeatMapSelectDialog
|
||||
from gui.ALSeatMapTable import ALSeatMapTable
|
||||
from gui.ALUserTreeWidget import (
|
||||
ALUserTreeItemType,
|
||||
ALUserTreeWidget
|
||||
)
|
||||
from gui.ALWebDriverDownloadDialog import ALWebDriverDownloadDialog
|
||||
from gui.ALWidgetMixin import CenterOnParentMixin
|
||||
from gui.resources.ui.Ui_ALConfigWidget import Ui_ALConfigWidget
|
||||
from interfaces.ConfigProvider import (
|
||||
CfgKey,
|
||||
ConfigProvider
|
||||
)
|
||||
from managers.config.ConfigUtils import ConfigUtils
|
||||
from utils.JSONReader import JSONReader
|
||||
from utils.JSONWriter import JSONWriter
|
||||
|
||||
|
||||
class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget):
|
||||
|
||||
configWidgetCloseSingal = Signal(dict)
|
||||
configWidgetIsClosed = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None,
|
||||
config_paths = {
|
||||
"run": "",
|
||||
"user": ""
|
||||
}
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self.__config_paths = config_paths
|
||||
self.__cfg_mgr: ConfigProvider = ConfigManager.instance()
|
||||
self.__config_paths = ConfigUtils.getAutomationConfigPaths()
|
||||
self.__config_data = {"run": {}, "user": {}}
|
||||
self.__seat_map_widget = None
|
||||
|
||||
self.setupUi(self)
|
||||
self.modifyUi()
|
||||
self.connectSignals()
|
||||
self.initlizeFloorRoomMap()
|
||||
self.initlizeDefaultConfigPaths()
|
||||
if not self.initlizeConfigs():
|
||||
if not self.initializeConfigs():
|
||||
self.close()
|
||||
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
@@ -70,9 +85,8 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
self.UserListLayout.insertWidget(0, self.UserTreeWidget)
|
||||
self.UserTreeWidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.UserTreeWidget.customContextMenuRequested.connect(self.onUserTreeWidgetContextMenu)
|
||||
self.initlizeFloorRoomMap()
|
||||
self.initilizeUserInfoWidget()
|
||||
|
||||
self.initializeFloorRoomMap()
|
||||
self.initializeUserInfoWidget()
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
@@ -86,6 +100,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
self.AddUserButton.clicked.connect(self.onAddUserButtonClicked)
|
||||
self.DelUserButton.clicked.connect(self.onDelUserButtonClicked)
|
||||
self.BrowseBrowserDriverButton.clicked.connect(self.onBrowseBrowserDriverButtonClicked)
|
||||
self.AutoDownloadWebDriverButton.clicked.connect(self.onAutoDownloadWebDriverButtonClicked)
|
||||
self.BrowseCurrentRunConfigButton.clicked.connect(self.onBrowseCurrentRunConfigButtonClicked)
|
||||
self.BrowseCurrentUserConfigButton.clicked.connect(self.onBrowseCurrentUserConfigButtonClicked)
|
||||
self.BrowseExportRunConfigButton.clicked.connect(self.onBrowseExportRunConfigButtonClicked)
|
||||
@@ -96,41 +111,15 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
|
||||
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
|
||||
|
||||
|
||||
def showEvent(
|
||||
self,
|
||||
event
|
||||
):
|
||||
|
||||
result = super().showEvent(event)
|
||||
|
||||
screen_rect = self.screen().geometry()
|
||||
target_pos = self.parent().geometry().center()
|
||||
target_pos.setX(target_pos.x() - self.width()//2)
|
||||
target_pos.setY(target_pos.y() - self.height()//2)
|
||||
if target_pos.x() < 0:
|
||||
target_pos.setX(0)
|
||||
if target_pos.x() + self.width() > screen_rect.width():
|
||||
target_pos.setX(screen_rect.width() - self.width())
|
||||
if target_pos.y() < 0:
|
||||
target_pos.setY(0)
|
||||
if target_pos.y() + self.height() > screen_rect.height():
|
||||
target_pos.setY(screen_rect.height() - self.height())
|
||||
self.move(target_pos)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def closeEvent(
|
||||
self,
|
||||
event: QCloseEvent
|
||||
):
|
||||
|
||||
self.configWidgetCloseSingal.emit(self.__config_paths)
|
||||
self.configWidgetIsClosed.emit()
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
def initlizeFloorRoomMap(
|
||||
def initializeFloorRoomMap(
|
||||
self
|
||||
):
|
||||
|
||||
@@ -163,20 +152,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
"五层": ["五层考研"]
|
||||
}
|
||||
|
||||
|
||||
def initlizeDefaultConfigPaths(
|
||||
self
|
||||
):
|
||||
|
||||
script_path = sys.executable
|
||||
script_dir = QFileInfo(script_path).absoluteDir()
|
||||
self.__default_config_paths = {
|
||||
"user": QDir.toNativeSeparators(script_dir.absoluteFilePath("user.json")),
|
||||
"run": QDir.toNativeSeparators(script_dir.absoluteFilePath("run.json"))
|
||||
}
|
||||
|
||||
|
||||
def initlizeConfigToWidget(
|
||||
def initializeConfigToWidget(
|
||||
self,
|
||||
which: str,
|
||||
config_data: dict
|
||||
@@ -186,23 +162,21 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
self.setRunConfigToWidget(config_data)
|
||||
self.CurrentRunConfigEdit.setText(self.__config_paths["run"])
|
||||
elif which == "user":
|
||||
self.initilizeUserInfoWidget()
|
||||
self.fillUserTree(config_data)
|
||||
self.initializeUserInfoWidget()
|
||||
self.setUsersToTreeWidget(config_data)
|
||||
self.CurrentUserConfigEdit.setText(self.__config_paths["user"])
|
||||
|
||||
|
||||
def initlizeConfig(
|
||||
def initializeConfig(
|
||||
self,
|
||||
which: str
|
||||
) -> bool:
|
||||
|
||||
msg = ""
|
||||
msg = "" # no use for now
|
||||
is_success = True
|
||||
if which == "run":
|
||||
run_config_path = self.__config_paths[which]
|
||||
if not os.path.exists(run_config_path):
|
||||
self.__config_data[which] = self.defaultRunConfig()
|
||||
self.__config_paths[which] = self.__default_config_paths[which]
|
||||
if self.saveRunConfig(self.__config_paths[which], self.__config_data[which]):
|
||||
msg += f"运行配置文件已初始化, 文件路径: \n{self.__config_paths[which]}\n"
|
||||
else:
|
||||
@@ -215,7 +189,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
user_config_path = self.__config_paths[which]
|
||||
if not os.path.exists(user_config_path):
|
||||
self.__config_data[which] = self.defaultUserConfig()
|
||||
self.__config_paths[which] = self.__default_config_paths[which]
|
||||
if self.saveUserConfig(self.__config_paths[which], self.__config_data[which]):
|
||||
msg += f"用户配置文件已初始化, 文件路径: \n{self.__config_paths[which]}\n"
|
||||
else:
|
||||
@@ -224,30 +197,20 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
self.__config_data[which] = self.loadUserConfig(user_config_path)
|
||||
if self.__config_data[which] is None:
|
||||
is_success = False
|
||||
if msg:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"提示 - AutoLibrary",
|
||||
f"配置文件初始化完成: \n{msg}"
|
||||
)
|
||||
return is_success
|
||||
|
||||
|
||||
def initlizeConfigs(
|
||||
def initializeConfigs(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
is_success = True
|
||||
for which in ["run", "user"]:
|
||||
if not self.__config_paths[which]:
|
||||
self.__config_paths[which] = self.__default_config_paths[which]
|
||||
if not self.initlizeConfig(which):
|
||||
if not self.initializeConfig(which):
|
||||
is_success = False
|
||||
break
|
||||
self.initlizeConfigToWidget(which, self.__config_data[which])
|
||||
self.initializeConfigToWidget(which, self.__config_data[which])
|
||||
return is_success
|
||||
|
||||
|
||||
def defaultRunConfig(
|
||||
self
|
||||
) -> dict:
|
||||
@@ -271,36 +234,15 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def defaultUserConfig(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
return {
|
||||
"groups": []
|
||||
"groups": [
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def defaultGroup(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
return {
|
||||
"name": "默认分组",
|
||||
"enabled": True,
|
||||
"users": []
|
||||
}
|
||||
|
||||
|
||||
def defaultUsers(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
return {
|
||||
"users": []
|
||||
}
|
||||
|
||||
|
||||
def collectRunConfigFromWidget(
|
||||
self
|
||||
) -> dict:
|
||||
@@ -322,30 +264,45 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
run_config["mode"]["run_mode"] = run_mode
|
||||
return run_config
|
||||
|
||||
|
||||
def setRunConfigToWidget(
|
||||
self,
|
||||
run_config: dict
|
||||
):
|
||||
|
||||
self.HostUrlEdit.setText(run_config["library"]["host_url"])
|
||||
self.LoginUrlEdit.setText(run_config["library"]["login_url"])
|
||||
self.AutoCaptchaCheckBox.setChecked(run_config["login"]["auto_captcha"])
|
||||
self.LoginAttemptSpinBox.setValue(run_config["login"]["max_attempt"])
|
||||
self.BrowserTypeComboBox.setCurrentText(run_config["web_driver"]["driver_type"])
|
||||
if run_config["web_driver"]["driver_path"]:
|
||||
driver_path = os.path.abspath(run_config["web_driver"]["driver_path"])
|
||||
else:
|
||||
driver_path = ""
|
||||
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(driver_path))
|
||||
self.HeadlessCheckBox.setChecked(run_config["web_driver"]["headless"])
|
||||
run_mode = run_config["mode"]["run_mode"]
|
||||
self.AutoReserveCheckBox.setChecked(run_mode&0x01)
|
||||
self.AutoCheckinCheckBox.setChecked(run_mode&0x02)
|
||||
self.AutoRenewalCheckBox.setChecked(run_mode&0x04)
|
||||
try:
|
||||
self.HostUrlEdit.setText(run_config["library"]["host_url"])
|
||||
self.LoginUrlEdit.setText(run_config["library"]["login_url"])
|
||||
self.AutoCaptchaCheckBox.setChecked(run_config["login"]["auto_captcha"])
|
||||
self.LoginAttemptSpinBox.setValue(run_config["login"]["max_attempt"])
|
||||
self.BrowserTypeComboBox.setCurrentText(run_config["web_driver"]["driver_type"])
|
||||
if run_config["web_driver"]["driver_path"]:
|
||||
driver_path = os.path.abspath(run_config["web_driver"]["driver_path"])
|
||||
else:
|
||||
driver_path = ""
|
||||
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(driver_path))
|
||||
self.HeadlessCheckBox.setChecked(run_config["web_driver"]["headless"])
|
||||
run_mode = run_config["mode"]["run_mode"]
|
||||
self.AutoReserveCheckBox.setChecked(run_mode&0x01)
|
||||
self.AutoCheckinCheckBox.setChecked(run_mode&0x02)
|
||||
self.AutoRenewalCheckBox.setChecked(run_mode&0x04)
|
||||
except KeyError as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"运行配置文件读取键 '{e}' 时发生错误 ! :\n"
|
||||
f"文件路径: {self.__config_paths['run']}\n"
|
||||
"文件可能被意外修改或已经损坏\n"
|
||||
)
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"运行配置文件读取键 '{e}' 时发生未知错误 ! :\n"
|
||||
f"文件路径: {self.__config_paths['run']}\n"
|
||||
"文件可能被意外修改或已经损坏\n"
|
||||
)
|
||||
|
||||
|
||||
def initilizeUserInfoWidget(
|
||||
def initializeUserInfoWidget(
|
||||
self
|
||||
):
|
||||
|
||||
@@ -369,8 +326,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
self.MaxRenewTimeDiffSpinBox.setValue(30)
|
||||
self.PreferLateRenewTimeCheckBox.setChecked(False)
|
||||
|
||||
|
||||
def collectUserFromUserInfoWidget(
|
||||
def collectUserFromWidget(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
@@ -402,8 +358,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
user["reserve_info"]["renew_time"]["prefer_early"] = not self.PreferLateRenewTimeCheckBox.isChecked()
|
||||
return user
|
||||
|
||||
|
||||
def collectUserConfigFromUserTreeWidget(
|
||||
def collectUsersFromTreeWidget(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
@@ -425,7 +380,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
user_config["groups"].append(group_config)
|
||||
return user_config
|
||||
|
||||
|
||||
def setUserToWidget(
|
||||
self,
|
||||
user: dict
|
||||
@@ -450,14 +404,63 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
self.ExpectRenewDurationSpinBox.setValue(user["reserve_info"]["renew_time"]["expect_duration"])
|
||||
self.MaxRenewTimeDiffSpinBox.setValue(user["reserve_info"]["renew_time"]["max_diff"])
|
||||
self.PreferLateRenewTimeCheckBox.setChecked(not user["reserve_info"]["renew_time"]["prefer_early"])
|
||||
except:
|
||||
except KeyError as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
"用户配置文件读取发生错误 !\n"\
|
||||
f"用户: {user['username']} 配置文件可能已损坏"
|
||||
f"用户配置文件读取键 '{e}' 时发生错误 ! :\n"
|
||||
f"文件路径: {self.__config_paths['user']}\n"
|
||||
"文件可能被意外修改或已经损坏\n"
|
||||
)
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"用户配置文件读取键 '{e}' 时发生未知错误 ! :\n"
|
||||
f"文件路径: {self.__config_paths['user']}\n"
|
||||
"文件可能被意外修改或已经损坏\n"
|
||||
)
|
||||
|
||||
def setUsersToTreeWidget(
|
||||
self,
|
||||
users: dict
|
||||
):
|
||||
|
||||
self.UserTreeWidget.clear()
|
||||
self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged)
|
||||
try:
|
||||
if "groups" in users:
|
||||
for group_config in users["groups"]:
|
||||
group_item = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value)
|
||||
group_item.setText(0, group_config["name"])
|
||||
group_item.setFlags(group_item.flags() | Qt.ItemIsEditable)
|
||||
group_item.setCheckState(1, Qt.Checked if group_config.get("enabled", True) else Qt.Unchecked)
|
||||
for user_config in group_config["users"]:
|
||||
user_item = QTreeWidgetItem(group_item, ALUserTreeItemType.USER.value)
|
||||
user_item.setText(0, user_config["username"])
|
||||
user_item.setText(1, "" if user_config.get("enabled", True) else "跳过")
|
||||
user_item.setData(0, Qt.UserRole, user_config)
|
||||
user_item.setCheckState(1, Qt.Checked if user_config.get("enabled", True) else Qt.Unchecked)
|
||||
user_item.setDisabled(not group_config.get("enabled", True))
|
||||
group_item.setExpanded(True)
|
||||
except KeyError as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"用户配置文件读取键 '{e}' 时发生错误 ! :\n"
|
||||
f"文件路径: {self.__config_paths['user']}\n"
|
||||
"文件可能被意外修改或已经损坏\n"
|
||||
)
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"用户配置文件读取键 '{e}' 时发生未知错误 ! :\n"
|
||||
f"文件路径: {self.__config_paths['user']}\n"
|
||||
"文件可能被意外修改或已经损坏\n"
|
||||
)
|
||||
finally:
|
||||
self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged)
|
||||
|
||||
def loadRunConfig(
|
||||
self,
|
||||
@@ -467,22 +470,21 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
try:
|
||||
if not run_config_path or not os.path.exists(run_config_path):
|
||||
raise Exception("文件路径不存在")
|
||||
run_config = ConfigReader(run_config_path).getConfigs()
|
||||
run_config = JSONReader(run_config_path).data()
|
||||
if run_config and "library" in run_config\
|
||||
and "web_driver" in run_config\
|
||||
and "login" in run_config:
|
||||
return run_config
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"运行配置文件读取发生错误 ! : {e}\n"\
|
||||
f"文件路径: {run_config_path}"
|
||||
f"运行配置文件读取发生错误 ! :\n{e}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def saveRunConfig(
|
||||
self,
|
||||
run_config_path: str,
|
||||
@@ -494,18 +496,16 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
raise Exception("文件路径为空")
|
||||
if not run_config_data or not isinstance(run_config_data, dict):
|
||||
raise Exception("运行配置数据为空或类型错误")
|
||||
ConfigWriter(run_config_path, run_config_data)
|
||||
JSONWriter(run_config_path, run_config_data)
|
||||
return True
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"配置文件写入发生错误 ! : {e}\n"\
|
||||
f"文件路径: {run_config_path}"
|
||||
f"配置文件写入发生错误 ! : \n{e}"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def loadUserConfig(
|
||||
self,
|
||||
user_config_path: str
|
||||
@@ -514,11 +514,11 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
try:
|
||||
if not user_config_path or not os.path.exists(user_config_path):
|
||||
raise Exception("文件路径不存在")
|
||||
user_config = ConfigReader(user_config_path).getConfigs()
|
||||
user_config = JSONReader(user_config_path).data()
|
||||
if user_config and "groups" in user_config:
|
||||
return user_config
|
||||
# compatibility with old version config format
|
||||
if user_config and "users" in user_config:
|
||||
elif user_config and "users" in user_config:
|
||||
user_config = {
|
||||
"groups": [
|
||||
{
|
||||
@@ -529,17 +529,16 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
]
|
||||
}
|
||||
return user_config
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"用户配置文件读取发生错误 ! : {e}\n"\
|
||||
f"文件路径: {user_config_path}"
|
||||
f"用户配置文件读取发生错误 ! :\n{e}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def saveUserConfig(
|
||||
self,
|
||||
user_config_path: str,
|
||||
@@ -551,18 +550,16 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
raise Exception("文件路径为空")
|
||||
if not user_config_data or not isinstance(user_config_data, dict):
|
||||
raise Exception("用户配置数据为空或类型错误")
|
||||
ConfigWriter(user_config_path, user_config_data)
|
||||
JSONWriter(user_config_path, user_config_data)
|
||||
return True
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"用户配置文件写入发生错误 ! : {e}\n"\
|
||||
f"文件路径: \n{user_config_path}"
|
||||
f"用户配置文件写入发生错误 ! :\n{e}"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def saveConfigs(
|
||||
self,
|
||||
run_config_path: str,
|
||||
@@ -570,7 +567,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
) -> bool:
|
||||
|
||||
if user_config_path:
|
||||
self.__config_data["user"] = self.collectUserConfigFromUserTreeWidget()
|
||||
self.__config_data["user"] = self.collectUsersFromTreeWidget()
|
||||
if not self.saveUserConfig(
|
||||
user_config_path,
|
||||
self.__config_data["user"]
|
||||
@@ -585,7 +582,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def loadConfig(
|
||||
self,
|
||||
config_path: str
|
||||
@@ -609,45 +605,18 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
return True
|
||||
if user_config is not None:
|
||||
self.__config_data["user"].update(user_config)
|
||||
self.fillUserTree(self.__config_data["user"])
|
||||
self.setUsersToTreeWidget(self.__config_data["user"])
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def fillUserTree(
|
||||
self,
|
||||
user_config_data: dict
|
||||
):
|
||||
|
||||
self.UserTreeWidget.clear()
|
||||
self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged)
|
||||
try:
|
||||
if "groups" in user_config_data:
|
||||
for group_config in user_config_data["groups"]:
|
||||
group_item = QTreeWidgetItem(self.UserTreeWidget, TreeItemType.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, TreeItemType.USER.value)
|
||||
user_item.setText(0, user_config["username"])
|
||||
user_item.setText(1, "" if user_config.get("enabled", True) else "跳过")
|
||||
user_item.setData(0, Qt.UserRole, user_config)
|
||||
user_item.setCheckState(1, Qt.Checked if user_config.get("enabled", True) else Qt.Unchecked)
|
||||
user_item.setDisabled(not group_config.get("enabled", True))
|
||||
group_item.setExpanded(True)
|
||||
finally:
|
||||
self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged)
|
||||
|
||||
|
||||
def addGroup(
|
||||
self,
|
||||
group_name: str = ""
|
||||
) -> QTreeWidgetItem:
|
||||
|
||||
self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged)
|
||||
group_item = QTreeWidgetItem(self.UserTreeWidget, TreeItemType.GROUP.value)
|
||||
group_item = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value)
|
||||
if not group_name:
|
||||
group_name = f"新分组-{self.UserTreeWidget.topLevelItemCount()}"
|
||||
group_item.setText(0, group_name)
|
||||
@@ -657,6 +626,17 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged)
|
||||
return group_item
|
||||
|
||||
def delGroup(
|
||||
self,
|
||||
group_item: QTreeWidgetItem = None
|
||||
):
|
||||
|
||||
if group_item is None:
|
||||
return
|
||||
if group_item.type() != ALUserTreeItemType.GROUP.value:
|
||||
return
|
||||
index = self.UserTreeWidget.indexOfTopLevelItem(group_item)
|
||||
self.UserTreeWidget.takeTopLevelItem(index)
|
||||
|
||||
def addUser(
|
||||
self,
|
||||
@@ -667,7 +647,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
current_item = self.UserTreeWidget.currentItem()
|
||||
if current_item is None:
|
||||
group_item = self.addGroup()
|
||||
if group_item.type() == TreeItemType.USER.value:
|
||||
if group_item.type() == ALUserTreeItemType.USER.value:
|
||||
group_item = group_item.parent()
|
||||
if group_item.checkState(1) == Qt.CheckState.Unchecked:
|
||||
return None
|
||||
@@ -701,7 +681,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
}
|
||||
}
|
||||
self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged)
|
||||
user_item = QTreeWidgetItem(group_item, TreeItemType.USER.value)
|
||||
user_item = QTreeWidgetItem(group_item, ALUserTreeItemType.USER.value)
|
||||
user_item.setText(0, new_user["username"])
|
||||
user_item.setText(1, "")
|
||||
user_item.setData(0, Qt.UserRole, new_user)
|
||||
@@ -712,7 +692,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged)
|
||||
return user_item
|
||||
|
||||
|
||||
def delUser(
|
||||
self,
|
||||
user_item: QTreeWidgetItem = None
|
||||
@@ -720,7 +699,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
|
||||
if user_item is None:
|
||||
return
|
||||
if user_item.type() != TreeItemType.USER.value:
|
||||
if user_item.type() != ALUserTreeItemType.USER.value:
|
||||
return
|
||||
parent_item = user_item.parent()
|
||||
index = parent_item.indexOfChild(user_item)
|
||||
@@ -728,20 +707,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
if parent_item.childCount() == 0:
|
||||
self.UserTreeWidget.setCurrentItem(None)
|
||||
|
||||
|
||||
def delGroup(
|
||||
self,
|
||||
group_item: QTreeWidgetItem = None
|
||||
):
|
||||
|
||||
if group_item is None:
|
||||
return
|
||||
if group_item.type() != TreeItemType.GROUP.value:
|
||||
return
|
||||
index = self.UserTreeWidget.indexOfTopLevelItem(group_item)
|
||||
self.UserTreeWidget.takeTopLevelItem(index)
|
||||
|
||||
|
||||
def renameItem(
|
||||
self,
|
||||
item: QTreeWidgetItem,
|
||||
@@ -761,7 +726,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
if not ok or not new_name:
|
||||
return
|
||||
item.setText(0, new_name)
|
||||
if item.type() == TreeItemType.GROUP.value:
|
||||
if item.type() == ALUserTreeItemType.GROUP.value:
|
||||
item.setText(0, new_name)
|
||||
else:
|
||||
user = item.data(0, Qt.UserRole)
|
||||
@@ -770,7 +735,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
item.setData(0, Qt.UserRole, user)
|
||||
self.setUserToWidget(user)
|
||||
|
||||
|
||||
@Slot()
|
||||
def onShowPasswordCheckBoxChecked(
|
||||
self,
|
||||
@@ -792,20 +756,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
self.RoomComboBox.addItems(self.__floor_room_map[floor])
|
||||
self.RoomComboBox.setCurrentIndex(0)
|
||||
|
||||
@Slot()
|
||||
def onSeatMapWidgetClosed(
|
||||
self,
|
||||
selected_seats: list[str]
|
||||
):
|
||||
|
||||
self.__seat_map_widget.seatMapWidgetClosed.disconnect(self.onSeatMapWidgetClosed)
|
||||
self.__seat_map_widget.deleteLater()
|
||||
self.__seat_map_widget = None
|
||||
if len(selected_seats) == 0:
|
||||
self.SeatIDEdit.clear() # no selected seat, we clear the edit
|
||||
return
|
||||
self.SeatIDEdit.setText(",".join(selected_seats))
|
||||
|
||||
@Slot()
|
||||
def onSelectSeatsButtonClicked(
|
||||
self
|
||||
@@ -815,18 +765,19 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
room = self.RoomComboBox.currentText()
|
||||
floor_idx = self.__floor_rmap[floor]
|
||||
room_idx = self.__room_rmap[room]
|
||||
if self.__seat_map_widget is None:
|
||||
self.__seat_map_widget = ALSeatMapWidget(
|
||||
self,
|
||||
floor,
|
||||
room,
|
||||
seats_maps[floor_idx][room_idx]
|
||||
)
|
||||
self.__seat_map_widget.seatMapWidgetClosed.connect(self.onSeatMapWidgetClosed)
|
||||
self.__seat_map_widget.show()
|
||||
self.__seat_map_widget.raise_()
|
||||
self.__seat_map_widget.activateWindow()
|
||||
self.__seat_map_widget.selectSeats(self.SeatIDEdit.text().split(","))
|
||||
Dialog = ALSeatMapSelectDialog(
|
||||
self,
|
||||
floor,
|
||||
room,
|
||||
ALSeatMapTable[floor_idx][room_idx]
|
||||
)
|
||||
Dialog.selectSeats(self.SeatIDEdit.text().split(","))
|
||||
if Dialog.exec() == QDialog.DialogCode.Accepted:
|
||||
selected_seats = Dialog.getSelectedSeats()
|
||||
if len(selected_seats) == 0:
|
||||
self.SeatIDEdit.clear()
|
||||
return
|
||||
self.SeatIDEdit.setText(",".join(Dialog.getSelectedSeats()))
|
||||
|
||||
@Slot()
|
||||
def onUserTreeWidgetCurrentItemChanged(
|
||||
@@ -838,8 +789,8 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
# cant effectively update the data of each user, due to the
|
||||
# possiblity of frequency edit. we just let the QListWidget
|
||||
# help us.
|
||||
if previous and previous.type() == TreeItemType.USER.value:
|
||||
user = self.collectUserFromUserInfoWidget()
|
||||
if previous and previous.type() == ALUserTreeItemType.USER.value:
|
||||
user = self.collectUserFromWidget()
|
||||
if user:
|
||||
self.UsernameEdit.textEdited.disconnect()
|
||||
user["enabled"] = previous.checkState(1) == Qt.Checked
|
||||
@@ -847,15 +798,15 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
previous.setText(1, "" if user.get("enabled", True) else "跳过")
|
||||
previous.setData(0, Qt.UserRole, user)
|
||||
if current is None:
|
||||
self.initilizeUserInfoWidget()
|
||||
self.initializeUserInfoWidget()
|
||||
return
|
||||
if current.type() == TreeItemType.USER.value:
|
||||
if current.type() == ALUserTreeItemType.USER.value:
|
||||
user = current.data(0, Qt.UserRole)
|
||||
if user:
|
||||
self.setUserToWidget(user)
|
||||
self.UsernameEdit.textEdited.connect(lambda text: current.setText(0, text))
|
||||
else:
|
||||
self.initilizeUserInfoWidget()
|
||||
self.initializeUserInfoWidget()
|
||||
|
||||
@Slot()
|
||||
def onUserTreeWidgetItemChanged(
|
||||
@@ -868,7 +819,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
return
|
||||
if column != 1:
|
||||
return
|
||||
if item.type() == TreeItemType.GROUP.value:
|
||||
if item.type() == ALUserTreeItemType.GROUP.value:
|
||||
is_checked = item.checkState(1) == Qt.CheckState.Checked
|
||||
for i in range(item.childCount()):
|
||||
child = item.child(i)
|
||||
@@ -879,16 +830,14 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
is_checked = item.checkState(1) == Qt.CheckState.Checked
|
||||
item.setText(1, "" if is_checked else "跳过")
|
||||
|
||||
|
||||
def showTreeMenu(
|
||||
self,
|
||||
menu: QMenu
|
||||
):
|
||||
|
||||
add_group_action = QAction("添加分组", menu)
|
||||
add_group_action.triggered.connect(self.addGroup)
|
||||
menu.addAction(add_group_action)
|
||||
|
||||
AddGroupAction = QAction("添加分组", menu)
|
||||
AddGroupAction.triggered.connect(self.addGroup)
|
||||
menu.addAction(AddGroupAction)
|
||||
|
||||
def showGroupMenu(
|
||||
self,
|
||||
@@ -896,19 +845,18 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
group_item: QTreeWidgetItem = None
|
||||
):
|
||||
|
||||
add_user_action = QAction("添加用户", menu)
|
||||
rename_group_action = QAction("重命名分组", menu)
|
||||
del_group_action = QAction("删除分组", menu)
|
||||
add_user_action.triggered.connect(lambda: self.addUser(group_item))
|
||||
rename_group_action.triggered.connect(lambda: self.renameItem(group_item))
|
||||
del_group_action.triggered.connect(lambda: self.delGroup(group_item))
|
||||
menu.addAction(add_user_action)
|
||||
AddUserAction = QAction("添加用户", menu)
|
||||
RenameGroupAction = QAction("重命名分组", menu)
|
||||
DelGroupAction = QAction("删除分组", menu)
|
||||
AddUserAction.triggered.connect(lambda: self.addUser(group_item))
|
||||
RenameGroupAction.triggered.connect(lambda: self.renameItem(group_item))
|
||||
DelGroupAction.triggered.connect(lambda: self.delGroup(group_item))
|
||||
menu.addAction(AddUserAction)
|
||||
menu.addSeparator()
|
||||
menu.addAction(rename_group_action)
|
||||
menu.addAction(del_group_action)
|
||||
menu.addAction(RenameGroupAction)
|
||||
menu.addAction(DelGroupAction)
|
||||
if group_item.checkState(1) == Qt.CheckState.Unchecked:
|
||||
add_user_action.setEnabled(False)
|
||||
|
||||
AddUserAction.setEnabled(False)
|
||||
|
||||
def showUserMenu(
|
||||
self,
|
||||
@@ -916,12 +864,12 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
user_item: QTreeWidgetItem = None
|
||||
):
|
||||
|
||||
rename_user_action = QAction("重命名用户", menu)
|
||||
del_user_action = QAction("删除用户", menu)
|
||||
rename_user_action.triggered.connect(lambda: self.renameItem(user_item))
|
||||
del_user_action.triggered.connect(lambda: self.delUser(user_item))
|
||||
menu.addAction(rename_user_action)
|
||||
menu.addAction(del_user_action)
|
||||
RenameUserAction = QAction("重命名用户", menu)
|
||||
DelUserAction = QAction("删除用户", menu)
|
||||
RenameUserAction.triggered.connect(lambda: self.renameItem(user_item))
|
||||
DelUserAction.triggered.connect(lambda: self.delUser(user_item))
|
||||
menu.addAction(RenameUserAction)
|
||||
menu.addAction(DelUserAction)
|
||||
|
||||
@Slot()
|
||||
def onUserTreeWidgetContextMenu(
|
||||
@@ -930,14 +878,14 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
):
|
||||
|
||||
current_item = self.UserTreeWidget.itemAt(pos)
|
||||
menu = QMenu(self.UserTreeWidget)
|
||||
Menu = QMenu(self.UserTreeWidget)
|
||||
if current_item is None:
|
||||
self.showTreeMenu(menu)
|
||||
elif current_item.type() == TreeItemType.GROUP.value:
|
||||
self.showGroupMenu(menu, current_item)
|
||||
self.showTreeMenu(Menu)
|
||||
elif current_item.type() == ALUserTreeItemType.GROUP.value:
|
||||
self.showGroupMenu(Menu, current_item)
|
||||
else:
|
||||
self.showUserMenu(menu, current_item)
|
||||
menu.exec_(self.UserTreeWidget.mapToGlobal(pos))
|
||||
self.showUserMenu(Menu, current_item)
|
||||
Menu.exec_(self.UserTreeWidget.mapToGlobal(pos))
|
||||
|
||||
@Slot()
|
||||
def onAddUserButtonClicked(
|
||||
@@ -969,6 +917,19 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
if browser_driver_path:
|
||||
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(browser_driver_path))
|
||||
|
||||
@Slot()
|
||||
def onAutoDownloadWebDriverButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
Dialog = ALWebDriverDownloadDialog(self)
|
||||
Dialog.show()
|
||||
Dialog.exec_()
|
||||
selected_driver_info = Dialog.getSelectedDriverInfo()
|
||||
if selected_driver_info and selected_driver_info.driver_path:
|
||||
self.BrowserTypeComboBox.setCurrentText(selected_driver_info.driver_type.value)
|
||||
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(str(selected_driver_info.driver_path)))
|
||||
|
||||
@Slot()
|
||||
def onBrowseCurrentRunConfigButtonClicked(
|
||||
self
|
||||
@@ -982,9 +943,27 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
)[0]
|
||||
if run_config_path:
|
||||
run_config_path = QDir.toNativeSeparators(run_config_path)
|
||||
if self.loadConfig(run_config_path):
|
||||
data = self.loadRunConfig(run_config_path)
|
||||
if data is not None:
|
||||
self.__config_data["run"].update(data)
|
||||
self.setRunConfigToWidget(data)
|
||||
self.__config_paths["run"] = run_config_path
|
||||
self.CurrentRunConfigEdit.setText(run_config_path)
|
||||
paths = self.__cfg_mgr.get(CfgKey.GLOBAL.AUTOMATION.RUN_PATH.PATHS, [])
|
||||
if run_config_path not in paths:
|
||||
paths.append(run_config_path)
|
||||
index = len(paths) - 1
|
||||
else:
|
||||
index = paths.index(run_config_path)
|
||||
self.__cfg_mgr.set(CfgKey.GLOBAL.AUTOMATION.RUN_PATH.ROOT, {"current": index, "paths": paths})
|
||||
else:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
"运行配置文件读取发生错误 ! :\n"\
|
||||
"无法从选择的运行配置文件中加载数据 ! :\n"\
|
||||
"可能选择了错误的配置文件类型"
|
||||
)
|
||||
|
||||
@Slot()
|
||||
def onBrowseCurrentUserConfigButtonClicked(
|
||||
@@ -999,9 +978,27 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
)[0]
|
||||
if user_config_path:
|
||||
user_config_path = QDir.toNativeSeparators(user_config_path)
|
||||
if self.loadConfig(user_config_path):
|
||||
data = self.loadUserConfig(user_config_path)
|
||||
if data is not None:
|
||||
self.__config_data["user"].update(data)
|
||||
self.setUsersToTreeWidget(data)
|
||||
self.__config_paths["user"] = user_config_path
|
||||
self.CurrentUserConfigEdit.setText(user_config_path)
|
||||
paths = self.__cfg_mgr.get(CfgKey.GLOBAL.AUTOMATION.USER_PATH.PATHS, [])
|
||||
if user_config_path not in paths:
|
||||
paths.append(user_config_path)
|
||||
index = len(paths) - 1
|
||||
else:
|
||||
index = paths.index(user_config_path)
|
||||
self.__cfg_mgr.set(CfgKey.GLOBAL.AUTOMATION.USER_PATH.ROOT, {"current": index, "paths": paths})
|
||||
else:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
"用户配置文件读取发生错误 ! :\n"\
|
||||
"无法从选择的用户配置文件中加载数据 ! :\n"\
|
||||
"可能选择了错误的配置文件类型"
|
||||
)
|
||||
|
||||
@Slot()
|
||||
def onBrowseExportRunConfigButtonClicked(
|
||||
@@ -1037,7 +1034,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
):
|
||||
|
||||
msg = ""
|
||||
|
||||
run_config_path = self.ExportRunConfigEdit.text()
|
||||
user_config_path = self.ExportUserConfigEdit.text()
|
||||
if run_config_path:
|
||||
@@ -1088,9 +1084,9 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
if run_exists or user_exists:
|
||||
exist_files = []
|
||||
if run_exists:
|
||||
exist_files.append(run_config_path)
|
||||
exist_files.append(f"运行配置文件: \n{run_config_path}")
|
||||
if user_exists:
|
||||
exist_files.append(user_config_path)
|
||||
exist_files.append(f"用户配置文件: \n{user_config_path}")
|
||||
reply = QMessageBox.information(
|
||||
self,
|
||||
"提示 - AutoLibrary",
|
||||
@@ -1106,8 +1102,8 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
"run": run_config_path,
|
||||
"user": user_config_path
|
||||
}
|
||||
self.initlizeConfigToWidget("run", self.__config_data["run"])
|
||||
self.initlizeConfigToWidget("user", self.__config_data["user"])
|
||||
self.initializeConfigToWidget("run", self.__config_data["run"])
|
||||
self.initializeConfigToWidget("user", self.__config_data["user"])
|
||||
|
||||
@Slot()
|
||||
def onConfirmButtonClicked(
|
||||
@@ -1115,7 +1111,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
):
|
||||
|
||||
current_item = self.UserTreeWidget.currentItem()
|
||||
if current_item and current_item.type() == TreeItemType.USER.value:
|
||||
if current_item and current_item.type() == ALUserTreeItemType.USER.value:
|
||||
self.UserTreeWidget.setCurrentItem(None)
|
||||
if self.saveConfigs(
|
||||
self.__config_paths["run"],
|
||||
@@ -1124,7 +1120,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"提示 - AutoLibrary",
|
||||
"配置文件保存成功 !\n"
|
||||
"配置文件保存成功 ! :\n"
|
||||
f"运行配置文件路径: \n{self.__config_paths['run']}\n"\
|
||||
f"用户配置文件路径: \n{self.__config_paths['user']}"
|
||||
)
|
||||
@@ -1132,7 +1128,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
"配置文件保存失败, 请检查文件路径权限"
|
||||
"配置文件保存失败 !\n"
|
||||
)
|
||||
self.close()
|
||||
|
||||
|
||||
@@ -1,137 +1,159 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 - 2026 KenanZhu.
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import sys
|
||||
import time
|
||||
import queue
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Qt, Signal, Slot, QDir, QFileInfo, QTimer, QUrl,
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QMainWindow, QMenu, QSystemTrayIcon
|
||||
QTimer,
|
||||
QUrl,
|
||||
Qt,
|
||||
Slot
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QTextCursor, QCloseEvent, QFont, QIcon, QDesktopServices
|
||||
QCloseEvent,
|
||||
QDesktopServices,
|
||||
QFont,
|
||||
QIcon,
|
||||
QTextCursor
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QMainWindow,
|
||||
QMenu,
|
||||
QMessageBox,
|
||||
QSystemTrayIcon
|
||||
)
|
||||
|
||||
from gui.Ui_ALMainWindow import Ui_ALMainWindow
|
||||
from gui.ALConfigWidget import ALConfigWidget
|
||||
from gui.ALTimerTaskWidget import ALTimerTaskWidget
|
||||
from base.MsgBase import MsgBase
|
||||
from gui.ALAboutDialog import ALAboutDialog
|
||||
from gui.ALMainWorkers import TimerTaskWorker, AutoLibWorker
|
||||
|
||||
from gui import AutoLibraryResource
|
||||
from gui.ALBulletinDialog import ALBulletinDialog
|
||||
from gui.ALConfigWidget import ALConfigWidget
|
||||
from gui.ALSettingsWidget import ALSettingsWidget
|
||||
from gui.ALTimerTaskManageWidget import ALTimerTaskManageWidget
|
||||
from gui.ALMainWorker import AutoLibWorker
|
||||
from gui.ALBulletinPoller import ALBulletinPoller
|
||||
from gui.ALTimerTaskPoller import ALTimerTaskPoller
|
||||
from gui.resources import ALResource
|
||||
from gui.resources.ui.Ui_ALMainWindow import Ui_ALMainWindow
|
||||
from managers.bulletin.BulletinManager import instance as bulletinInstance
|
||||
from managers.config.ConfigUtils import ConfigUtils
|
||||
|
||||
|
||||
class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
|
||||
timerTaskIsRunning = Signal(dict)
|
||||
timerTaskIsExecuted = Signal(dict)
|
||||
timerTaskIsError = Signal(dict)
|
||||
class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
||||
|
||||
def __init__(
|
||||
self
|
||||
):
|
||||
|
||||
super().__init__()
|
||||
self.__class_name = self.__class__.__name__
|
||||
self.__input_queue = queue.Queue()
|
||||
self.__output_queue = queue.Queue()
|
||||
self.__timer_task_queue = queue.Queue()
|
||||
script_path = sys.executable
|
||||
script_dir = QFileInfo(script_path).absoluteDir()
|
||||
self.__config_paths = {
|
||||
"run": QDir.toNativeSeparators(script_dir.absoluteFilePath("run.json")),
|
||||
"user": QDir.toNativeSeparators(script_dir.absoluteFilePath("user.json")),
|
||||
"timer_task": QDir.toNativeSeparators(script_dir.absoluteFilePath("timer_task.json")),
|
||||
}
|
||||
self.__alTimerTaskWidget = None
|
||||
self.__alConfigWidget = None
|
||||
MsgBase.__init__(self, queue.Queue(), queue.Queue())
|
||||
QMainWindow.__init__(self)
|
||||
self.__config_paths = ConfigUtils.getAutomationConfigPaths()
|
||||
self.__ALTimerTaskManageWidget = None
|
||||
self.__ALConfigWidget = None
|
||||
self.__ALSettingsWidget = None
|
||||
self.__ALBulletinDialog = None
|
||||
self.__auto_lib_thread = None
|
||||
self.__current_timer_task_thread = None
|
||||
self.__is_running_timer_task = False
|
||||
self.__bulletin_poller = ALBulletinPoller(self)
|
||||
self.__timer_task_poller = ALTimerTaskPoller(
|
||||
self,
|
||||
self._input_queue,
|
||||
self._output_queue,
|
||||
self.__config_paths
|
||||
)
|
||||
self.__notification_type = ""
|
||||
|
||||
self.setupUi(self)
|
||||
self.modifyUi()
|
||||
self.setupTray()
|
||||
self.connectSignals()
|
||||
self.startMsgPolling()
|
||||
self.startTimerTaskPolling()
|
||||
|
||||
self.__timer_task_poller.start()
|
||||
self.__bulletin_poller.start()
|
||||
if bulletinInstance().autoFetch():
|
||||
QTimer.singleShot(1000, self.__bulletin_poller.fetchNow)
|
||||
self._showLog("主窗口初始化完成")
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.icon = QIcon(":/res/icon/icons/AutoLibrary_32x32.ico")
|
||||
self.setWindowIcon(self.icon)
|
||||
self.Icon = QIcon(":/res/icons/AutoLibrary_Logo_64.svg")
|
||||
self.setWindowIcon(self.Icon)
|
||||
self.MessageIOTextEdit.setFont(QFont("Courier New", 10))
|
||||
self.ManualAction.triggered.connect(self.onManualActionTriggered)
|
||||
self.AboutAction.triggered.connect(self.onAboutActionTriggered)
|
||||
|
||||
self.SettingsAction.triggered.connect(self.onSettingsActionTriggered)
|
||||
if hasattr(self, 'BulletinAction'):
|
||||
self.BulletinAction.triggered.connect(self.onBulletinActionTriggered)
|
||||
# initialize timer task widget, but not show it
|
||||
self.__alTimerTaskWidget = ALTimerTaskWidget(self, self.__config_paths["timer_task"])
|
||||
self.timerTaskIsRunning.connect(self.__alTimerTaskWidget.onTimerTaskIsRunning)
|
||||
self.timerTaskIsExecuted.connect(self.__alTimerTaskWidget.onTimerTaskIsExecuted)
|
||||
self.timerTaskIsError.connect(self.__alTimerTaskWidget.onTimerTaskIsError)
|
||||
self.__alTimerTaskWidget.timerTaskIsReady.connect(self.onTimerTaskIsReady)
|
||||
self.__alTimerTaskWidget.timerTaskWidgetClosed.connect(self.onTimerTaskWidgetClosed)
|
||||
self.__alTimerTaskWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint)
|
||||
|
||||
try:
|
||||
self.__ALTimerTaskManageWidget = ALTimerTaskManageWidget(self)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
"错误 - AutoLibrary",
|
||||
f"初始化定时任务功能失败: \n{e}"
|
||||
)
|
||||
self.__ALTimerTaskManageWidget = None
|
||||
self.TimerTaskManageWidgetButton.setEnabled(False)
|
||||
self.TimerTaskManageWidgetButton.setToolTip("定时任务功能初始化失败, 请检查配置文件。")
|
||||
return
|
||||
self.__timer_task_poller.taskRunning.connect(self.onTimerTaskRunning)
|
||||
self.__timer_task_poller.taskFinished.connect(self.onTimerTaskFinished)
|
||||
self.__timer_task_poller.taskRunning.connect(self.__ALTimerTaskManageWidget.onTimerTaskIsRunning)
|
||||
self.__timer_task_poller.taskExecuted.connect(self.__ALTimerTaskManageWidget.onTimerTaskIsExecuted)
|
||||
self.__timer_task_poller.taskError.connect(self.__ALTimerTaskManageWidget.onTimerTaskIsError)
|
||||
self.__ALTimerTaskManageWidget.timerTaskIsReady.connect(self.__timer_task_poller.enqueue)
|
||||
self.__ALTimerTaskManageWidget.timerTaskManageWidgetIsClosed.connect(self.onTimerTaskManageWidgetClosed)
|
||||
self.__ALTimerTaskManageWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint)
|
||||
|
||||
def onAboutActionTriggered(
|
||||
self
|
||||
):
|
||||
|
||||
about_dialog = ALAboutDialog(self)
|
||||
about_dialog.exec()
|
||||
|
||||
AboutDialog = ALAboutDialog(self)
|
||||
AboutDialog.exec()
|
||||
|
||||
def onManualActionTriggered(
|
||||
self
|
||||
):
|
||||
|
||||
url = QUrl("https://www.autolibrary.cv/docs/manual_lists.html")
|
||||
url = QUrl("https://manuals.autolibrary.kenanzhu.com")
|
||||
QDesktopServices.openUrl(url)
|
||||
|
||||
|
||||
def setupTray(
|
||||
self
|
||||
):
|
||||
|
||||
if not QSystemTrayIcon.isSystemTrayAvailable():
|
||||
self.showTraceSignal.emit(
|
||||
"系统不支持系统托盘功能, 无法创建系统托盘图标。"
|
||||
)
|
||||
self._showTrace("操作系统不支持系统托盘功能, 无法创建系统托盘图标", self.TraceLevel.WARNING)
|
||||
return
|
||||
self.TrayIcon = QSystemTrayIcon(self.icon, self)
|
||||
self.TrayIcon = QSystemTrayIcon(self.Icon, self)
|
||||
self.TrayIcon.setToolTip("AutoLibrary")
|
||||
|
||||
self.TrayMenu = QMenu()
|
||||
self.TrayMenu.addAction("显示主窗口", self.showNormal)
|
||||
self.TrayMenu.addAction("显示定时窗口", self.onTimerTaskWidgetButtonClicked)
|
||||
self.TrayMenu.addAction("显示定时窗口", self.onTimerTaskManageWidgetButtonClicked)
|
||||
self.TrayMenu.addAction("最小化到托盘", self.hideToTray)
|
||||
self.TrayMenu.addSeparator()
|
||||
self.TrayMenu.addAction("退出", self.close)
|
||||
self.TrayIcon.setContextMenu(self.TrayMenu)
|
||||
|
||||
self.TrayIcon.setContextMenu(self.TrayMenu)
|
||||
self.TrayIcon.activated.connect(self.onTrayIconActivated)
|
||||
self.TrayIcon.messageClicked.connect(self.onTrayMessageClicked)
|
||||
self.TrayIcon.show()
|
||||
|
||||
|
||||
def hideToTray(
|
||||
self
|
||||
):
|
||||
|
||||
self.hide()
|
||||
self.__notification_type = ""
|
||||
if not hasattr(self, "TrayIcon"):
|
||||
return
|
||||
self.TrayIcon.showMessage(
|
||||
"AutoLibrary",
|
||||
"\n已最小化到托盘",
|
||||
@@ -139,7 +161,6 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
2000
|
||||
)
|
||||
|
||||
|
||||
def onTrayIconActivated(
|
||||
self,
|
||||
reason: QSystemTrayIcon.ActivationReason
|
||||
@@ -148,39 +169,47 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
if reason == QSystemTrayIcon.DoubleClick:
|
||||
self.showNormal()
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.ConfigButton.clicked.connect(self.onConfigButtonClicked)
|
||||
self.TimerTaskWidgetButton.clicked.connect(self.onTimerTaskWidgetButtonClicked)
|
||||
self.TimerTaskManageWidgetButton.clicked.connect(self.onTimerTaskManageWidgetButtonClicked)
|
||||
self.StartButton.clicked.connect(self.onStartButtonClicked)
|
||||
self.StopButton.clicked.connect(self.onStopButtonClicked)
|
||||
self.SendButton.clicked.connect(self.onSendButtonClicked)
|
||||
self.MessageEdit.returnPressed.connect(self.onSendButtonClicked)
|
||||
|
||||
self.__bulletin_poller.newBulletinsDetected.connect(self.onBulletinPollerNewBulletins)
|
||||
|
||||
def closeEvent(
|
||||
self,
|
||||
event: QCloseEvent
|
||||
):
|
||||
|
||||
if not self.isVisible():
|
||||
self.showNormal()
|
||||
event.ignore()
|
||||
return
|
||||
if self.__msg_queue_timer and self.__msg_queue_timer.isActive():
|
||||
self.__msg_queue_timer.stop()
|
||||
if self.__timer_task_timer and self.__timer_task_timer.isActive():
|
||||
self.__timer_task_timer.stop()
|
||||
if self.__is_running_timer_task:
|
||||
self.__current_timer_task_thread.wait(2000)
|
||||
self.__current_timer_task_thread.deleteLater()
|
||||
if self.__alTimerTaskWidget:
|
||||
self.__alTimerTaskWidget.close()
|
||||
self.__alTimerTaskWidget.deleteLater()
|
||||
if self.__alConfigWidget:
|
||||
self.__alConfigWidget.close()
|
||||
if self.__timer_task_poller:
|
||||
self.__timer_task_poller.stop()
|
||||
if self.__ALTimerTaskManageWidget:
|
||||
self.__ALTimerTaskManageWidget.close()
|
||||
self.__ALTimerTaskManageWidget.deleteLater()
|
||||
if self.__ALConfigWidget:
|
||||
self.__ALConfigWidget.close()
|
||||
# the config widget is already deleted in the 'self.onConfigWidgetClosed'
|
||||
super().closeEvent(event)
|
||||
|
||||
if self.__ALSettingsWidget:
|
||||
self.__ALSettingsWidget.close()
|
||||
# the settings widget is already deleted in the 'self.onSettingsWidgetClosed'
|
||||
if self.__ALBulletinDialog:
|
||||
self.__ALBulletinDialog.close()
|
||||
# the bulletin dialog is already deleted in the 'self.onBulletinDialogClosed'
|
||||
if self.__bulletin_poller:
|
||||
self.__bulletin_poller.stop()
|
||||
self._showLog("主窗口关闭")
|
||||
QMainWindow.closeEvent(self, event)
|
||||
|
||||
def appendToTextEdit(
|
||||
self,
|
||||
@@ -195,7 +224,6 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
scrollbar = self.MessageIOTextEdit.verticalScrollBar()
|
||||
scrollbar.setValue(scrollbar.maximum())
|
||||
|
||||
|
||||
def startMsgPolling(
|
||||
self
|
||||
):
|
||||
@@ -204,50 +232,6 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
self.__msg_queue_timer.timeout.connect(self.pollMsgQueue)
|
||||
self.__msg_queue_timer.start(100)
|
||||
|
||||
|
||||
def startTimerTaskPolling(
|
||||
self
|
||||
):
|
||||
|
||||
self.__timer_task_timer = QTimer()
|
||||
self.__timer_task_timer.timeout.connect(self.pollTimerTaskQueue)
|
||||
self.__timer_task_timer.start(500)
|
||||
|
||||
|
||||
def pollTimerTaskQueue(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__is_running_timer_task:
|
||||
return
|
||||
try:
|
||||
while not self.__is_running_timer_task:
|
||||
timer_task = self.__timer_task_queue.get_nowait()
|
||||
self.timerTaskIsRunning.emit(timer_task)
|
||||
self.__timer_task_timer.stop()
|
||||
self.__is_running_timer_task = True
|
||||
self.setControlButtons(True, True, False)
|
||||
if not timer_task["silent"]:
|
||||
self.TrayIcon.showMessage(
|
||||
"定时任务 - AutoLibrary",
|
||||
f"\n已开始执行定时任务: \n{timer_task['name']}",
|
||||
QSystemTrayIcon.MessageIcon.Information,
|
||||
1000
|
||||
)
|
||||
self.showNormal()
|
||||
self.__current_timer_task_thread = TimerTaskWorker(
|
||||
timer_task,
|
||||
self.__input_queue,
|
||||
self.__output_queue,
|
||||
self.__config_paths
|
||||
)
|
||||
self.__current_timer_task_thread.finishedSignal_TimerWorker.connect(self.onTimerTaskFinished)
|
||||
self.__current_timer_task_thread.start()
|
||||
except queue.Empty:
|
||||
self.__is_running_timer_task = False
|
||||
pass
|
||||
|
||||
|
||||
def setControlButtons(
|
||||
self,
|
||||
config_button_enabled: bool,
|
||||
@@ -263,23 +247,6 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
if start_button_enabled is not None:
|
||||
self.StartButton.setEnabled(start_button_enabled)
|
||||
|
||||
@Slot()
|
||||
def showMsg(
|
||||
self,
|
||||
msg: str
|
||||
):
|
||||
|
||||
self.__output_queue.put(f"[{self.__class_name:<15}] >>> : {msg}")
|
||||
|
||||
@Slot()
|
||||
def showTrace(
|
||||
self,
|
||||
msg: str
|
||||
):
|
||||
|
||||
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
self.__output_queue.put(f"{timestamp}-[{self.__class_name:<15}] : {msg}")
|
||||
|
||||
@Slot()
|
||||
def pollMsgQueue(
|
||||
self
|
||||
@@ -287,93 +254,179 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
|
||||
try:
|
||||
while True:
|
||||
msg = self.__output_queue.get_nowait()
|
||||
msg = self._output_queue.get_nowait()
|
||||
self.appendToTextEdit(msg)
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
@Slot()
|
||||
def onTimerTaskWidgetClosed(
|
||||
self
|
||||
):
|
||||
|
||||
self.TimerTaskWidgetButton.setEnabled(True)
|
||||
|
||||
@Slot(dict)
|
||||
def onConfigWidgetClosed(
|
||||
@Slot(int)
|
||||
def onBulletinPollerNewBulletins(
|
||||
self,
|
||||
config_paths: dict
|
||||
count: int
|
||||
):
|
||||
|
||||
if self.__alConfigWidget:
|
||||
self.__alConfigWidget.configWidgetCloseSingal.disconnect(self.onConfigWidgetClosed)
|
||||
self.__alConfigWidget.deleteLater()
|
||||
self.__alConfigWidget = None
|
||||
self.setControlButtons(True, None, None)
|
||||
self.__config_paths = config_paths
|
||||
if not hasattr(self, "TrayIcon"):
|
||||
return
|
||||
self.__notification_type = "bulletin"
|
||||
self.TrayIcon.showMessage(
|
||||
"公告栏 - AutoLibrary",
|
||||
f"有 {count} 条新公告,点击查看详情。",
|
||||
QSystemTrayIcon.MessageIcon.Information,
|
||||
3000
|
||||
)
|
||||
|
||||
@Slot(dict)
|
||||
def onTimerTaskIsReady(
|
||||
def onTimerTaskRunning(
|
||||
self,
|
||||
timer_task: dict
|
||||
):
|
||||
|
||||
self.__timer_task_queue.put(timer_task)
|
||||
self.setControlButtons(None, True, False)
|
||||
self.__notification_type = ""
|
||||
if not hasattr(self, "TrayIcon"):
|
||||
return
|
||||
if not timer_task.get("silent", False):
|
||||
self.TrayIcon.showMessage(
|
||||
"定时任务 - AutoLibrary",
|
||||
f"\n已开始执行定时任务: \n{timer_task['name']}",
|
||||
QSystemTrayIcon.MessageIcon.Information,
|
||||
1000
|
||||
)
|
||||
self.showNormal()
|
||||
|
||||
@Slot(dict)
|
||||
@Slot(bool, dict)
|
||||
def onTimerTaskFinished(
|
||||
self,
|
||||
is_error: bool,
|
||||
timer_task: dict
|
||||
):
|
||||
|
||||
self.__current_timer_task_thread.wait(1000)
|
||||
self.__current_timer_task_thread.finishedSignal_TimerWorker.disconnect(self.onTimerTaskFinished)
|
||||
self.__current_timer_task_thread.deleteLater()
|
||||
self.__current_timer_task_thread = None
|
||||
self.setControlButtons(None, False, True)
|
||||
self.__is_running_timer_task = False
|
||||
self.__timer_task_timer.start(500)
|
||||
timer_task["executed"] = True
|
||||
self.__notification_type = ""
|
||||
if not hasattr(self, "TrayIcon"):
|
||||
return
|
||||
self.TrayIcon.showMessage(
|
||||
"定时任务 - AutoLibrary",
|
||||
f"\n定时任务 '{timer_task['name']}' 执行{'失败' if is_error else '完成'}",
|
||||
QSystemTrayIcon.MessageIcon.Information,
|
||||
QSystemTrayIcon.MessageIcon.Warning if is_error else QSystemTrayIcon.MessageIcon.Information,
|
||||
1000
|
||||
)
|
||||
self.showTrace(
|
||||
f"定时任务 {timer_task['name']} 执行{'失败' if is_error else '完成'}, uuid: {timer_task['task_uuid']}"
|
||||
self._showTrace(
|
||||
f"定时任务 {timer_task['name']} 执行{'失败' if is_error else '完成'}, uuid: {timer_task['uuid']}"
|
||||
)
|
||||
if not is_error:
|
||||
self.timerTaskIsExecuted.emit(timer_task)
|
||||
else:
|
||||
self.timerTaskIsError.emit(timer_task)
|
||||
|
||||
@Slot()
|
||||
def onTimerTaskWidgetButtonClicked(
|
||||
def onBulletinDialogClosed(
|
||||
self
|
||||
):
|
||||
|
||||
self.__alTimerTaskWidget.show()
|
||||
self.__alTimerTaskWidget.raise_()
|
||||
self.__alTimerTaskWidget.activateWindow()
|
||||
self.TimerTaskWidgetButton.setEnabled(False)
|
||||
if self.__ALBulletinDialog:
|
||||
self.__ALBulletinDialog.finished.disconnect(self.onBulletinDialogClosed)
|
||||
self.__ALBulletinDialog.deleteLater()
|
||||
self.__ALBulletinDialog = None
|
||||
self.__bulletin_poller.setDialogOpen(False)
|
||||
|
||||
@Slot()
|
||||
def onSettingsWidgetClosed(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__ALSettingsWidget:
|
||||
self.__ALSettingsWidget.settingsWidgetIsClosed.disconnect(self.onSettingsWidgetClosed)
|
||||
self.__ALSettingsWidget.deleteLater()
|
||||
self.__ALSettingsWidget = None
|
||||
self.SettingsAction.setEnabled(True)
|
||||
|
||||
@Slot()
|
||||
def onTimerTaskManageWidgetClosed(
|
||||
self
|
||||
):
|
||||
|
||||
self.TimerTaskManageWidgetButton.setEnabled(True)
|
||||
|
||||
@Slot(dict)
|
||||
def onConfigWidgetClosed(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__ALConfigWidget:
|
||||
self.__ALConfigWidget.configWidgetIsClosed.disconnect(self.onConfigWidgetClosed)
|
||||
self.__ALConfigWidget.deleteLater()
|
||||
self.__ALConfigWidget = None
|
||||
self.__config_paths = ConfigUtils.getAutomationConfigPaths()
|
||||
self.__timer_task_poller.updateConfigPaths(self.__config_paths)
|
||||
self.setControlButtons(True, None, None)
|
||||
self._showLog("配置窗口已关闭,配置文件路径已更新")
|
||||
|
||||
@Slot()
|
||||
def onTrayMessageClicked(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__notification_type == "bulletin":
|
||||
self.__notification_type = ""
|
||||
self.onBulletinActionTriggered()
|
||||
|
||||
@Slot()
|
||||
def onBulletinActionTriggered(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__ALBulletinDialog is None:
|
||||
self.__ALBulletinDialog = ALBulletinDialog(self)
|
||||
self.__ALBulletinDialog.finished.connect(self.onBulletinDialogClosed)
|
||||
self.__bulletin_poller.setDialogOpen(True)
|
||||
self.__ALBulletinDialog.show()
|
||||
self.__ALBulletinDialog.raise_()
|
||||
self.__ALBulletinDialog.activateWindow()
|
||||
self._showLog("打开公告栏窗口")
|
||||
|
||||
@Slot()
|
||||
def onSettingsActionTriggered(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__ALSettingsWidget is None:
|
||||
self.__ALSettingsWidget = ALSettingsWidget(self)
|
||||
self.__ALSettingsWidget.settingsWidgetIsClosed.connect(self.onSettingsWidgetClosed)
|
||||
self.__ALSettingsWidget.openBulletinRequested.connect(self.onBulletinActionTriggered)
|
||||
self.__ALSettingsWidget.show()
|
||||
self.__ALSettingsWidget.raise_()
|
||||
self.__ALSettingsWidget.activateWindow()
|
||||
self.SettingsAction.setEnabled(False)
|
||||
self._showLog("打开全局设置窗口")
|
||||
|
||||
@Slot()
|
||||
def onTimerTaskManageWidgetButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__ALTimerTaskManageWidget is None:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
"定时任务功能初始化失败, 请检查配置文件。"
|
||||
)
|
||||
return
|
||||
self.__ALTimerTaskManageWidget.show()
|
||||
self.__ALTimerTaskManageWidget.raise_()
|
||||
self.__ALTimerTaskManageWidget.activateWindow()
|
||||
self.TimerTaskManageWidgetButton.setEnabled(False)
|
||||
self._showLog("打开定时任务管理窗口")
|
||||
|
||||
@Slot()
|
||||
def onConfigButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__alConfigWidget is None:
|
||||
self.__alConfigWidget = ALConfigWidget(
|
||||
self,
|
||||
self.__config_paths
|
||||
)
|
||||
self.__alConfigWidget.configWidgetCloseSingal.connect(self.onConfigWidgetClosed)
|
||||
self.__alConfigWidget.show()
|
||||
self.__alConfigWidget.raise_()
|
||||
self.__alConfigWidget.activateWindow()
|
||||
if self.__ALConfigWidget is None:
|
||||
self.__ALConfigWidget = ALConfigWidget(self)
|
||||
self.__ALConfigWidget.configWidgetIsClosed.connect(self.onConfigWidgetClosed)
|
||||
self.__ALConfigWidget.show()
|
||||
self.__ALConfigWidget.raise_()
|
||||
self.__ALConfigWidget.activateWindow()
|
||||
self.ConfigButton.setEnabled(False)
|
||||
self._showLog("打开配置窗口")
|
||||
|
||||
@Slot()
|
||||
def onStartButtonClicked(
|
||||
@@ -383,13 +436,14 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
self.setControlButtons(None, True, False)
|
||||
if self.__auto_lib_thread is None:
|
||||
self.__auto_lib_thread = AutoLibWorker(
|
||||
self.__input_queue,
|
||||
self.__output_queue,
|
||||
self._input_queue,
|
||||
self._output_queue,
|
||||
self.__config_paths
|
||||
)
|
||||
self.__auto_lib_thread.finishedSignal.connect(self.onStopButtonClicked)
|
||||
self.__auto_lib_thread.finishedWithErrorSignal.connect(self.onStopButtonClicked)
|
||||
self.__auto_lib_thread.autoLibWorkerIsFinished.connect(self.onStopButtonClicked)
|
||||
self.__auto_lib_thread.autoLibWorkerFinishedWithError.connect(self.onStopButtonClicked)
|
||||
self.__auto_lib_thread.start()
|
||||
self._showLog("开始手动执行任务")
|
||||
|
||||
@Slot()
|
||||
def onStopButtonClicked(
|
||||
@@ -397,14 +451,15 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
):
|
||||
|
||||
if self.__auto_lib_thread:
|
||||
self.showTrace("正在停止操作......")
|
||||
self._showTrace("正在停止操作......", no_log=True)
|
||||
self.__auto_lib_thread.wait(2000)
|
||||
self.showTrace("操作已停止")
|
||||
self.__auto_lib_thread.finishedSignal.disconnect(self.onStopButtonClicked)
|
||||
self.__auto_lib_thread.finishedWithErrorSignal.disconnect(self.onStopButtonClicked)
|
||||
self._showTrace("操作已停止", no_log=True)
|
||||
self.__auto_lib_thread.autoLibWorkerIsFinished.disconnect(self.onStopButtonClicked)
|
||||
self.__auto_lib_thread.autoLibWorkerFinishedWithError.disconnect(self.onStopButtonClicked)
|
||||
self.__auto_lib_thread.deleteLater()
|
||||
self.__auto_lib_thread = None
|
||||
self.setControlButtons(None, False, True)
|
||||
self._showLog("任务已停止")
|
||||
|
||||
@Slot()
|
||||
def onSendButtonClicked(
|
||||
@@ -414,6 +469,6 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
msg = self.MessageEdit.text().strip()
|
||||
if not msg:
|
||||
return
|
||||
self.showMsg(msg)
|
||||
self.__input_queue.put(msg) # put message to input queue
|
||||
self._showMsg(msg)
|
||||
self._input_queue.put(msg) # put message to input queue
|
||||
self.MessageEdit.clear()
|
||||
@@ -0,0 +1,248 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import queue
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Signal,
|
||||
QThread,
|
||||
)
|
||||
|
||||
from base.MsgBase import MsgBase
|
||||
from pages.AutoLib import AutoLib
|
||||
from utils.JSONReader import JSONReader
|
||||
from autoscript import createEngine
|
||||
|
||||
|
||||
class AutoLibWorker(MsgBase, QThread):
|
||||
|
||||
autoLibWorkerIsFinished = Signal()
|
||||
autoLibWorkerFinishedWithError = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
config_paths: dict,
|
||||
):
|
||||
|
||||
MsgBase.__init__(self, input_queue, output_queue)
|
||||
QThread.__init__(self)
|
||||
self.__config_paths = config_paths
|
||||
|
||||
def checkTimeAvailable(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
current_time = time.strftime("%H:%M", time.localtime())
|
||||
if current_time >= "23:30" or current_time <= "07:30":
|
||||
self._showTrace(
|
||||
"当前时间不在图书馆开放时间内, 请在 07:30 - 23:30 之间尝试",
|
||||
self.TraceLevel.WARNING,
|
||||
)
|
||||
return False
|
||||
self._showLog(f"时间检查通过, 当前时间: {current_time}", self.TraceLevel.INFO)
|
||||
return True
|
||||
|
||||
def checkConfigPaths(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
if not all(
|
||||
os.path.exists(path) for path in self.__config_paths.values()
|
||||
):
|
||||
self._showTrace(
|
||||
"配置文件路径不存在, 请检查配置文件路径是否正确",
|
||||
self.TraceLevel.ERROR,
|
||||
)
|
||||
return False
|
||||
self._showLog(
|
||||
f"配置文件路径检查通过, 路径: {self.__config_paths}",
|
||||
self.TraceLevel.INFO,
|
||||
)
|
||||
return True
|
||||
|
||||
def loadConfigs(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
self._showTrace(
|
||||
f"正在加载配置文件, 运行配置文件路径: {self.__config_paths["run"]}",
|
||||
no_log=True,
|
||||
)
|
||||
self._run_config = JSONReader(self.__config_paths["run"]).data()
|
||||
self._showTrace(
|
||||
f"正在加载配置文件, 用户配置文件路径: {self.__config_paths["user"]}",
|
||||
no_log=True,
|
||||
)
|
||||
self._user_config = JSONReader(self.__config_paths["user"]).data()
|
||||
if self._run_config is None or self._user_config is None:
|
||||
self._showTrace(
|
||||
"配置文件加载失败, 请检查配置文件是否正确",
|
||||
self.TraceLevel.ERROR,
|
||||
)
|
||||
return False
|
||||
if not self._user_config.get("groups"):
|
||||
self._showTrace(
|
||||
"用户配置文件中无有效任务组, 请检查用户配置文件是否正确",
|
||||
self.TraceLevel.WARNING,
|
||||
)
|
||||
return False
|
||||
self._showLog(
|
||||
f"配置文件加载成功, 任务组数量: {len(self._user_config.get("groups"))}",
|
||||
self.TraceLevel.INFO,
|
||||
)
|
||||
return True
|
||||
|
||||
def _runName(
|
||||
self,
|
||||
) -> str:
|
||||
|
||||
return "常规任务"
|
||||
|
||||
def _beforeCreateAutoLib(
|
||||
self,
|
||||
):
|
||||
|
||||
return
|
||||
|
||||
def _onChecksFailed(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
return True
|
||||
|
||||
def _onFinished(
|
||||
self,
|
||||
):
|
||||
|
||||
self.autoLibWorkerIsFinished.emit()
|
||||
|
||||
def _onError(
|
||||
self,
|
||||
error_msg: str,
|
||||
):
|
||||
|
||||
self._showTrace(error_msg, self.TraceLevel.ERROR)
|
||||
self.autoLibWorkerFinishedWithError.emit()
|
||||
|
||||
def run(
|
||||
self,
|
||||
):
|
||||
|
||||
auto_lib = None
|
||||
self._showTrace(f"{self._runName()} 开始运行")
|
||||
|
||||
if not self.checkTimeAvailable() or not self.checkConfigPaths():
|
||||
if not self._onChecksFailed():
|
||||
return
|
||||
else:
|
||||
try:
|
||||
if not self.loadConfigs():
|
||||
raise Exception("配置文件加载失败")
|
||||
self._beforeCreateAutoLib()
|
||||
auto_lib = AutoLib(
|
||||
self._input_queue,
|
||||
self._output_queue,
|
||||
self._run_config,
|
||||
)
|
||||
groups = self._user_config.get("groups")
|
||||
for group in groups:
|
||||
if not group.get("enabled", False):
|
||||
self._showTrace(f"任务组 {group.get("name", "未知")} 已跳过", no_log=True)
|
||||
continue
|
||||
self._showTrace(f"正在运行任务组 {group.get("name", "未知")}", no_log=True)
|
||||
auto_lib.run({"users": group.get("users", [])})
|
||||
except Exception as e:
|
||||
self._onError(f"{self._runName()} 运行时发生异常 : {e}")
|
||||
return
|
||||
if auto_lib:
|
||||
auto_lib.close()
|
||||
self._showTrace(f"{self._runName()} 运行结束")
|
||||
self._onFinished()
|
||||
|
||||
|
||||
class TimerTaskWorker(AutoLibWorker):
|
||||
|
||||
timerTaskWorkerIsFinished = Signal(bool, dict)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
timer_task: dict,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
config_paths: dict,
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue, config_paths)
|
||||
self.__timer_task = timer_task
|
||||
|
||||
def _runName(
|
||||
self,
|
||||
) -> str:
|
||||
|
||||
return f"定时任务 '{self.__timer_task.get("name", "未知")}'"
|
||||
|
||||
def _beforeCreateAutoLib(
|
||||
self,
|
||||
):
|
||||
|
||||
self.applyRepeatAutoScript()
|
||||
|
||||
def _onChecksFailed(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
self._showTrace("定时任务跳过执行: 时间或配置文件检查未通过")
|
||||
self.timerTaskWorkerIsFinished.emit(False, self.__timer_task)
|
||||
return False
|
||||
|
||||
def _onFinished(
|
||||
self,
|
||||
):
|
||||
|
||||
self.timerTaskWorkerIsFinished.emit(False, self.__timer_task)
|
||||
|
||||
def _onError(
|
||||
self,
|
||||
error_msg: str,
|
||||
):
|
||||
|
||||
self._showTrace(error_msg, self.TraceLevel.ERROR)
|
||||
self.timerTaskWorkerIsFinished.emit(True, self.__timer_task)
|
||||
|
||||
def applyRepeatAutoScript(
|
||||
self,
|
||||
):
|
||||
|
||||
auto_script = self.__timer_task.get("repeat_auto_script", "")
|
||||
if not auto_script or not auto_script.strip():
|
||||
return
|
||||
self._showTrace("检测到重复定时任务 AutoScript, 开始执行...", no_log=True)
|
||||
groups = self._user_config.get("groups", [])
|
||||
affected_count = 0
|
||||
for group in groups:
|
||||
if not group.get("enabled", False):
|
||||
continue
|
||||
for user in group.get("users", []):
|
||||
try:
|
||||
engine = createEngine()
|
||||
engine.execute(auto_script, user)
|
||||
affected_count += 1
|
||||
except ValueError as e:
|
||||
self._showTrace(
|
||||
f"AutoScript 执行错误 (用户 {user.get("username", "未知")}): {e}",
|
||||
self.TraceLevel.ERROR,
|
||||
)
|
||||
self._showLog(
|
||||
f"AutoScript 执行完毕, 影响 {affected_count} 个用户",
|
||||
self.TraceLevel.INFO,
|
||||
)
|
||||
@@ -1,166 +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 os
|
||||
import time
|
||||
import queue
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Slot, Signal, QThread
|
||||
)
|
||||
|
||||
from base.MsgBase import MsgBase
|
||||
from operators.AutoLib import AutoLib
|
||||
from utils.ConfigReader import ConfigReader
|
||||
|
||||
|
||||
class AutoLibWorker(QThread, MsgBase):
|
||||
|
||||
finishedSignal = Signal()
|
||||
finishedWithErrorSignal = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
config_paths: dict
|
||||
):
|
||||
|
||||
super().__init__(input_queue=input_queue, output_queue=output_queue)
|
||||
|
||||
self.__config_paths = config_paths
|
||||
|
||||
|
||||
def checkTimeAvailable(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
current_time = time.strftime("%H:%M", time.localtime())
|
||||
if current_time >= "23:30" or current_time <= "07:30":
|
||||
self._showTrace(
|
||||
"当前时间不在图书馆开放时间内, 请在 07:30 - 23:30 之间尝试"
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def checkConfigPaths(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
if not all(
|
||||
os.path.exists(path) for path in self.__config_paths.values()
|
||||
):
|
||||
self._showTrace("配置文件路径不存在, 请检查配置文件路径是否正确")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def loadConfigs(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
self._showTrace(
|
||||
f"正在加载配置文件, 运行配置文件路径: {self.__config_paths["run"]}"
|
||||
)
|
||||
self.__run_config = ConfigReader(self.__config_paths["run"]).getConfigs()
|
||||
self._showTrace(
|
||||
f"正在加载配置文件, 用户配置文件路径: {self.__config_paths["user"]}"
|
||||
)
|
||||
self.__user_config = ConfigReader(self.__config_paths["user"]).getConfigs()
|
||||
if self.__run_config is None or self.__user_config is None:
|
||||
self._showTrace("配置文件加载失败, 请检查配置文件是否正确")
|
||||
self._showTrace("配置文件加载失败, 请检查配置文件是否正确")
|
||||
return False
|
||||
if not self.__user_config.get("groups"):
|
||||
self._showTrace(
|
||||
"用户配置文件中无有效任务组, 请检查用户配置文件是否正确"
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def run(
|
||||
self
|
||||
):
|
||||
|
||||
auto_lib = None
|
||||
self._showTrace("AutoLibrary 开始运行")
|
||||
if not self.checkTimeAvailable()\
|
||||
or not self.checkConfigPaths():
|
||||
# time or config existence check failed, skip and finish
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
if not self.loadConfigs():
|
||||
raise Exception("配置文件加载失败")
|
||||
auto_lib = AutoLib(
|
||||
self._input_queue,
|
||||
self._output_queue,
|
||||
self.__run_config
|
||||
)
|
||||
groups = self.__user_config.get("groups")
|
||||
for group in groups:
|
||||
if not group["enabled"]:
|
||||
self._showTrace(f"任务组 {group["name"]} 已跳过")
|
||||
continue
|
||||
self._showTrace(f"正在运行任务组 {group["name"]}")
|
||||
auto_lib.run(
|
||||
{ "users": group.get("users", []) }
|
||||
)
|
||||
except Exception as e:
|
||||
self._showTrace(f"AutoLibrary 运行时发生异常 : {e}")
|
||||
self.finishedWithErrorSignal.emit()
|
||||
return
|
||||
if auto_lib:
|
||||
auto_lib.close()
|
||||
self._showTrace("AutoLibrary 运行结束")
|
||||
self.finishedSignal.emit()
|
||||
|
||||
|
||||
class TimerTaskWorker(AutoLibWorker):
|
||||
|
||||
finishedSignal_TimerWorker = Signal(bool, dict)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
timer_task: dict,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
config_paths: dict
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue, config_paths)
|
||||
|
||||
self.__timer_task = timer_task
|
||||
self.finishedSignal.connect(self.onTimerTaskIsFinished)
|
||||
self.finishedWithErrorSignal.connect(self.onTimerTaskIsError)
|
||||
|
||||
def run(
|
||||
self
|
||||
):
|
||||
|
||||
self._showTrace(f"定时任务 {self.__timer_task['name']} 开始运行")
|
||||
super().run()
|
||||
|
||||
@Slot(dict)
|
||||
def onTimerTaskIsError(
|
||||
self
|
||||
):
|
||||
|
||||
self._showTrace(f"定时任务 {self.__timer_task['name']} 运行时发生异常")
|
||||
self.finishedSignal_TimerWorker.emit(True, self.__timer_task)
|
||||
|
||||
@Slot(dict)
|
||||
def onTimerTaskIsFinished(
|
||||
self
|
||||
):
|
||||
|
||||
self._showTrace(f"定时任务 {self.__timer_task['name']} 运行结束")
|
||||
self.finishedSignal_TimerWorker.emit(False, self.__timer_task)
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 - 2026 KenanZhu.
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
@@ -22,13 +22,13 @@ class ALSeatFrame(QFrame):
|
||||
def __init__(
|
||||
self,
|
||||
seat_number,
|
||||
parent=None
|
||||
parent = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self.__seat_number = seat_number
|
||||
self.__is_selected = False
|
||||
|
||||
|
||||
self.setupUi()
|
||||
|
||||
def setupUi(
|
||||
@@ -40,18 +40,19 @@ class ALSeatFrame(QFrame):
|
||||
self.setLineWidth(2)
|
||||
self.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #4196EB;
|
||||
border: 2px solid #4196EB;
|
||||
background-color: #2294FF;
|
||||
border: 2px solid #2294FF;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QLabel {
|
||||
color: #F0F0F0;
|
||||
color: #FFFFFF;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
self.label = QLabel(self.__seat_number, self)
|
||||
self.label.setAlignment(Qt.AlignCenter)
|
||||
self.label.setGeometry(0, 0, 60, 40)
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.Label = QLabel(self.__seat_number, self)
|
||||
self.Label.setAlignment(Qt.AlignCenter)
|
||||
self.Label.setGeometry(0, 0, 60, 40)
|
||||
|
||||
def mousePressEvent(
|
||||
self,
|
||||
@@ -62,14 +63,12 @@ class ALSeatFrame(QFrame):
|
||||
self.toggleSelection()
|
||||
self.clicked.emit(self.__seat_number)
|
||||
|
||||
|
||||
def isSelected(
|
||||
self
|
||||
):
|
||||
|
||||
return self.__is_selected
|
||||
|
||||
|
||||
def toggleSelection(self):
|
||||
|
||||
self.__is_selected = not self.__is_selected
|
||||
@@ -77,24 +76,24 @@ class ALSeatFrame(QFrame):
|
||||
self.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #4CAF50;
|
||||
border: 2px solid #388E3C;
|
||||
border: 2px solid #4CAF50;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
}
|
||||
QLabel {
|
||||
color: #F0F0F0;
|
||||
color: #FFFFFF;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
else:
|
||||
self.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #4196EB;
|
||||
border: 2px solid #4196EB;
|
||||
background-color: #2294FF;
|
||||
border: 2px solid #2294FF;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QLabel {
|
||||
color: #F0F0F0;
|
||||
color: #FFFFFF;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
@@ -0,0 +1,148 @@
|
||||
# -*- 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 PySide6.QtCore import (
|
||||
Qt,
|
||||
Signal,
|
||||
Slot
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QCloseEvent
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QVBoxLayout
|
||||
)
|
||||
|
||||
from gui.ALSeatMapView import ALSeatMapView
|
||||
from gui.ALWidgetMixin import CenterOnParentMixin
|
||||
|
||||
class ALSeatMapSelectDialog(CenterOnParentMixin, QDialog):
|
||||
|
||||
seatMapSelectDialogIsClosed = Signal(list)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: QDialog = None,
|
||||
floor: str = "",
|
||||
room: str = "",
|
||||
seats_data: str = ""
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self.__floor = floor
|
||||
self.__room = room
|
||||
self.__seats_data = seats_data
|
||||
self.__confirmed = False
|
||||
|
||||
self.setupUi()
|
||||
self.connectSignals()
|
||||
|
||||
def setupUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.setModal(True)
|
||||
self.setMinimumSize(800, 600)
|
||||
self.resize(800, 600)
|
||||
self.setWindowTitle(f"选择楼层座位 - AutoLibrary")
|
||||
self.SeatMapWidgetMainLayout = QVBoxLayout(self)
|
||||
self.SeatMapWidgetMainLayout.setContentsMargins(5, 5, 5, 5)
|
||||
self.SeatMapWidgetMainLayout.setSpacing(5)
|
||||
self.TitleLabel = QLabel(f"楼层座位分布图: {self.__floor}-{self.__room}")
|
||||
self.TitleLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.TitleLabel.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
|
||||
self.SeatMapWidgetMainLayout.addWidget(self.TitleLabel)
|
||||
self.SeatMapGraphicsView = ALSeatMapView(None, self.__seats_data)
|
||||
self.SeatMapWidgetMainLayout.addWidget(self.SeatMapGraphicsView)
|
||||
self.TipsLabel = QLabel(
|
||||
" 点击座位进行选择/取消选择, 最多选择1个座位 \n"
|
||||
" [操作方法: Ctrl+鼠标滚轮缩放 | 滚轮/拖拽/方向键 移动]"
|
||||
)
|
||||
self.TipsLabel.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.TipsLabel.setStyleSheet("color: #666; margin: 5px;")
|
||||
self.SeatMapWidgetMainLayout.addWidget(self.TipsLabel)
|
||||
self.ConfirmButton = QPushButton("确认")
|
||||
self.ConfirmButton.setFixedSize(80, 25)
|
||||
self.ConfirmButton.setAutoDefault(True)
|
||||
self.ConfirmButton.setDefault(True)
|
||||
self.CancelButton = QPushButton("取消")
|
||||
self.CancelButton.setFixedSize(80, 25)
|
||||
self.SeatMapWidgetControlLayout = QHBoxLayout()
|
||||
self.SeatMapWidgetControlLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.SeatMapWidgetControlLayout.setSpacing(5)
|
||||
self.SeatMapWidgetControlLayout.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
self.SeatMapWidgetControlLayout.addWidget(self.CancelButton)
|
||||
self.SeatMapWidgetControlLayout.addWidget(self.ConfirmButton)
|
||||
self.SeatMapWidgetMainLayout.addLayout(self.SeatMapWidgetControlLayout)
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
|
||||
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
|
||||
|
||||
def closeEvent(
|
||||
self,
|
||||
event: QCloseEvent
|
||||
):
|
||||
|
||||
if not self.__confirmed:
|
||||
self.clearSelections()
|
||||
self.reject()
|
||||
else:
|
||||
self.accept()
|
||||
self.seatMapSelectDialogIsClosed.emit(self.getSelectedSeats())
|
||||
super().closeEvent(event)
|
||||
|
||||
def selectSeat(
|
||||
self,
|
||||
seat_number: str
|
||||
):
|
||||
|
||||
self.SeatMapGraphicsView.selectSeat(seat_number)
|
||||
|
||||
def selectSeats(
|
||||
self,
|
||||
seat_numbers: list[str]
|
||||
) -> bool:
|
||||
|
||||
return self.SeatMapGraphicsView.selectSeats(seat_numbers)
|
||||
|
||||
def getSelectedSeats(
|
||||
self
|
||||
) -> list[str]:
|
||||
|
||||
return self.SeatMapGraphicsView.getSelectedSeats()
|
||||
|
||||
def clearSelections(
|
||||
self
|
||||
):
|
||||
|
||||
self.SeatMapGraphicsView.clearSelections()
|
||||
|
||||
@Slot()
|
||||
def onConfirmButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.__confirmed = True
|
||||
self.accept()
|
||||
|
||||
@Slot()
|
||||
def onCancelButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.__confirmed = False
|
||||
self.reject()
|
||||
@@ -1,4 +1,4 @@
|
||||
seats_maps = {
|
||||
ALSeatMapTable = {
|
||||
"2": {
|
||||
"1": """
|
||||
,,,,,,,,,,,039A,039B,,040A,040B,,041A,041B,,042A,042B,,043A,043B,,044A,044B,,,,,,,,,
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
# -*- 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 PySide6.QtCore import (
|
||||
Qt,
|
||||
Slot,
|
||||
QEvent
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QWidget,
|
||||
QGridLayout,
|
||||
QGraphicsView,
|
||||
QGraphicsScene,
|
||||
QGraphicsItem
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QPainter,
|
||||
QWheelEvent
|
||||
)
|
||||
|
||||
from gui.ALSeatFrame import ALSeatFrame
|
||||
|
||||
|
||||
class ALSeatMapView(QGraphicsView):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: QWidget = None,
|
||||
seats_data: dict = {},
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.__seats_data = seats_data
|
||||
self.__selected_seats = []
|
||||
self.__seat_frames = {}
|
||||
|
||||
self.setupUi()
|
||||
|
||||
def eventFilter(
|
||||
self,
|
||||
watched,
|
||||
event
|
||||
):
|
||||
|
||||
if (watched is self.viewport() and
|
||||
event.type() == QEvent.Type.Wheel and
|
||||
event.modifiers() == Qt.KeyboardModifier.ControlModifier
|
||||
):
|
||||
self.zoomGraphicsView(event)
|
||||
return True
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
def zoomGraphicsView(
|
||||
self,
|
||||
event: QWheelEvent
|
||||
):
|
||||
|
||||
delta = event.angleDelta().y()
|
||||
min_scale = 0.1
|
||||
max_scale = 4.0
|
||||
current_scale = self.transform().m11()
|
||||
zoom_factor = 1.2 if delta > 0 else 1/1.2
|
||||
target_scale = current_scale*zoom_factor
|
||||
if target_scale < min_scale and delta < 0:
|
||||
return
|
||||
if target_scale > max_scale and delta > 0:
|
||||
return
|
||||
self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
||||
self.scale(zoom_factor, zoom_factor)
|
||||
|
||||
def setupUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.SeatMapGraphicsScene = QGraphicsScene(self)
|
||||
self.setScene(self.SeatMapGraphicsScene)
|
||||
self.setRenderHint(QPainter.RenderHint.LosslessImageRendering)
|
||||
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.viewport().installEventFilter(self)
|
||||
self.SeatsContainerWidget = QWidget()
|
||||
self.SeatsContainerLayout = QGridLayout(self.SeatsContainerWidget)
|
||||
self.setupSeatMap()
|
||||
self.ContainerProxy = self.SeatMapGraphicsScene.addWidget(self.SeatsContainerWidget)
|
||||
self.ContainerProxy.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, False)
|
||||
|
||||
def setupSeatMap(
|
||||
self
|
||||
):
|
||||
|
||||
rows = self.__seats_data.strip().split("\n")
|
||||
for row_idx, row in enumerate(rows):
|
||||
col_idx = 0
|
||||
seats_number = [seat.strip() for seat in row.split(",")]
|
||||
for seat_number in seats_number:
|
||||
if seat_number:
|
||||
SeatWidget = ALSeatFrame(seat_number)
|
||||
SeatWidget.clicked.connect(self.onSeatClicked)
|
||||
self.SeatsContainerLayout.addWidget(SeatWidget, row_idx, col_idx)
|
||||
self.__seat_frames[seat_number] = SeatWidget
|
||||
else:
|
||||
Spacer = QFrame()
|
||||
Spacer.setFixedSize(20, 30)
|
||||
Spacer.setStyleSheet("background-color: transparent; border: none;")
|
||||
self.SeatsContainerLayout.addWidget(Spacer, row_idx, col_idx)
|
||||
col_idx += 1
|
||||
self.SeatsContainerLayout.setSpacing(20)
|
||||
self.SeatsContainerLayout.setContentsMargins(20, 20, 20, 20)
|
||||
self.SeatsContainerWidget.adjustSize()
|
||||
|
||||
def selectSeat(
|
||||
self,
|
||||
seat_number: str
|
||||
):
|
||||
|
||||
if len(self.__selected_seats) >= 1:
|
||||
return
|
||||
seat_number = self.formatSeatNumber(seat_number)
|
||||
if seat_number not in self.__seat_frames:
|
||||
return
|
||||
widget = self.__seat_frames[seat_number]
|
||||
if widget.isSelected():
|
||||
return
|
||||
widget.toggleSelection()
|
||||
self.__selected_seats.append(seat_number)
|
||||
|
||||
def selectSeats(
|
||||
self,
|
||||
selected_seats: list
|
||||
):
|
||||
|
||||
self.clearSelections()
|
||||
for seat_number in selected_seats:
|
||||
self.selectSeat(seat_number)
|
||||
|
||||
def getSelectedSeats(
|
||||
self
|
||||
) -> list[str]:
|
||||
|
||||
return self.__selected_seats
|
||||
|
||||
def clearSelections(
|
||||
self
|
||||
):
|
||||
|
||||
seats_to_clear = self.__selected_seats.copy()
|
||||
for seat_number in seats_to_clear:
|
||||
if seat_number not in self.__seat_frames:
|
||||
continue
|
||||
widget = self.__seat_frames[seat_number]
|
||||
if widget.isSelected():
|
||||
widget.toggleSelection()
|
||||
self.__selected_seats = []
|
||||
|
||||
@Slot(str)
|
||||
def onSeatClicked(
|
||||
self,
|
||||
seat_number: str
|
||||
):
|
||||
|
||||
if seat_number in self.__selected_seats:
|
||||
self.__selected_seats.remove(seat_number)
|
||||
else:
|
||||
if len(self.__selected_seats) < 1:
|
||||
self.__selected_seats.append(seat_number)
|
||||
else:
|
||||
self.__seat_frames[seat_number].toggleSelection()
|
||||
|
||||
@staticmethod
|
||||
def formatSeatNumber(
|
||||
seat_number: str
|
||||
) -> str:
|
||||
|
||||
if seat_number and not seat_number[-1].isdigit():
|
||||
digits = seat_number[:-1]
|
||||
letter = seat_number[-1]
|
||||
return digits.zfill(3) + letter
|
||||
return seat_number.zfill(3)
|
||||
@@ -1,280 +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.
|
||||
"""
|
||||
from PySide6.QtCore import (
|
||||
Qt, Slot, Signal, QEvent
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame, QWidget, QLabel, QHBoxLayout, QVBoxLayout,
|
||||
QGridLayout, QGraphicsView, QGraphicsScene, QGraphicsItem,
|
||||
QPushButton,
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QPainter, QWheelEvent, QCloseEvent
|
||||
)
|
||||
from gui.ALSeatFrame import ALSeatFrame
|
||||
|
||||
|
||||
class ALSeatMapWidget(QWidget):
|
||||
|
||||
seatMapWidgetClosed = Signal(list)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: QWidget = None,
|
||||
floor: str = "",
|
||||
room: str = "",
|
||||
seats_data: dict = {},
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self.__floor = floor
|
||||
self.__room = room
|
||||
self.__seats_data = seats_data
|
||||
self.__selected_seats = []
|
||||
self.__seat_frames = {}
|
||||
self.__confirmed = False
|
||||
|
||||
self.setupUi()
|
||||
self.connectSignals()
|
||||
|
||||
@staticmethod
|
||||
def formatSeatNumber(
|
||||
seat_number: str
|
||||
) -> str:
|
||||
|
||||
if seat_number and not seat_number[-1].isdigit():
|
||||
digits = seat_number[:-1]
|
||||
letter = seat_number[-1]
|
||||
return digits.zfill(3) + letter
|
||||
return seat_number.zfill(3)
|
||||
|
||||
|
||||
def setupUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.setWindowFlags(Qt.WindowType.Window)
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.setMinimumSize(800, 600)
|
||||
self.resize(800, 600)
|
||||
self.setWindowTitle(f"选择楼层座位 - AutoLibrary")
|
||||
|
||||
self.SeatMapWidgetMainLayout = QVBoxLayout(self)
|
||||
self.TitleLabel = QLabel(f"楼层座位分布图: {self.__floor}-{self.__room}")
|
||||
self.TitleLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.TitleLabel.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
|
||||
self.SeatMapWidgetMainLayout.addWidget(self.TitleLabel)
|
||||
|
||||
self.SeatMapGraphicsView = QGraphicsView(self)
|
||||
self.SeatMapGraphicsScene = QGraphicsScene(self)
|
||||
self.SeatMapGraphicsView.setScene(self.SeatMapGraphicsScene)
|
||||
self.SeatMapGraphicsView.setRenderHint(QPainter.RenderHint.LosslessImageRendering)
|
||||
self.SeatMapGraphicsView.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
|
||||
self.SeatMapGraphicsView.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.SeatMapGraphicsView.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.SeatMapGraphicsView.viewport().installEventFilter(self)
|
||||
|
||||
self.SeatsContainerWidget = QWidget()
|
||||
self.SeatsContainerLayout = QGridLayout(self.SeatsContainerWidget)
|
||||
self.createSeatMap()
|
||||
|
||||
self.ContainerProxy = self.SeatMapGraphicsScene.addWidget(self.SeatsContainerWidget)
|
||||
self.ContainerProxy.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, False)
|
||||
self.SeatMapWidgetMainLayout.addWidget(self.SeatMapGraphicsView)
|
||||
|
||||
self.TipsLabel = QLabel(
|
||||
" 点击座位进行选择/取消选择, 最多选择1个座位 \n"
|
||||
" [操作方法: Ctrl+鼠标滚轮缩放 | 滚轮/拖拽/方向键 移动]"
|
||||
)
|
||||
self.TipsLabel.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.TipsLabel.setStyleSheet("color: #666; margin: 5px;")
|
||||
self.SeatMapWidgetMainLayout.addWidget(self.TipsLabel)
|
||||
|
||||
self.ConfirmButton = QPushButton("确认")
|
||||
self.ConfirmButton.setFixedSize(80, 25)
|
||||
self.CancelButton = QPushButton("取消")
|
||||
self.CancelButton.setFixedSize(80, 25)
|
||||
self.SeatMapWidgetControlLayout = QHBoxLayout()
|
||||
self.SeatMapWidgetControlLayout.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
self.SeatMapWidgetControlLayout.addWidget(self.CancelButton)
|
||||
self.SeatMapWidgetControlLayout.addWidget(self.ConfirmButton)
|
||||
self.SeatMapWidgetMainLayout.addLayout(self.SeatMapWidgetControlLayout)
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
|
||||
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
|
||||
|
||||
|
||||
def showEvent(
|
||||
self,
|
||||
event
|
||||
):
|
||||
|
||||
result = super().showEvent(event)
|
||||
|
||||
screen_rect = self.screen().geometry()
|
||||
target_pos = self.parent().geometry().center()
|
||||
target_pos.setX(target_pos.x() - self.width()//2)
|
||||
target_pos.setY(target_pos.y() - self.height()//2)
|
||||
if target_pos.x() < 0:
|
||||
target_pos.setX(0)
|
||||
if target_pos.x() + self.width() > screen_rect.width():
|
||||
target_pos.setX(screen_rect.width() - self.width())
|
||||
if target_pos.y() < 0:
|
||||
target_pos.setY(0)
|
||||
if target_pos.y() + self.height() > screen_rect.height():
|
||||
target_pos.setY(screen_rect.height() - self.height())
|
||||
self.move(target_pos)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def closeEvent(
|
||||
self,
|
||||
event: QCloseEvent
|
||||
):
|
||||
|
||||
if not self.__confirmed:
|
||||
self.clearSelections()
|
||||
self.seatMapWidgetClosed.emit(self.__selected_seats)
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
def eventFilter(
|
||||
self,
|
||||
watched,
|
||||
event
|
||||
):
|
||||
|
||||
if (watched is self.SeatMapGraphicsView.viewport() and
|
||||
event.type() == QEvent.Type.Wheel and
|
||||
event.modifiers() == Qt.KeyboardModifier.ControlModifier
|
||||
):
|
||||
self.zoomGraphicsView(event)
|
||||
return True
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
|
||||
def zoomGraphicsView(
|
||||
self,
|
||||
event: QWheelEvent
|
||||
):
|
||||
|
||||
delta = event.angleDelta().y()
|
||||
zoom_factor = 1.2 if delta > 0 else 1/1.2
|
||||
self.SeatMapGraphicsView.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
||||
self.SeatMapGraphicsView.scale(zoom_factor, zoom_factor)
|
||||
|
||||
|
||||
def createSeatMap(
|
||||
self
|
||||
):
|
||||
|
||||
rows = self.__seats_data.strip().split("\n")
|
||||
for row_idx, row in enumerate(rows):
|
||||
col_idx = 0
|
||||
seats_number = [seat.strip() for seat in row.split(",")]
|
||||
for seat_number in seats_number:
|
||||
if seat_number:
|
||||
seat_widget = ALSeatFrame(seat_number)
|
||||
seat_widget.clicked.connect(self.onSeatClicked)
|
||||
self.SeatsContainerLayout.addWidget(seat_widget, row_idx, col_idx)
|
||||
self.__seat_frames[seat_number] = seat_widget
|
||||
else:
|
||||
spacer = QFrame()
|
||||
spacer.setFixedSize(20, 30)
|
||||
spacer.setStyleSheet("background-color: transparent; border: none;")
|
||||
self.SeatsContainerLayout.addWidget(spacer, row_idx, col_idx)
|
||||
col_idx += 1
|
||||
self.SeatsContainerLayout.setSpacing(20)
|
||||
self.SeatsContainerLayout.setContentsMargins(20, 20, 20, 20)
|
||||
self.SeatsContainerWidget.adjustSize()
|
||||
|
||||
|
||||
def selectSeat(
|
||||
self,
|
||||
seat_number: str
|
||||
):
|
||||
|
||||
if len(self.__selected_seats) >= 1:
|
||||
return
|
||||
seat_number = self.formatSeatNumber(seat_number)
|
||||
if seat_number not in self.__seat_frames:
|
||||
return
|
||||
widget = self.__seat_frames[seat_number]
|
||||
if widget.isSelected():
|
||||
return
|
||||
widget.toggleSelection()
|
||||
self.__selected_seats.append(seat_number)
|
||||
|
||||
|
||||
def selectSeats(
|
||||
self,
|
||||
selected_seats: list
|
||||
):
|
||||
|
||||
self.clearSelections()
|
||||
for seat_number in selected_seats:
|
||||
self.selectSeat(seat_number)
|
||||
|
||||
|
||||
def getSelectedSeats(
|
||||
self
|
||||
) -> list[str]:
|
||||
|
||||
return self.__selected_seats
|
||||
|
||||
|
||||
def clearSelections(
|
||||
self
|
||||
):
|
||||
|
||||
seats_to_clear = self.__selected_seats.copy()
|
||||
for seat_number in seats_to_clear:
|
||||
if seat_number not in self.__seat_frames:
|
||||
continue
|
||||
widget = self.__seat_frames[seat_number]
|
||||
if widget.isSelected():
|
||||
widget.toggleSelection()
|
||||
self.__selected_seats = []
|
||||
|
||||
@Slot(str)
|
||||
def onSeatClicked(
|
||||
self,
|
||||
seat_number: str
|
||||
):
|
||||
|
||||
if seat_number in self.__selected_seats:
|
||||
self.__selected_seats.remove(seat_number)
|
||||
else:
|
||||
if len(self.__selected_seats) < 1:
|
||||
self.__selected_seats.append(seat_number)
|
||||
else:
|
||||
self.__seat_frames[seat_number].toggleSelection()
|
||||
|
||||
@Slot()
|
||||
def onConfirmButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.__confirmed = True
|
||||
self.close()
|
||||
|
||||
@Slot()
|
||||
def onCancelButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.__confirmed = False
|
||||
self.close()
|
||||
@@ -0,0 +1,559 @@
|
||||
# -*- 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 sys
|
||||
import time
|
||||
|
||||
import requests
|
||||
import qtawesome as qta
|
||||
|
||||
from PySide6.QtCore import (
|
||||
QProcess,
|
||||
Qt,
|
||||
QTimer,
|
||||
Signal,
|
||||
Slot
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QCloseEvent
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QFileDialog,
|
||||
QMessageBox,
|
||||
QStyleFactory,
|
||||
QWidget
|
||||
)
|
||||
|
||||
import managers.config.ConfigManager as ConfigManager
|
||||
from managers.bulletin.BulletinManager import instance as bulletinInstance
|
||||
from managers.log.LogManager import instance as logInstance
|
||||
from managers.theme.ThemeManager import(
|
||||
getActiveStyle,
|
||||
setActiveStyle,
|
||||
instance as themeInstance
|
||||
)
|
||||
|
||||
from gui.ALBulletinWorker import ALBulletinFetchWorker
|
||||
from gui.ALWidgetMixin import CenterOnParentMixin
|
||||
from gui.resources.ui.Ui_ALSettingsWidget import Ui_ALSettingsWidget
|
||||
from interfaces.ConfigProvider import (
|
||||
CfgKey,
|
||||
ConfigProvider
|
||||
)
|
||||
|
||||
|
||||
def _applyCustomTheme(
|
||||
name: str,
|
||||
fallback_theme: str = "system"
|
||||
) -> bool:
|
||||
|
||||
if not name:
|
||||
themeInstance().clearTheme(fallback_theme)
|
||||
return True
|
||||
try:
|
||||
themeInstance().applyTheme(name)
|
||||
return True
|
||||
except Exception as e:
|
||||
logInstance().getLogger("ALSettingsWidget").warning(
|
||||
f"无法应用自定义主题 '{name}',回退到 {fallback_theme} 外观: {e}"
|
||||
)
|
||||
themeInstance().clearTheme(fallback_theme)
|
||||
return False
|
||||
|
||||
def _themeToReadable(
|
||||
need_theme: str
|
||||
) -> str:
|
||||
|
||||
if need_theme == "dark":
|
||||
return "深色"
|
||||
elif need_theme == "light":
|
||||
return "浅色"
|
||||
elif need_theme == "both":
|
||||
return "所有"
|
||||
else:
|
||||
return "未知"
|
||||
|
||||
def _restartApp(
|
||||
):
|
||||
|
||||
QApplication.instance().quit()
|
||||
QProcess.startDetached(sys.executable, sys.argv)
|
||||
|
||||
|
||||
class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
|
||||
|
||||
settingsWidgetIsClosed = Signal()
|
||||
openBulletinRequested = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.__cfg_mgr: ConfigProvider = ConfigManager.instance()
|
||||
self.__original_theme: str = ""
|
||||
self.__original_custom_theme: str = ""
|
||||
self.__original_style: str = ""
|
||||
self.__original_bulletin_url: str = ""
|
||||
self.__original_bulletin_auto_fetch: bool = False
|
||||
self.__original_bulletin_sync_interval: int = 10
|
||||
|
||||
self.setupUi(self)
|
||||
self.modifyUi()
|
||||
self.connectSignals()
|
||||
self.loadSettings()
|
||||
|
||||
def closeEvent(
|
||||
self,
|
||||
event: QCloseEvent
|
||||
):
|
||||
|
||||
self.settingsWidgetIsClosed.emit()
|
||||
super().closeEvent(event)
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.setWindowFlags(Qt.WindowType.Window)
|
||||
self.setNavigationIcons()
|
||||
color = QApplication.instance().palette().color(
|
||||
QApplication.instance().palette().ColorRole.WindowText
|
||||
).name()
|
||||
self.ImportCustomThemeButton.setIcon(qta.icon("fa6s.plus", color=color))
|
||||
self.ImportCustomThemeButton.setText("")
|
||||
self.RemoveCustomThemeButton.setIcon(qta.icon("fa6s.minus", color=color))
|
||||
self.RemoveCustomThemeButton.setText("")
|
||||
self.CustomThemeInfoLabel.setTextFormat(Qt.TextFormat.RichText)
|
||||
self.CustomThemeInfoLabel.setStyleSheet(
|
||||
"border: 1px solid palette(mid);"\
|
||||
"border-radius: 2px;"\
|
||||
"padding: 5px;"
|
||||
)
|
||||
self.NavigationList.setCurrentRow(0)
|
||||
self.NavigationList.currentRowChanged.connect(self.PageStack.setCurrentIndex)
|
||||
self.populateStyles()
|
||||
self.populateCustomThemes()
|
||||
|
||||
def setNavigationIcons(
|
||||
self
|
||||
):
|
||||
|
||||
app : QApplication | None = QApplication.instance()
|
||||
color = app.palette().color(app.palette().ColorRole.WindowText).name()
|
||||
item0 = self.NavigationList.item(0)
|
||||
if item0:
|
||||
item0.setIcon(qta.icon("fa6s.palette", color=color))
|
||||
item1 = self.NavigationList.item(1)
|
||||
if item1:
|
||||
item1.setIcon(qta.icon("fa6s.bullhorn", color=color))
|
||||
|
||||
def populateStyles(
|
||||
self
|
||||
):
|
||||
|
||||
self.StyleComboBox.clear()
|
||||
self.StyleComboBox.addItems(QStyleFactory.keys())
|
||||
|
||||
def populateCustomThemes(
|
||||
self
|
||||
):
|
||||
|
||||
self.CustomThemeComboBox.blockSignals(True)
|
||||
self.CustomThemeComboBox.clear()
|
||||
self.CustomThemeComboBox.addItem("默认", "")
|
||||
self.__theme_cache = {}
|
||||
themes = themeInstance().listThemes()
|
||||
for t in themes:
|
||||
name = t.get("name", "")
|
||||
file = t.get("file", name)
|
||||
author = t.get("author", "")
|
||||
if name:
|
||||
self.__theme_cache[file] = t
|
||||
self.CustomThemeComboBox.addItem(name, file)
|
||||
self.CustomThemeComboBox.blockSignals(False)
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.ImportCustomThemeButton.clicked.connect(self.onImportCustomThemeButtonClicked)
|
||||
self.RemoveCustomThemeButton.clicked.connect(self.onRemoveCustomThemeButtonClicked)
|
||||
self.CustomThemeComboBox.currentIndexChanged.connect(self.onCustomThemeComboBoxChanged)
|
||||
self.ResetCustomThemeButton.clicked.connect(self.onResetCustomThemeButtonClicked)
|
||||
self.BulletinTestButton.clicked.connect(self.onBulletinTestButtonClicked)
|
||||
self.BulletinOpenButton.clicked.connect(self.openBulletinRequested.emit)
|
||||
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
|
||||
self.ApplyButton.clicked.connect(self.onApplyButtonClicked)
|
||||
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
|
||||
|
||||
def loadSettings(
|
||||
self
|
||||
):
|
||||
|
||||
# appearance settings
|
||||
theme = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.THEME, "system")
|
||||
style = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.STYLE, "Fusion")
|
||||
custom_theme = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "")
|
||||
self.__original_theme = theme
|
||||
self.__original_custom_theme = custom_theme
|
||||
self.__original_style = getActiveStyle()
|
||||
if theme == "light":
|
||||
self.LightThemeRadio.setChecked(True)
|
||||
elif theme == "dark":
|
||||
self.DarkThemeRadio.setChecked(True)
|
||||
else:
|
||||
self.SystemThemeRadio.setChecked(True)
|
||||
index = self.StyleComboBox.findText(style)
|
||||
if index < 0:
|
||||
index = 0
|
||||
self.StyleComboBox.setCurrentIndex(index)
|
||||
if custom_theme:
|
||||
idx = self.CustomThemeComboBox.findData(custom_theme)
|
||||
if idx >= 0:
|
||||
self.CustomThemeComboBox.setCurrentIndex(idx)
|
||||
self.updateCustomThemeInfo()
|
||||
self.updateCustomThemeStatus()
|
||||
# bulletin settings
|
||||
bulletin_mgr = bulletinInstance()
|
||||
self.__original_bulletin_url = bulletin_mgr.serverUrl()
|
||||
self.__original_bulletin_auto_fetch = bulletin_mgr.autoFetch()
|
||||
self.__original_bulletin_sync_interval = bulletin_mgr.syncInterval()
|
||||
self.BulletinServerUrlEdit.setText(self.__original_bulletin_url)
|
||||
self.BulletinAutoFetchCheck.setChecked(self.__original_bulletin_auto_fetch)
|
||||
self.BulletinSyncIntervalSpin.setValue(self.__original_bulletin_sync_interval)
|
||||
|
||||
def updateCustomThemeInfo(
|
||||
self
|
||||
):
|
||||
|
||||
file = self.CustomThemeComboBox.currentData()
|
||||
if not file:
|
||||
self.CustomThemeInfoLabel.setText("")
|
||||
return
|
||||
t = self.__theme_cache.get(file)
|
||||
if t:
|
||||
name = t.get("name", "未知")
|
||||
author = t.get("author", "未知作者")
|
||||
need_theme = t.get("need_theme", "both")
|
||||
brief = t.get("brief", "没有相关简介")
|
||||
self.CustomThemeInfoLabel.setText(
|
||||
f"<b>{name}</b> - 适用于 <i>{_themeToReadable(need_theme)}</i> 主题<br>"
|
||||
f"作者:{author}<br><br>"
|
||||
f"{brief}"
|
||||
)
|
||||
else:
|
||||
self.CustomThemeInfoLabel.setText("")
|
||||
|
||||
def updateCustomThemeStatus(
|
||||
self
|
||||
):
|
||||
|
||||
file = self.CustomThemeComboBox.currentData()
|
||||
t = self.__theme_cache.get(file) if file else None
|
||||
name = t.get("name", "") if t else ""
|
||||
if name:
|
||||
self.CustomThemeStatusLabel.setText(f"当前使用 {name} 主题。")
|
||||
else:
|
||||
self.CustomThemeStatusLabel.setText("当前使用 默认 主题。")
|
||||
|
||||
def syncRadioFromNeedTheme(
|
||||
self,
|
||||
name: str
|
||||
):
|
||||
|
||||
t = self.__theme_cache.get(name)
|
||||
if t:
|
||||
need_theme = t.get("need_theme", "both")
|
||||
if need_theme == "light":
|
||||
self.LightThemeRadio.setChecked(True)
|
||||
elif need_theme == "dark":
|
||||
self.DarkThemeRadio.setChecked(True)
|
||||
|
||||
def clearBulletinTestStatus(
|
||||
self
|
||||
):
|
||||
|
||||
self.BulletinTestStatusLabel.setText("")
|
||||
self.BulletinTestStatusLabel.setStyleSheet("")
|
||||
|
||||
def collectAppearanceSettings(
|
||||
self
|
||||
) -> tuple[str, str, str]:
|
||||
|
||||
if self.LightThemeRadio.isChecked():
|
||||
theme = "light"
|
||||
elif self.DarkThemeRadio.isChecked():
|
||||
theme = "dark"
|
||||
else:
|
||||
theme = "system"
|
||||
style = self.StyleComboBox.currentText()
|
||||
custom_theme = self.CustomThemeComboBox.currentData() or ""
|
||||
if not custom_theme:
|
||||
custom_theme = ""
|
||||
return theme, style, custom_theme
|
||||
|
||||
def collectBulletinSettings(
|
||||
self
|
||||
) -> tuple[str, bool, int]:
|
||||
|
||||
url = self.BulletinServerUrlEdit.text().strip()
|
||||
auto_fetch = self.BulletinAutoFetchCheck.isChecked()
|
||||
sync_interval = self.BulletinSyncIntervalSpin.value()
|
||||
if sync_interval < 1:
|
||||
sync_interval = 5
|
||||
return url, auto_fetch, sync_interval
|
||||
|
||||
def saveAppearanceSettings(
|
||||
self
|
||||
):
|
||||
|
||||
theme, style, custom_theme = self.collectAppearanceSettings()
|
||||
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.STYLE, style)
|
||||
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, custom_theme)
|
||||
setActiveStyle(style)
|
||||
if not _applyCustomTheme(custom_theme, theme):
|
||||
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "")
|
||||
self.syncRadioFromNeedTheme(custom_theme)
|
||||
theme, _, _ = self.collectAppearanceSettings()
|
||||
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.THEME, theme)
|
||||
self.setNavigationIcons()
|
||||
self.updateCustomThemeStatus()
|
||||
self.updateCustomThemeInfo()
|
||||
self.__original_theme = theme
|
||||
self.__original_custom_theme = custom_theme if custom_theme else ""
|
||||
self.__original_style = getActiveStyle()
|
||||
|
||||
def saveBulletinSettings(
|
||||
self
|
||||
):
|
||||
|
||||
url, auto_fetch, sync_interval = self.collectBulletinSettings()
|
||||
self.__cfg_mgr.set(CfgKey.GLOBAL.BULLETIN.SERVER_URL, url)
|
||||
self.__cfg_mgr.set(CfgKey.GLOBAL.BULLETIN.AUTO_FETCH, auto_fetch)
|
||||
self.__cfg_mgr.set(CfgKey.GLOBAL.BULLETIN.SYNC_INTERVAL, sync_interval)
|
||||
self.__original_bulletin_url = url
|
||||
self.__original_bulletin_auto_fetch = auto_fetch
|
||||
self.__original_bulletin_sync_interval = sync_interval
|
||||
|
||||
def saveSettings(
|
||||
self
|
||||
):
|
||||
|
||||
self.saveBulletinSettings()
|
||||
self.saveAppearanceSettings()
|
||||
|
||||
def maybeRestart(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"提示 - AutoLibrary",
|
||||
"界面风格已修改,需要重启程序才能生效。是否立即重启?",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.Yes
|
||||
)
|
||||
if reply == QMessageBox.Yes:
|
||||
_restartApp()
|
||||
return True
|
||||
return False
|
||||
|
||||
@Slot()
|
||||
def onImportCustomThemeButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
file_path, _ = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"导入主题 - AutoLibrary",
|
||||
"",
|
||||
"主题文件 (*.altheme *.qss);;所有文件 (*)"
|
||||
)
|
||||
if not file_path:
|
||||
return
|
||||
try:
|
||||
file_id = themeInstance().importTheme(file_path)
|
||||
self.populateCustomThemes()
|
||||
idx = self.CustomThemeComboBox.findData(file_id)
|
||||
if idx >= 0:
|
||||
self.CustomThemeComboBox.setCurrentIndex(idx)
|
||||
self.updateCustomThemeStatus()
|
||||
self.updateCustomThemeInfo()
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"导入失败 - AutoLibrary",
|
||||
f"无法导入主题文件:{e}"
|
||||
)
|
||||
|
||||
@Slot()
|
||||
def onRemoveCustomThemeButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
file = self.CustomThemeComboBox.currentData()
|
||||
if not file:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"提示 - AutoLibrary",
|
||||
"请先选择一个主题。"
|
||||
)
|
||||
return
|
||||
t = self.__theme_cache.get(file)
|
||||
name = t.get("name", file) if t else file
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"删除主题 - AutoLibrary",
|
||||
f"确定要删除主题 \"{name}\" 吗?",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No
|
||||
)
|
||||
if reply != QMessageBox.Yes:
|
||||
return
|
||||
try:
|
||||
themeInstance().removeTheme(file)
|
||||
self.populateCustomThemes()
|
||||
self.CustomThemeComboBox.setCurrentIndex(0)
|
||||
self.updateCustomThemeStatus()
|
||||
self.updateCustomThemeInfo()
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"删除失败 - AutoLibrary",
|
||||
f"无法删除主题:{e}"
|
||||
)
|
||||
|
||||
@Slot()
|
||||
def onCustomThemeComboBoxChanged(
|
||||
self,
|
||||
index: int
|
||||
):
|
||||
|
||||
self.updateCustomThemeInfo()
|
||||
|
||||
@Slot()
|
||||
def onResetCustomThemeButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.CustomThemeComboBox.blockSignals(True)
|
||||
if self.__original_custom_theme:
|
||||
idx = self.CustomThemeComboBox.findData(self.__original_custom_theme)
|
||||
if idx >= 0:
|
||||
self.CustomThemeComboBox.setCurrentIndex(idx)
|
||||
else:
|
||||
self.CustomThemeComboBox.setCurrentIndex(0)
|
||||
else:
|
||||
self.CustomThemeComboBox.setCurrentIndex(0)
|
||||
self.CustomThemeComboBox.blockSignals(False)
|
||||
if self.__original_theme == "light":
|
||||
self.LightThemeRadio.setChecked(True)
|
||||
elif self.__original_theme == "dark":
|
||||
self.DarkThemeRadio.setChecked(True)
|
||||
else:
|
||||
self.SystemThemeRadio.setChecked(True)
|
||||
self.updateCustomThemeInfo()
|
||||
self.updateCustomThemeStatus()
|
||||
|
||||
@Slot()
|
||||
def onBulletinTestButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
url = self.BulletinServerUrlEdit.text().strip()
|
||||
if not url:
|
||||
self.BulletinTestStatusLabel.setText("请先输入服务器地址。")
|
||||
self.BulletinTestStatusLabel.setStyleSheet("color: red;")
|
||||
return
|
||||
if hasattr(self, '__bulletin_test_worker') and self.__bulletin_test_worker is not None:
|
||||
return
|
||||
self.BulletinTestButton.setEnabled(False)
|
||||
self.BulletinTestStatusLabel.setText("正在测试连接...")
|
||||
self.BulletinTestStatusLabel.setStyleSheet("")
|
||||
self.__bulletin_test_t0 = time.monotonic()
|
||||
api_url = url.rstrip("/") + "/bulletins"
|
||||
self.__bulletin_test_worker = ALBulletinFetchWorker(
|
||||
self, api_url, {"date": "", "time": "", "range_hour": "1"}
|
||||
)
|
||||
self.__bulletin_test_worker.fetchWorkerIsFinished.connect(
|
||||
self.onBulletinTestFetched
|
||||
)
|
||||
self.__bulletin_test_worker.fetchWorkerFinishedWithError.connect(
|
||||
self.onBulletinTestError
|
||||
)
|
||||
self.__bulletin_test_worker.start()
|
||||
|
||||
@Slot(dict)
|
||||
def onBulletinTestFetched(
|
||||
self,
|
||||
data: dict
|
||||
):
|
||||
|
||||
self.__bulletin_test_worker.fetchWorkerIsFinished.disconnect(
|
||||
self.onBulletinTestFetched
|
||||
)
|
||||
self.__bulletin_test_worker.fetchWorkerFinishedWithError.disconnect(
|
||||
self.onBulletinTestError
|
||||
)
|
||||
self.__bulletin_test_worker.deleteLater()
|
||||
self.__bulletin_test_worker = None
|
||||
elapsed_ms = (time.monotonic() - self.__bulletin_test_t0) * 1000
|
||||
self.BulletinTestStatusLabel.setText(
|
||||
f"连接成功!响应延迟 {elapsed_ms:.0f} ms"
|
||||
)
|
||||
self.BulletinTestStatusLabel.setStyleSheet("color: green;")
|
||||
self.BulletinTestButton.setEnabled(True)
|
||||
QTimer.singleShot(3000, self, self.clearBulletinTestStatus)
|
||||
|
||||
@Slot(str)
|
||||
def onBulletinTestError(
|
||||
self,
|
||||
error_message: str
|
||||
):
|
||||
|
||||
self.__bulletin_test_worker.fetchWorkerIsFinished.disconnect(
|
||||
self.onBulletinTestFetched
|
||||
)
|
||||
self.__bulletin_test_worker.fetchWorkerFinishedWithError.disconnect(
|
||||
self.onBulletinTestError
|
||||
)
|
||||
self.__bulletin_test_worker.deleteLater()
|
||||
self.__bulletin_test_worker = None
|
||||
self.BulletinTestStatusLabel.setText(f"连接失败:{error_message}")
|
||||
self.BulletinTestStatusLabel.setStyleSheet("color: red;")
|
||||
self.BulletinTestButton.setEnabled(True)
|
||||
|
||||
@Slot()
|
||||
def onCancelButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.close()
|
||||
|
||||
@Slot()
|
||||
def onApplyButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
_, style, _ = self.collectAppearanceSettings()
|
||||
style_changed = self.__original_style != style
|
||||
self.saveSettings()
|
||||
if style_changed:
|
||||
self.maybeRestart()
|
||||
|
||||
@Slot()
|
||||
def onConfirmButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.onApplyButtonClicked()
|
||||
self.close()
|
||||
@@ -0,0 +1,251 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2026 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
from enum import Enum
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Property,
|
||||
QEasingCurve,
|
||||
QPropertyAnimation,
|
||||
Qt
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QColor,
|
||||
QConicalGradient,
|
||||
QPainter,
|
||||
QPalette
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QLabel
|
||||
)
|
||||
|
||||
|
||||
class ALStatusLabel(QLabel):
|
||||
|
||||
class Status(Enum):
|
||||
"""
|
||||
Enum class for representing the status of ALStatusLabel.
|
||||
"""
|
||||
|
||||
WAITING = 0
|
||||
RUNNING = 1
|
||||
SUCCESS = 2
|
||||
WARNING = 3
|
||||
FAILURE = 4
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self.__status = self.Status.WAITING
|
||||
self.__icon_angle = 0
|
||||
|
||||
self.setupUi()
|
||||
|
||||
def setupUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.setFixedSize(36, 36)
|
||||
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.RunningAnimation = QPropertyAnimation(self, b"iconAngle")
|
||||
self.RunningAnimation.setDuration(1000)
|
||||
self.RunningAnimation.setStartValue(0)
|
||||
self.RunningAnimation.setEndValue(-360)
|
||||
self.RunningAnimation.setLoopCount(-1)
|
||||
self.RunningAnimation.setEasingCurve(QEasingCurve.Type.Linear)
|
||||
|
||||
def isDarkMode(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
return self.palette().color(QPalette.ColorRole.Window).value() < 128
|
||||
|
||||
def getMarkColor(
|
||||
self
|
||||
) -> QColor:
|
||||
|
||||
return QColor("#FFFFFF") if self.isDarkMode() else QColor("#454545")
|
||||
|
||||
@Property(Status)
|
||||
def status(
|
||||
self
|
||||
) -> Status:
|
||||
|
||||
return self.__status
|
||||
|
||||
@Property(int)
|
||||
def iconAngle(
|
||||
self
|
||||
) -> int:
|
||||
|
||||
return self.__icon_angle
|
||||
|
||||
@status.setter
|
||||
def status(
|
||||
self,
|
||||
status: Status
|
||||
):
|
||||
|
||||
if status not in self.Status:
|
||||
raise ValueError(f"Invalid (class)Status[enum.Enum] value: {status}")
|
||||
self.__status = status
|
||||
if self.__status == self.Status.RUNNING:
|
||||
self.RunningAnimation.start()
|
||||
else:
|
||||
self.RunningAnimation.stop()
|
||||
self.update()
|
||||
|
||||
@iconAngle.setter
|
||||
def iconAngle(
|
||||
self,
|
||||
value: int
|
||||
):
|
||||
|
||||
self.__icon_angle = value
|
||||
self.update()
|
||||
|
||||
def paintEvent(
|
||||
self,
|
||||
event
|
||||
):
|
||||
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
center_x = self.width()/2
|
||||
center_y = self.height()/2
|
||||
radius = min(center_x, center_y) - 3
|
||||
match self.__status:
|
||||
case self.Status.WAITING:
|
||||
pen = painter.pen()
|
||||
pen.setWidth(2)
|
||||
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
||||
pen.setColor(QColor("#969696")) # grey
|
||||
painter.setPen(pen)
|
||||
painter.drawEllipse(
|
||||
int(center_x - radius),
|
||||
int(center_y - radius),
|
||||
int(radius*2),
|
||||
int(radius*2)
|
||||
)
|
||||
case self.Status.RUNNING:
|
||||
gradient = QConicalGradient(center_x, center_y, self.__icon_angle)
|
||||
gradient.setColorAt(0.0, QColor("#2294FF" if self.isDarkMode() else "#0094FF"))
|
||||
gradient.setColorAt(1.0, QColor("#2294FF00"))
|
||||
pen = painter.pen()
|
||||
pen.setWidth(3)
|
||||
pen.setBrush(gradient)
|
||||
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
||||
painter.setPen(pen)
|
||||
painter.drawEllipse(
|
||||
int(center_x - radius),
|
||||
int(center_y - radius),
|
||||
int(radius*2),
|
||||
int(radius*2)
|
||||
)
|
||||
case self.Status.SUCCESS:
|
||||
# draw the success green circle
|
||||
pen = painter.pen()
|
||||
pen.setWidth(2)
|
||||
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
||||
pen.setColor(QColor("#4CAF50" if self.isDarkMode() else "#00AF50")) # green
|
||||
painter.setPen(pen)
|
||||
painter.drawEllipse(
|
||||
int(center_x - radius),
|
||||
int(center_y - radius),
|
||||
int(radius*2),
|
||||
int(radius*2)
|
||||
)
|
||||
# draw the success check mark '✓'
|
||||
painter.setPen(Qt.PenStyle.SolidLine)
|
||||
pen = painter.pen()
|
||||
pen.setWidth(3)
|
||||
pen.setBrush(Qt.BrushStyle.NoBrush)
|
||||
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
||||
# white when dark mode, black when light mode
|
||||
pen.setColor(self.getMarkColor())
|
||||
painter.setPen(pen)
|
||||
mark_size = radius/2
|
||||
mark_path = [
|
||||
(center_x - mark_size, center_y),
|
||||
(center_x - mark_size/3, center_y + mark_size/2),
|
||||
(center_x + mark_size, center_y - mark_size/2)
|
||||
]
|
||||
painter.drawLine(
|
||||
int(mark_path[0][0]),int(mark_path[0][1]),
|
||||
int(mark_path[1][0]),int(mark_path[1][1])
|
||||
)
|
||||
painter.drawLine(
|
||||
int(mark_path[1][0]),int(mark_path[1][1]),
|
||||
int(mark_path[2][0]),int(mark_path[2][1])
|
||||
)
|
||||
case self.Status.WARNING:
|
||||
# draw the warning orange circle
|
||||
pen = painter.pen()
|
||||
pen.setWidth(2)
|
||||
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
||||
pen.setColor(QColor("#FF9800")) # orange
|
||||
painter.setPen(pen)
|
||||
painter.drawEllipse(
|
||||
int(center_x - radius),
|
||||
int(center_y - radius),
|
||||
int(radius*2),
|
||||
int(radius*2)
|
||||
)
|
||||
# draw the warning exclamation mark '!'
|
||||
painter.setPen(Qt.PenStyle.SolidLine)
|
||||
pen = painter.pen()
|
||||
pen.setWidth(3)
|
||||
pen.setBrush(Qt.BrushStyle.NoBrush)
|
||||
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
||||
# white when dark mode, black when light mode
|
||||
pen.setColor(self.getMarkColor())
|
||||
painter.setPen(pen)
|
||||
painter.drawLine(
|
||||
int(center_x), int(center_y - radius/2),
|
||||
int(center_x), int(center_y + radius/6)
|
||||
)
|
||||
painter.drawPoint(
|
||||
int(center_x), int(center_y + radius/2)
|
||||
)
|
||||
case self.Status.FAILURE:
|
||||
# draw the failure red circle
|
||||
pen = painter.pen()
|
||||
pen.setWidth(2)
|
||||
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
||||
pen.setColor(QColor("#DC0000")) # red
|
||||
painter.setPen(pen)
|
||||
painter.drawEllipse(
|
||||
int(center_x - radius),
|
||||
int(center_y - radius),
|
||||
int(radius*2),
|
||||
int(radius*2)
|
||||
)
|
||||
# draw the failure cross mark '✗'
|
||||
painter.setPen(Qt.PenStyle.SolidLine)
|
||||
pen = painter.pen()
|
||||
pen.setWidth(3)
|
||||
pen.setBrush(Qt.BrushStyle.NoBrush)
|
||||
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
||||
# white when dark mode, black when light mode
|
||||
pen.setColor(self.getMarkColor())
|
||||
painter.setPen(pen)
|
||||
mark_size = radius/3
|
||||
painter.drawLine(
|
||||
int(center_x - mark_size), int(center_y - mark_size),
|
||||
int(center_x + mark_size), int(center_y + mark_size)
|
||||
)
|
||||
painter.drawLine(
|
||||
int(center_x + mark_size), int(center_y - mark_size),
|
||||
int(center_x - mark_size), int(center_y + mark_size)
|
||||
)
|
||||
painter.end()
|
||||
super().paintEvent(event)
|
||||
@@ -0,0 +1,326 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 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 uuid
|
||||
|
||||
from enum import Enum
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Slot,
|
||||
QDateTime,
|
||||
QUrl
|
||||
)
|
||||
from PySide6.QtGui import QDesktopServices
|
||||
from PySide6.QtWidgets import (
|
||||
QLabel,
|
||||
QDialog,
|
||||
QWidget,
|
||||
QSpinBox,
|
||||
QHBoxLayout,
|
||||
QVBoxLayout,
|
||||
QDateTimeEdit,
|
||||
QGroupBox,
|
||||
QPushButton
|
||||
)
|
||||
|
||||
from gui.resources.ui.Ui_ALTimerTaskAddDialog import Ui_ALTimerTaskAddDialog
|
||||
from utils.TimerUtils import TimerUtils
|
||||
|
||||
|
||||
class ALTimerTaskStatus(Enum):
|
||||
|
||||
PENDING = "等待中"
|
||||
READY = "已就绪"
|
||||
RUNNING = "执行中"
|
||||
EXECUTED = "已执行"
|
||||
ERROR = "执行失败"
|
||||
OUTDATED = "已过期"
|
||||
UNKNOWN = "未知"
|
||||
|
||||
|
||||
class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None,
|
||||
timer_task: dict = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self.__edit_timer_task = timer_task
|
||||
|
||||
self.setupUi(self)
|
||||
self.modifyUi()
|
||||
self.connectSignals()
|
||||
|
||||
if self.__edit_timer_task:
|
||||
self.loadTask(self.__edit_timer_task)
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.TimerTypeComboBox.setCurrentIndex(0)
|
||||
self.SpecificTimerWidget = QWidget()
|
||||
self.SpecificTimerLayout = QHBoxLayout(self.SpecificTimerWidget)
|
||||
self.SpecificTimerLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.SpecificTimerLayout.setSpacing(5)
|
||||
self.SpecificTimerLayout.addWidget(QLabel("定时时间:"))
|
||||
self.SpecificDateTimeEdit = QDateTimeEdit()
|
||||
self.SpecificDateTimeEdit.setCalendarPopup(True)
|
||||
self.SpecificDateTimeEdit.setDisplayFormat("yyyy-MM-dd HH:mm:ss")
|
||||
self.SpecificDateTimeEdit.setMinimumDateTime(QDateTime.currentDateTime())
|
||||
self.SpecificDateTimeEdit.setDateTime(QDateTime.currentDateTime().addSecs(60))
|
||||
self.SpecificTimerLayout.addWidget(self.SpecificDateTimeEdit)
|
||||
self.TimerConfigLayout.addWidget(self.SpecificTimerWidget)
|
||||
self.RelativeTimerWidget = QWidget()
|
||||
self.RelativeTimerLayout = QHBoxLayout(self.RelativeTimerWidget)
|
||||
self.RelativeTimerLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.RelativeTimerLayout.setSpacing(5)
|
||||
self.RelativeTimerLayout.addWidget(QLabel("相对时间:"))
|
||||
self.RelativeDaySpinBox = QSpinBox()
|
||||
self.RelativeDaySpinBox.setMinimum(0)
|
||||
self.RelativeDaySpinBox.setMaximum(364)
|
||||
self.RelativeDaySpinBox.setSuffix("天")
|
||||
self.RelativeTimerLayout.addWidget(self.RelativeDaySpinBox)
|
||||
self.RelativeHourSpinBox = QSpinBox()
|
||||
self.RelativeHourSpinBox.setMinimum(0)
|
||||
self.RelativeHourSpinBox.setMaximum(23)
|
||||
self.RelativeHourSpinBox.setSuffix("时")
|
||||
self.RelativeTimerLayout.addWidget(self.RelativeHourSpinBox)
|
||||
self.RelativeMinuteSpinBox = QSpinBox()
|
||||
self.RelativeMinuteSpinBox.setMinimum(0)
|
||||
self.RelativeMinuteSpinBox.setMaximum(59)
|
||||
self.RelativeMinuteSpinBox.setSuffix("分")
|
||||
self.RelativeTimerLayout.addWidget(self.RelativeMinuteSpinBox)
|
||||
self.RelativeSecondSpinBox = QSpinBox()
|
||||
self.RelativeSecondSpinBox.setMinimum(0)
|
||||
self.RelativeSecondSpinBox.setMaximum(59)
|
||||
self.RelativeSecondSpinBox.setSuffix("秒")
|
||||
self.RelativeTimerLayout.addWidget(self.RelativeSecondSpinBox)
|
||||
self.TimerConfigLayout.addWidget(self.RelativeTimerWidget)
|
||||
self.RelativeTimerWidget.setVisible(False)
|
||||
self.AutoScriptGroupBox = QGroupBox("AutoScript 指令")
|
||||
self.AutoScriptLayout = QVBoxLayout(self.AutoScriptGroupBox)
|
||||
self.AutoScriptLayout.setContentsMargins(3, 3, 3, 3)
|
||||
self.AutoScriptLayout.setSpacing(3)
|
||||
AutoScriptBtnLayout = QHBoxLayout()
|
||||
self.AutoScriptEditButton = QPushButton("编辑")
|
||||
self.AutoScriptEditButton.setMinimumHeight(25)
|
||||
self.AutoScriptEditButton.setFixedWidth(80)
|
||||
AutoScriptBtnLayout.addWidget(self.AutoScriptEditButton)
|
||||
AutoScriptBtnLayout.addStretch()
|
||||
self.AutoScriptHelpButton = QPushButton("?")
|
||||
self.AutoScriptHelpButton.setFixedSize(20, 20)
|
||||
self.AutoScriptHelpButton.setToolTip(
|
||||
"AutoScript 是一种轻量级 DSL 语言,基于 Lua 实现。\n"
|
||||
"用于在重复定时任务执行前,对用户的预约数据进行预处理\n"
|
||||
"\n"
|
||||
"点击查看完整在线文档"
|
||||
)
|
||||
self.AutoScriptHelpButton.setStyleSheet(
|
||||
"QPushButton { border-radius: 10px; border: 1px solid #999; "
|
||||
"font-weight: bold; color: #555; }"
|
||||
"QPushButton:hover { background-color: #E0E0E0; }"
|
||||
)
|
||||
AutoScriptBtnLayout.addWidget(self.AutoScriptHelpButton)
|
||||
self.AutoScriptStatusLabel = QLabel("未设置")
|
||||
self.AutoScriptStatusLabel.setStyleSheet("color: #969696;")
|
||||
self.AutoScriptStatusLabel.setFixedHeight(25)
|
||||
AutoScriptBtnLayout.addWidget(self.AutoScriptStatusLabel)
|
||||
self.AutoScriptLayout.addLayout(AutoScriptBtnLayout)
|
||||
self.ALAddTimerTaskLayout.insertWidget(
|
||||
self.ALAddTimerTaskLayout.indexOf(self.TaskConfigGroupBox) + 1,
|
||||
self.AutoScriptGroupBox
|
||||
)
|
||||
self.AutoScriptGroupBox.setVisible(False)
|
||||
self.__auto_script = ""
|
||||
self.__mock_target_data = None
|
||||
|
||||
def loadTask(
|
||||
self,
|
||||
task: dict
|
||||
):
|
||||
|
||||
self.TaskNameLineEdit.setText(task.get("name", ""))
|
||||
time_type = task.get("time_type", "特定时间")
|
||||
self.TimerTypeComboBox.setCurrentText(time_type)
|
||||
self.SpecificDateTimeEdit.setDateTime(
|
||||
QDateTime(task["execute_time"])
|
||||
)
|
||||
self.RelativeDaySpinBox.setValue(0)
|
||||
self.RelativeHourSpinBox.setValue(0)
|
||||
self.RelativeMinuteSpinBox.setValue(0)
|
||||
self.RelativeSecondSpinBox.setValue(0)
|
||||
if task.get("silent", False):
|
||||
self.SilentlyRunRadioButton.setChecked(True)
|
||||
else:
|
||||
self.ShowBeforeRunRadioButton.setChecked(True)
|
||||
repeat = task.get("repeat", False)
|
||||
self.RepeatCheckBox.setChecked(repeat)
|
||||
if repeat:
|
||||
repeat_days = task.get("repeat_days", [])
|
||||
self.MonCheckBox.setChecked(0 in repeat_days)
|
||||
self.TueCheckBox.setChecked(1 in repeat_days)
|
||||
self.WedCheckBox.setChecked(2 in repeat_days)
|
||||
self.ThuCheckBox.setChecked(3 in repeat_days)
|
||||
self.FriCheckBox.setChecked(4 in repeat_days)
|
||||
self.SatCheckBox.setChecked(5 in repeat_days)
|
||||
self.SunCheckBox.setChecked(6 in repeat_days)
|
||||
auto_script = task.get("repeat_auto_script", "")
|
||||
if auto_script:
|
||||
self.__auto_script = auto_script
|
||||
self.AutoScriptStatusLabel.setText("已设置")
|
||||
self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;")
|
||||
mock_data = task.get("mock_target_data")
|
||||
if mock_data:
|
||||
self.__mock_target_data = mock_data
|
||||
self.ConfirmButton.setText("保存")
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.CancelButton.clicked.connect(self.reject)
|
||||
self.ConfirmButton.clicked.connect(self.accept)
|
||||
self.TimerTypeComboBox.currentIndexChanged.connect(self.onTimerTypeComboBoxIndexChanged)
|
||||
self.RepeatCheckBox.toggled.connect(self.onRepeatCheckBoxToggled)
|
||||
self.AutoScriptEditButton.clicked.connect(self.onPreviewAutoScript)
|
||||
self.AutoScriptHelpButton.clicked.connect(self.onAutoScriptHelp)
|
||||
|
||||
def getTimerTask(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
added_time = datetime.now()
|
||||
if not self.TaskNameLineEdit.text():
|
||||
name = f"未命名任务-{added_time.strftime("%Y%m%d%H%M%S")}"
|
||||
else:
|
||||
name = self.TaskNameLineEdit.text()
|
||||
timer_type_index = self.TimerTypeComboBox.currentIndex()
|
||||
silent = not self.ShowBeforeRunRadioButton.isChecked()
|
||||
if timer_type_index == 0:
|
||||
execute_time = self.SpecificDateTimeEdit.dateTime()
|
||||
tmp_time_str = execute_time.toString("yyyy-MM-dd HH:mm:ss")
|
||||
execute_time = datetime.strptime(tmp_time_str, "%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
execute_time = datetime.now() + timedelta(
|
||||
days = self.RelativeDaySpinBox.value(),
|
||||
hours = self.RelativeHourSpinBox.value(),
|
||||
minutes = self.RelativeMinuteSpinBox.value(),
|
||||
seconds = self.RelativeSecondSpinBox.value()
|
||||
)
|
||||
|
||||
if self.__edit_timer_task:
|
||||
task_data = dict(self.__edit_timer_task)
|
||||
task_data["name"] = name
|
||||
task_data["execute_time"] = execute_time
|
||||
task_data["silent"] = silent
|
||||
task_data["status"] = ALTimerTaskStatus.PENDING
|
||||
task_data["executed"] = False
|
||||
task_data["repeat_auto_script"] = self.__auto_script
|
||||
task_data["mock_target_data"] = self.__mock_target_data
|
||||
else:
|
||||
task_data = {
|
||||
"name": name,
|
||||
"uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}",
|
||||
"time_type": self.TimerTypeComboBox.currentText(),
|
||||
"execute_time": execute_time,
|
||||
"silent": silent,
|
||||
"added_time": added_time,
|
||||
"status": ALTimerTaskStatus.PENDING,
|
||||
"executed": False,
|
||||
"repeat": self.RepeatCheckBox.isChecked(),
|
||||
"repeat_auto_script": self.__auto_script,
|
||||
"mock_target_data": self.__mock_target_data,
|
||||
}
|
||||
|
||||
repeat = self.RepeatCheckBox.isChecked()
|
||||
task_data["repeat"] = repeat
|
||||
if repeat:
|
||||
if "repeat_history" not in task_data:
|
||||
task_data["repeat_history"] = []
|
||||
repeat_days = []
|
||||
if self.MonCheckBox.isChecked():
|
||||
repeat_days.append(0)
|
||||
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.getNextTimerRepeatTime(
|
||||
task_data["repeat_days"],
|
||||
task_data["repeat_hour"],
|
||||
task_data["repeat_minute"],
|
||||
task_data["repeat_second"]
|
||||
)
|
||||
return task_data
|
||||
|
||||
@Slot(int)
|
||||
def onTimerTypeComboBoxIndexChanged(
|
||||
self,
|
||||
index: int
|
||||
):
|
||||
|
||||
self.SpecificTimerWidget.setVisible(index == 0)
|
||||
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)
|
||||
self.AutoScriptGroupBox.setVisible(checked)
|
||||
|
||||
@Slot()
|
||||
def onPreviewAutoScript(self):
|
||||
from gui.ALAutoScriptEditDialog import ALAutoScriptEditDialog
|
||||
Dlg = ALAutoScriptEditDialog(self, self.__auto_script, self.__mock_target_data)
|
||||
if Dlg.exec() == QDialog.DialogCode.Accepted:
|
||||
script = Dlg.getScript()
|
||||
self.__auto_script = script
|
||||
self.__mock_target_data = Dlg.getMockData()
|
||||
if script:
|
||||
self.AutoScriptStatusLabel.setText("已设置")
|
||||
self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;")
|
||||
else:
|
||||
self.AutoScriptStatusLabel.setText("未设置")
|
||||
self.AutoScriptStatusLabel.setStyleSheet("color: #969696;")
|
||||
Dlg.deleteLater()
|
||||
|
||||
@Slot()
|
||||
def onAutoScriptHelp(
|
||||
self
|
||||
):
|
||||
|
||||
QDesktopServices.openUrl(
|
||||
QUrl("https://manuals.autolibrary.kenanzhu.com/zh/autoscript")
|
||||
)
|
||||
@@ -0,0 +1,138 @@
|
||||
# -*- 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("repeat_history", [])
|
||||
|
||||
self.setupUi()
|
||||
self.connectSignals()
|
||||
|
||||
def setupUi(
|
||||
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: 12px;")
|
||||
InfoLayout.addWidget(TaskNameLabel, 0, 0)
|
||||
TaskUUIDLabel = QLabel(f"UUID: {self.__task_data.get('uuid', '未命名')}")
|
||||
TaskUUIDLabel.setStyleSheet("color: #969696; font-size: 11px;")
|
||||
InfoLayout.addWidget(TaskUUIDLabel, 1, 0)
|
||||
InfoLayout.setColumnStretch(0, 1)
|
||||
if self.__task_data.get("repeat", False):
|
||||
RepeatLabel = QLabel("可重复性任务")
|
||||
RepeatLabel.setStyleSheet("color: #2294FF; 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 getHistory(
|
||||
self
|
||||
) -> list:
|
||||
|
||||
return self.__history
|
||||
|
||||
def addHistoryRow(
|
||||
self,
|
||||
row: int,
|
||||
record: dict
|
||||
):
|
||||
|
||||
execute_time = record.get("execute_time", "")
|
||||
result = record.get("result", ALTimerTaskStatus.UNKNOWN)
|
||||
duration = record.get("duration", 0)
|
||||
execute_time_item = QTableWidgetItem(execute_time)
|
||||
execute_time_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.HistoryTableWidget.setItem(row, 0, execute_time_item)
|
||||
result_item = QTableWidgetItem(result.value)
|
||||
result_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
match result:
|
||||
case ALTimerTaskStatus.EXECUTED:
|
||||
result_item.setForeground(Qt.GlobalColor.green)
|
||||
case ALTimerTaskStatus.ERROR:
|
||||
result_item.setForeground(Qt.GlobalColor.red)
|
||||
case ALTimerTaskStatus.OUTDATED:
|
||||
result_item.setForeground(Qt.GlobalColor.red)
|
||||
case _:
|
||||
result_item.setForeground(Qt.GlobalColor.black)
|
||||
self.HistoryTableWidget.setItem(row, 1, result_item)
|
||||
duration_item = QTableWidgetItem(f"{duration:.2f}")
|
||||
duration_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.HistoryTableWidget.setItem(row, 2, duration_item)
|
||||
self.HistoryTableWidget.setRowHeight(row, 25)
|
||||
|
||||
@Slot()
|
||||
def onClearHistoryButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.__history.clear()
|
||||
self.HistoryTableWidget.setRowCount(0)
|
||||
self.__task_data["repeat_history"] = self.__history # = []
|
||||
@@ -0,0 +1,679 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 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 sys
|
||||
import copy
|
||||
|
||||
from enum import Enum
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from PySide6.QtCore import (
|
||||
QTimer,
|
||||
Qt,
|
||||
Signal,
|
||||
Slot
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QAction,
|
||||
QCloseEvent
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QListWidgetItem,
|
||||
QMenu,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget
|
||||
)
|
||||
|
||||
import managers.config.ConfigManager as ConfigManager
|
||||
|
||||
from gui.ALTimerTaskAddDialog import (
|
||||
ALTimerTaskAddDialog,
|
||||
ALTimerTaskStatus
|
||||
)
|
||||
from gui.ALTimerTaskHistoryDialog import ALTimerTaskHistoryDialog
|
||||
from gui.ALWidgetMixin import CenterOnParentMixin
|
||||
from gui.resources.ui.Ui_ALTimerTaskManageWidget import Ui_ALTimerTaskManageWidget
|
||||
from interfaces.ConfigProvider import (
|
||||
CfgKey,
|
||||
ConfigProvider
|
||||
)
|
||||
from utils.TimerUtils import TimerUtils
|
||||
|
||||
|
||||
class ALTimerTaskItemWidget(QWidget):
|
||||
|
||||
editRequested = Signal(dict)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None,
|
||||
timer_task: dict = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self.__timer_task = timer_task
|
||||
self.__manage_widget = parent
|
||||
|
||||
self.modifyUi()
|
||||
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self.showContextMenu)
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.ItemWidgetLayout = QHBoxLayout(self)
|
||||
self.ItemWidgetLayout.setSpacing(10)
|
||||
self.ItemWidgetLayout.setContentsMargins(10, 5, 10, 5)
|
||||
|
||||
self.TaskInfoLayout = QVBoxLayout()
|
||||
self.TaskInfoLayout.setSpacing(5)
|
||||
TaskNameLabel = QLabel(self.__timer_task["name"])
|
||||
task_name_label_font = TaskNameLabel.font()
|
||||
task_name_label_font.setBold(True)
|
||||
TaskNameLabel.setFont(task_name_label_font)
|
||||
TaskNameLabel.setFixedHeight(25)
|
||||
self.TaskInfoLayout.addWidget(TaskNameLabel)
|
||||
execute_time_str = 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"下次执行时间: {execute_time_str} (每日 {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"下次执行时间: {execute_time_str} (每{','.join(selected_days)} {time_str})")
|
||||
else:
|
||||
ExecuteTimeLabel = QLabel(f"执行时间: {execute_time_str}")
|
||||
ExecuteTimeLabel.setStyleSheet("color: #969696;")
|
||||
ExecuteTimeLabel.setFixedHeight(20)
|
||||
self.TaskInfoLayout.addWidget(ExecuteTimeLabel)
|
||||
self.ItemWidgetLayout.addLayout(self.TaskInfoLayout)
|
||||
self.ItemWidgetLayout.addStretch()
|
||||
|
||||
match self.__timer_task["status"]:
|
||||
case ALTimerTaskStatus.PENDING:
|
||||
TaskStatusText = "等待中"
|
||||
TaskStatusColor = "#FF9800"
|
||||
case ALTimerTaskStatus.READY:
|
||||
TaskStatusText = "已就绪"
|
||||
TaskStatusColor = "#316BFF"
|
||||
case ALTimerTaskStatus.RUNNING:
|
||||
TaskStatusText = "执行中"
|
||||
TaskStatusColor = "#2294FF"
|
||||
case ALTimerTaskStatus.EXECUTED:
|
||||
TaskStatusText = "已执行"
|
||||
TaskStatusColor = "#4CAF50"
|
||||
case ALTimerTaskStatus.ERROR:
|
||||
TaskStatusText = "执行失败"
|
||||
TaskStatusColor = "#DC0000"
|
||||
case ALTimerTaskStatus.OUTDATED:
|
||||
TaskStatusText = "已过期"
|
||||
TaskStatusColor = "#DC0000"
|
||||
TaskStatusLabel = QLabel(TaskStatusText)
|
||||
TaskStatusLabel.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
background-color: {TaskStatusColor};
|
||||
color: #FFFFFF;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
""")
|
||||
TaskStatusLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
TaskStatusLabel.setFixedSize(80, 25)
|
||||
self.ItemWidgetLayout.addWidget(TaskStatusLabel)
|
||||
|
||||
TaskModeText = "静默" if self.__timer_task["silent"] else "显示"
|
||||
TaskModeColor = "#6325FF" if self.__timer_task["silent"] else "#2294FF"
|
||||
TaskModeLabel = QLabel(TaskModeText)
|
||||
TaskModeLabel.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
background-color: {TaskModeColor};
|
||||
color: #FFFFFF;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
""")
|
||||
TaskModeLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
TaskModeLabel.setFixedSize(60, 25)
|
||||
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.setFixedSize(80, 25)
|
||||
self.DeleteButton.setStyleSheet("color: #DC0000;")
|
||||
self.ItemWidgetLayout.addWidget(self.DeleteButton)
|
||||
if self.__timer_task["status"] == ALTimerTaskStatus.READY\
|
||||
or self.__timer_task["status"] == ALTimerTaskStatus.RUNNING:
|
||||
self.DeleteButton.setEnabled(False)
|
||||
self.setFixedHeight(55)
|
||||
|
||||
@Slot(object)
|
||||
def showContextMenu(
|
||||
self,
|
||||
pos
|
||||
):
|
||||
|
||||
Menu = QMenu(self)
|
||||
EditAction = QAction("编辑", self)
|
||||
EditAction.triggered.connect(
|
||||
lambda: self.editRequested.emit(self.__timer_task)
|
||||
)
|
||||
Menu.addAction(EditAction)
|
||||
if self.__timer_task["status"] != ALTimerTaskStatus.RUNNING\
|
||||
and self.__timer_task["status"] != ALTimerTaskStatus.READY:
|
||||
DeleteAction = QAction("删除", self)
|
||||
DeleteAction.triggered.connect(
|
||||
lambda: self.__manage_widget.deleteTask(self.__timer_task)
|
||||
)
|
||||
Menu.addAction(DeleteAction)
|
||||
Menu.exec(self.mapToGlobal(pos))
|
||||
|
||||
|
||||
class ALTimerTaskManageWidget(CenterOnParentMixin, QWidget, Ui_ALTimerTaskManageWidget):
|
||||
|
||||
class SortPolicy(Enum):
|
||||
|
||||
BY_NAME = "按名称"
|
||||
BY_ADD_TIME = "按添加时间"
|
||||
BY_EXECUTE_TIME = "按执行时间"
|
||||
|
||||
timerTaskIsReady = Signal(dict)
|
||||
timerTasksChanged = Signal()
|
||||
timerTaskManageWidgetIsClosed = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self.__cfg_mgr: ConfigProvider = ConfigManager.instance()
|
||||
self.__timer_tasks = []
|
||||
self.__CheckTimer = None
|
||||
self.__sort_policy = self.SortPolicy.BY_EXECUTE_TIME
|
||||
self.__sort_order = Qt.SortOrder.AscendingOrder
|
||||
|
||||
self.setupUi(self)
|
||||
self.connectSignals()
|
||||
self.setupTimer()
|
||||
if not self.initializeTimerTasks():
|
||||
raise Exception("定时任务配置文件初始化失败 !")
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.AddTimerTaskButton.clicked.connect(self.addTask)
|
||||
self.ClearAllTimerTasksButton.clicked.connect(self.clearAllTasks)
|
||||
self.TimerTaskSortTypeComboBox.currentIndexChanged.connect(self.onSortPolicyComboBoxChanged)
|
||||
self.TimerTaskSortOrderToggleButton.clicked.connect(self.onSortOrderToggleButtonClicked)
|
||||
self.timerTasksChanged.connect(self.onTimerTasksChanged)
|
||||
|
||||
def setupTimer(
|
||||
self
|
||||
):
|
||||
|
||||
self.__CheckTimer = QTimer(self)
|
||||
self.__CheckTimer.timeout.connect(self.checkTasks)
|
||||
self.__CheckTimer.start(500)
|
||||
|
||||
def initializeTimerTasks(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
timer_tasks = self.getTimerTasks()
|
||||
if timer_tasks is not None:
|
||||
self.__timer_tasks = timer_tasks
|
||||
self.timerTasksChanged.emit()
|
||||
return True
|
||||
timer_tasks = []
|
||||
if self.setTimerTasks(copy.deepcopy(timer_tasks)):
|
||||
self.__timer_tasks = timer_tasks
|
||||
return True
|
||||
return False
|
||||
|
||||
def getTimerTasks(
|
||||
self
|
||||
) -> list:
|
||||
|
||||
try:
|
||||
timer_tasks = self.__cfg_mgr.get(CfgKey.TIMERTASK.ROOT)
|
||||
if timer_tasks and "timer_tasks" in timer_tasks:
|
||||
for task in timer_tasks["timer_tasks"]:
|
||||
task["added_time"] = datetime.strptime(task["added_time"], "%Y-%m-%d %H:%M:%S")
|
||||
task["execute_time"] = datetime.strptime(task["execute_time"], "%Y-%m-%d %H:%M:%S")
|
||||
task["status"] = ALTimerTaskStatus(task["status"])
|
||||
if "repeat_history" in task:
|
||||
for item in task["repeat_history"]:
|
||||
item["result"] = ALTimerTaskStatus(item["result"])
|
||||
return timer_tasks["timer_tasks"]
|
||||
raise Exception("定时任务配置文件格式错误")
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"加载定时任务配置发生错误 ! : \n{e}"
|
||||
)
|
||||
return None
|
||||
|
||||
def setTimerTasks(
|
||||
self,
|
||||
timer_tasks: list
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
for task in timer_tasks:
|
||||
task["added_time"] = task["added_time"].strftime("%Y-%m-%d %H:%M:%S")
|
||||
task["execute_time"] = task["execute_time"].strftime("%Y-%m-%d %H:%M:%S")
|
||||
task["status"] = task["status"].value
|
||||
if "repeat_history" in task:
|
||||
for item in task["repeat_history"]:
|
||||
item["result"] = item["result"].value
|
||||
self.__cfg_mgr.set(CfgKey.TIMERTASK.ROOT, { "timer_tasks": timer_tasks })
|
||||
return True
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"保存定时任务配置发生错误 ! : \n{e}"
|
||||
)
|
||||
return False
|
||||
|
||||
def closeEvent(
|
||||
self,
|
||||
event: QCloseEvent
|
||||
):
|
||||
|
||||
self.hide()
|
||||
self.timerTaskManageWidgetIsClosed.emit()
|
||||
event.ignore()
|
||||
|
||||
def sortTimerTasks(
|
||||
self,
|
||||
policy: SortPolicy = SortPolicy.BY_EXECUTE_TIME,
|
||||
order: Qt.SortOrder = Qt.SortOrder.AscendingOrder
|
||||
):
|
||||
|
||||
if policy == self.SortPolicy.BY_NAME:
|
||||
self.__timer_tasks.sort(
|
||||
key = lambda x: x["name"],
|
||||
reverse = order is Qt.SortOrder.DescendingOrder
|
||||
)
|
||||
elif policy == self.SortPolicy.BY_ADD_TIME:
|
||||
self.__timer_tasks.sort(
|
||||
key = lambda x: x["added_time"],
|
||||
reverse = order is Qt.SortOrder.DescendingOrder
|
||||
)
|
||||
elif policy == self.SortPolicy.BY_EXECUTE_TIME:
|
||||
self.__timer_tasks.sort(
|
||||
key = lambda x: x["execute_time"],
|
||||
reverse = order is Qt.SortOrder.DescendingOrder
|
||||
)
|
||||
|
||||
def updateStat(
|
||||
self
|
||||
):
|
||||
|
||||
pending = 0
|
||||
in_queue = 0
|
||||
executed = 0
|
||||
invalid = 0
|
||||
total = len(self.__timer_tasks)
|
||||
for timer_task in self.__timer_tasks:
|
||||
if timer_task["status"] == ALTimerTaskStatus.PENDING:
|
||||
pending += 1
|
||||
elif timer_task["status"] == ALTimerTaskStatus.READY\
|
||||
or timer_task["status"] == ALTimerTaskStatus.RUNNING:
|
||||
in_queue += 1
|
||||
elif timer_task["status"] == ALTimerTaskStatus.EXECUTED:
|
||||
executed += 1
|
||||
elif timer_task["status"] == ALTimerTaskStatus.ERROR\
|
||||
or timer_task["status"] == ALTimerTaskStatus.OUTDATED:
|
||||
invalid += 1
|
||||
self.TotalTaskLabel.setText(f"总任务:{total}")
|
||||
self.PendingTaskLabel.setText(f"待执行:{pending}")
|
||||
self.InQueueTaskLabel.setText(f"队列中:{in_queue}")
|
||||
self.ExecutedTaskLabel.setText(f"已执行:{executed}")
|
||||
self.InvalidTaskLabel.setText(f"无效的:{invalid}")
|
||||
|
||||
def updateTimerTaskList(
|
||||
self
|
||||
):
|
||||
|
||||
self.TimerTasksListWidget.clear()
|
||||
self.sortTimerTasks(self.__sort_policy, self.__sort_order)
|
||||
for timer_task in self.__timer_tasks:
|
||||
Item = QListWidgetItem()
|
||||
Item.setData(Qt.UserRole, timer_task)
|
||||
Widget = ALTimerTaskItemWidget(self, timer_task)
|
||||
Widget.DeleteButton.clicked.connect(
|
||||
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)
|
||||
)
|
||||
Widget.editRequested.connect(self.editTask)
|
||||
Item.setSizeHint(Widget.size())
|
||||
self.TimerTasksListWidget.addItem(Item)
|
||||
self.TimerTasksListWidget.setItemWidget(Item, Widget)
|
||||
|
||||
def addTask(
|
||||
self
|
||||
):
|
||||
|
||||
Dialog = ALTimerTaskAddDialog(self)
|
||||
if Dialog.exec() == QDialog.DialogCode.Accepted:
|
||||
timer_task = Dialog.getTimerTask()
|
||||
self.__timer_tasks.append(timer_task)
|
||||
self.timerTasksChanged.emit()
|
||||
|
||||
def editTask(
|
||||
self,
|
||||
timer_task: dict
|
||||
):
|
||||
|
||||
Dialog = ALTimerTaskAddDialog(self, timer_task)
|
||||
if Dialog.exec() == QDialog.DialogCode.Accepted:
|
||||
updated = Dialog.getTimerTask()
|
||||
for i, task in enumerate(self.__timer_tasks):
|
||||
if task["uuid"] == updated["uuid"]:
|
||||
self.__timer_tasks[i] = updated
|
||||
break
|
||||
self.timerTasksChanged.emit()
|
||||
|
||||
@staticmethod
|
||||
def getTimerTaskDetailMessage(
|
||||
timer_task: dict
|
||||
):
|
||||
|
||||
if "repeat_history" not in timer_task:
|
||||
history = []
|
||||
else:
|
||||
history = timer_task["repeat_history"]
|
||||
history_count = len(history)
|
||||
return (
|
||||
f"任务名称:{timer_task["name"]}\n"
|
||||
f"添加时间:{timer_task["added_time"]}\n"
|
||||
f"当前状态:{timer_task["status"].value}\n"
|
||||
f"下次执行时间:{datetime.strftime(timer_task["execute_time"], "%Y-%m-%d %H:%M:%S")}\n"
|
||||
f"已记录次数:{history_count}"
|
||||
)
|
||||
|
||||
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["uuid"]
|
||||
self.__timer_tasks = [
|
||||
x for x in self.__timer_tasks
|
||||
if x["uuid"] != task_uuid
|
||||
]
|
||||
self.timerTasksChanged.emit()
|
||||
|
||||
def clearAllTasks(
|
||||
self
|
||||
):
|
||||
|
||||
if not self.__timer_tasks:
|
||||
return
|
||||
result = QMessageBox.question(
|
||||
self,
|
||||
"确认 - AutoLibrary",
|
||||
"是否要清除所有定时任务 ?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
if result == QMessageBox.StandardButton.No:
|
||||
return
|
||||
# READY and RUNNING tasks cannot be cleared
|
||||
in_queue_tasks = [
|
||||
x for x in self.__timer_tasks
|
||||
if x["status"] == ALTimerTaskStatus.READY
|
||||
or x["status"] == ALTimerTaskStatus.RUNNING
|
||||
]
|
||||
in_queue_count = len(in_queue_tasks)
|
||||
if in_queue_count > 0:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"存在 {in_queue_count} 个正在执行或已就绪的队列任务,无法清除所有定时任务 !"
|
||||
)
|
||||
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
|
||||
):
|
||||
|
||||
# Here we use reference to task data, not copy.
|
||||
# So any change of task data in history dialog will be
|
||||
# reflected in the timer task list
|
||||
# Thus we can emit timerTasksChanged signal to
|
||||
# update config file
|
||||
Dialog = ALTimerTaskHistoryDialog(self, task)
|
||||
if Dialog.exec() == QDialog.DialogCode.Accepted:
|
||||
self.timerTasksChanged.emit()
|
||||
|
||||
def checkTasks(
|
||||
self
|
||||
):
|
||||
|
||||
need_update = False
|
||||
|
||||
now = datetime.now()
|
||||
for timer_task in self.__timer_tasks:
|
||||
if timer_task["execute_time"] > now:
|
||||
continue
|
||||
if timer_task["status"] is not ALTimerTaskStatus.PENDING:
|
||||
continue
|
||||
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
|
||||
need_update = True
|
||||
else:
|
||||
timer_task["status"] = ALTimerTaskStatus.READY
|
||||
self.timerTaskIsReady.emit(timer_task)
|
||||
need_update = True
|
||||
if need_update:
|
||||
self.timerTasksChanged.emit()
|
||||
|
||||
@Slot(int)
|
||||
def onSortPolicyComboBoxChanged(
|
||||
self,
|
||||
policy: int
|
||||
):
|
||||
|
||||
mapping = {
|
||||
0: self.SortPolicy.BY_NAME,
|
||||
1: self.SortPolicy.BY_ADD_TIME,
|
||||
2: self.SortPolicy.BY_EXECUTE_TIME
|
||||
}
|
||||
self.__sort_policy = mapping[policy]
|
||||
self.updateTimerTaskList()
|
||||
|
||||
@Slot()
|
||||
def onSortOrderToggleButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.__sort_order = Qt.SortOrder.AscendingOrder\
|
||||
if self.__sort_order is Qt.SortOrder.DescendingOrder\
|
||||
else Qt.SortOrder.DescendingOrder
|
||||
self.TimerTaskSortOrderToggleButton.setText(
|
||||
"↑" if self.__sort_order is Qt.SortOrder.AscendingOrder else "↓"
|
||||
)
|
||||
self.updateTimerTaskList()
|
||||
|
||||
@Slot()
|
||||
def onTimerTasksChanged(
|
||||
self
|
||||
):
|
||||
|
||||
self.setTimerTasks(copy.deepcopy(self.__timer_tasks))
|
||||
self.updateTimerTaskList()
|
||||
self.updateStat()
|
||||
|
||||
@Slot(dict)
|
||||
def onTimerTaskIsRunning(
|
||||
self,
|
||||
timer_task: dict
|
||||
):
|
||||
|
||||
for task in self.__timer_tasks:
|
||||
if task["uuid"] == timer_task["uuid"]:
|
||||
task["status"] = ALTimerTaskStatus.RUNNING
|
||||
break
|
||||
self.timerTasksChanged.emit()
|
||||
|
||||
def onRepeatTimerTaskIs(
|
||||
self,
|
||||
status: ALTimerTaskStatus,
|
||||
timer_task: dict
|
||||
) -> dict:
|
||||
|
||||
# only these status are valid
|
||||
valid_statuses = {ALTimerTaskStatus.EXECUTED, ALTimerTaskStatus.ERROR,
|
||||
ALTimerTaskStatus.OUTDATED}
|
||||
if status not in valid_statuses:
|
||||
return timer_task
|
||||
if "repeat_history" not in timer_task:
|
||||
timer_task["repeat_history"] = []
|
||||
if status != ALTimerTaskStatus.OUTDATED:
|
||||
executed_time = datetime.now()
|
||||
duration = (executed_time - timer_task["execute_time"]).total_seconds()
|
||||
timer_task["repeat_history"].append({
|
||||
"execute_time": timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"executed_time": executed_time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"result": status,
|
||||
"duration": duration,
|
||||
"uuid": timer_task["uuid"]
|
||||
})
|
||||
else:
|
||||
current_time = datetime.now()
|
||||
execute_time = timer_task["execute_time"]
|
||||
execute_weekday = execute_time.weekday()
|
||||
delta_days = (current_time - execute_time).days
|
||||
for i in range(delta_days + 1):
|
||||
if (execute_weekday + i)%7 in timer_task["repeat_days"]:
|
||||
timer_task["repeat_history"].append({
|
||||
"execute_time": (execute_time + timedelta(days=i)).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"executed_time": (execute_time + timedelta(days=i)).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"result": status,
|
||||
"duration": 0,
|
||||
"uuid": timer_task["uuid"]
|
||||
})
|
||||
next_time = TimerUtils.getNextTimerRepeatTime(
|
||||
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
|
||||
return timer_task
|
||||
|
||||
@Slot(dict)
|
||||
def onTimerTaskIsExecuted(
|
||||
self,
|
||||
timer_task: dict
|
||||
):
|
||||
|
||||
for task in self.__timer_tasks:
|
||||
if task["uuid"] == timer_task["uuid"]:
|
||||
if task.get("repeat", False):
|
||||
self.onRepeatTimerTaskIs(ALTimerTaskStatus.EXECUTED, task)
|
||||
else:
|
||||
task["status"] = ALTimerTaskStatus.EXECUTED
|
||||
break
|
||||
self.timerTasksChanged.emit()
|
||||
|
||||
@Slot(dict)
|
||||
def onTimerTaskIsError(
|
||||
self,
|
||||
timer_task: dict
|
||||
):
|
||||
|
||||
for task in self.__timer_tasks:
|
||||
if task["uuid"] == timer_task["uuid"]:
|
||||
if task.get("repeat", False):
|
||||
self.onRepeatTimerTaskIs(ALTimerTaskStatus.ERROR, task)
|
||||
else:
|
||||
task["status"] = ALTimerTaskStatus.ERROR
|
||||
break
|
||||
self.timerTasksChanged.emit()
|
||||
@@ -0,0 +1,142 @@
|
||||
# -*- 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 queue
|
||||
|
||||
from PySide6.QtCore import (
|
||||
QObject,
|
||||
QTimer,
|
||||
Signal,
|
||||
Slot
|
||||
)
|
||||
|
||||
from gui.ALMainWorker import TimerTaskWorker
|
||||
|
||||
|
||||
class ALTimerTaskPoller(QObject):
|
||||
|
||||
taskRunning = Signal(dict)
|
||||
taskFinished = Signal(bool, dict)
|
||||
taskExecuted = Signal(dict)
|
||||
taskError = Signal(dict)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
input_queue: queue.Queue = None,
|
||||
output_queue: queue.Queue = None,
|
||||
config_paths: list = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self.__input_queue = input_queue or queue.Queue()
|
||||
self.__output_queue = output_queue or queue.Queue()
|
||||
self.__config_paths = config_paths or []
|
||||
self.__task_queue = queue.Queue()
|
||||
self.__timer = QTimer(self)
|
||||
self.__timer.timeout.connect(self.__poll)
|
||||
self.__worker = None
|
||||
self.__stopped = False
|
||||
|
||||
def start(
|
||||
self
|
||||
):
|
||||
|
||||
self.__stopped = False
|
||||
self.__timer.start(500)
|
||||
|
||||
def stop(
|
||||
self
|
||||
):
|
||||
|
||||
self.__stopped = True
|
||||
self.__timer.stop()
|
||||
self.__cleanupWorker()
|
||||
|
||||
def enqueue(
|
||||
self,
|
||||
task: dict
|
||||
):
|
||||
|
||||
self.__task_queue.put(task)
|
||||
|
||||
def isRunning(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
return self.__worker is not None
|
||||
|
||||
def updateConfigPaths(
|
||||
self,
|
||||
config_paths: list
|
||||
):
|
||||
|
||||
self.__config_paths = config_paths
|
||||
|
||||
@Slot()
|
||||
def __poll(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__worker is not None:
|
||||
return
|
||||
try:
|
||||
task = self.__task_queue.get_nowait()
|
||||
except queue.Empty:
|
||||
return
|
||||
self.__timer.stop()
|
||||
self.taskRunning.emit(task)
|
||||
try:
|
||||
self.__worker = TimerTaskWorker(
|
||||
task,
|
||||
self.__input_queue,
|
||||
self.__output_queue,
|
||||
self.__config_paths
|
||||
)
|
||||
self.__worker.timerTaskWorkerIsFinished.connect(self.__onFinished)
|
||||
self.__worker.start()
|
||||
except Exception:
|
||||
self.__worker = None
|
||||
if not self.__stopped:
|
||||
self.__timer.start(500)
|
||||
|
||||
@Slot(bool, dict)
|
||||
def __onFinished(
|
||||
self,
|
||||
is_error: bool,
|
||||
task: dict
|
||||
):
|
||||
|
||||
self.__worker.timerTaskWorkerIsFinished.disconnect(self.__onFinished)
|
||||
self.__worker.wait(1000)
|
||||
self.__worker.deleteLater()
|
||||
self.__worker = None
|
||||
try:
|
||||
self.taskFinished.emit(is_error, task)
|
||||
if not is_error:
|
||||
self.taskExecuted.emit(task)
|
||||
else:
|
||||
self.taskError.emit(task)
|
||||
finally:
|
||||
if not self.__stopped:
|
||||
self.__timer.start(500)
|
||||
|
||||
def __cleanupWorker(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__worker is None:
|
||||
return
|
||||
try:
|
||||
self.__worker.timerTaskWorkerIsFinished.disconnect(self.__onFinished)
|
||||
except (TypeError, RuntimeError):
|
||||
pass
|
||||
self.__worker.wait(500)
|
||||
self.__worker.deleteLater()
|
||||
self.__worker = None
|
||||
@@ -1,506 +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 os
|
||||
import copy
|
||||
|
||||
from enum import Enum
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Qt, Signal, Slot, QTimer
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QWidget, QListWidgetItem, QMessageBox,
|
||||
QHBoxLayout, QVBoxLayout, QLabel, QPushButton
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QCloseEvent
|
||||
)
|
||||
|
||||
from gui.Ui_ALTimerTaskWidget import Ui_ALTimerTaskWidget
|
||||
from gui.ALAddTimerTaskDialog import ALAddTimerTaskWidget, TimerTaskStatus
|
||||
|
||||
from utils.ConfigReader import ConfigReader
|
||||
from utils.ConfigWriter import ConfigWriter
|
||||
|
||||
|
||||
class SortPolicy(Enum):
|
||||
|
||||
BY_NAME = "按名称"
|
||||
BY_ADD_TIME = "按添加时间"
|
||||
BY_EXECUTE_TIME = "按执行时间"
|
||||
|
||||
|
||||
class TimerTaskItemWidget(QWidget):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None,
|
||||
timer_task: dict = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self.__timer_task = timer_task
|
||||
|
||||
self.modifyUi()
|
||||
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.ItemWidgetLayout = QHBoxLayout(self)
|
||||
self.ItemWidgetLayout.setSpacing(10)
|
||||
self.ItemWidgetLayout.setContentsMargins(10, 5, 10, 5)
|
||||
|
||||
self.TaskInfoLayout = QVBoxLayout()
|
||||
self.TaskInfoLayout.setSpacing(5)
|
||||
TaskNameLabel = QLabel(self.__timer_task["name"])
|
||||
TaskNameLabelFont = TaskNameLabel.font()
|
||||
TaskNameLabelFont.setBold(True)
|
||||
TaskNameLabel.setFont(TaskNameLabelFont)
|
||||
TaskNameLabel.setFixedHeight(25)
|
||||
self.TaskInfoLayout.addWidget(TaskNameLabel)
|
||||
|
||||
ExecuteTimeStr = self.__timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S")
|
||||
ExecuteTimeLabel = QLabel(f"执行时间: {ExecuteTimeStr}")
|
||||
ExecuteTimeLabel.setStyleSheet("color: gray;")
|
||||
ExecuteTimeLabel.setFixedHeight(20)
|
||||
self.TaskInfoLayout.addWidget(ExecuteTimeLabel)
|
||||
|
||||
self.ItemWidgetLayout.addLayout(self.TaskInfoLayout)
|
||||
self.ItemWidgetLayout.addStretch()
|
||||
|
||||
match self.__timer_task["status"]:
|
||||
case TimerTaskStatus.PENDING:
|
||||
TaskStatusText = "等待中"
|
||||
TaskStatusColor = "#FF9800"
|
||||
case TimerTaskStatus.READY:
|
||||
TaskStatusText = "已就绪"
|
||||
TaskStatusColor = "#316BFF"
|
||||
case TimerTaskStatus.RUNNING:
|
||||
TaskStatusText = "执行中"
|
||||
TaskStatusColor = "#2294FF"
|
||||
case TimerTaskStatus.EXECUTED:
|
||||
TaskStatusText = "已执行"
|
||||
TaskStatusColor = "#4CAF50"
|
||||
case TimerTaskStatus.ERROR:
|
||||
TaskStatusText = "执行失败"
|
||||
TaskStatusColor = "#FF5722"
|
||||
case TimerTaskStatus.OUTDATED:
|
||||
TaskStatusText = "已过期"
|
||||
TaskStatusColor = "#FF5722"
|
||||
TaskStatusLabel = QLabel(TaskStatusText)
|
||||
TaskStatusLabel.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
background-color: {TaskStatusColor};
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
""")
|
||||
TaskStatusLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
TaskStatusLabel.setFixedSize(80, 25)
|
||||
self.ItemWidgetLayout.addWidget(TaskStatusLabel)
|
||||
|
||||
TaskModeText = "静默" if self.__timer_task["silent"] else "显示"
|
||||
TaskModeColor = "#6325FF" if self.__timer_task["silent"] else "#2294FF"
|
||||
TaskModeLabel = QLabel(TaskModeText)
|
||||
TaskModeLabel.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
background-color: {TaskModeColor};
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
""")
|
||||
TaskModeLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
TaskModeLabel.setFixedSize(60, 25)
|
||||
self.ItemWidgetLayout.addWidget(TaskModeLabel)
|
||||
|
||||
self.DeleteButton = QPushButton("删除")
|
||||
self.DeleteButton.setFixedSize(80, 25)
|
||||
self.ItemWidgetLayout.addWidget(self.DeleteButton)
|
||||
if self.__timer_task["status"] == TimerTaskStatus.READY\
|
||||
or self.__timer_task["status"] == TimerTaskStatus.RUNNING:
|
||||
self.DeleteButton.setEnabled(False)
|
||||
self.setFixedHeight(55)
|
||||
|
||||
|
||||
class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
|
||||
|
||||
timerTasksChanged = Signal()
|
||||
timerTaskIsReady = Signal(dict)
|
||||
timerTaskWidgetClosed = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None,
|
||||
timer_tasks_config_path: str = ""
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self.__timer_tasks = []
|
||||
self.__check_timer = None
|
||||
self.__sort_policy = SortPolicy.BY_EXECUTE_TIME
|
||||
self.__sort_order = Qt.SortOrder.AscendingOrder
|
||||
self.__timer_tasks_config_path = timer_tasks_config_path
|
||||
|
||||
self.setupUi(self)
|
||||
self.connectSignals()
|
||||
self.setupTimer()
|
||||
if not self.initializeTimerTasks():
|
||||
return
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.AddTimerTaskButton.clicked.connect(self.addTask)
|
||||
self.ClearAllTimerTasksButton.clicked.connect(self.clearAllTasks)
|
||||
self.TimerTaskSortTypeComboBox.currentIndexChanged.connect(self.onSortPolicyComboBoxChanged)
|
||||
self.TimerTaskSortOrderToggleButton.clicked.connect(self.onSortOrderToggleButtonClicked)
|
||||
self.timerTasksChanged.connect(self.onTimerTasksChanged)
|
||||
|
||||
|
||||
def setupTimer(
|
||||
self
|
||||
):
|
||||
|
||||
self.__check_timer = QTimer(self)
|
||||
self.__check_timer.timeout.connect(self.checkTasks)
|
||||
self.__check_timer.start(500)
|
||||
|
||||
|
||||
def initializeTimerTasks(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
timer_tasks = self.loadTimerTasks(self.__timer_tasks_config_path)
|
||||
if timer_tasks is not None:
|
||||
self.__timer_tasks = timer_tasks
|
||||
self.timerTasksChanged.emit()
|
||||
return True
|
||||
timer_tasks = []
|
||||
if self.saveTimerTasks(self.__timer_tasks_config_path, copy.deepcopy(timer_tasks)):
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"信息 - AutoLibrary",
|
||||
f"定时任务配置文件初始化完成: \n{self.__timer_tasks_config_path}"
|
||||
)
|
||||
self.__timer_tasks = timer_tasks
|
||||
self.updateTimerTaskList()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def loadTimerTasks(
|
||||
self,
|
||||
timer_tasks_config_path: str
|
||||
) -> list:
|
||||
|
||||
try:
|
||||
if not timer_tasks_config_path or not os.path.exists(timer_tasks_config_path):
|
||||
raise Exception("定时任务配置文件不存在")
|
||||
timer_tasks = ConfigReader(timer_tasks_config_path).getConfigs()
|
||||
if timer_tasks and "timer_tasks" in timer_tasks:
|
||||
for task in timer_tasks["timer_tasks"]:
|
||||
task["add_time"] = datetime.strptime(task["add_time"], "%Y-%m-%d %H:%M:%S")
|
||||
task["execute_time"] = datetime.strptime(task["execute_time"], "%Y-%m-%d %H:%M:%S")
|
||||
task["status"] = TimerTaskStatus(task["status"])
|
||||
return timer_tasks["timer_tasks"]
|
||||
raise Exception("定时任务配置文件格式错误")
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"加载定时任务配置发生错误 ! : {e}\n"\
|
||||
f"文件路径: {timer_tasks_config_path}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def saveTimerTasks(
|
||||
self,
|
||||
timer_tasks_config_path: str,
|
||||
timer_tasks: list
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
if not timer_tasks_config_path:
|
||||
raise Exception("配置文件路径为空")
|
||||
for task in timer_tasks:
|
||||
task["add_time"] = task["add_time"].strftime("%Y-%m-%d %H:%M:%S")
|
||||
task["execute_time"] = task["execute_time"].strftime("%Y-%m-%d %H:%M:%S")
|
||||
task["status"] = task["status"].value
|
||||
ConfigWriter(
|
||||
timer_tasks_config_path,
|
||||
{ "timer_tasks": timer_tasks }
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"保存定时任务配置发生错误 ! : {e}\n"\
|
||||
f"文件路径: {timer_tasks_config_path}"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def showEvent(
|
||||
self,
|
||||
event
|
||||
):
|
||||
|
||||
result = super().showEvent(event)
|
||||
|
||||
screen_rect = self.screen().geometry()
|
||||
target_pos = self.parent().geometry().center()
|
||||
target_pos.setX(target_pos.x() - self.width()//2)
|
||||
target_pos.setY(target_pos.y() - self.height()//2)
|
||||
if target_pos.x() < 0:
|
||||
target_pos.setX(0)
|
||||
if target_pos.x() + self.width() > screen_rect.width():
|
||||
target_pos.setX(screen_rect.width() - self.width())
|
||||
if target_pos.y() < 0:
|
||||
target_pos.setY(0)
|
||||
if target_pos.y() + self.height() > screen_rect.height():
|
||||
target_pos.setY(screen_rect.height() - self.height())
|
||||
self.move(target_pos)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def closeEvent(
|
||||
self,
|
||||
event: QCloseEvent
|
||||
):
|
||||
|
||||
self.hide()
|
||||
self.timerTaskWidgetClosed.emit()
|
||||
event.ignore()
|
||||
|
||||
|
||||
def sortTimerTasks(
|
||||
self,
|
||||
policy: SortPolicy = SortPolicy.BY_EXECUTE_TIME,
|
||||
order: Qt.SortOrder = Qt.SortOrder.AscendingOrder
|
||||
):
|
||||
|
||||
if policy == SortPolicy.BY_NAME:
|
||||
self.__timer_tasks.sort(
|
||||
key = lambda x: x["name"],
|
||||
reverse = order is Qt.SortOrder.DescendingOrder
|
||||
)
|
||||
elif policy == SortPolicy.BY_ADD_TIME:
|
||||
self.__timer_tasks.sort(
|
||||
key = lambda x: x["add_time"],
|
||||
reverse = order is Qt.SortOrder.DescendingOrder
|
||||
)
|
||||
elif policy == SortPolicy.BY_EXECUTE_TIME:
|
||||
self.__timer_tasks.sort(
|
||||
key = lambda x: x["execute_time"],
|
||||
reverse = order is Qt.SortOrder.DescendingOrder
|
||||
)
|
||||
|
||||
|
||||
def updateStat(
|
||||
self
|
||||
):
|
||||
|
||||
pending = 0
|
||||
in_queue = 0
|
||||
executed = 0
|
||||
invalid = 0
|
||||
total = len(self.__timer_tasks)
|
||||
for timer_task in self.__timer_tasks:
|
||||
if timer_task["status"] == TimerTaskStatus.PENDING:
|
||||
pending += 1
|
||||
elif timer_task["status"] == TimerTaskStatus.READY\
|
||||
or timer_task["status"] == TimerTaskStatus.RUNNING:
|
||||
in_queue += 1
|
||||
elif timer_task["status"] == TimerTaskStatus.EXECUTED:
|
||||
executed += 1
|
||||
elif timer_task["status"] == TimerTaskStatus.ERROR\
|
||||
or timer_task["status"] == TimerTaskStatus.OUTDATED:
|
||||
invalid += 1
|
||||
self.TotalTaskLabel.setText(f"总任务:{total}")
|
||||
self.PendingTaskLabel.setText(f"待执行:{pending}")
|
||||
self.InQueueTaskLabel.setText(f"队列中:{in_queue}")
|
||||
self.ExecutedTaskLabel.setText(f"已执行:{executed}")
|
||||
self.InvalidTaskLabel.setText(f"无效的:{invalid}")
|
||||
|
||||
|
||||
def updateTimerTaskList(
|
||||
self
|
||||
):
|
||||
|
||||
self.TimerTasksListWidget.clear()
|
||||
self.sortTimerTasks(self.__sort_policy, self.__sort_order)
|
||||
for timer_task in self.__timer_tasks:
|
||||
item = QListWidgetItem()
|
||||
item.setData(Qt.UserRole, timer_task)
|
||||
widget = TimerTaskItemWidget(self, timer_task)
|
||||
widget.DeleteButton.clicked.connect(
|
||||
lambda _, uuid = timer_task["task_uuid"]: self.deleteTask(uuid)
|
||||
)
|
||||
item.setSizeHint(widget.size())
|
||||
self.TimerTasksListWidget.addItem(item)
|
||||
self.TimerTasksListWidget.setItemWidget(item, widget)
|
||||
|
||||
|
||||
def addTask(
|
||||
self
|
||||
):
|
||||
|
||||
dialog = ALAddTimerTaskWidget(self)
|
||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||
timer_task = dialog.getTimerTask()
|
||||
self.__timer_tasks.append(timer_task)
|
||||
self.timerTasksChanged.emit()
|
||||
|
||||
|
||||
def deleteTask(
|
||||
self,
|
||||
task_uuid: str
|
||||
):
|
||||
|
||||
self.__timer_tasks = [
|
||||
x for x in self.__timer_tasks
|
||||
if x["task_uuid"] != task_uuid
|
||||
]
|
||||
self.timerTasksChanged.emit()
|
||||
|
||||
|
||||
def clearAllTasks(
|
||||
self
|
||||
):
|
||||
|
||||
if not self.__timer_tasks:
|
||||
return
|
||||
result = QMessageBox.question(
|
||||
self,
|
||||
"确认 - AutoLibrary",
|
||||
"是否要清除所有定时任务 ?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
if result is QMessageBox.StandardButton.No:
|
||||
return
|
||||
in_queue_tasks = [
|
||||
x for x in self.__timer_tasks
|
||||
if x["status"] == TimerTaskStatus.READY
|
||||
or x["status"] == TimerTaskStatus.RUNNING
|
||||
]
|
||||
in_queue_count = len(in_queue_tasks)
|
||||
if in_queue_count > 0:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
"存在正在执行或已就绪的队列任务,无法清除所有定时任务 !"
|
||||
)
|
||||
self.__timer_tasks = in_queue_tasks
|
||||
self.timerTasksChanged.emit()
|
||||
|
||||
|
||||
def checkTasks(
|
||||
self
|
||||
):
|
||||
|
||||
need_update = False
|
||||
|
||||
now = datetime.now()
|
||||
for timer_task in self.__timer_tasks:
|
||||
if timer_task["execute_time"] > now:
|
||||
continue
|
||||
if timer_task["status"] is not TimerTaskStatus.PENDING:
|
||||
continue
|
||||
if timer_task["execute_time"] <= now + timedelta(seconds = -5):
|
||||
timer_task["status"] = TimerTaskStatus.OUTDATED
|
||||
need_update = True
|
||||
else:
|
||||
timer_task["status"] = TimerTaskStatus.READY
|
||||
self.timerTaskIsReady.emit(timer_task)
|
||||
need_update = True
|
||||
if need_update:
|
||||
self.timerTasksChanged.emit()
|
||||
|
||||
@Slot(int)
|
||||
def onSortPolicyComboBoxChanged(
|
||||
self,
|
||||
policy: int
|
||||
):
|
||||
|
||||
mapping = {
|
||||
0: SortPolicy.BY_NAME,
|
||||
1: SortPolicy.BY_ADD_TIME,
|
||||
2: SortPolicy.BY_EXECUTE_TIME
|
||||
}
|
||||
self.__sort_policy = mapping[policy]
|
||||
self.updateTimerTaskList()
|
||||
|
||||
@Slot()
|
||||
def onSortOrderToggleButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.__sort_order = Qt.SortOrder.AscendingOrder\
|
||||
if self.__sort_order is Qt.SortOrder.DescendingOrder\
|
||||
else Qt.SortOrder.DescendingOrder
|
||||
self.TimerTaskSortOrderToggleButton.setText(
|
||||
"↑" if self.__sort_order is Qt.SortOrder.AscendingOrder else "↓"
|
||||
)
|
||||
self.updateTimerTaskList()
|
||||
|
||||
@Slot()
|
||||
def onTimerTasksChanged(
|
||||
self
|
||||
):
|
||||
|
||||
self.saveTimerTasks(self.__timer_tasks_config_path, copy.deepcopy(self.__timer_tasks))
|
||||
self.updateTimerTaskList()
|
||||
self.updateStat()
|
||||
|
||||
|
||||
@Slot(dict)
|
||||
def onTimerTaskIsRunning(
|
||||
self,
|
||||
timer_task: dict
|
||||
):
|
||||
|
||||
for task in self.__timer_tasks:
|
||||
if task["task_uuid"] == timer_task["task_uuid"]:
|
||||
task["status"] = TimerTaskStatus.RUNNING
|
||||
self.timerTasksChanged.emit()
|
||||
|
||||
|
||||
@Slot(dict)
|
||||
def onTimerTaskIsExecuted(
|
||||
self,
|
||||
timer_task: dict
|
||||
):
|
||||
|
||||
for task in self.__timer_tasks:
|
||||
if task["task_uuid"] == timer_task["task_uuid"]:
|
||||
task["status"] = TimerTaskStatus.EXECUTED
|
||||
self.timerTasksChanged.emit()
|
||||
|
||||
@Slot(dict)
|
||||
def onTimerTaskIsError(
|
||||
self,
|
||||
timer_task: dict
|
||||
):
|
||||
|
||||
for task in self.__timer_tasks:
|
||||
if task["task_uuid"] == timer_task["task_uuid"]:
|
||||
task["status"] = TimerTaskStatus.ERROR
|
||||
self.timerTasksChanged.emit()
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 - 2026 KenanZhu.
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
@@ -10,18 +10,26 @@ See the LICENSE file for details.
|
||||
from enum import Enum
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Qt, QSize, QCoreApplication, QRect, QPoint
|
||||
Qt,
|
||||
QSize,
|
||||
QCoreApplication,
|
||||
QRect,
|
||||
QPoint
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QAbstractScrollArea, QAbstractItemView,
|
||||
QTreeWidget, QTreeWidgetItem
|
||||
QAbstractScrollArea,
|
||||
QAbstractItemView,
|
||||
QTreeWidget,
|
||||
QTreeWidgetItem
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QDragEnterEvent, QDragMoveEvent, QDropEvent
|
||||
QDragEnterEvent,
|
||||
QDragMoveEvent,
|
||||
QDropEvent
|
||||
)
|
||||
|
||||
|
||||
class TreeItemType(Enum):
|
||||
class ALUserTreeItemType(Enum):
|
||||
|
||||
GROUP = 0
|
||||
USER = 1
|
||||
@@ -39,14 +47,13 @@ class ALUserTreeWidget(QTreeWidget):
|
||||
self.setupUi()
|
||||
self.translateUi()
|
||||
|
||||
|
||||
def setupUi(
|
||||
self
|
||||
):
|
||||
|
||||
__qtreewidgetitem = QTreeWidgetItem()
|
||||
__qtreewidgetitem.setText(0, u"\u5206\u7ec4/\u7528\u6237");
|
||||
self.setHeaderItem(__qtreewidgetitem)
|
||||
__QTreeWidgetItem = QTreeWidgetItem()
|
||||
__QTreeWidgetItem.setText(0, u"\u5206\u7ec4/\u7528\u6237");
|
||||
self.setHeaderItem(__QTreeWidgetItem)
|
||||
self.setObjectName(u"UserTreeWidget")
|
||||
self.setMinimumSize(QSize(230, 0))
|
||||
self.setMaximumSize(QSize(250, 16777215))
|
||||
@@ -70,14 +77,12 @@ class ALUserTreeWidget(QTreeWidget):
|
||||
self.header().setHighlightSections(False)
|
||||
self.header().setProperty(u"showSortIndicator", True)
|
||||
|
||||
|
||||
def translateUi(
|
||||
self
|
||||
):
|
||||
|
||||
___qtreewidgetitem = self.headerItem()
|
||||
___qtreewidgetitem.setText(1, QCoreApplication.translate("ALConfigWidget", u"\u72b6\u6001", None));
|
||||
|
||||
___QTreeWidgetItem = self.headerItem()
|
||||
___QTreeWidgetItem.setText(1, QCoreApplication.translate("ALConfigWidget", u"\u72b6\u6001", None));
|
||||
|
||||
@staticmethod
|
||||
def isDragPositionValid(
|
||||
@@ -90,7 +95,6 @@ class ALUserTreeWidget(QTreeWidget):
|
||||
y_offset < target_rect.height()*0.8)
|
||||
return valid
|
||||
|
||||
|
||||
def dragEnterEvent(
|
||||
self,
|
||||
event: QDragEnterEvent
|
||||
@@ -98,7 +102,6 @@ class ALUserTreeWidget(QTreeWidget):
|
||||
|
||||
super().dragEnterEvent(event)
|
||||
|
||||
|
||||
def dragMoveEvent(
|
||||
self,
|
||||
event: QDragMoveEvent
|
||||
@@ -111,15 +114,15 @@ class ALUserTreeWidget(QTreeWidget):
|
||||
if source_item is None:
|
||||
event.ignore()
|
||||
return
|
||||
if source_item.type() == TreeItemType.GROUP.value:
|
||||
if source_item.type() == ALUserTreeItemType.GROUP.value:
|
||||
if target_item is not None:
|
||||
event.ignore()
|
||||
return
|
||||
elif source_item.type() == TreeItemType.USER.value:
|
||||
elif source_item.type() == ALUserTreeItemType.USER.value:
|
||||
if target_item is None:
|
||||
event.ignore()
|
||||
return
|
||||
if target_item.type() != TreeItemType.GROUP.value:
|
||||
if target_item.type() != ALUserTreeItemType.GROUP.value:
|
||||
event.ignore()
|
||||
return
|
||||
if target_item.checkState(1) == Qt.CheckState.Unchecked:
|
||||
@@ -136,7 +139,6 @@ class ALUserTreeWidget(QTreeWidget):
|
||||
return
|
||||
event.acceptProposedAction()
|
||||
|
||||
|
||||
def dropEvent(
|
||||
self,
|
||||
event: QDropEvent
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
workflow process. Do not edit manually.
|
||||
|
||||
This file is auto-generated during the workflow process.
|
||||
Last updated: 2026-01-17 17:51:55 UTC
|
||||
Last updated: 2026-05-09 06:05:13 UTC
|
||||
"""
|
||||
|
||||
AL_VERSION = "1.0.3"
|
||||
AL_TAG = "v1.0.3"
|
||||
AL_VERSION = "1.3.0"
|
||||
AL_TAG = "v1.3.0"
|
||||
AL_COMMIT_SHA = "local"
|
||||
AL_COMMIT_DATE = "null" # time zone : UTC
|
||||
AL_BUILD_DATE = "null" # time zone : UTC
|
||||
|
||||
@@ -0,0 +1,550 @@
|
||||
# -*- 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 threading
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Qt,
|
||||
Slot,
|
||||
QThread,
|
||||
Signal
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QLabel,
|
||||
QComboBox,
|
||||
QProgressBar,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QMessageBox,
|
||||
QFrame,
|
||||
QLineEdit
|
||||
)
|
||||
from PySide6.QtGui import QCloseEvent
|
||||
|
||||
from managers.driver.WebDriverManager import (
|
||||
instance as webdriverInstance,
|
||||
WebDriverManager,
|
||||
WebDriverCtx,
|
||||
WebDriverType,
|
||||
WebDriverStatus
|
||||
)
|
||||
from gui.ALStatusLabel import ALStatusLabel
|
||||
from gui.ALWidgetMixin import CenterOnParentMixin
|
||||
|
||||
|
||||
class DownloadWorker(QThread):
|
||||
"""
|
||||
Worker thread for downloading web drivers.
|
||||
"""
|
||||
|
||||
progress = Signal(float, int, float, str)
|
||||
downloadFinished = Signal(object, str)
|
||||
downloadError = Signal(str)
|
||||
downloadCancelled = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
driver_manager: WebDriverManager,
|
||||
driver_ctx: WebDriverCtx
|
||||
):
|
||||
super().__init__()
|
||||
self.__driver_manager = driver_manager
|
||||
self.__driver_ctx = driver_ctx
|
||||
self.__driver_path: Optional[Path] = None
|
||||
self.__cancelled = False
|
||||
self.__cancel_event = threading.Event()
|
||||
|
||||
def cancel(
|
||||
self
|
||||
):
|
||||
"""
|
||||
Cancel the download operation.
|
||||
"""
|
||||
|
||||
self.__cancelled = True
|
||||
self.__cancel_event.set()
|
||||
|
||||
def run(
|
||||
self
|
||||
):
|
||||
try:
|
||||
if self.__cancelled:
|
||||
self.downloadCancelled.emit()
|
||||
return
|
||||
self.__driver_path = self.__driver_manager.installDriver(
|
||||
self.__driver_ctx,
|
||||
progress_callback=self.onProgress,
|
||||
cancel_event=self.__cancel_event
|
||||
)
|
||||
if self.__cancelled:
|
||||
self.downloadCancelled.emit()
|
||||
return
|
||||
if self.__driver_path:
|
||||
self.downloadFinished.emit(self.__driver_path, "")
|
||||
else:
|
||||
self.downloadError.emit("下载失败: 未返回有效路径")
|
||||
except Exception as e:
|
||||
if not self.__cancelled:
|
||||
self.downloadError.emit(f"下载失败: {str(e)}")
|
||||
|
||||
def onProgress(
|
||||
self,
|
||||
downloaded: float,
|
||||
total: int,
|
||||
speed: float,
|
||||
message: str
|
||||
):
|
||||
|
||||
if self.__cancel_event.is_set():
|
||||
self.__cancelled = True
|
||||
if not self.__cancelled:
|
||||
self.progress.emit(downloaded, total, speed, message)
|
||||
|
||||
def stop(
|
||||
self
|
||||
):
|
||||
"""
|
||||
Cancel and wait for the thread to finish.
|
||||
Must only be called from the main thread.
|
||||
"""
|
||||
|
||||
self.cancel()
|
||||
if not self.isFinished():
|
||||
if not self.wait(5000):
|
||||
self.terminate()
|
||||
self.wait()
|
||||
|
||||
|
||||
class ALWebDriverDownloadDialog(CenterOnParentMixin, QDialog):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: Optional[QDialog] = None,
|
||||
driver_dir: str = ""
|
||||
):
|
||||
"""
|
||||
Web driver download dialog.
|
||||
|
||||
Args:
|
||||
parent: Parent widget.
|
||||
driver_dir: Driver directory path.
|
||||
"""
|
||||
|
||||
super().__init__(parent)
|
||||
self.__driver_dir = driver_dir
|
||||
self.__driver_manager: Optional[WebDriverManager] = None
|
||||
self.__confirmed = False
|
||||
self.__selected_driver_ctx: Optional[WebDriverCtx] = None
|
||||
self.__driver_ctxs: list[WebDriverCtx] = []
|
||||
self.__download_thread: Optional[DownloadWorker] = None
|
||||
|
||||
self.setupUi()
|
||||
self.connectSignals()
|
||||
self.initializeDriverManager()
|
||||
self.refreshDriverList()
|
||||
|
||||
def closeEvent(
|
||||
self,
|
||||
event: QCloseEvent
|
||||
):
|
||||
|
||||
if self.__download_thread and self.__download_thread.isRunning():
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"确认关闭 - AutoLibrary",
|
||||
"驱动正在下载中, 确定要取消并关闭对话框吗 ?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
if reply == QMessageBox.StandardButton.No:
|
||||
event.ignore()
|
||||
return
|
||||
self.__download_thread.stop()
|
||||
if not self.__confirmed:
|
||||
self.__selected_driver_ctx = None
|
||||
event.accept()
|
||||
super().closeEvent(event)
|
||||
|
||||
def setupUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.setModal(True)
|
||||
self.setMaximumHeight(240)
|
||||
self.setMinimumHeight(240)
|
||||
self.setWindowTitle("浏览器驱动下载 - AutoLibrary")
|
||||
self.MainLayout = QVBoxLayout(self)
|
||||
self.MainLayout.setContentsMargins(5, 5, 5, 5)
|
||||
self.MainLayout.setSpacing(5)
|
||||
self.BrowserCountLabel = QLabel("检测到 0 个可用浏览器:")
|
||||
self.MainLayout.addWidget(self.BrowserCountLabel)
|
||||
self.DriverInfoLayout = QHBoxLayout()
|
||||
self.DriverInfoLayout.setSpacing(5)
|
||||
self.DriverComboBox = QComboBox()
|
||||
self.DriverInfoLayout.addWidget(self.DriverComboBox)
|
||||
self.StatusLabel = ALStatusLabel()
|
||||
self.StatusLabel.setFixedSize(32, 32)
|
||||
self.DriverInfoLayout.addWidget(self.StatusLabel)
|
||||
self.MainLayout.addLayout(self.DriverInfoLayout)
|
||||
self.DetailLayout = QVBoxLayout()
|
||||
self.DetailLayout.setSpacing(5)
|
||||
self.DetailLayout.setContentsMargins(5, 5, 5, 5)
|
||||
self.BrowserTypeLabel = QLabel("类型:")
|
||||
self.DetailLayout.addWidget(self.BrowserTypeLabel)
|
||||
self.VersionLabel = QLabel("版本:")
|
||||
self.DetailLayout.addWidget(self.VersionLabel)
|
||||
self.PathLabel = QLineEdit()
|
||||
self.PathLabel.setReadOnly(True)
|
||||
self.PathLabel.setText("路径:未安装")
|
||||
self.DetailLayout.addWidget(self.PathLabel)
|
||||
self.MainLayout.addLayout(self.DetailLayout)
|
||||
self.Line = QFrame()
|
||||
self.Line.setFrameShape(QFrame.Shape.HLine)
|
||||
self.Line.setFrameShadow(QFrame.Shadow.Sunken)
|
||||
self.MainLayout.addWidget(self.Line)
|
||||
self.ProgressBar = QProgressBar()
|
||||
self.ProgressBar.setValue(0)
|
||||
self.ProgressBar.setTextVisible(False)
|
||||
self.MainLayout.addWidget(self.ProgressBar)
|
||||
self.ProgressText = QLabel("")
|
||||
self.ProgressText.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.MainLayout.addWidget(self.ProgressText)
|
||||
self.ControlLayout = QHBoxLayout()
|
||||
self.ControlLayout.setSpacing(8)
|
||||
self.ControlLayout.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
self.RefreshButton = QPushButton("刷新")
|
||||
self.RefreshButton.setFixedSize(80, 25)
|
||||
self.DownloadButton = QPushButton("下载驱动")
|
||||
self.DownloadButton.setFixedSize(80, 25)
|
||||
self.DeleteButton = QPushButton("删除驱动")
|
||||
self.DeleteButton.setFixedSize(80, 25)
|
||||
self.CancelButton = QPushButton("取消")
|
||||
self.CancelButton.setFixedSize(80, 25)
|
||||
self.ConfirmButton = QPushButton("确认")
|
||||
self.ConfirmButton.setFixedSize(80, 25)
|
||||
self.ConfirmButton.setEnabled(False)
|
||||
self.ControlLayout.addWidget(self.RefreshButton)
|
||||
self.ControlLayout.addWidget(self.DownloadButton)
|
||||
self.ControlLayout.addWidget(self.DeleteButton)
|
||||
self.ControlLayout.addWidget(self.CancelButton)
|
||||
self.ControlLayout.addWidget(self.ConfirmButton)
|
||||
self.MainLayout.addLayout(self.ControlLayout)
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.RefreshButton.clicked.connect(self.onRefreshButtonClicked)
|
||||
self.DownloadButton.clicked.connect(self.onDownloadButtonClicked)
|
||||
self.DeleteButton.clicked.connect(self.onDeleteButtonClicked)
|
||||
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
|
||||
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
|
||||
self.DriverComboBox.currentIndexChanged.connect(self.onDriverComboBoxChanged)
|
||||
|
||||
def initializeDriverManager(
|
||||
self
|
||||
):
|
||||
|
||||
try:
|
||||
self.__driver_manager = webdriverInstance(self.__driver_dir)
|
||||
except ValueError as e:
|
||||
QMessageBox.warning(self, "初始化失败", f"WebDriverManager 初始化失败:\n{str(e)}")
|
||||
self.reject()
|
||||
|
||||
def refreshDriverList(
|
||||
self
|
||||
):
|
||||
|
||||
if not self.__driver_manager:
|
||||
return
|
||||
self.__driver_manager.refresh()
|
||||
self.__driver_ctxs = self.__driver_manager.getDriverCtxs()
|
||||
self.DriverComboBox.clear()
|
||||
installed = 0
|
||||
installed_idx = 0
|
||||
for i, driver_ctx in enumerate(self.__driver_ctxs):
|
||||
display_text = f"{driver_ctx.driver_type.value} - {driver_ctx.browser_version}"
|
||||
if driver_ctx.driver_status == WebDriverStatus.INSTALLED:
|
||||
installed += 1
|
||||
installed_idx = i # get the installed driver index
|
||||
display_text += " - 已安装"
|
||||
self.DriverComboBox.addItem(display_text)
|
||||
count = len(self.__driver_ctxs)
|
||||
self.BrowserCountLabel.setText(f"检测到 {count} 个可用浏览器,{installed} 个已安装驱动:")
|
||||
if self.__driver_ctxs:
|
||||
self.DriverComboBox.setCurrentIndex(installed_idx)
|
||||
|
||||
def updateDriverInfoDisplay(
|
||||
self,
|
||||
driver_ctx: WebDriverCtx
|
||||
):
|
||||
|
||||
if driver_ctx.driver_type == WebDriverType.CHROME:
|
||||
driver_type = "Google Chrome"
|
||||
elif driver_ctx.driver_type == WebDriverType.FIREFOX:
|
||||
driver_type = "Mozilla Firefox"
|
||||
elif driver_ctx.driver_type == WebDriverType.EDGE:
|
||||
driver_type = "Microsoft Edge"
|
||||
else:
|
||||
driver_type = "未知"
|
||||
self.BrowserTypeLabel.setText(f"类型:{driver_type}")
|
||||
self.VersionLabel.setText(f"版本:{driver_ctx.driver_version}")
|
||||
if driver_ctx.driver_path:
|
||||
self.PathLabel.setText(str(driver_ctx.driver_path))
|
||||
else:
|
||||
self.PathLabel.setText("未安装")
|
||||
match driver_ctx.driver_status:
|
||||
case WebDriverStatus.NOT_INSTALLED:
|
||||
self.StatusLabel.status = ALStatusLabel.Status.WAITING
|
||||
case WebDriverStatus.INSTALLED:
|
||||
self.StatusLabel.status = ALStatusLabel.Status.SUCCESS
|
||||
case WebDriverStatus.DOWNLOADING:
|
||||
self.StatusLabel.status = ALStatusLabel.Status.RUNNING
|
||||
case WebDriverStatus.ERROR:
|
||||
self.StatusLabel.status = ALStatusLabel.Status.FAILURE
|
||||
|
||||
def updateProgressBarStates(
|
||||
self,
|
||||
driver_ctx: WebDriverCtx
|
||||
):
|
||||
|
||||
if driver_ctx.driver_status == WebDriverStatus.NOT_INSTALLED:
|
||||
self.ProgressBar.setValue(0)
|
||||
self.ProgressText.setText("未安装")
|
||||
elif driver_ctx.driver_status == WebDriverStatus.INSTALLED:
|
||||
self.ProgressBar.setValue(100)
|
||||
self.ProgressText.setText("已安装")
|
||||
elif driver_ctx.driver_status == WebDriverStatus.DOWNLOADING:
|
||||
pass # update by worker thread
|
||||
elif driver_ctx.driver_status == WebDriverStatus.ERROR:
|
||||
self.ProgressBar.setValue(0)
|
||||
self.ProgressText.setText("下载失败")
|
||||
|
||||
def updateButtonStates(
|
||||
self,
|
||||
driver_ctx: WebDriverCtx
|
||||
):
|
||||
|
||||
if driver_ctx.driver_status == WebDriverStatus.NOT_INSTALLED:
|
||||
self.RefreshButton.setEnabled(True)
|
||||
self.DeleteButton.setEnabled(False)
|
||||
self.DownloadButton.setEnabled(True)
|
||||
self.CancelButton.setEnabled(True)
|
||||
self.ConfirmButton.setEnabled(False)
|
||||
elif driver_ctx.driver_status == WebDriverStatus.INSTALLED:
|
||||
self.RefreshButton.setEnabled(True)
|
||||
self.DownloadButton.setEnabled(False)
|
||||
self.DeleteButton.setEnabled(True)
|
||||
self.CancelButton.setEnabled(True)
|
||||
self.ConfirmButton.setEnabled(True)
|
||||
elif driver_ctx.driver_status == WebDriverStatus.DOWNLOADING:
|
||||
self.RefreshButton.setEnabled(False)
|
||||
self.DownloadButton.setEnabled(False)
|
||||
self.DeleteButton.setEnabled(False)
|
||||
self.CancelButton.setEnabled(True)
|
||||
self.ConfirmButton.setEnabled(False)
|
||||
elif driver_ctx.driver_status == WebDriverStatus.ERROR:
|
||||
self.RefreshButton.setEnabled(True)
|
||||
self.DownloadButton.setEnabled(True)
|
||||
self.DeleteButton.setEnabled(False)
|
||||
self.CancelButton.setEnabled(True)
|
||||
self.ConfirmButton.setEnabled(False)
|
||||
|
||||
def getSelectedDriverInfo(
|
||||
self
|
||||
) -> Optional[WebDriverCtx]:
|
||||
|
||||
return self.__selected_driver_ctx
|
||||
|
||||
@Slot(int)
|
||||
def onDriverComboBoxChanged(
|
||||
self,
|
||||
index: int
|
||||
):
|
||||
|
||||
if not self.__driver_ctxs or index < 0 or index >= len(self.__driver_ctxs):
|
||||
return
|
||||
driver_ctx = self.__driver_ctxs[index]
|
||||
self.updateDriverInfoDisplay(driver_ctx)
|
||||
self.updateProgressBarStates(driver_ctx)
|
||||
self.updateButtonStates(driver_ctx)
|
||||
|
||||
@Slot()
|
||||
def onRefreshButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.refreshDriverList()
|
||||
|
||||
@Slot()
|
||||
def onDeleteButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
index = self.DriverComboBox.currentIndex()
|
||||
if index < 0 or index >= len(self.__driver_ctxs):
|
||||
return
|
||||
driver_info = self.__driver_ctxs[index]
|
||||
if driver_info.driver_status.name != "INSTALLED":
|
||||
QMessageBox.information(self, "提示 - AutoLibrary", "该驱动未安装, 无需删除")
|
||||
return
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"确认删除 - AutoLibrary",
|
||||
f"确定要删除 {driver_info.driver_type.value} 驱动吗 ?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
if reply == QMessageBox.StandardButton.No:
|
||||
return
|
||||
try:
|
||||
self.__driver_manager.uninstallDriver(driver_info)
|
||||
self.refreshDriverList()
|
||||
QMessageBox.information(self, "删除成功 - AutoLibrary", "驱动已成功删除")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "删除失败 - AutoLibrary", f"删除驱动时出错:\n{str(e)}")
|
||||
|
||||
@Slot()
|
||||
def onDownloadButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.DriverComboBox.setEnabled(False)
|
||||
index = self.DriverComboBox.currentIndex()
|
||||
if index < 0 or index >= len(self.__driver_ctxs):
|
||||
return
|
||||
driver_info = self.__driver_ctxs[index]
|
||||
if driver_info.driver_status == WebDriverStatus.INSTALLED:
|
||||
return
|
||||
driver_info.driver_status = WebDriverStatus.DOWNLOADING # we set this only to update
|
||||
# the display, and we will set to not installed in the download thread
|
||||
self.updateDriverInfoDisplay(driver_info)
|
||||
self.updateProgressBarStates(driver_info)
|
||||
self.ProgressText.setText("准备开始下载...")
|
||||
self.updateButtonStates(driver_info)
|
||||
# set to not installed
|
||||
driver_info.driver_status = WebDriverStatus.NOT_INSTALLED
|
||||
self.__download_thread = DownloadWorker(self.__driver_manager, driver_info)
|
||||
self.__download_thread.progress.connect(self.onDownloadProgress)
|
||||
self.__download_thread.downloadFinished.connect(self.onDownloadFinished)
|
||||
self.__download_thread.downloadError.connect(self.onDownloadError)
|
||||
self.__download_thread.downloadCancelled.connect(self.onDownloadCancelled)
|
||||
self.__download_thread.finished.connect(self.onDownloadThreadFinished)
|
||||
self.__download_thread.start()
|
||||
|
||||
@Slot()
|
||||
def onDownloadProgress(
|
||||
self,
|
||||
downloaded: float,
|
||||
total: int,
|
||||
speed: float,
|
||||
message: str
|
||||
):
|
||||
|
||||
progress = downloaded
|
||||
self.ProgressBar.setValue(progress)
|
||||
if speed >= 1024:
|
||||
speed_text = f"{speed/1024:.1f} MB/s"
|
||||
else:
|
||||
speed_text = f"{speed:.1f} KB/s"
|
||||
progress_text = f"{message}... {downloaded:.1f}% - {speed_text}"
|
||||
self.ProgressText.setText(progress_text)
|
||||
|
||||
@Slot()
|
||||
def onDownloadFinished(
|
||||
self
|
||||
):
|
||||
|
||||
self.DriverComboBox.setEnabled(True)
|
||||
index = self.DriverComboBox.currentIndex()
|
||||
if 0 <= index < len(self.__driver_ctxs):
|
||||
driver_info = self.__driver_ctxs[index]
|
||||
driver_info.driver_status = WebDriverStatus.INSTALLED
|
||||
current_text = self.DriverComboBox.itemText(index)
|
||||
self.DriverComboBox.setItemText(index, current_text + " - 已安装")
|
||||
self.updateDriverInfoDisplay(driver_info)
|
||||
self.updateProgressBarStates(driver_info)
|
||||
self.updateButtonStates(driver_info)
|
||||
|
||||
@Slot()
|
||||
def onDownloadError(
|
||||
self,
|
||||
error_message: str
|
||||
):
|
||||
|
||||
self.DriverComboBox.setEnabled(True)
|
||||
index = self.DriverComboBox.currentIndex()
|
||||
if 0 <= index < len(self.__driver_ctxs):
|
||||
driver_info = self.__driver_ctxs[index]
|
||||
driver_info.driver_status = WebDriverStatus.ERROR
|
||||
self.updateDriverInfoDisplay(driver_info)
|
||||
self.updateProgressBarStates(driver_info)
|
||||
self.updateButtonStates(driver_info)
|
||||
QMessageBox.critical(self, "下载失败 - AutoLibrary", error_message)
|
||||
|
||||
@Slot()
|
||||
def onDownloadCancelled(
|
||||
self
|
||||
):
|
||||
|
||||
self.DriverComboBox.setEnabled(True)
|
||||
index = self.DriverComboBox.currentIndex()
|
||||
if 0 <= index < len(self.__driver_ctxs):
|
||||
driver_info = self.__driver_ctxs[index]
|
||||
self.__driver_manager.cancelDriverDownload(driver_info)
|
||||
driver_info.driver_status = WebDriverStatus.NOT_INSTALLED
|
||||
self.updateDriverInfoDisplay(driver_info)
|
||||
self.updateProgressBarStates(driver_info)
|
||||
self.updateButtonStates(driver_info)
|
||||
self.ProgressText.setText("下载已取消")
|
||||
|
||||
@Slot()
|
||||
def onDownloadThreadFinished(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__download_thread:
|
||||
self.__download_thread.deleteLater()
|
||||
self.__download_thread = None
|
||||
|
||||
@Slot()
|
||||
def onConfirmButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
index = self.DriverComboBox.currentIndex()
|
||||
if index < 0 or index >= len(self.__driver_ctxs):
|
||||
return
|
||||
driver_ctx = self.__driver_ctxs[index]
|
||||
if driver_ctx.driver_status != WebDriverStatus.INSTALLED:
|
||||
return
|
||||
self.__selected_driver_ctx = driver_ctx
|
||||
self.__confirmed = True
|
||||
self.accept()
|
||||
|
||||
@Slot()
|
||||
def onCancelButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__download_thread:
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"确认取消 - AutoLibrary",
|
||||
"正在下载中, 确定要取消下载吗 ?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
self.__download_thread.cancel()
|
||||
else:
|
||||
self.__confirmed = False
|
||||
self.__selected_driver_ctx = None
|
||||
self.reject()
|
||||
@@ -0,0 +1,49 @@
|
||||
# -*- 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 PySide6.QtGui import QShowEvent
|
||||
|
||||
|
||||
class CenterOnParentMixin:
|
||||
"""
|
||||
Mixin that centres the widget relative to its parent on first show,
|
||||
clamping the position to the screen bounds.
|
||||
|
||||
Usage::
|
||||
|
||||
class MyWidget(CenterOnParentMixin, QWidget, Ui_MyWidget):
|
||||
pass
|
||||
|
||||
class MyDialog(CenterOnParentMixin, QDialog):
|
||||
pass
|
||||
|
||||
The mixin must appear **before** QWidget / QDialog in the base list
|
||||
so that ``super().showEvent(event)`` resolves up the MRO correctly.
|
||||
"""
|
||||
|
||||
def showEvent(
|
||||
self,
|
||||
event: QShowEvent
|
||||
):
|
||||
|
||||
super().showEvent(event)
|
||||
if self.parent():
|
||||
screen_rect = self.screen().geometry()
|
||||
target_pos = self.parent().geometry().center()
|
||||
target_pos.setX(target_pos.x() - self.width() // 2)
|
||||
target_pos.setY(target_pos.y() - self.height() // 2)
|
||||
if target_pos.x() < 0:
|
||||
target_pos.setX(0)
|
||||
if target_pos.x() + self.width() > screen_rect.width():
|
||||
target_pos.setX(screen_rect.width() - self.width())
|
||||
if target_pos.y() < 0:
|
||||
target_pos.setY(0)
|
||||
if target_pos.y() + self.height() > screen_rect.height():
|
||||
target_pos.setY(screen_rect.height() - self.height())
|
||||
self.move(target_pos)
|
||||
@@ -1,8 +0,0 @@
|
||||
<RCC>
|
||||
<qresource prefix="/res/icon">
|
||||
<file>icons/AutoLibrary_32x32.ico</file>
|
||||
</qresource>
|
||||
<qresource prefix="/res/trans">
|
||||
<file>translators/qtbase_zh_CN.qm</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- 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.
|
||||
"""
|
||||
@@ -1 +0,0 @@
|
||||
this folder is used to store the batch scripts.
|
||||
@@ -1 +0,0 @@
|
||||
this folder is used to store the config files.
|
||||
|
Before Width: | Height: | Size: 785 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,11 @@
|
||||
<RCC>
|
||||
<qresource prefix="/res">
|
||||
<file>icons/AutoLibrary_Logo_64.svg</file>
|
||||
<file>icons/AutoLibrary_Logo_128.svg</file>
|
||||
|
||||
<file>icons/Copy.svg</file>
|
||||
<file>icons/Reset.svg</file>
|
||||
|
||||
<file>translators/qtbase_zh_CN.qm</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- 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.
|
||||
"""
|
||||
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 7H7V5H13V7Z" fill="currentColor"/>
|
||||
<path d="M13 11H7V9H13V11Z" fill="currentColor"/>
|
||||
<path d="M7 15H13V13H7V15Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 19V1H17V5H21V23H7V19H3ZM15 17V3H5V17H15ZM17 7V19H9V21H19V7H17Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 396 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20.817 11.186a8.94 8.94 0 0 0-1.355-3.219 9.053 9.053 0 0 0-2.43-2.43 8.95 8.95 0 0 0-3.219-1.355 9.028 9.028 0 0 0-1.838-.18V2L8 5l3.975 3V6.002c.484-.002.968.044 1.435.14a6.961 6.961 0 0 1 2.502 1.053 7.005 7.005 0 0 1 1.892 1.892A6.967 6.967 0 0 1 19 13a7.032 7.032 0 0 1-.55 2.725 7.11 7.11 0 0 1-.644 1.188 7.2 7.2 0 0 1-.858 1.039 7.028 7.028 0 0 1-3.536 1.907 7.13 7.13 0 0 1-2.822 0 6.961 6.961 0 0 1-2.503-1.054 7.002 7.002 0 0 1-1.89-1.89A6.996 6.996 0 0 1 5 13H3a9.02 9.02 0 0 0 1.539 5.034 9.096 9.096 0 0 0 2.428 2.428A8.95 8.95 0 0 0 12 22a9.09 9.09 0 0 0 1.814-.183 9.014 9.014 0 0 0 3.218-1.355 8.886 8.886 0 0 0 1.331-1.099 9.228 9.228 0 0 0 1.1-1.332A8.952 8.952 0 0 0 21 13a9.09 9.09 0 0 0-.183-1.814z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 866 B |
@@ -0,0 +1,539 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
*
|
||||
* AutoLibrary Official Theme : BlueForest
|
||||
*/
|
||||
|
||||
/* ---- Global ---- */
|
||||
QMainWindow::separator {
|
||||
background-color: #1c2840;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
/* ---- Menu Bar ---- */
|
||||
QMenuBar {
|
||||
background-color: #0f1628;
|
||||
border-bottom: 1px solid #1c2840;
|
||||
padding: 2px 5px;
|
||||
color: #d0daf0;
|
||||
}
|
||||
QMenuBar::item {
|
||||
padding: 2px 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QMenuBar::item:selected {
|
||||
background-color: #1c2840;
|
||||
}
|
||||
QMenu {
|
||||
background-color: #162038;
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
QMenu::item {
|
||||
padding: 5px 15px 5px 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QMenu::item:selected {
|
||||
background-color: #2dd4bf;
|
||||
color: #0f1119;
|
||||
}
|
||||
QMenu::separator {
|
||||
height: 1px;
|
||||
background-color: #253250;
|
||||
margin: 4px 8px;
|
||||
}
|
||||
|
||||
/* ---- Button ---- */
|
||||
QPushButton {
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
color: #d0daf0;
|
||||
padding: 4px 12px;
|
||||
background-color: #1c2840;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #243458;
|
||||
border-color: #334478;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #162038;
|
||||
border-color: #2dd4bf;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #162038;
|
||||
color: #5568a0;
|
||||
border-color: #1c2840;
|
||||
}
|
||||
QPushButton[default="true"] {
|
||||
background-color: #2dd4bf;
|
||||
color: #0f1119;
|
||||
border-color: #2dd4bf;
|
||||
}
|
||||
QPushButton[default="true"]:hover {
|
||||
background-color: #3de0cc;
|
||||
}
|
||||
|
||||
/* ---- Input ---- */
|
||||
QLineEdit,
|
||||
QPlainTextEdit,
|
||||
QTextEdit,
|
||||
QSpinBox,
|
||||
QDoubleSpinBox,
|
||||
QDateEdit,
|
||||
QTimeEdit {
|
||||
background-color: #0a1020;
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
padding: 4px 8px;
|
||||
color: #d0daf0;
|
||||
selection-background-color: #2dd4bf;
|
||||
selection-color: #0f1119;
|
||||
}
|
||||
QLineEdit:focus,
|
||||
QPlainTextEdit:focus,
|
||||
QTextEdit:focus,
|
||||
QSpinBox:focus,
|
||||
QDoubleSpinBox:focus,
|
||||
QDateEdit:focus,
|
||||
QTimeEdit:focus {
|
||||
border-color: #2dd4bf;
|
||||
}
|
||||
QPlainTextEdit,
|
||||
QTextEdit {
|
||||
background-color: #0a1020;
|
||||
}
|
||||
QLineEdit:disabled,
|
||||
QPlainTextEdit:disabled,
|
||||
QTextEdit:disabled,
|
||||
QSpinBox:disabled,
|
||||
QDoubleSpinBox:disabled,
|
||||
QDateEdit:disabled,
|
||||
QTimeEdit:disabled {
|
||||
background-color: #162038;
|
||||
color: #5568a0;
|
||||
border-color: #1c2840;
|
||||
}
|
||||
|
||||
/* ---- Spin Button Arrows ---- */
|
||||
QSpinBox::up-button,
|
||||
QDoubleSpinBox::up-button,
|
||||
QDateEdit::up-button,
|
||||
QTimeEdit::up-button {
|
||||
subcontrol-origin: border;
|
||||
subcontrol-position: top right;
|
||||
width: 10px;
|
||||
border-left: 1px solid #253250;
|
||||
border-bottom: 1px solid #253250;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
QSpinBox::up-button:hover,
|
||||
QDoubleSpinBox::up-button:hover,
|
||||
QDateEdit::up-button:hover,
|
||||
QTimeEdit::up-button:hover {
|
||||
background-color: #1c2840;
|
||||
}
|
||||
QSpinBox::up-arrow,
|
||||
QDoubleSpinBox::up-arrow,
|
||||
QDateEdit::up-arrow,
|
||||
QTimeEdit::up-arrow {
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-bottom: 5px solid #7888b8;
|
||||
margin-top: 2px;
|
||||
}
|
||||
QSpinBox::down-button,
|
||||
QDoubleSpinBox::down-button,
|
||||
QDateEdit::down-button,
|
||||
QTimeEdit::down-button {
|
||||
width: 10px;
|
||||
subcontrol-origin: border;
|
||||
subcontrol-position: bottom right;
|
||||
border-left: 1px solid #253250;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
QSpinBox::down-button:hover,
|
||||
QDoubleSpinBox::down-button:hover,
|
||||
QDateEdit::down-button:hover,
|
||||
QTimeEdit::down-button:hover {
|
||||
background-color: #1c2840;
|
||||
}
|
||||
QSpinBox::down-arrow,
|
||||
QDoubleSpinBox::down-arrow,
|
||||
QDateEdit::down-arrow,
|
||||
QTimeEdit::down-arrow {
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 5px solid #7888b8;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
QSpinBox::up-button:disabled,
|
||||
QDoubleSpinBox::up-button:disabled,
|
||||
QDateEdit::up-button:disabled,
|
||||
QTimeEdit::up-button:disabled,
|
||||
QSpinBox::down-button:disabled,
|
||||
QDoubleSpinBox::down-button:disabled,
|
||||
QDateEdit::down-button:disabled,
|
||||
QTimeEdit::down-button:disabled {
|
||||
background-color: #162038;
|
||||
}
|
||||
QSpinBox::up-arrow:disabled,
|
||||
QDoubleSpinBox::up-arrow:disabled,
|
||||
QDateEdit::up-arrow:disabled,
|
||||
QTimeEdit::up-arrow:disabled {
|
||||
border-bottom-color: #5568a0;
|
||||
}
|
||||
QSpinBox::down-arrow:disabled,
|
||||
QDoubleSpinBox::down-arrow:disabled,
|
||||
QDateEdit::down-arrow:disabled,
|
||||
QTimeEdit::down-arrow:disabled {
|
||||
border-top-color: #5568a0;
|
||||
}
|
||||
|
||||
/* ---- Combo Box ---- */
|
||||
QComboBox {
|
||||
background-color: #1c2840;
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
padding: 4px 10px;
|
||||
color: #d0daf0;
|
||||
}
|
||||
QComboBox:hover {
|
||||
border-color: #334478;
|
||||
}
|
||||
QComboBox:focus {
|
||||
border-color: #2dd4bf;
|
||||
}
|
||||
QComboBox::drop-down {
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: top right;
|
||||
width: 24px;
|
||||
border-left: 1px solid #253250;
|
||||
border-top-right-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
QComboBox::down-arrow {
|
||||
image: none;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 6px solid #7888b8;
|
||||
margin-right: 6px;
|
||||
}
|
||||
QComboBox QAbstractItemView {
|
||||
background-color: #162038;
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
border-radius: 4px;
|
||||
selection-background-color: #2dd4bf;
|
||||
selection-color: #0f1119;
|
||||
outline: none;
|
||||
}
|
||||
QComboBox:disabled {
|
||||
background-color: #162038;
|
||||
color: #5568a0;
|
||||
border-color: #1c2840;
|
||||
}
|
||||
|
||||
/* ---- Check Box / Radio Button ---- */
|
||||
QCheckBox,
|
||||
QRadioButton {
|
||||
spacing: 5px;
|
||||
color: #d0daf0;
|
||||
}
|
||||
QCheckBox::indicator,
|
||||
QRadioButton::indicator {
|
||||
border-style: solid;
|
||||
border-color: #334478;
|
||||
border-width: 2px;
|
||||
background-color: #0a1020;
|
||||
}
|
||||
QCheckBox::indicator {
|
||||
border-radius: 3px;
|
||||
}
|
||||
QRadioButton::indicator {
|
||||
border-radius: 7px;
|
||||
}
|
||||
QCheckBox::indicator:hover,
|
||||
QRadioButton::indicator:hover {
|
||||
border-color: #2dd4bf;
|
||||
}
|
||||
QCheckBox::indicator:checked {
|
||||
background-color: #2dd4bf;
|
||||
border-color: #2dd4bf;
|
||||
}
|
||||
QRadioButton::indicator:checked {
|
||||
background-color: #2dd4bf;
|
||||
border-color: #2dd4bf;
|
||||
}
|
||||
QCheckBox::indicator:disabled,
|
||||
QRadioButton::indicator:disabled {
|
||||
border-color: #253250;
|
||||
background-color: #162038;
|
||||
}
|
||||
QCheckBox::indicator:checked:hover,
|
||||
QRadioButton::indicator:checked:hover {
|
||||
border-color: #a0f0e8;
|
||||
}
|
||||
|
||||
/* Tree / List / Table Widget CheckBox Indicator */
|
||||
QTreeWidget::indicator,
|
||||
QListWidget::indicator,
|
||||
QTableWidget::indicator {
|
||||
border: 2px solid #5568a0;
|
||||
border-radius: 3px;
|
||||
background-color: #162038;
|
||||
}
|
||||
QTreeWidget::indicator:hover,
|
||||
QListWidget::indicator:hover,
|
||||
QTableWidget::indicator:hover {
|
||||
border-color: #a0f0e8;
|
||||
}
|
||||
QTreeWidget::indicator:checked,
|
||||
QListWidget::indicator:checked,
|
||||
QTableWidget::indicator:checked {
|
||||
background-color: #2dd4bf;
|
||||
border-color: #2dd4bf;
|
||||
}
|
||||
QTreeWidget::indicator:checked:hover,
|
||||
QListWidget::indicator:checked:hover,
|
||||
QTableWidget::indicator:checked:hover {
|
||||
border-color: #a0f0e8;
|
||||
}
|
||||
QTreeWidget::indicator:disabled,
|
||||
QListWidget::indicator:disabled,
|
||||
QTableWidget::indicator:disabled {
|
||||
background-color: #1c2840;
|
||||
border-color: #334478;
|
||||
}
|
||||
QTreeWidget::indicator:indeterminate,
|
||||
QListWidget::indicator:indeterminate,
|
||||
QTableWidget::indicator:indeterminate {
|
||||
background-color: #2dd4bf;
|
||||
border-color: #a0f0e8;
|
||||
}
|
||||
|
||||
/* ---- Group Box ---- */
|
||||
QGroupBox {
|
||||
margin-top: 5px;
|
||||
padding-top: 15px;
|
||||
color: #b4c2f5;
|
||||
font-weight: bold;
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* ---- Tab ---- */
|
||||
QTabWidget::pane {
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
background-color: #0f1a2e;
|
||||
top: -1px;
|
||||
}
|
||||
QTabBar::tab {
|
||||
background-color: #162038;
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
border-bottom: none;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
padding: 6px 16px;
|
||||
margin-right: 2px;
|
||||
color: #7888b8;
|
||||
}
|
||||
QTabBar::tab:selected {
|
||||
background-color: #0f1a2e;
|
||||
color: #2dd4bf;
|
||||
border-bottom: 2px solid #2dd4bf;
|
||||
}
|
||||
|
||||
/* ---- List / Tree ---- */
|
||||
QListWidget,
|
||||
QTreeWidget,
|
||||
QTableWidget {
|
||||
background-color: #0a1020;
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
outline: none;
|
||||
color: #d0daf0;
|
||||
alternate-background-color: #101c30;
|
||||
}
|
||||
QListWidget::item,
|
||||
QTreeWidget::item,
|
||||
QTableWidget::item {
|
||||
padding: 5px 5px;
|
||||
}
|
||||
QHeaderView::section {
|
||||
background-color: #0f1628;
|
||||
border-right: 1px solid #253250;
|
||||
border-bottom: 1px solid #253250;
|
||||
padding: 5px 10px;
|
||||
color: #8b9ad0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* ---- Scroll Bar ---- */
|
||||
QScrollBar:vertical {
|
||||
background-color: #0f1a2e;
|
||||
width: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QScrollBar::handle:vertical {
|
||||
background-color: #334478;
|
||||
min-height: 30px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QScrollBar::handle:vertical:hover {
|
||||
background-color: #5568a0;
|
||||
}
|
||||
QScrollBar::add-line:vertical,
|
||||
QScrollBar::sub-line:vertical {
|
||||
height: 0;
|
||||
}
|
||||
QScrollBar:horizontal {
|
||||
background-color: #0f1a2e;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QScrollBar::handle:horizontal {
|
||||
background-color: #334478;
|
||||
min-width: 30px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QScrollBar::handle:horizontal:hover {
|
||||
background-color: #5568a0;
|
||||
}
|
||||
QScrollBar::add-line:horizontal,
|
||||
QScrollBar::sub-line:horizontal {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
/* ---- Progress Bar ---- */
|
||||
QProgressBar {
|
||||
background-color: #0a1020;
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
height: 10px;
|
||||
text-align: center;
|
||||
color: #d0daf0;
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
background-color: #2dd4bf;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ---- Slider ---- */
|
||||
QSlider::groove:horizontal {
|
||||
background-color: #1c2840;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
QSlider::handle:horizontal {
|
||||
background-color: #2dd4bf;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: -5px 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
QSlider::sub-page:horizontal {
|
||||
background-color: #2dd4bf;
|
||||
border-radius: 3px;
|
||||
}
|
||||
QSlider::handle:horizontal:disabled {
|
||||
background-color: #5568a0;
|
||||
}
|
||||
QSlider::sub-page:horizontal:disabled {
|
||||
background-color: #5568a0;
|
||||
}
|
||||
|
||||
/* ---- Tool Tip ---- */
|
||||
QToolTip {
|
||||
background-color: #1c2840;
|
||||
border-style: solid;
|
||||
border-color: #2dd4bf;
|
||||
border-width: 1px;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
color: #d0daf0;
|
||||
}
|
||||
|
||||
/* ---- Status Bar ---- */
|
||||
QStatusBar {
|
||||
background-color: #0f1628;
|
||||
border-top: 1px solid #1c2840;
|
||||
color: #7888b8;
|
||||
}
|
||||
|
||||
/* ---- Splitter ---- */
|
||||
QSplitter::handle {
|
||||
background-color: #253250;
|
||||
margin: 1px;
|
||||
}
|
||||
QSplitter::handle:horizontal {
|
||||
width: 2px;
|
||||
}
|
||||
QSplitter::handle:vertical {
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
/* ---- Dialog ---- */
|
||||
QDialog {
|
||||
background-color: #0f1a2e;
|
||||
}
|
||||
|
||||
/* ---- Date / Time Editor Drop-down ---- */
|
||||
QDateEdit::drop-down,
|
||||
QTimeEdit::drop-down {
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: top right;
|
||||
width: 24px;
|
||||
border-left: 1px solid #253250;
|
||||
}
|
||||
QCalendarWidget {
|
||||
background-color: #162038;
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
QCalendarWidget QToolButton {
|
||||
color: #d0daf0;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
QCalendarWidget QToolButton:hover {
|
||||
background-color: #1c2840;
|
||||
}
|
||||
QCalendarWidget QMenu {
|
||||
background-color: #162038;
|
||||
}
|
||||
|
||||
/* ---- Frame ---- */
|
||||
QFrame[frameShape="4"], /* HLine */
|
||||
QFrame[frameShape="5"] /* VLine */ {
|
||||
background-color: #253250;
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
*
|
||||
* AutoLibrary Official Theme : LightLake
|
||||
*/
|
||||
|
||||
/* ---- Global ---- */
|
||||
QMainWindow::separator {
|
||||
background-color: #c0cdda;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
/* ---- Menu Bar ---- */
|
||||
QMenuBar {
|
||||
background-color: #dce4ee;
|
||||
border-bottom: 1px solid #c0cdda;
|
||||
padding: 2px 5px;
|
||||
color: #1a2740;
|
||||
}
|
||||
QMenuBar::item {
|
||||
padding: 2px 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QMenuBar::item:selected {
|
||||
background-color: #d5dde8;
|
||||
}
|
||||
QMenu {
|
||||
background-color: #ffffff;
|
||||
border-style: solid;
|
||||
border-color: #d0d8e4;
|
||||
border-width: 1px;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
QMenu::item {
|
||||
padding: 5px 15px 5px 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QMenu::item:selected {
|
||||
background-color: #0ea58a;
|
||||
color: #ffffff;
|
||||
}
|
||||
QMenu::separator {
|
||||
height: 1px;
|
||||
background-color: #d0d8e4;
|
||||
margin: 4px 8px;
|
||||
}
|
||||
|
||||
/* ---- Button ---- */
|
||||
QPushButton {
|
||||
border-style: solid;
|
||||
border-color: #c0cdda;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
color: #1a2740;
|
||||
padding: 4px 12px;
|
||||
background-color: #d5dde8;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #c8d4e2;
|
||||
border-color: #90a4c4;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #e2e8f0;
|
||||
border-color: #0ea58a;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #e8ecf2;
|
||||
color: #98a8c0;
|
||||
border-color: #d5dde8;
|
||||
}
|
||||
QPushButton[default="true"] {
|
||||
background-color: #0ea58a;
|
||||
color: #ffffff;
|
||||
border-color: #0ea58a;
|
||||
}
|
||||
QPushButton[default="true"]:hover {
|
||||
background-color: #14c7a4;
|
||||
}
|
||||
|
||||
/* ---- Input ---- */
|
||||
QLineEdit,
|
||||
QPlainTextEdit,
|
||||
QTextEdit,
|
||||
QSpinBox,
|
||||
QDoubleSpinBox,
|
||||
QDateEdit,
|
||||
QTimeEdit {
|
||||
background-color: #ffffff;
|
||||
border-style: solid;
|
||||
border-color: #c0cdda;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
padding: 4px 8px;
|
||||
color: #1a2740;
|
||||
selection-background-color: #0ea58a;
|
||||
selection-color: #ffffff;
|
||||
}
|
||||
QLineEdit:focus,
|
||||
QPlainTextEdit:focus,
|
||||
QTextEdit:focus,
|
||||
QSpinBox:focus,
|
||||
QDoubleSpinBox:focus,
|
||||
QDateEdit:focus,
|
||||
QTimeEdit:focus {
|
||||
border-color: #0ea58a;
|
||||
}
|
||||
QPlainTextEdit,
|
||||
QTextEdit {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
QLineEdit:disabled,
|
||||
QPlainTextEdit:disabled,
|
||||
QTextEdit:disabled,
|
||||
QSpinBox:disabled,
|
||||
QDoubleSpinBox:disabled,
|
||||
QDateEdit:disabled,
|
||||
QTimeEdit:disabled {
|
||||
background-color: #e8ecf2;
|
||||
color: #98a8c0;
|
||||
border-color: #d5dde8;
|
||||
}
|
||||
|
||||
/* ---- Spin Button Arrows ---- */
|
||||
QSpinBox::up-button,
|
||||
QDoubleSpinBox::up-button,
|
||||
QDateEdit::up-button,
|
||||
QTimeEdit::up-button {
|
||||
subcontrol-origin: border;
|
||||
subcontrol-position: top right;
|
||||
width: 10px;
|
||||
border-left: 1px solid #c0cdda;
|
||||
border-bottom: 1px solid #c0cdda;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
QSpinBox::up-button:hover,
|
||||
QDoubleSpinBox::up-button:hover,
|
||||
QDateEdit::up-button:hover,
|
||||
QTimeEdit::up-button:hover {
|
||||
background-color: #d5dde8;
|
||||
}
|
||||
QSpinBox::up-arrow,
|
||||
QDoubleSpinBox::up-arrow,
|
||||
QDateEdit::up-arrow,
|
||||
QTimeEdit::up-arrow {
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-bottom: 5px solid #6a7898;
|
||||
margin-top: 2px;
|
||||
}
|
||||
QSpinBox::down-button,
|
||||
QDoubleSpinBox::down-button,
|
||||
QDateEdit::down-button,
|
||||
QTimeEdit::down-button {
|
||||
width: 10px;
|
||||
subcontrol-origin: border;
|
||||
subcontrol-position: bottom right;
|
||||
border-left: 1px solid #c0cdda;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
QSpinBox::down-button:hover,
|
||||
QDoubleSpinBox::down-button:hover,
|
||||
QDateEdit::down-button:hover,
|
||||
QTimeEdit::down-button:hover {
|
||||
background-color: #d5dde8;
|
||||
}
|
||||
QSpinBox::down-arrow,
|
||||
QDoubleSpinBox::down-arrow,
|
||||
QDateEdit::down-arrow,
|
||||
QTimeEdit::down-arrow {
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 5px solid #6a7898;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
QSpinBox::up-button:disabled,
|
||||
QDoubleSpinBox::up-button:disabled,
|
||||
QDateEdit::up-button:disabled,
|
||||
QTimeEdit::up-button:disabled,
|
||||
QSpinBox::down-button:disabled,
|
||||
QDoubleSpinBox::down-button:disabled,
|
||||
QDateEdit::down-button:disabled,
|
||||
QTimeEdit::down-button:disabled {
|
||||
background-color: #e8ecf2;
|
||||
}
|
||||
QSpinBox::up-arrow:disabled,
|
||||
QDoubleSpinBox::up-arrow:disabled,
|
||||
QDateEdit::up-arrow:disabled,
|
||||
QTimeEdit::up-arrow:disabled {
|
||||
border-bottom-color: #98a8c0;
|
||||
}
|
||||
QSpinBox::down-arrow:disabled,
|
||||
QDoubleSpinBox::down-arrow:disabled,
|
||||
QDateEdit::down-arrow:disabled,
|
||||
QTimeEdit::down-arrow:disabled {
|
||||
border-top-color: #98a8c0;
|
||||
}
|
||||
|
||||
/* ---- Combo Box ---- */
|
||||
QComboBox {
|
||||
background-color: #d5dde8;
|
||||
border-style: solid;
|
||||
border-color: #c0cdda;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
padding: 4px 10px;
|
||||
color: #1a2740;
|
||||
}
|
||||
QComboBox:hover {
|
||||
border-color: #90a4c4;
|
||||
}
|
||||
QComboBox:focus {
|
||||
border-color: #0ea58a;
|
||||
}
|
||||
QComboBox::drop-down {
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: top right;
|
||||
width: 24px;
|
||||
border-left: 1px solid #c0cdda;
|
||||
border-top-right-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
QComboBox::down-arrow {
|
||||
image: none;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 6px solid #6a7898;
|
||||
margin-right: 6px;
|
||||
}
|
||||
QComboBox QAbstractItemView {
|
||||
background-color: #ffffff;
|
||||
border-style: solid;
|
||||
border-color: #d0d8e4;
|
||||
border-width: 1px;
|
||||
border-radius: 4px;
|
||||
selection-background-color: #0ea58a;
|
||||
selection-color: #ffffff;
|
||||
outline: none;
|
||||
}
|
||||
QComboBox:disabled {
|
||||
background-color: #e8ecf2;
|
||||
color: #98a8c0;
|
||||
border-color: #d5dde8;
|
||||
}
|
||||
|
||||
/* ---- Check Box / Radio Button ---- */
|
||||
QCheckBox,
|
||||
QRadioButton {
|
||||
spacing: 5px;
|
||||
color: #1a2740;
|
||||
}
|
||||
QCheckBox::indicator,
|
||||
QRadioButton::indicator {
|
||||
border-style: solid;
|
||||
border-color: #90a4c4;
|
||||
border-width: 2px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
QCheckBox::indicator {
|
||||
border-radius: 3px;
|
||||
}
|
||||
QRadioButton::indicator {
|
||||
border-radius: 7px;
|
||||
}
|
||||
QCheckBox::indicator:hover,
|
||||
QRadioButton::indicator:hover {
|
||||
border-color: #0ea58a;
|
||||
}
|
||||
QCheckBox::indicator:checked {
|
||||
background-color: #0ea58a;
|
||||
border-color: #0ea58a;
|
||||
}
|
||||
QRadioButton::indicator:checked {
|
||||
background-color: #0ea58a;
|
||||
border-color: #0ea58a;
|
||||
}
|
||||
QCheckBox::indicator:disabled,
|
||||
QRadioButton::indicator:disabled {
|
||||
border-color: #c0cdda;
|
||||
background-color: #e8ecf2;
|
||||
}
|
||||
QCheckBox::indicator:checked:hover,
|
||||
QRadioButton::indicator:checked:hover {
|
||||
border-color: #14c7a4;
|
||||
}
|
||||
|
||||
/* Tree / List / Table Widget CheckBox Indicator */
|
||||
QTreeWidget::indicator,
|
||||
QListWidget::indicator,
|
||||
QTableWidget::indicator {
|
||||
border: 2px solid #a0b4cc;
|
||||
border-radius: 3px;
|
||||
background-color: #e8ecf2;
|
||||
}
|
||||
QTreeWidget::indicator:hover,
|
||||
QListWidget::indicator:hover,
|
||||
QTableWidget::indicator:hover {
|
||||
border-color: #14c7a4;
|
||||
}
|
||||
QTreeWidget::indicator:checked,
|
||||
QListWidget::indicator:checked,
|
||||
QTableWidget::indicator:checked {
|
||||
background-color: #0ea58a;
|
||||
border-color: #0ea58a;
|
||||
}
|
||||
QTreeWidget::indicator:checked:hover,
|
||||
QListWidget::indicator:checked:hover,
|
||||
QTableWidget::indicator:checked:hover {
|
||||
border-color: #14c7a4;
|
||||
}
|
||||
QTreeWidget::indicator:disabled,
|
||||
QListWidget::indicator:disabled,
|
||||
QTableWidget::indicator:disabled {
|
||||
background-color: #d5dde8;
|
||||
border-color: #c0cdda;
|
||||
}
|
||||
QTreeWidget::indicator:indeterminate,
|
||||
QListWidget::indicator:indeterminate,
|
||||
QTableWidget::indicator:indeterminate {
|
||||
background-color: #0ea58a;
|
||||
border-color: #14c7a4;
|
||||
}
|
||||
|
||||
/* ---- Group Box ---- */
|
||||
QGroupBox {
|
||||
margin-top: 5px;
|
||||
padding-top: 15px;
|
||||
color: #4a6080;
|
||||
font-weight: bold;
|
||||
border-style: solid;
|
||||
border-color: #c0cdda;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* ---- Tab ---- */
|
||||
QTabWidget::pane {
|
||||
border-style: solid;
|
||||
border-color: #c0cdda;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
background-color: #f0f4f8;
|
||||
top: -1px;
|
||||
}
|
||||
QTabBar::tab {
|
||||
background-color: #e0e6ee;
|
||||
border-style: solid;
|
||||
border-color: #c0cdda;
|
||||
border-width: 1px;
|
||||
border-bottom: none;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
padding: 6px 16px;
|
||||
margin-right: 2px;
|
||||
color: #6a7898;
|
||||
}
|
||||
QTabBar::tab:selected {
|
||||
background-color: #f0f4f8;
|
||||
color: #0ea58a;
|
||||
border-bottom: 2px solid #0ea58a;
|
||||
}
|
||||
|
||||
/* ---- List / Tree ---- */
|
||||
QListWidget,
|
||||
QTreeWidget,
|
||||
QTableWidget {
|
||||
background-color: #ffffff;
|
||||
border-style: solid;
|
||||
border-color: #c0cdda;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
outline: none;
|
||||
color: #1a2740;
|
||||
alternate-background-color: #f4f7fa;
|
||||
}
|
||||
QListWidget::item,
|
||||
QTreeWidget::item,
|
||||
QTableWidget::item {
|
||||
padding: 5px 5px;
|
||||
}
|
||||
QHeaderView::section {
|
||||
background-color: #dce4ee;
|
||||
border-right: 1px solid #c0cdda;
|
||||
border-bottom: 1px solid #c0cdda;
|
||||
padding: 5px 10px;
|
||||
color: #4a6080;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* ---- Scroll Bar ---- */
|
||||
QScrollBar:vertical {
|
||||
background-color: #eef2f6;
|
||||
width: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QScrollBar::handle:vertical {
|
||||
background-color: #a0b4cc;
|
||||
min-height: 30px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QScrollBar::handle:vertical:hover {
|
||||
background-color: #8098b8;
|
||||
}
|
||||
QScrollBar::add-line:vertical,
|
||||
QScrollBar::sub-line:vertical {
|
||||
height: 0;
|
||||
}
|
||||
QScrollBar:horizontal {
|
||||
background-color: #eef2f6;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QScrollBar::handle:horizontal {
|
||||
background-color: #a0b4cc;
|
||||
min-width: 30px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QScrollBar::handle:horizontal:hover {
|
||||
background-color: #8098b8;
|
||||
}
|
||||
QScrollBar::add-line:horizontal,
|
||||
QScrollBar::sub-line:horizontal {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
/* ---- Progress Bar ---- */
|
||||
QProgressBar {
|
||||
background-color: #ffffff;
|
||||
border-style: solid;
|
||||
border-color: #c0cdda;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
height: 10px;
|
||||
text-align: center;
|
||||
color: #1a2740;
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
background-color: #0ea58a;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ---- Slider ---- */
|
||||
QSlider::groove:horizontal {
|
||||
background-color: #d5dde8;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
QSlider::handle:horizontal {
|
||||
background-color: #0ea58a;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: -5px 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
QSlider::sub-page:horizontal {
|
||||
background-color: #0ea58a;
|
||||
border-radius: 3px;
|
||||
}
|
||||
QSlider::handle:horizontal:disabled {
|
||||
background-color: #98a8c0;
|
||||
}
|
||||
QSlider::sub-page:horizontal:disabled {
|
||||
background-color: #98a8c0;
|
||||
}
|
||||
|
||||
/* ---- Tool Tip ---- */
|
||||
QToolTip {
|
||||
background-color: #d5dde8;
|
||||
border-style: solid;
|
||||
border-color: #0ea58a;
|
||||
border-width: 1px;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
color: #1a2740;
|
||||
}
|
||||
|
||||
/* ---- Status Bar ---- */
|
||||
QStatusBar {
|
||||
background-color: #e8ecf2;
|
||||
border-top: 1px solid #c0cdda;
|
||||
color: #6a7898;
|
||||
}
|
||||
|
||||
/* ---- Splitter ---- */
|
||||
QSplitter::handle {
|
||||
background-color: #c0cdda;
|
||||
margin: 1px;
|
||||
}
|
||||
QSplitter::handle:horizontal {
|
||||
width: 2px;
|
||||
}
|
||||
QSplitter::handle:vertical {
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
/* ---- Dialog ---- */
|
||||
QDialog {
|
||||
background-color: #f0f4f8;
|
||||
}
|
||||
|
||||
/* ---- Date / Time Editor Drop-down ---- */
|
||||
QDateEdit::drop-down,
|
||||
QTimeEdit::drop-down {
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: top right;
|
||||
width: 24px;
|
||||
border-left: 1px solid #c0cdda;
|
||||
}
|
||||
QCalendarWidget {
|
||||
background-color: #ffffff;
|
||||
border-style: solid;
|
||||
border-color: #c0cdda;
|
||||
border-width: 1px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
QCalendarWidget QToolButton {
|
||||
color: #1a2740;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
QCalendarWidget QToolButton:hover {
|
||||
background-color: #d5dde8;
|
||||
}
|
||||
QCalendarWidget QMenu {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* ---- Frame ---- */
|
||||
QFrame[frameShape="4"], /* HLine */
|
||||
QFrame[frameShape="5"] /* VLine */ {
|
||||
background-color: #c0cdda;
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>800</width>
|
||||
<height>400</height>
|
||||
<height>600</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -103,50 +103,6 @@
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QFrame" name="frame">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>56</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>56</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Shadow::Plain</enum>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="AboutInfoEdit">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Courier New</family>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="lineWrapMode">
|
||||
<enum>QTextEdit::LineWrapMode::NoWrap</enum>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextInteractionFlag::TextBrowserInteraction</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
@@ -0,0 +1,385 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ALBulletinDialog</class>
|
||||
<widget class="QDialog" name="ALBulletinDialog">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::WindowModality::NonModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>640</width>
|
||||
<height>450</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>400</width>
|
||||
<height>450</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>800</width>
|
||||
<height>450</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>公告栏 - AutoLibrary</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="ALBulletinDialogLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="SyncLayout">
|
||||
<property name="spacing">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="SyncButton">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::FocusPolicy::NoFocus</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>同步</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="SyncStatusPlaceholder">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="SyncStatusDetailLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="SyncSpaceFrame">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>800</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Shadow::Plain</enum>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="LastSyncTimeLabel">
|
||||
<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>
|
||||
<widget class="QDateTimeEdit" name="LastSyncDateTimeEdit">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>130</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>130</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frame">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="buttonSymbols">
|
||||
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
|
||||
</property>
|
||||
<property name="keyboardTracking">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="displayFormat">
|
||||
<string>yyyy/MM/dd HH:mm:ss</string>
|
||||
</property>
|
||||
<property name="timeSpec">
|
||||
<enum>Qt::TimeSpec::LocalTime</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSplitter" name="BulletinSplitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="QFrame" name="LeftFrame">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Shadow::Plain</enum>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="LeftFrameLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QListWidget" name="BulletinListWidget">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SelectionMode::SingleSelection</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QFrame" name="RightFrame">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Shadow::Plain</enum>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="RightFrameLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="BulletinTitleLabel">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="BulletinInfoLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="BulletinDateLabel">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="BulletinAuthorLabel">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="BulletinIsEditedLabel">
|
||||
<property name="text">
|
||||
<string>(已编辑)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="BulletinInfoSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextBrowser" name="BulletinContentTextBrowser">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="BulletinDialogButtonBox">
|
||||
<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="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>BulletinDialogButtonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>ALBulletinDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>BulletinDialogButtonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>ALBulletinDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -94,19 +94,19 @@
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="UserListLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>5</number>
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>5</number>
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>5</number>
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>5</number>
|
||||
<number>3</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QTreeWidget" name="UserTreeWidget">
|
||||
@@ -178,6 +178,9 @@
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="UserListControlLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="DelUserButton">
|
||||
<property name="minimumSize">
|
||||
@@ -192,6 +195,11 @@
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPushButton {
|
||||
color: #DC0000;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>删除用户</string>
|
||||
</property>
|
||||
@@ -309,7 +317,7 @@
|
||||
<item row="2" column="1">
|
||||
<layout class="QHBoxLayout" name="PasswordLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="PasswordEdit">
|
||||
@@ -546,7 +554,7 @@
|
||||
<item row="4" column="4">
|
||||
<layout class="QHBoxLayout" name="SeatIDLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="SeatIDEdit">
|
||||
@@ -703,7 +711,7 @@
|
||||
<item row="10" column="4">
|
||||
<layout class="QHBoxLayout" name="EndTimeDiffLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="MaxEndTimeDiffSpinBox">
|
||||
@@ -899,7 +907,7 @@
|
||||
<item row="7" column="4">
|
||||
<layout class="QHBoxLayout" name="BeginTimeDiffLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="MaxBeginTimeDiffSpinBox">
|
||||
@@ -1049,7 +1057,7 @@
|
||||
<item row="15" column="4">
|
||||
<layout class="QHBoxLayout" name="RenewTimeDiffLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="MaxRenewTimeDiffSpinBox">
|
||||
@@ -1092,7 +1100,7 @@
|
||||
</layout>
|
||||
</item>
|
||||
<item row="15" column="1">
|
||||
<widget class="QLabel" name="label">
|
||||
<widget class="QLabel" name="MaxRenewTimeDiffLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
@@ -1153,10 +1161,10 @@
|
||||
<property name="rightMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="horizontalSpacing">
|
||||
<property name="bottomMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="verticalSpacing">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item row="0" column="0" colspan="2">
|
||||
@@ -1225,12 +1233,31 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QGroupBox" name="BrowserConfigGroupBox">
|
||||
<property name="title">
|
||||
<string>浏览器设置</string>
|
||||
<item row="2" column="0">
|
||||
<widget class="QFrame" name="SystemConfigSpaceFrame">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>270</height>
|
||||
</size>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="BrowserConfigLayout">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Shadow::Plain</enum>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1" colspan="2">
|
||||
<widget class="QGroupBox" name="RunModeConfigGroupBox">
|
||||
<property name="title">
|
||||
<string>运行模式</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="RunModeConfigLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
@@ -1247,162 +1274,59 @@
|
||||
<number>3</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="BrowserTypeLabel">
|
||||
<widget class="QCheckBox" name="AutoReserveCheckBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>浏览器类型:</string>
|
||||
<string>自动预约</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="BrowserTypeComboBox">
|
||||
<widget class="QCheckBox" name="AutoCheckinCheckBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>脚本运行使用的浏览器类型</p></body></html></string>
|
||||
</property>
|
||||
<property name="whatsThis">
|
||||
<string><html><head/><body><p><br/></p></body></html></string>
|
||||
</property>
|
||||
<property name="currentText">
|
||||
<string>edge</string>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="maxVisibleItems">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="maxCount">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>edge</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>chrome</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>firefox</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="BrowserDriverLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>驱动路径:</string>
|
||||
<string>自动签到</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="BrowserDriverLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="BrowseBrowserDriverEdit">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>250</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>300</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>详情请参阅 <a href="https://www.autolibrary.cv/docs/manual_lists.html"><span style=" text-decoration: underline; color:#69fcff;">用户手册</span></a></p></body></html></string>
|
||||
</property>
|
||||
<property name="whatsThis">
|
||||
<string><html><head/><body><p><br/></p></body></html></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="BrowseBrowserDriverButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>35</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>35</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="HeadlessCheckBox">
|
||||
<widget class="QCheckBox" name="AutoRenewalCheckBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>运行时不显示浏览器</p></body></html></string>
|
||||
</property>
|
||||
<property name="whatsThis">
|
||||
<string><html><head/><body><p><br/></p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>无头模式</string>
|
||||
<string>自动续约</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -1521,15 +1445,12 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1" colspan="2">
|
||||
<widget class="QGroupBox" name="RunModeConfigGroupBox">
|
||||
<item row="1" column="0">
|
||||
<widget class="QGroupBox" name="BrowserConfigGroupBox">
|
||||
<property name="title">
|
||||
<string>运行模式</string>
|
||||
<string>浏览器设置</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="RunModeConfigLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="BrowserConfigLayout">
|
||||
<property name="leftMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
@@ -1542,85 +1463,203 @@
|
||||
<property name="bottomMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="AutoReserveCheckBox">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="BrowserTypeLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>自动预约</string>
|
||||
<string>浏览器类型:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="AutoCheckinCheckBox">
|
||||
<item row="1" column="0">
|
||||
<widget class="QComboBox" name="BrowserTypeComboBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>脚本运行使用的浏览器类型</p></body></html></string>
|
||||
</property>
|
||||
<property name="whatsThis">
|
||||
<string><html><head/><body><p><br/></p></body></html></string>
|
||||
</property>
|
||||
<property name="currentText">
|
||||
<string>edge</string>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="maxVisibleItems">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="maxCount">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>edge</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>chrome</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>firefox</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="BrowserDriverLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>175</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>175</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>自动签到</string>
|
||||
<string>驱动路径:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="AutoRenewalCheckBox">
|
||||
<item row="3" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout" name="BrowserDriverLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="BrowseBrowserDriverEdit">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>250</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>300</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<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 name="whatsThis">
|
||||
<string><html><head/><body><p><br/></p></body></html></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="BrowseBrowserDriverButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>35</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>35</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QCheckBox" name="HeadlessCheckBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<width>0</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<width>16777215</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>运行时不显示浏览器</p></body></html></string>
|
||||
</property>
|
||||
<property name="whatsThis">
|
||||
<string><html><head/><body><p><br/></p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>自动续约</string>
|
||||
<string>无头模式</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QPushButton" name="AutoDownloadWebDriverButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>120</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>120</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="layoutDirection">
|
||||
<enum>Qt::LayoutDirection::LeftToRight</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>自动下载驱动</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset theme="document-properties"/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QFrame" name="SystemConfigSpaceFrame">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>270</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Shadow::Plain</enum>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="OtherConfigWidget">
|
||||
@@ -1649,6 +1688,21 @@
|
||||
<string>当前配置</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="CurrentConfigLayout">
|
||||
<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>
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLineEdit" name="CurrentRunConfigEdit">
|
||||
<property name="minimumSize">
|
||||
@@ -1775,6 +1829,21 @@
|
||||
<string>导出路径</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="ExportConfigLayout">
|
||||
<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>
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLineEdit" name="ExportUserConfigEdit">
|
||||
<property name="minimumSize">
|
||||
@@ -1887,13 +1956,13 @@
|
||||
<widget class="QPushButton" name="ExportConfigButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<width>120</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<width>120</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
@@ -34,13 +34,13 @@
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>3</number>
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>3</number>
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
@@ -51,7 +51,7 @@
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="TimerTaskWidgetButton">
|
||||
<widget class="QPushButton" name="TimerTaskManageWidgetButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>25</width>
|
||||
@@ -156,11 +156,9 @@
|
||||
<string><html><head/><body><p><br/></p></body></html></string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">background-color: rgb(10, 170, 10);
|
||||
font: 12pt "Microsoft YaHei UI";
|
||||
color: rgb(255, 255, 255);
|
||||
font: 9pt "Segoe UI";
|
||||
font: 700 9pt "Microsoft YaHei UI";</string>
|
||||
<string notr="true">background-color: #0AAA0A;
|
||||
color: #FFFFFF;
|
||||
font: 700 9pt;</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>启动脚本</string>
|
||||
@@ -283,6 +281,12 @@ font: 700 9pt "Microsoft YaHei UI";</string>
|
||||
<property name="nativeMenuBar">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QMenu" name="ToolsMenu">
|
||||
<property name="title">
|
||||
<string>工具</string>
|
||||
</property>
|
||||
<addaction name="SettingsAction"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="HelpMenu">
|
||||
<property name="mouseTracking">
|
||||
<bool>true</bool>
|
||||
@@ -293,6 +297,7 @@ font: 700 9pt "Microsoft YaHei UI";</string>
|
||||
<addaction name="ManualAction"/>
|
||||
<addaction name="AboutAction"/>
|
||||
</widget>
|
||||
<addaction name="ToolsMenu"/>
|
||||
<addaction name="HelpMenu"/>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="StatusBar">
|
||||
@@ -310,6 +315,11 @@ font: 700 9pt "Microsoft YaHei UI";</string>
|
||||
<string>关于</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="SettingsAction">
|
||||
<property name="text">
|
||||
<string>全局设置</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
@@ -0,0 +1,773 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ALSettingsWidget</class>
|
||||
<widget class="QWidget" name="ALSettingsWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>520</width>
|
||||
<height>420</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>480</width>
|
||||
<height>420</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>580</width>
|
||||
<height>420</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>全局设置 - AutoLibrary</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="ALSettingsWidgetLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="ContentLayout">
|
||||
<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="QListWidget" name="NavigationList">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::FocusPolicy::StrongFocus</enum>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SelectionMode::SingleSelection</enum>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="currentRow">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>外观</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset theme="preferences-desktop-color"/>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>公告</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset theme="preferences-desktop-color"/>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QStackedWidget" name="PageStack">
|
||||
<widget class="QScrollArea" name="AppearanceScrollArea">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="AppearancePageContent">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>397</width>
|
||||
<height>434</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="AppearancePageLayout">
|
||||
<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="QGroupBox" name="AppearanceGroupBox">
|
||||
<property name="title">
|
||||
<string>主题模式</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="AppearanceGroupBoxLayout">
|
||||
<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="QRadioButton" name="LightThemeRadio">
|
||||
<property name="text">
|
||||
<string>浅色</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="DarkThemeRadio">
|
||||
<property name="text">
|
||||
<string>深色</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="SystemThemeRadio">
|
||||
<property name="text">
|
||||
<string>跟随系统</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="InterfaceGroupBox">
|
||||
<property name="title">
|
||||
<string>界面风格</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="InterfaceGroupBoxLayout">
|
||||
<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>
|
||||
<layout class="QHBoxLayout" name="StyleSelectLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="StyleSelectLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>应用程序样式:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="StyleComboBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>160</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="StyleHintLabel">
|
||||
<property name="text">
|
||||
<string>更改样式将在下次启动应用程序时生效。</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="CustomThemeGroupBox">
|
||||
<property name="title">
|
||||
<string>自定义外观</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="CustomQssGroupBoxLayout">
|
||||
<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="QLabel" name="CustomThemeHintLabel">
|
||||
<property name="text">
|
||||
<string>选择一个主题,或导入新的主题文件:</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="CustomThemePathLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QComboBox" name="CustomThemeComboBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>160</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="CustomThemePathEdit">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="visible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>选择或输入 QSS 样式表文件路径...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="ImportCustomThemeButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>+</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="RemoveCustomThemeButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>-</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="CustomThemeInfoLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>60</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::TextFormat::RichText</enum>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="CustomThemeActionLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="ResetCustomThemeButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>重置主题</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="CustomThemeActionSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="CustomThemeStatusLabel">
|
||||
<property name="text">
|
||||
<string>当前使用程序 默认 外观。</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="AppearancePageSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QScrollArea" name="BulletinScrollArea">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="BulletinPageContent">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>397</width>
|
||||
<height>434</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="BulletinPageLayout">
|
||||
<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="QGroupBox" name="BulletinGroupBox">
|
||||
<property name="title">
|
||||
<string>公告设置</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="BulletinGroupBoxLayout">
|
||||
<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>
|
||||
<layout class="QHBoxLayout" name="BulletinServerUrlLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="BulletinServerUrlLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>服务器地址:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="BulletinServerUrlEdit">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>https://api.autolibrary.kenanzhu.com</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="BulletinAutoFetchCheck">
|
||||
<property name="text">
|
||||
<string>软件启动时自动获取公告</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="BulletinSyncIntervalLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="BulletinSyncIntervalLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>自动同步间隔:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="BulletinSyncIntervalSpin">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>1440</number>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string> 分钟</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="BulletinSyncIntervalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="BulletinTestButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>测试连接</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="BulletinTestStatusLabel">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="BulletinOpenButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>查看公告栏</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="BulletinPageSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="ButtonLayout">
|
||||
<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>
|
||||
<spacer name="ButtonLeftSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="CancelButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>取消</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="ApplyButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>应用</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="ConfirmButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>确认</string>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,496 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ALTimerTaskAddDialog</class>
|
||||
<widget class="QDialog" name="ALTimerTaskAddDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>350</width>
|
||||
<height>500</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>350</width>
|
||||
<height>500</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>350</width>
|
||||
<height>500</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>添加定时任务 - AutoLibrary</string>
|
||||
</property>
|
||||
<property name="sizeGripEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="ALAddTimerTaskLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="TaskNameLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="TaskNameLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>60</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="QLineEdit" name="TaskNameLineEdit"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="TimerConfigGroupBox">
|
||||
<property name="title">
|
||||
<string>定时设置</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="TimerConfigLayout">
|
||||
<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>
|
||||
<layout class="QHBoxLayout" name="TimerTypeSelectLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="TimerTypeLabel">
|
||||
<property name="text">
|
||||
<string>定时类型:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="TimerTypeComboBox">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>特定时间</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>相对时间</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="TaskConfigGroupBox">
|
||||
<property name="title">
|
||||
<string>运行设置</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="TaskConfigLayout">
|
||||
<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>
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item row="1" column="0">
|
||||
<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">
|
||||
<string>静默运行</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="autoRepeat">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="autoExclusive">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<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">
|
||||
<string>运行前提示</string>
|
||||
</property>
|
||||
</widget>
|
||||
</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>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="ControLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QFrame" name="ControlSpaceFrame">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Shadow::Plain</enum>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="CancelButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>取消</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="ConfirmButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>添加</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -1,31 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ALTimerTaskWidget</class>
|
||||
<widget class="QWidget" name="ALTimerTaskWidget">
|
||||
<class>ALTimerTaskManageWidget</class>
|
||||
<widget class="QWidget" name="ALTimerTaskManageWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<width>500</width>
|
||||
<height>400</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>400</width>
|
||||
<width>500</width>
|
||||
<height>400</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>600</width>
|
||||
<width>800</width>
|
||||
<height>400</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>定时任务 - AutoLibrary</string>
|
||||
<string>定时任务管理 - AutoLibrary</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="ALTimerTaskWidgetLayout">
|
||||
<layout class="QVBoxLayout" name="ALTimerTaskManageWidgetLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
@@ -153,7 +153,7 @@
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLabel {
|
||||
color: #FF5722
|
||||
color: #DC0000
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -306,6 +306,11 @@
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPushButton {
|
||||
color: #DC0000;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>清除全部</string>
|
||||
</property>
|
||||
@@ -0,0 +1,131 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2026 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Any, Optional, Protocol
|
||||
|
||||
|
||||
class ConfigType(Enum):
|
||||
"""
|
||||
Config type enum. Values represent the default filename.
|
||||
"""
|
||||
|
||||
GLOBAL = "autolibrary.json"
|
||||
BULLETIN = "bulletin.json"
|
||||
TIMERTASK = "timer_task.json"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ConfigPath:
|
||||
"""
|
||||
A typed configuration path that carries both the config file
|
||||
and the dot-separated key in a single object.
|
||||
|
||||
Consumers pass this directly to ConfigProvider.get/set,
|
||||
eliminating the need to import ConfigType separately.
|
||||
"""
|
||||
|
||||
config_type: ConfigType
|
||||
key: str = ""
|
||||
|
||||
|
||||
class CfgKey:
|
||||
"""
|
||||
Type-safe hierarchical configuration key constants.
|
||||
|
||||
Each leaf is a ConfigPath that can be passed directly to
|
||||
``ConfigProvider.get()`` or ``ConfigProvider.set()``.
|
||||
|
||||
Usage::
|
||||
|
||||
CfgKey.GLOBAL.AUTOMATION.RUN_PATH.PATHS
|
||||
# -> ConfigPath(ConfigType.GLOBAL, "automation.run_path.paths")
|
||||
|
||||
config.get(CfgKey.GLOBAL.AUTOMATION.RUN_PATH.PATHS, [])
|
||||
config.set(CfgKey.GLOBAL.AUTOMATION.RUN_PATH.PATHS, value)
|
||||
"""
|
||||
|
||||
class GLOBAL:
|
||||
class AUTOMATION:
|
||||
ROOT = ConfigPath(ConfigType.GLOBAL, "automation")
|
||||
|
||||
class RUN_PATH:
|
||||
ROOT = ConfigPath(ConfigType.GLOBAL, "automation.run_path")
|
||||
CURRENT = ConfigPath(ConfigType.GLOBAL, "automation.run_path.current")
|
||||
PATHS = ConfigPath(ConfigType.GLOBAL, "automation.run_path.paths")
|
||||
|
||||
class USER_PATH:
|
||||
ROOT = ConfigPath(ConfigType.GLOBAL, "automation.user_path")
|
||||
CURRENT = ConfigPath(ConfigType.GLOBAL, "automation.user_path.current")
|
||||
PATHS = ConfigPath(ConfigType.GLOBAL, "automation.user_path.paths")
|
||||
|
||||
class APPEARANCE:
|
||||
ROOT = ConfigPath(ConfigType.GLOBAL, "appearance")
|
||||
THEME = ConfigPath(ConfigType.GLOBAL, "appearance.theme")
|
||||
STYLE = ConfigPath(ConfigType.GLOBAL, "appearance.style")
|
||||
CUSTOM_THEME = ConfigPath(ConfigType.GLOBAL, "appearance.custom_theme")
|
||||
|
||||
class BULLETIN:
|
||||
ROOT = ConfigPath(ConfigType.GLOBAL, "bulletin")
|
||||
AUTO_FETCH = ConfigPath(ConfigType.GLOBAL, "bulletin.auto_fetch")
|
||||
SERVER_URL = ConfigPath(ConfigType.GLOBAL, "bulletin.server_url")
|
||||
SYNC_INTERVAL = ConfigPath(ConfigType.GLOBAL, "bulletin.sync_interval")
|
||||
|
||||
class TIMERTASK:
|
||||
ROOT = ConfigPath(ConfigType.TIMERTASK, "")
|
||||
TIMER_TASKS = ConfigPath(ConfigType.TIMERTASK, "timer_tasks")
|
||||
|
||||
class BULLETIN:
|
||||
ROOT = ConfigPath(ConfigType.BULLETIN, "")
|
||||
BULLETIN = ConfigPath(ConfigType.BULLETIN, "bulletin")
|
||||
LAST_SYNC_TIME = ConfigPath(ConfigType.BULLETIN, "last_sync_time")
|
||||
|
||||
|
||||
class ConfigProvider(Protocol):
|
||||
"""
|
||||
Abstract interface for configuration storage access.
|
||||
|
||||
Concrete implementations (e.g. ConfigManager) conform to
|
||||
this protocol structurally rather than through explicit
|
||||
inheritance.
|
||||
"""
|
||||
|
||||
def get(
|
||||
self,
|
||||
key: ConfigPath,
|
||||
default: Optional[Any] = None
|
||||
) -> Any:
|
||||
"""
|
||||
Retrieve a configuration value.
|
||||
|
||||
Args:
|
||||
key: A ConfigPath object specifying which config file
|
||||
and key to read from.
|
||||
default: Fallback value if the key is not found.
|
||||
|
||||
Returns:
|
||||
The configuration value at the given key path.
|
||||
"""
|
||||
...
|
||||
|
||||
def set(
|
||||
self,
|
||||
key: ConfigPath,
|
||||
value: Any = None
|
||||
) -> None:
|
||||
"""
|
||||
Set a configuration value and persist to disk.
|
||||
|
||||
Args:
|
||||
key: A ConfigPath object specifying which config file
|
||||
and key to write to.
|
||||
value: The value to store.
|
||||
"""
|
||||
...
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- 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.
|
||||
"""
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- 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.
|
||||
"""
|
||||
@@ -0,0 +1,340 @@
|
||||
# -*- 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 threading
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
|
||||
from interfaces.ConfigProvider import (
|
||||
CfgKey,
|
||||
ConfigProvider
|
||||
)
|
||||
from managers.config.ConfigManager import instance as configInstance
|
||||
|
||||
|
||||
class BulletinManager:
|
||||
"""
|
||||
Bulletin Manager Singleton Class
|
||||
|
||||
Manages bulletin data persistence via ConfigManager and provides
|
||||
merge, read-status and sync-parameter logic. The HTTP fetch and
|
||||
UI concerns are handled by the GUI layer.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self
|
||||
):
|
||||
|
||||
self.__lock = threading.Lock()
|
||||
self.__cfg: ConfigProvider = configInstance()
|
||||
|
||||
def bulletins(
|
||||
self
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Get cached bulletins.
|
||||
|
||||
Returns:
|
||||
list[dict]: Cached bulletin list.
|
||||
"""
|
||||
|
||||
return self.__cfg.get(CfgKey.BULLETIN.BULLETIN, [])
|
||||
|
||||
def setBulletins(
|
||||
self,
|
||||
bulletins: list[dict]
|
||||
):
|
||||
"""
|
||||
Replace the bulletin cache.
|
||||
|
||||
Args:
|
||||
bulletins (list[dict]): Bulletin list to store.
|
||||
"""
|
||||
|
||||
self.__cfg.set(CfgKey.BULLETIN.BULLETIN, bulletins)
|
||||
|
||||
def lastSyncTime(
|
||||
self
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Get last sync time string.
|
||||
|
||||
Returns:
|
||||
Optional[str]: ISO-format last sync time, or None.
|
||||
"""
|
||||
|
||||
return self.__cfg.get(CfgKey.BULLETIN.LAST_SYNC_TIME, None)
|
||||
|
||||
def setLastSyncTime(
|
||||
self,
|
||||
value: Optional[str]
|
||||
):
|
||||
"""
|
||||
Set last sync time.
|
||||
|
||||
Args:
|
||||
value (Optional[str]): ISO-format datetime string.
|
||||
"""
|
||||
|
||||
self.__cfg.set(CfgKey.BULLETIN.LAST_SYNC_TIME, value)
|
||||
|
||||
def autoFetch(
|
||||
self
|
||||
) -> bool:
|
||||
"""
|
||||
Get auto-fetch-on-startup setting.
|
||||
|
||||
Returns:
|
||||
bool: Whether to fetch bulletins on application startup.
|
||||
"""
|
||||
|
||||
return self.__cfg.get(CfgKey.GLOBAL.BULLETIN.AUTO_FETCH, False)
|
||||
|
||||
def serverUrl(
|
||||
self
|
||||
) -> str:
|
||||
"""
|
||||
Get bulletin server URL.
|
||||
|
||||
Returns:
|
||||
str: Server base URL.
|
||||
"""
|
||||
|
||||
return self.__cfg.get(
|
||||
CfgKey.GLOBAL.BULLETIN.SERVER_URL,
|
||||
"https://api.autolibrary.kenanzhu.com"
|
||||
)
|
||||
|
||||
def syncInterval(
|
||||
self
|
||||
) -> int:
|
||||
"""
|
||||
Get auto-sync interval in minutes.
|
||||
|
||||
Values below 1 are clamped upward. Non-integer or missing
|
||||
values fall back to the default of 10 minutes.
|
||||
|
||||
Returns:
|
||||
int: Sync interval (minutes), minimum 1.
|
||||
"""
|
||||
|
||||
interval = self.__cfg.get(CfgKey.GLOBAL.BULLETIN.SYNC_INTERVAL, 10)
|
||||
try:
|
||||
interval = int(interval)
|
||||
except (ValueError, TypeError):
|
||||
return 10
|
||||
if interval < 1:
|
||||
return 5
|
||||
return interval
|
||||
|
||||
def apiUrl(
|
||||
self
|
||||
) -> str:
|
||||
"""
|
||||
Build the full bulletin API endpoint URL.
|
||||
|
||||
Returns:
|
||||
str: Full API URL (base + /bulletins).
|
||||
"""
|
||||
|
||||
base = self.serverUrl().rstrip("/")
|
||||
return f"{base}/bulletins"
|
||||
|
||||
@staticmethod
|
||||
def _ensureAware(
|
||||
dt_value: datetime
|
||||
) -> datetime:
|
||||
|
||||
if dt_value.tzinfo is None:
|
||||
return dt_value.replace(tzinfo=timezone.utc)
|
||||
return dt_value
|
||||
|
||||
@staticmethod
|
||||
def _parseBulletinDateTime(
|
||||
b: dict
|
||||
) -> datetime:
|
||||
|
||||
raw = b.get("dateTime", "")
|
||||
if not raw:
|
||||
raise ValueError("empty dateTime")
|
||||
dt_value = datetime.fromisoformat(raw)
|
||||
return BulletinManager._ensureAware(dt_value)
|
||||
|
||||
def isFirstSync(
|
||||
self
|
||||
) -> bool:
|
||||
"""
|
||||
Check whether this is the first sync.
|
||||
|
||||
Returns:
|
||||
bool: True if no cached bulletins or no last sync time.
|
||||
"""
|
||||
|
||||
with self.__lock:
|
||||
last = self.lastSyncTime()
|
||||
bulletins = self.bulletins()
|
||||
return last is None or not bulletins
|
||||
|
||||
def shouldFullSync(
|
||||
self
|
||||
) -> bool:
|
||||
"""
|
||||
Check whether a full sync is needed.
|
||||
|
||||
A full sync is triggered when the last sync time is more than
|
||||
one hour ago.
|
||||
|
||||
Returns:
|
||||
bool: True if a full sync should be performed.
|
||||
"""
|
||||
|
||||
with self.__lock:
|
||||
last = self.lastSyncTime()
|
||||
if last is None:
|
||||
return True
|
||||
try:
|
||||
last_dt = datetime.fromisoformat(last)
|
||||
last_dt = self._ensureAware(last_dt)
|
||||
now = datetime.now(timezone.utc).astimezone()
|
||||
return (now - last_dt) > timedelta(hours=1)
|
||||
except (ValueError, TypeError):
|
||||
return True
|
||||
|
||||
def getSyncDateTimeAndRange(
|
||||
self
|
||||
) -> dict:
|
||||
"""
|
||||
Calculate the date / time / range_hour query parameters.
|
||||
|
||||
Falls back to a first-sync (7-day window) on any parsing error.
|
||||
|
||||
Returns:
|
||||
dict: Keys "date", "time", "range_hour" for the API request.
|
||||
"""
|
||||
|
||||
now = datetime.now(timezone.utc).astimezone()
|
||||
try:
|
||||
if self.isFirstSync():
|
||||
start_date = now - timedelta(days=7)
|
||||
range_hour = str(24*8)
|
||||
elif self.shouldFullSync():
|
||||
with self.__lock:
|
||||
bulletins = self.bulletins()
|
||||
earliest = min(bulletins, key=self._parseBulletinDateTime)
|
||||
start_date = self._parseBulletinDateTime(earliest)
|
||||
diff = now - start_date
|
||||
range_hour = str(int(diff.total_seconds()/3600) + 1)
|
||||
else:
|
||||
with self.__lock:
|
||||
bulletins = self.bulletins()
|
||||
latest = max(bulletins, key=self._parseBulletinDateTime)
|
||||
start_date = self._parseBulletinDateTime(latest)
|
||||
diff = now - start_date
|
||||
range_hour = str(int(diff.total_seconds()/3600) + 1)
|
||||
except (ValueError, TypeError, KeyError):
|
||||
start_date = now - timedelta(days=7)
|
||||
range_hour = str(24*8)
|
||||
|
||||
return {
|
||||
"date": start_date.strftime("%Y-%m-%d"),
|
||||
"time": start_date.strftime("%H:%M:%S"),
|
||||
"range_hour": range_hour,
|
||||
}
|
||||
|
||||
def updateAndMergeBulletins(
|
||||
self,
|
||||
new_bulletins: list[dict],
|
||||
delete_ids: list[str]
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Merge incoming bulletins into the cache.
|
||||
|
||||
- New bulletins are added with isNew=True.
|
||||
- Existing bulletins keep their current isNew state.
|
||||
- Entries listed in delete_ids are removed.
|
||||
- Bulletins missing an "id" field are skipped.
|
||||
|
||||
Args:
|
||||
new_bulletins (list[dict]): Incoming bulletin list.
|
||||
delete_ids (list[str]): IDs to remove.
|
||||
|
||||
Returns:
|
||||
list[dict]: Merged bulletin list sorted by id.
|
||||
"""
|
||||
|
||||
with self.__lock:
|
||||
delete_set = set(str(d) for d in delete_ids)
|
||||
bulletins_dict = {}
|
||||
for b in self.bulletins():
|
||||
bid = b.get("id")
|
||||
if bid is not None:
|
||||
bulletins_dict[str(bid)] = b
|
||||
for bulletin in new_bulletins:
|
||||
bid = bulletin.get("id")
|
||||
if bid is None:
|
||||
continue
|
||||
bid = str(bid)
|
||||
if bid in delete_set:
|
||||
bulletins_dict.pop(bid, None)
|
||||
continue
|
||||
bulletin["isNew"] = (
|
||||
True
|
||||
if bid not in bulletins_dict
|
||||
else bulletins_dict[bid].get("isNew", True)
|
||||
)
|
||||
bulletins_dict[bid] = bulletin
|
||||
for bid in delete_set:
|
||||
bulletins_dict.pop(bid, None)
|
||||
result = list(bulletins_dict.values())
|
||||
result.sort(key=lambda x: str(x.get("id", "")))
|
||||
self.setBulletins(result)
|
||||
return result
|
||||
|
||||
def markBulletinAsRead(
|
||||
self,
|
||||
bulletin_id: str
|
||||
):
|
||||
"""
|
||||
Mark a bulletin as read by its id.
|
||||
|
||||
Args:
|
||||
bulletin_id (str): The bulletin id.
|
||||
"""
|
||||
|
||||
with self.__lock:
|
||||
bulletins = self.bulletins()
|
||||
for b in bulletins:
|
||||
if str(b.get("id", "")) == str(bulletin_id):
|
||||
b["isNew"] = False
|
||||
self.setBulletins(bulletins)
|
||||
break
|
||||
|
||||
|
||||
# BulletinManager singleton instance.
|
||||
_bulletin_manager_instance = None
|
||||
|
||||
# Singleton instance lock.
|
||||
_instance_lock = threading.Lock()
|
||||
|
||||
|
||||
def instance(
|
||||
) -> BulletinManager:
|
||||
"""
|
||||
Get the BulletinManager singleton instance.
|
||||
|
||||
Returns:
|
||||
BulletinManager: The singleton BulletinManager instance.
|
||||
"""
|
||||
|
||||
global _bulletin_manager_instance
|
||||
with _instance_lock:
|
||||
if _bulletin_manager_instance is None:
|
||||
_bulletin_manager_instance = BulletinManager()
|
||||
return _bulletin_manager_instance
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- 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.
|
||||
"""
|
||||
@@ -0,0 +1,197 @@
|
||||
# -*- 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 copy
|
||||
import os
|
||||
import threading
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from utils.JSONReader import JSONReader
|
||||
from utils.JSONWriter import JSONWriter
|
||||
from interfaces.ConfigProvider import ConfigType, ConfigPath
|
||||
|
||||
|
||||
# This config manager class only responsible for global and other
|
||||
# config files. NOT include run and user config files.
|
||||
|
||||
|
||||
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": []
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
"theme": "system",
|
||||
"style": "Fusion",
|
||||
"custom_theme": ""
|
||||
},
|
||||
"bulletin": {
|
||||
"auto_fetch": False,
|
||||
"server_url": "https://api.autolibrary.kenanzhu.com",
|
||||
"sync_interval": 10
|
||||
}
|
||||
}
|
||||
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_data = {}
|
||||
self.__lock = threading.Lock()
|
||||
|
||||
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 Exception:
|
||||
pass
|
||||
self.__config_data[config_type.value] = ConfigTemplate(config_type).template()
|
||||
JSONWriter(config_path, self.__config_data[config_type.value])
|
||||
|
||||
def get(
|
||||
self,
|
||||
key: ConfigPath,
|
||||
default: Optional[Any] = None
|
||||
) -> Any:
|
||||
|
||||
with self.__lock:
|
||||
config_data = self.__config_data[key.config_type.value]
|
||||
if key.key == "":
|
||||
return copy.deepcopy(config_data)
|
||||
keys = key.key.split('.')
|
||||
for k in keys[:-1]:
|
||||
config_data = config_data.get(k, None)
|
||||
if config_data is None:
|
||||
return default
|
||||
return copy.deepcopy(config_data.get(keys[-1], default))
|
||||
|
||||
def set(
|
||||
self,
|
||||
key: ConfigPath,
|
||||
value: Any = None
|
||||
):
|
||||
|
||||
with self.__lock:
|
||||
root_data = self.__config_data[key.config_type.value]
|
||||
if key.key == "":
|
||||
self.__config_data[key.config_type.value] = value
|
||||
else:
|
||||
keys = key.key.split('.')
|
||||
config_data = root_data
|
||||
for k in keys[:-1]:
|
||||
if k not in config_data:
|
||||
config_data[k] = {}
|
||||
config_data = config_data[k]
|
||||
config_data[keys[-1]] = value
|
||||
self.save(key.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 : ConfigManager | None = None
|
||||
|
||||
# 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:
|
||||
if not config_dir:
|
||||
raise ValueError("ConfigManager 需要配置目录参数")
|
||||
_config_manager_instance = ConfigManager(config_dir)
|
||||
else:
|
||||
if config_dir == "":
|
||||
return _config_manager_instance
|
||||
if _config_manager_instance.configDir() != config_dir:
|
||||
raise ValueError("ConfigManager 的实例已初始化,不能使用不同的配置目录。")
|
||||
return _config_manager_instance
|
||||
@@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2026 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import os
|
||||
|
||||
import managers.config.ConfigManager as ConfigManager
|
||||
|
||||
from interfaces.ConfigProvider import CfgKey
|
||||
|
||||
class ConfigUtils:
|
||||
"""
|
||||
Config utilities class.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def getAutomationConfigPaths(
|
||||
) -> dict[str]:
|
||||
"""
|
||||
Get validated automation config paths from ConfigManager instance.
|
||||
These function will validate the config paths and return the validated paths in a dict.
|
||||
|
||||
Returns:
|
||||
dict[str]: Validated automation config paths (include user and run config paths).
|
||||
"""
|
||||
cfg_mgr = ConfigManager.instance() # config manager instance
|
||||
|
||||
config_paths = {"run": "", "user": ""}
|
||||
auto_config = cfg_mgr.get(CfgKey.GLOBAL.AUTOMATION.ROOT, {})
|
||||
for cfg_type in ["run", "user"]:
|
||||
paths = auto_config.get(f"{cfg_type}_path", {}).get("paths", [])
|
||||
index = auto_config.get(f"{cfg_type}_path", {}).get("current", 0)
|
||||
if paths == []:
|
||||
paths.append(os.path.join(cfg_mgr.configDir(), f"{cfg_type}.json"))
|
||||
if index < 0:
|
||||
index = 0
|
||||
if index >= len(paths):
|
||||
index = len(paths) - 1
|
||||
config_paths[cfg_type] = paths[index]
|
||||
data = {"current": index, "paths": paths}
|
||||
auto_config[f"{cfg_type}_path"] = data
|
||||
cfg_mgr.set(CfgKey.GLOBAL.AUTOMATION.ROOT, auto_config)
|
||||
return config_paths
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- 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.
|
||||
"""
|
||||
@@ -0,0 +1,183 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2026 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import platform
|
||||
import browsers
|
||||
|
||||
from pathlib import Path
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class WebBrowserType(Enum):
|
||||
"""
|
||||
Web browser type
|
||||
"""
|
||||
|
||||
CHROME = "chrome"
|
||||
FIREFOX = "firefox"
|
||||
EDGE = "edge"
|
||||
|
||||
|
||||
class WebBrowserArch(Enum):
|
||||
"""
|
||||
Web browser architecture
|
||||
"""
|
||||
|
||||
WINX86_32 = 0
|
||||
WINX86_64 = 1
|
||||
WINARM = 2
|
||||
|
||||
LINUXX86_32 = 3
|
||||
LINUXX86_64 = 4
|
||||
LINUXARM = 5
|
||||
|
||||
MACX86_64 = 6
|
||||
MACARM = 7
|
||||
|
||||
|
||||
@dataclass
|
||||
class WebBrowserInfo:
|
||||
"""
|
||||
Web browser information
|
||||
|
||||
Attributes:
|
||||
browser_arch (WebBrowserArch): Web browser architecture
|
||||
browser_type (WebBrowserType): Web browser type
|
||||
browser_version (str): Web browser version
|
||||
browser_path (Path): Web browser executable file path
|
||||
"""
|
||||
|
||||
browser_arch: WebBrowserArch
|
||||
browser_type: WebBrowserType
|
||||
browser_version: str
|
||||
browser_path: Path
|
||||
|
||||
|
||||
class WebBrowserArchDetector:
|
||||
"""
|
||||
Web browser architecture detector
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self
|
||||
):
|
||||
|
||||
pass
|
||||
|
||||
def detect(
|
||||
self
|
||||
) -> WebBrowserArch:
|
||||
"""
|
||||
Detect system architecture
|
||||
|
||||
Returns:
|
||||
WebBrowserArch: System architecture
|
||||
"""
|
||||
|
||||
system = platform.system()
|
||||
machine = platform.machine().lower()
|
||||
if system == "Windows":
|
||||
if machine in ["amd64", "x86_64"]:
|
||||
return WebBrowserArch.WINX86_64
|
||||
elif machine in ["i386", "i686", "x86"]:
|
||||
return WebBrowserArch.WINX86_32
|
||||
elif machine in ["arm64", "aarch64"]:
|
||||
return WebBrowserArch.WINARM
|
||||
else:
|
||||
return WebBrowserArch.WINX86_64
|
||||
elif system == "Darwin":
|
||||
if machine in ["arm64", "aarch64"]:
|
||||
return WebBrowserArch.MACARM
|
||||
else:
|
||||
return WebBrowserArch.MACX86_64
|
||||
elif system == "Linux":
|
||||
if machine in ["amd64", "x86_64"]:
|
||||
return WebBrowserArch.LINUXX86_64
|
||||
elif machine in ["i386", "i686", "x86"]:
|
||||
return WebBrowserArch.LINUXX86_32
|
||||
elif machine in ["arm64", "aarch64"]:
|
||||
return WebBrowserArch.LINUXARM
|
||||
elif machine.startswith("arm"):
|
||||
return WebBrowserArch.LINUXARM
|
||||
else:
|
||||
return WebBrowserArch.LINUXX86_64
|
||||
raise ValueError(f"不支持的系统架构 : {system} {machine}")
|
||||
|
||||
|
||||
class WebBrowserDetector:
|
||||
"""
|
||||
Web browser detector
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self
|
||||
):
|
||||
|
||||
self.browser_arch = WebBrowserArchDetector().detect()
|
||||
self.browser_infos : list[WebBrowserInfo] = []
|
||||
|
||||
def detect(
|
||||
self
|
||||
) -> list[WebBrowserInfo]:
|
||||
|
||||
"""
|
||||
Detect installed web browsers on the system.
|
||||
|
||||
Returns:
|
||||
list[WebBrowserInfo]: List of detected browser information objects.
|
||||
"""
|
||||
|
||||
self.browser_infos = []
|
||||
try:
|
||||
all_browsers = list(browsers.browsers())
|
||||
except Exception as e:
|
||||
self.browser_infos = []
|
||||
return self.browser_infos
|
||||
|
||||
# Mapping from internal library name to our enum
|
||||
type_map = {
|
||||
'chrome': WebBrowserType.CHROME,
|
||||
'firefox': WebBrowserType.FIREFOX,
|
||||
'msedge': WebBrowserType.EDGE,
|
||||
}
|
||||
for browser in all_browsers:
|
||||
internal_name = browser.get("browser_type", "").lower()
|
||||
if internal_name not in type_map:
|
||||
continue # Not one of the browsers we care about
|
||||
version = browser.get("version", "")
|
||||
if not version:
|
||||
# Skip browsers with no version info (unlikely, but defensive)
|
||||
continue
|
||||
exe_path = browser.get("path", "")
|
||||
if not exe_path:
|
||||
continue
|
||||
try:
|
||||
path = Path(exe_path)
|
||||
if not path.is_file():
|
||||
continue
|
||||
except Exception:
|
||||
continue # Invalid path
|
||||
info = WebBrowserInfo(
|
||||
browser_arch=self.browser_arch, # Use system architecture as fallback
|
||||
browser_type=type_map[internal_name],
|
||||
browser_version=version,
|
||||
browser_path=path,
|
||||
)
|
||||
self.browser_infos.append(info)
|
||||
# Deduplicate: keep only one entry per (type, version)
|
||||
seen = set()
|
||||
unique = []
|
||||
for info in self.browser_infos:
|
||||
key = (info.browser_type, info.browser_version)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
unique.append(info)
|
||||
self.browser_infos = unique
|
||||
return self.browser_infos
|
||||
@@ -0,0 +1,457 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2026 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import shutil
|
||||
import threading
|
||||
import requests
|
||||
import zipfile
|
||||
import tarfile
|
||||
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Optional, Callable
|
||||
|
||||
|
||||
class WebDriverType(Enum):
|
||||
"""
|
||||
Web driver type
|
||||
"""
|
||||
|
||||
CHROME = "chrome"
|
||||
FIREFOX = "firefox"
|
||||
EDGE = "edge"
|
||||
|
||||
|
||||
class WebDriverArch(Enum):
|
||||
"""
|
||||
Web driver architecture
|
||||
"""
|
||||
|
||||
class Chrome(Enum):
|
||||
"""
|
||||
Chrome web driver architecture
|
||||
"""
|
||||
|
||||
WINX86_32 = "win32"
|
||||
WINX86_64 = "win64"
|
||||
|
||||
# LINUX86_32 : no support for linux 32bit
|
||||
LINUX86_64 = "linux64"
|
||||
# LINUXARM : no support for linux arm64
|
||||
|
||||
MACX86_64 = "mac-x64"
|
||||
MACARM = "mac-arm64"
|
||||
|
||||
class Firefox(Enum):
|
||||
"""
|
||||
Firefox web driver architecture
|
||||
"""
|
||||
|
||||
WINX86_32 = "win32"
|
||||
WINX86_64 = "win64"
|
||||
WINARM = "win-aarch64"
|
||||
|
||||
LINUXX86_32 = "linux32"
|
||||
LINUXX86_64 = "linux64"
|
||||
LINUXARM = "linux-aarch64"
|
||||
|
||||
MACX86_64 = "macos"
|
||||
MACARM = "macos-aarch64"
|
||||
|
||||
class Edge(Enum):
|
||||
"""
|
||||
Edge web driver architecture
|
||||
"""
|
||||
|
||||
WINX86_32 = "win32"
|
||||
WINX86_64 = "win64"
|
||||
WINARM = "arm64"
|
||||
|
||||
# LINUX86_32 : no support for linux 32bit
|
||||
LINUXX86_64 = "linux64"
|
||||
# LINUXARM : no support for linux arm64
|
||||
|
||||
MACX86_64 = "mac64"
|
||||
MACARM = "mac64_m1"
|
||||
|
||||
|
||||
class WebDriverName:
|
||||
"""
|
||||
Web driver name
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
driver_type: WebDriverType
|
||||
):
|
||||
|
||||
self.driver_type = driver_type
|
||||
|
||||
def __str__(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
match self.driver_type:
|
||||
case WebDriverType.CHROME:
|
||||
return "chromedriver"
|
||||
case WebDriverType.FIREFOX:
|
||||
return "geckodriver"
|
||||
case WebDriverType.EDGE:
|
||||
return "msedgedriver"
|
||||
case _:
|
||||
raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}")
|
||||
|
||||
|
||||
class WebDriverExecName:
|
||||
"""
|
||||
Web driver executable file name
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
driver_type: WebDriverType,
|
||||
arch: WebDriverArch
|
||||
):
|
||||
|
||||
self.driver_type = driver_type
|
||||
self.arch = arch
|
||||
|
||||
def __str__(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
is_win = True if self.arch is WebDriverArch.Chrome.WINX86_32 or\
|
||||
self.arch is WebDriverArch.Chrome.WINX86_64 or\
|
||||
self.arch is WebDriverArch.Firefox.WINX86_32 or\
|
||||
self.arch is WebDriverArch.Firefox.WINX86_64 or\
|
||||
self.arch is WebDriverArch.Edge.WINX86_32 or\
|
||||
self.arch is WebDriverArch.Edge.WINX86_64 else False
|
||||
match self.driver_type:
|
||||
case WebDriverType.CHROME:
|
||||
return f"{WebDriverName(self.driver_type)}" + (".exe" if is_win else "")
|
||||
case WebDriverType.FIREFOX:
|
||||
return f"{WebDriverName(self.driver_type)}" + (".exe" if is_win else "")
|
||||
case WebDriverType.EDGE:
|
||||
return f"{WebDriverName(self.driver_type)}" + (".exe" if is_win else "")
|
||||
case _:
|
||||
raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}")
|
||||
|
||||
|
||||
class WebDriverFileName:
|
||||
"""\
|
||||
Web driver compressed file name
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
version: str,
|
||||
driver_type: WebDriverType,
|
||||
arch: WebDriverArch
|
||||
):
|
||||
|
||||
self.version = version
|
||||
self.driver_type = driver_type
|
||||
self.arch = arch
|
||||
|
||||
def __str__(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
match self.driver_type:
|
||||
case WebDriverType.CHROME:
|
||||
return f"{WebDriverName(self.driver_type)}-{self.arch.value}.zip"
|
||||
case WebDriverType.FIREFOX:
|
||||
if self.arch is WebDriverArch.Firefox.WINX86_32 or\
|
||||
self.arch is WebDriverArch.Firefox.WINX86_64:
|
||||
suffix = "zip"
|
||||
else:
|
||||
suffix = "tar.gz"
|
||||
return f"{WebDriverName(self.driver_type)}-v{self.version}-{self.arch.value}.{suffix}"
|
||||
case WebDriverType.EDGE:
|
||||
return f"edgedriver_{self.arch.value}.zip" # Edge web driver file name is different
|
||||
case _:
|
||||
raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}")
|
||||
|
||||
|
||||
class WebDriverDownloadURL:
|
||||
"""
|
||||
Web driver download URL
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
version: str,
|
||||
driver_type: WebDriverType,
|
||||
arch: WebDriverArch
|
||||
):
|
||||
|
||||
self.version = version
|
||||
self.driver_type = driver_type
|
||||
self.arch = arch
|
||||
self.file_name = str(WebDriverFileName(self.version, self.driver_type, self.arch))
|
||||
|
||||
def __str__(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
match self.driver_type:
|
||||
case WebDriverType.CHROME:
|
||||
return f"https://storage.googleapis.com/chrome-for-testing-public/"\
|
||||
f"{self.version}/"\
|
||||
f"{self.arch.value}/"\
|
||||
f"{self.file_name}"
|
||||
case WebDriverType.FIREFOX:
|
||||
return f"https://github.com/mozilla/geckodriver/releases/download/"\
|
||||
f"v{self.version}/"\
|
||||
f"{self.file_name}"
|
||||
case WebDriverType.EDGE:
|
||||
return f"https://msedgedriver.microsoft.com/"\
|
||||
f"{self.version}/"\
|
||||
f"{self.file_name}"
|
||||
case _:
|
||||
raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}")
|
||||
|
||||
|
||||
class WebDriverDownloader:
|
||||
"""
|
||||
Base class for WebDriver downloaders
|
||||
|
||||
Args:
|
||||
driver_type (WebDriverType): Web driver type
|
||||
version (str): WebDriver version
|
||||
arch (WebDriverArch): WebDriver architecture
|
||||
download_dir (str): Download directory
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
driver_type: WebDriverType,
|
||||
driver_version: str,
|
||||
driver_arch: WebDriverArch,
|
||||
download_dir: str
|
||||
):
|
||||
|
||||
self.driver_type = driver_type
|
||||
self.arch = driver_arch
|
||||
self.version = driver_version
|
||||
self.download_url = str(WebDriverDownloadURL(self.version, self.driver_type, self.arch))
|
||||
self.download_dir = Path(download_dir)/self.driver_type.value/self.version/self.arch.value
|
||||
self.download_dir.mkdir(mode=0o0755, parents=True, exist_ok=True)
|
||||
self.download_path = self.download_dir/str(WebDriverFileName(self.version, self.driver_type, self.arch))
|
||||
|
||||
def _download(
|
||||
self,
|
||||
progress_callback: Optional[Callable[[float, int, float, str], None]] = None,
|
||||
max_retries: int = 3,
|
||||
cancel_event: Optional[threading.Event] = None
|
||||
) -> bool:
|
||||
|
||||
CHUNK_SIZE = 8192*8 # 64KB chunk
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
}
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
if cancel_event and cancel_event.is_set():
|
||||
return False
|
||||
# resume download if file exists
|
||||
if self.download_path.exists():
|
||||
downloaded_size = self.download_path.stat().st_size
|
||||
headers_ = headers.copy()
|
||||
headers_['Range'] = f"bytes={downloaded_size}-"
|
||||
mode = 'ab'
|
||||
else:
|
||||
downloaded_size = 0
|
||||
headers_ = headers
|
||||
mode = 'wb'
|
||||
# get response
|
||||
response = requests.get(str(self.download_url), headers=headers_, stream=True, timeout=10)
|
||||
if response.status_code not in [200, 206]:
|
||||
if self.download_path.exists():
|
||||
self.download_path.unlink()
|
||||
downloaded_size = 0
|
||||
mode = 'wb'
|
||||
response = requests.get(str(self.download_url), headers=headers, stream=True)
|
||||
response.raise_for_status()
|
||||
# get total size
|
||||
total_size = int(response.headers.get('Content-Length', 0))
|
||||
if response.status_code == 206: # Partial Content - server supports Range
|
||||
total_size += downloaded_size
|
||||
last_callback_time = time.time()
|
||||
last_callback_size = downloaded_size
|
||||
callback_interval = 0.1
|
||||
with open(self.download_path, mode) as f:
|
||||
for chunk in response.iter_content(CHUNK_SIZE):
|
||||
current_time = time.time()
|
||||
if cancel_event and cancel_event.is_set():
|
||||
response.close()
|
||||
return False
|
||||
if not chunk:
|
||||
continue
|
||||
f.write(chunk)
|
||||
downloaded_size += len(chunk)
|
||||
if not progress_callback or total_size <= 0:
|
||||
continue
|
||||
current_progress = (downloaded_size/total_size)*98.0
|
||||
if current_time - last_callback_time >= callback_interval or current_progress >= 98.0:
|
||||
elapsed = current_time - last_callback_time
|
||||
if elapsed > 0:
|
||||
speed = (downloaded_size - last_callback_size)/(elapsed*1024.0)
|
||||
else:
|
||||
speed = 0.0
|
||||
progress_callback(current_progress, 100, speed, "下载中...")
|
||||
last_callback_time = current_time
|
||||
last_callback_size = downloaded_size
|
||||
if total_size > 0 and self.download_path.stat().st_size < total_size:
|
||||
raise Exception(f"下载不完整 : {self.download_path.stat().st_size}/{total_size} 字节")
|
||||
return True
|
||||
except Exception as e:
|
||||
if cancel_event and cancel_event.is_set():
|
||||
return False
|
||||
if attempt < max_retries - 1:
|
||||
progress_callback(0, 100, 0.0, f"第 {attempt+1} 次重试...")
|
||||
time.sleep(1)
|
||||
continue
|
||||
raise e
|
||||
|
||||
def _verify(
|
||||
self,
|
||||
progress_callback: Optional[Callable[[float, int, float, str], None]] = None
|
||||
) -> bool:
|
||||
|
||||
progress_callback(98, 100, 0.0, "验证完成")
|
||||
return True
|
||||
|
||||
def _extract(
|
||||
self,
|
||||
progress_callback: Optional[Callable[[float, int, float, str], None]] = None
|
||||
) -> Optional[Path]:
|
||||
|
||||
try:
|
||||
progress_callback(98, 100, 0.0, "解压中...")
|
||||
file_path_str = str(self.download_path)
|
||||
if file_path_str.endswith('.tar.gz'):
|
||||
with tarfile.open(self.download_path, 'r:gz') as tar_ref:
|
||||
tar_ref.extractall(self.download_dir)
|
||||
else:
|
||||
with zipfile.ZipFile(self.download_path, 'r') as zip_ref:
|
||||
zip_ref.extractall(self.download_dir)
|
||||
driver_file = None
|
||||
for root, _, files in os.walk(self.download_dir):
|
||||
for file in files:
|
||||
expected_name = str(WebDriverExecName(self.driver_type, self.arch))
|
||||
if file == str(expected_name):
|
||||
src_path = Path(root, file)
|
||||
dst_path = self.download_dir/file
|
||||
src_path.rename(dst_path)
|
||||
driver_file = dst_path
|
||||
break
|
||||
if driver_file:
|
||||
break
|
||||
if not driver_file:
|
||||
raise FileNotFoundError(f"未找到 web driver 文件 : {expected_name}")
|
||||
# Ensure executable permissions on Unix systems (zipfile
|
||||
# extraction does not preserve the execute bit).
|
||||
if os.name != 'nt':
|
||||
os.chmod(driver_file, 0o755)
|
||||
progress_callback(100, 100, 0.0, "解压完成")
|
||||
self.download_path.unlink()
|
||||
self._cleanup(driver_file)
|
||||
return driver_file
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _cleanup(
|
||||
self,
|
||||
driver_file: Path
|
||||
) -> None:
|
||||
|
||||
for item in self.download_dir.iterdir():
|
||||
if item != driver_file:
|
||||
if item.is_dir():
|
||||
shutil.rmtree(item)
|
||||
else:
|
||||
item.unlink()
|
||||
|
||||
def download(
|
||||
self,
|
||||
progress_callback: Optional[Callable[[float, int, float, str], None]] = None,
|
||||
cancel_event: Optional[threading.Event] = None
|
||||
) -> Optional[Path]:
|
||||
|
||||
try:
|
||||
# downlaod file : 0% - 98%
|
||||
if not self._download(progress_callback, cancel_event=cancel_event):
|
||||
return None
|
||||
# verify file : 98% - 99%
|
||||
if not self._verify(progress_callback):
|
||||
progress_callback(0, 100, 0.0, "验证失败")
|
||||
return None
|
||||
# extract file : 99% - 100%
|
||||
driver_path = self._extract(progress_callback)
|
||||
if not driver_path:
|
||||
progress_callback(0, 100, 0.0, "解压失败")
|
||||
return None
|
||||
return driver_path
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
|
||||
class ChromeDriverDownloader(WebDriverDownloader):
|
||||
"""
|
||||
Chrome web driver downloader
|
||||
|
||||
Only support version higher than 114
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
version: str,
|
||||
arch: WebDriverArch,
|
||||
download_dir: str
|
||||
):
|
||||
|
||||
super().__init__(WebDriverType.CHROME, version, arch, download_dir)
|
||||
|
||||
|
||||
class FirefoxDriverDownloader(WebDriverDownloader):
|
||||
"""
|
||||
Firefox web driver downloader
|
||||
|
||||
This class do not resolve version mapping,
|
||||
only support driver version higher than 0.17.0
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
version: str,
|
||||
arch: WebDriverArch,
|
||||
download_dir: str
|
||||
):
|
||||
|
||||
super().__init__(WebDriverType.FIREFOX, version, arch, download_dir)
|
||||
|
||||
|
||||
class EdgeDriverDownloader(WebDriverDownloader):
|
||||
"""
|
||||
Edge web driver downloader
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
version: str,
|
||||
arch: WebDriverArch,
|
||||
download_dir: str
|
||||
):
|
||||
|
||||
super().__init__(WebDriverType.EDGE, version, arch, download_dir)
|
||||