mirror of
https://github.com/KenanZhu/AutoLibrary.git
synced 2026-06-18 15:33:03 +08:00
Compare commits
23 Commits
main
..
67f297b434
| Author | SHA1 | Date | |
|---|---|---|---|
| 67f297b434 | |||
| 86f0761eed | |||
| 79e5b43498 | |||
| b56d2c203e | |||
| 44dbde3355 | |||
| 62f8ec3d91 | |||
| 2d77cbec79 | |||
| 10d731518a | |||
| 9fdb6f7652 | |||
| ef903ee817 | |||
| d6e8eef8c8 | |||
| e893752c25 | |||
| 1cfd7382be | |||
| 1d9e41ab86 | |||
| 645f07b4d2 | |||
| 732f104c5c | |||
| a2bc1881bc | |||
| c1004ed2bc | |||
| 38489191f5 | |||
| 35253dadbb | |||
| c0b6e0899c | |||
| 9c1772b186 | |||
| 05b93799d4 |
@@ -4,9 +4,6 @@ name: Build Test
|
|||||||
# It is triggered when a pull request is opened, synchronized, or reopened against the main branch.
|
# It is triggered when a pull request is opened, synchronized, or reopened against the main branch.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -212,283 +209,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload build summary
|
- name: Upload build summary
|
||||||
run: |
|
run: |
|
||||||
"## Build Test Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
|
Write-Host "## Build Test Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
|
||||||
"" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
Write-Host "" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||||
"========================================" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
Write-Host "========================================" | 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
|
Write-Host "✓ 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
|
Write-Host "- 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
|
Write-Host "- 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
|
Write-Host "- 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
|
Write-Host "- 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
|
Write-Host "- Event: ${{ github.event_name }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||||
shell: pwsh
|
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
|
|
||||||
|
|||||||
+10
-311
@@ -1,10 +1,7 @@
|
|||||||
name: Build
|
name: Build
|
||||||
|
|
||||||
# This workflow compiles the application for Windows and macOS platforms using
|
# This workflow compiles the application for Windows platform using PyInstaller, and
|
||||||
# PyInstaller, and archives the built artifacts.
|
# archives the built artifacts as 'AutoLibrary.<tag_name>-windows-x86_64.zip'.
|
||||||
#
|
|
||||||
# - Windows: AutoLibrary.<tag_name>-windows-x86_64.zip
|
|
||||||
# - macOS: AutoLibrary.<tag_name>-macos-arm64.dmg
|
|
||||||
#
|
#
|
||||||
# It is triggered when called by the release workflow.
|
# It is triggered when called by the release workflow.
|
||||||
|
|
||||||
@@ -254,310 +251,12 @@ jobs:
|
|||||||
- name: Upload build summary
|
- name: Upload build summary
|
||||||
if: ${{ inputs.is_test == 'true' }}
|
if: ${{ inputs.is_test == 'true' }}
|
||||||
run: |
|
run: |
|
||||||
"## Build Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
|
Write-Host "## Build Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
|
||||||
"" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
Write-Host "" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||||
"========================================" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
Write-Host "========================================" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||||
"✓ Build test completed successfully!" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
Write-Host "✓ Build test completed successfully!" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||||
"- Version: ${{ steps.get_version.outputs.VERSION }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
Write-Host "- Version: ${{ steps.get_version.outputs.VERSION }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||||
"- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
Write-Host "- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||||
"- Event: ${{ github.event_name }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
Write-Host "- Event: ${{ github.event_name }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||||
"- Ref: ${{ github.ref }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
Write-Host "- Ref: ${{ github.ref }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||||
shell: pwsh
|
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
|
|
||||||
|
|||||||
@@ -21,10 +21,8 @@ name: Release
|
|||||||
# Commits version changes to release branch and creates the release tag.
|
# Commits version changes to release branch and creates the release tag.
|
||||||
|
|
||||||
# 4. Build:
|
# 4. Build:
|
||||||
# Compiles the application for Windows and macOS platforms using PyInstaller, and
|
# Compiles the application for Windows platform using PyInstaller, and
|
||||||
# archives the built artifacts.
|
# archives the built artifacts as 'AutoLibrary.<tag_name>-windows-x86_64.zip'.
|
||||||
# - Windows: AutoLibrary.<tag_name>-windows-x86_64.zip
|
|
||||||
# - macOS: AutoLibrary.<tag_name>-macos-arm64.dmg
|
|
||||||
|
|
||||||
# 5. Release:
|
# 5. Release:
|
||||||
# Creates GitHub release with generated artifacts and release notes
|
# Creates GitHub release with generated artifacts and release notes
|
||||||
@@ -183,18 +181,12 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download Windows artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: AutoLibrary.${{ needs.extract-version.outputs.tag_name }}-windows-x86_64
|
name: AutoLibrary.${{ needs.extract-version.outputs.tag_name }}-windows-x86_64
|
||||||
path: artifacts/
|
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
|
- name: Create release
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
@@ -203,7 +195,6 @@ jobs:
|
|||||||
name: AutoLibrary ${{ needs.extract-version.outputs.tag_name }}
|
name: AutoLibrary ${{ needs.extract-version.outputs.tag_name }}
|
||||||
files: |
|
files: |
|
||||||
artifacts/AutoLibrary.${{ needs.extract-version.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
|
draft: false
|
||||||
prerelease: ${{ needs.extract-version.outputs.is_rc == 'true' }}
|
prerelease: ${{ needs.extract-version.outputs.is_rc == 'true' }}
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
|
|||||||
Binary file not shown.
@@ -24,7 +24,6 @@ def main():
|
|||||||
translator = QTranslator()
|
translator = QTranslator()
|
||||||
if translator.load(":/res/translators/qtbase_zh_CN.ts"):
|
if translator.load(":/res/translators/qtbase_zh_CN.ts"):
|
||||||
app.installTranslator(translator)
|
app.installTranslator(translator)
|
||||||
app.setStyle("Fusion")
|
|
||||||
app.setApplicationName("AutoLibrary")
|
app.setApplicationName("AutoLibrary")
|
||||||
if not initializeApp():
|
if not initializeApp():
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|||||||
@@ -10,10 +10,16 @@ See the LICENSE file for details.
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from PySide6.QtCore import QStandardPaths, QDir
|
from PySide6.QtCore import QStandardPaths, QDir
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
from managers.log.LogManager import instance as logInstance
|
from interfaces.ConfigProvider import CfgKey
|
||||||
from managers.config.ConfigManager import instance as configInstance
|
from managers.config.ConfigManager import instance as configInstance
|
||||||
from managers.driver.WebDriverManager import instance as webdriverInstance
|
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(
|
def _initializeLogManager(
|
||||||
@@ -64,13 +70,35 @@ def _initializeWebDriverManager(
|
|||||||
webdriverInstance(driver_dir)
|
webdriverInstance(driver_dir)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _initializeAppearance(
|
||||||
|
):
|
||||||
|
|
||||||
|
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)
|
||||||
|
logger = logInstance().getLogger("AppInitializer")
|
||||||
|
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(
|
def initializeApp(
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Initialize the application components
|
Initialize the application components
|
||||||
|
|
||||||
Order:
|
Order:
|
||||||
LogManager -> ConfigManager -> WebDriverManager
|
LogManager -> ConfigManager -> WebDriverManager -> Appearance
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not _initializeLogManager():
|
if not _initializeLogManager():
|
||||||
@@ -79,4 +107,5 @@ def initializeApp(
|
|||||||
return False
|
return False
|
||||||
if not _initializeWebDriverManager():
|
if not _initializeWebDriverManager():
|
||||||
return False
|
return False
|
||||||
|
_initializeAppearance()
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ See the LICENSE file for details.
|
|||||||
"""
|
"""
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
|
import qtawesome as qta
|
||||||
|
|
||||||
from PySide6.QtCore import (
|
from PySide6.QtCore import (
|
||||||
QDate,
|
QDate,
|
||||||
QSize,
|
QSize,
|
||||||
@@ -20,7 +22,6 @@ from PySide6.QtCore import (
|
|||||||
from PySide6.QtGui import (
|
from PySide6.QtGui import (
|
||||||
QColor,
|
QColor,
|
||||||
QFont,
|
QFont,
|
||||||
QIcon,
|
|
||||||
QSyntaxHighlighter,
|
QSyntaxHighlighter,
|
||||||
QTextCharFormat,
|
QTextCharFormat,
|
||||||
)
|
)
|
||||||
@@ -215,18 +216,18 @@ class ALAutoScriptEditDialog(QDialog):
|
|||||||
self.ZoomOutBtn = QPushButton("-")
|
self.ZoomOutBtn = QPushButton("-")
|
||||||
self.ZoomOutBtn.setFixedSize(25, 25)
|
self.ZoomOutBtn.setFixedSize(25, 25)
|
||||||
self.ZoomResetBtn = QPushButton("")
|
self.ZoomResetBtn = QPushButton("")
|
||||||
self.ZoomResetBtn.setIcon(QIcon(":/res/icons/Reset.svg"))
|
self.ZoomResetBtn.setIcon(qta.icon("fa5s.undo", color=self._iconColor()))
|
||||||
self.ZoomResetBtn.setIconSize(QSize(20, 20))
|
self.ZoomResetBtn.setIconSize(QSize(14, 14))
|
||||||
self.ZoomResetBtn.setFixedSize(25, 25)
|
self.ZoomResetBtn.setFixedSize(25, 25)
|
||||||
self.ZoomResetBtn.setToolTip("重置缩放")
|
self.ZoomResetBtn.setToolTip("重置缩放")
|
||||||
self.ZoomLabel = QLabel(f"{self._fontSize}px")
|
self.ZoomLabel = QLabel(f"{self._fontSize}px")
|
||||||
self.ZoomLabel.setFixedHeight(25)
|
self.ZoomLabel.setFixedHeight(25)
|
||||||
self.OrchBtn = QPushButton("编排")
|
self.OrchBtn = QPushButton("编排")
|
||||||
self.OrchBtn.setFixedHeight(25)
|
self.OrchBtn.setFixedSize(80, 25)
|
||||||
self.OrchBtn.setToolTip("可视化生成 AutoScript 代码并插入到光标位置")
|
self.OrchBtn.setToolTip("可视化生成 AutoScript 代码并插入到光标位置")
|
||||||
ToolbarLayout.addWidget(self.OrchBtn)
|
ToolbarLayout.addWidget(self.OrchBtn)
|
||||||
self.DebugBtn = QPushButton("▶ 调试运行")
|
self.DebugBtn = QPushButton("▶ 调试运行")
|
||||||
self.DebugBtn.setFixedHeight(25)
|
self.DebugBtn.setFixedSize(80, 25)
|
||||||
self.DebugBtn.setToolTip("使用右侧模拟数据执行脚本,查看目标变量变化")
|
self.DebugBtn.setToolTip("使用右侧模拟数据执行脚本,查看目标变量变化")
|
||||||
ToolbarLayout.addWidget(self.DebugBtn)
|
ToolbarLayout.addWidget(self.DebugBtn)
|
||||||
Sep = QFrame()
|
Sep = QFrame()
|
||||||
@@ -240,8 +241,8 @@ class ALAutoScriptEditDialog(QDialog):
|
|||||||
ToolbarLayout.addWidget(self.ZoomLabel)
|
ToolbarLayout.addWidget(self.ZoomLabel)
|
||||||
ToolbarLayout.addStretch()
|
ToolbarLayout.addStretch()
|
||||||
self.CopyBtn = QPushButton("")
|
self.CopyBtn = QPushButton("")
|
||||||
self.CopyBtn.setIcon(QIcon(":/res/icons/Copy.svg"))
|
self.CopyBtn.setIcon(qta.icon("fa5s.copy", color=self._iconColor()))
|
||||||
self.CopyBtn.setIconSize(QSize(20, 20))
|
self.CopyBtn.setIconSize(QSize(14, 14))
|
||||||
self.CopyBtn.setFixedSize(25, 25)
|
self.CopyBtn.setFixedSize(25, 25)
|
||||||
self.CopyBtn.setToolTip("复制脚本")
|
self.CopyBtn.setToolTip("复制脚本")
|
||||||
ToolbarLayout.addWidget(self.CopyBtn)
|
ToolbarLayout.addWidget(self.CopyBtn)
|
||||||
@@ -264,7 +265,9 @@ class ALAutoScriptEditDialog(QDialog):
|
|||||||
QDialogButtonBox.StandardButton.Cancel
|
QDialogButtonBox.StandardButton.Cancel
|
||||||
)
|
)
|
||||||
self.BtnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
|
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).setText("取消")
|
||||||
|
self.BtnBox.button(QDialogButtonBox.StandardButton.Cancel).setFixedSize(80, 25)
|
||||||
Layout.addWidget(self.BtnBox)
|
Layout.addWidget(self.BtnBox)
|
||||||
|
|
||||||
def createButtonPanel(
|
def createButtonPanel(
|
||||||
@@ -537,6 +540,14 @@ class ALAutoScriptEditDialog(QDialog):
|
|||||||
else:
|
else:
|
||||||
widget.setText(str(value))
|
widget.setText(str(value))
|
||||||
|
|
||||||
|
def _iconColor(
|
||||||
|
self
|
||||||
|
) -> str:
|
||||||
|
|
||||||
|
return QApplication.instance().palette().color(
|
||||||
|
QApplication.instance().palette().ColorRole.WindowText
|
||||||
|
).name()
|
||||||
|
|
||||||
def connectSignals(
|
def connectSignals(
|
||||||
self
|
self
|
||||||
):
|
):
|
||||||
|
|||||||
+31
-1
@@ -33,6 +33,7 @@ from PySide6.QtWidgets import (
|
|||||||
from base.MsgBase import MsgBase
|
from base.MsgBase import MsgBase
|
||||||
from gui.ALAboutDialog import ALAboutDialog
|
from gui.ALAboutDialog import ALAboutDialog
|
||||||
from gui.ALConfigWidget import ALConfigWidget
|
from gui.ALConfigWidget import ALConfigWidget
|
||||||
|
from gui.ALSettingsWidget import ALSettingsWidget
|
||||||
from gui.ALMainWorkers import (
|
from gui.ALMainWorkers import (
|
||||||
AutoLibWorker,
|
AutoLibWorker,
|
||||||
TimerTaskWorker
|
TimerTaskWorker
|
||||||
@@ -60,6 +61,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
|||||||
self.__config_paths = ConfigUtils.getAutomationConfigPaths()
|
self.__config_paths = ConfigUtils.getAutomationConfigPaths()
|
||||||
self.__alTimerTaskManageWidget = None
|
self.__alTimerTaskManageWidget = None
|
||||||
self.__alConfigWidget = None
|
self.__alConfigWidget = None
|
||||||
|
self.__alSettingsWidget = None
|
||||||
self.__auto_lib_thread = None
|
self.__auto_lib_thread = None
|
||||||
self.__current_timer_task_thread = None
|
self.__current_timer_task_thread = None
|
||||||
self.__is_running_timer_task = False
|
self.__is_running_timer_task = False
|
||||||
@@ -81,6 +83,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
|||||||
self.MessageIOTextEdit.setFont(QFont("Courier New", 10))
|
self.MessageIOTextEdit.setFont(QFont("Courier New", 10))
|
||||||
self.ManualAction.triggered.connect(self.onManualActionTriggered)
|
self.ManualAction.triggered.connect(self.onManualActionTriggered)
|
||||||
self.AboutAction.triggered.connect(self.onAboutActionTriggered)
|
self.AboutAction.triggered.connect(self.onAboutActionTriggered)
|
||||||
|
self.SettingsAction.triggered.connect(self.onSettingsActionTriggered)
|
||||||
|
|
||||||
# initialize timer task widget, but not show it
|
# initialize timer task widget, but not show it
|
||||||
try:
|
try:
|
||||||
@@ -125,7 +128,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
|||||||
return
|
return
|
||||||
self.TrayIcon = QSystemTrayIcon(self.Icon, self)
|
self.TrayIcon = QSystemTrayIcon(self.Icon, self)
|
||||||
self.TrayIcon.setToolTip("AutoLibrary")
|
self.TrayIcon.setToolTip("AutoLibrary")
|
||||||
|
|
||||||
self.TrayMenu = QMenu()
|
self.TrayMenu = QMenu()
|
||||||
self.TrayMenu.addAction("显示主窗口", self.showNormal)
|
self.TrayMenu.addAction("显示主窗口", self.showNormal)
|
||||||
self.TrayMenu.addAction("显示定时窗口", self.onTimerTaskManageWidgetButtonClicked)
|
self.TrayMenu.addAction("显示定时窗口", self.onTimerTaskManageWidgetButtonClicked)
|
||||||
@@ -190,6 +192,9 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
|||||||
if self.__alConfigWidget:
|
if self.__alConfigWidget:
|
||||||
self.__alConfigWidget.close()
|
self.__alConfigWidget.close()
|
||||||
# the config widget is already deleted in the 'self.onConfigWidgetClosed'
|
# the config widget is already deleted in the 'self.onConfigWidgetClosed'
|
||||||
|
if self.__alSettingsWidget:
|
||||||
|
self.__alSettingsWidget.close()
|
||||||
|
# the settings widget is already deleted in the 'self.onSettingsWidgetClosed'
|
||||||
self._showLog("主窗口关闭")
|
self._showLog("主窗口关闭")
|
||||||
QMainWindow.closeEvent(self, event)
|
QMainWindow.closeEvent(self, event)
|
||||||
|
|
||||||
@@ -302,6 +307,31 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
|||||||
self.setControlButtons(True, None, None)
|
self.setControlButtons(True, None, None)
|
||||||
self._showLog("配置窗口已关闭,配置文件路径已更新")
|
self._showLog("配置窗口已关闭,配置文件路径已更新")
|
||||||
|
|
||||||
|
@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 onSettingsActionTriggered(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
if self.__alSettingsWidget is None:
|
||||||
|
self.__alSettingsWidget = ALSettingsWidget(self)
|
||||||
|
self.__alSettingsWidget.settingsWidgetIsClosed.connect(self.onSettingsWidgetClosed)
|
||||||
|
self.__alSettingsWidget.show()
|
||||||
|
self.__alSettingsWidget.raise_()
|
||||||
|
self.__alSettingsWidget.activateWindow()
|
||||||
|
self.SettingsAction.setEnabled(False)
|
||||||
|
self._showLog("打开全局设置窗口")
|
||||||
|
|
||||||
@Slot(dict)
|
@Slot(dict)
|
||||||
def onTimerTaskIsReady(
|
def onTimerTaskIsReady(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -0,0 +1,408 @@
|
|||||||
|
# -*- 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 qtawesome as qta
|
||||||
|
|
||||||
|
from PySide6.QtCore import (
|
||||||
|
QProcess,
|
||||||
|
Qt,
|
||||||
|
Signal,
|
||||||
|
Slot
|
||||||
|
)
|
||||||
|
from PySide6.QtGui import (
|
||||||
|
QCloseEvent,
|
||||||
|
QShowEvent
|
||||||
|
)
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QApplication,
|
||||||
|
QFileDialog,
|
||||||
|
QMessageBox,
|
||||||
|
QStyleFactory,
|
||||||
|
QWidget
|
||||||
|
)
|
||||||
|
|
||||||
|
import managers.config.ConfigManager as ConfigManager
|
||||||
|
from managers.log.LogManager import instance as logInstance
|
||||||
|
from managers.theme.ThemeManager import(
|
||||||
|
getActiveStyle,
|
||||||
|
setActiveStyle,
|
||||||
|
instance as themeInstance
|
||||||
|
)
|
||||||
|
|
||||||
|
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(QWidget, Ui_ALSettingsWidget):
|
||||||
|
|
||||||
|
settingsWidgetIsClosed = 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.setupUi(self)
|
||||||
|
self.modifyUi()
|
||||||
|
self.connectSignals()
|
||||||
|
self.loadSettings()
|
||||||
|
|
||||||
|
def modifyUi(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
self.setWindowFlags(Qt.WindowType.Window)
|
||||||
|
self.NavigationList.setCurrentRow(0)
|
||||||
|
self.populateStyles()
|
||||||
|
self.setNavigationIcons()
|
||||||
|
self.ThemeInfoLabel.setTextFormat(Qt.TextFormat.RichText)
|
||||||
|
self.ThemeInfoLabel.setStyleSheet(
|
||||||
|
"border: 1px solid palette(mid);"\
|
||||||
|
"border-radius: 2px;"\
|
||||||
|
"padding: 5px;"
|
||||||
|
)
|
||||||
|
|
||||||
|
def setNavigationIcons(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
app : QApplication | None = QApplication.instance()
|
||||||
|
color = app.palette().color(app.palette().ColorRole.WindowText).name()
|
||||||
|
item = self.NavigationList.item(0)
|
||||||
|
if item:
|
||||||
|
item.setIcon(qta.icon("fa5s.palette", color=color))
|
||||||
|
|
||||||
|
def populateStyles(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
self.StyleComboBox.clear()
|
||||||
|
self.StyleComboBox.addItems(QStyleFactory.keys())
|
||||||
|
|
||||||
|
def connectSignals(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
self.BrowseQssButton.clicked.connect(self.onImportThemeButtonClicked)
|
||||||
|
self.ThemeComboBox.currentIndexChanged.connect(self.onThemeComboBoxChanged)
|
||||||
|
self.ResetThemeButton.clicked.connect(self.onResetThemeButtonClicked)
|
||||||
|
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
|
||||||
|
self.ApplyButton.clicked.connect(self.onApplyButtonClicked)
|
||||||
|
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
|
||||||
|
|
||||||
|
def showEvent(
|
||||||
|
self,
|
||||||
|
event: QShowEvent
|
||||||
|
):
|
||||||
|
|
||||||
|
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.settingsWidgetIsClosed.emit()
|
||||||
|
super().closeEvent(event)
|
||||||
|
|
||||||
|
def loadSettings(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
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)
|
||||||
|
self.populateThemeList()
|
||||||
|
if custom_theme:
|
||||||
|
idx = self.ThemeComboBox.findText(custom_theme)
|
||||||
|
if idx >= 0:
|
||||||
|
self.ThemeComboBox.setCurrentIndex(idx)
|
||||||
|
self.updateThemeStatus()
|
||||||
|
self.updateThemeInfo()
|
||||||
|
|
||||||
|
def updateThemeStatus(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
name = self.ThemeComboBox.currentText()
|
||||||
|
if name and name != "默认":
|
||||||
|
self.QssStatusLabel.setText(f"当前使用 {name} 主题。")
|
||||||
|
else:
|
||||||
|
self.QssStatusLabel.setText("当前使用 默认 主题。")
|
||||||
|
|
||||||
|
def updateThemeInfo(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
name = self.ThemeComboBox.currentText()
|
||||||
|
if not name or name == "默认":
|
||||||
|
self.ThemeInfoLabel.setText("")
|
||||||
|
return
|
||||||
|
t = self.__theme_cache.get(name)
|
||||||
|
if t:
|
||||||
|
author = t.get("author", "未知")
|
||||||
|
need_theme = t.get("need_theme", "both")
|
||||||
|
brief = t.get("brief", "没有相关简介")
|
||||||
|
self.ThemeInfoLabel.setText(
|
||||||
|
f"<b>{name}</b> - 适用于 <i>{_themeToReadable(need_theme)}</i> 主题<br>"
|
||||||
|
f"作者:{author}<br><br>"
|
||||||
|
f"{brief}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.ThemeInfoLabel.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 collectSettings(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
if self.LightThemeRadio.isChecked():
|
||||||
|
theme = "light"
|
||||||
|
elif self.DarkThemeRadio.isChecked():
|
||||||
|
theme = "dark"
|
||||||
|
else:
|
||||||
|
theme = "system"
|
||||||
|
style = self.StyleComboBox.currentText()
|
||||||
|
custom_theme = self.ThemeComboBox.currentText()
|
||||||
|
if custom_theme == "默认":
|
||||||
|
custom_theme = ""
|
||||||
|
return theme, style, custom_theme
|
||||||
|
|
||||||
|
def saveAndApply(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
theme, style, custom_theme = self.collectSettings()
|
||||||
|
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)
|
||||||
|
# Re-read theme after syncRadioFromNeedTheme — the radio may have
|
||||||
|
# changed to match the custom theme's need_theme
|
||||||
|
theme, _, _ = self.collectSettings()
|
||||||
|
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.THEME, theme)
|
||||||
|
self.setNavigationIcons()
|
||||||
|
self.updateThemeStatus()
|
||||||
|
self.updateThemeInfo()
|
||||||
|
self.__original_theme = theme
|
||||||
|
self.__original_custom_theme = custom_theme if custom_theme else ""
|
||||||
|
self.__original_style = getActiveStyle()
|
||||||
|
|
||||||
|
def maybeRestart(
|
||||||
|
self
|
||||||
|
) -> bool:
|
||||||
|
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
self,
|
||||||
|
"提示 - AutoLibrary",
|
||||||
|
"界面风格已修改,需要重启程序才能生效。是否立即重启?",
|
||||||
|
QMessageBox.Yes | QMessageBox.No,
|
||||||
|
QMessageBox.Yes
|
||||||
|
)
|
||||||
|
if reply == QMessageBox.Yes:
|
||||||
|
_restartApp()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def populateThemeList(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
self.ThemeComboBox.blockSignals(True)
|
||||||
|
self.ThemeComboBox.clear()
|
||||||
|
self.ThemeComboBox.addItem("默认")
|
||||||
|
self.__theme_cache = {}
|
||||||
|
themes = themeInstance().listThemes()
|
||||||
|
for t in themes:
|
||||||
|
name = t.get("name", "")
|
||||||
|
if name:
|
||||||
|
self.__theme_cache[name] = t
|
||||||
|
self.ThemeComboBox.addItem(name)
|
||||||
|
self.ThemeComboBox.blockSignals(False)
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def onImportThemeButtonClicked(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
file_path, _ = QFileDialog.getOpenFileName(
|
||||||
|
self,
|
||||||
|
"导入主题 - AutoLibrary",
|
||||||
|
"",
|
||||||
|
"主题文件 (*.altheme *.qss);;所有文件 (*)"
|
||||||
|
)
|
||||||
|
if not file_path:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
name = themeInstance().importTheme(file_path)
|
||||||
|
self.populateThemeList()
|
||||||
|
idx = self.ThemeComboBox.findText(name)
|
||||||
|
if idx >= 0:
|
||||||
|
self.ThemeComboBox.setCurrentIndex(idx)
|
||||||
|
self.updateThemeStatus()
|
||||||
|
self.updateThemeInfo()
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"导入失败 - AutoLibrary",
|
||||||
|
f"无法导入主题文件:{e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def onThemeComboBoxChanged(
|
||||||
|
self,
|
||||||
|
index: int
|
||||||
|
):
|
||||||
|
|
||||||
|
self.updateThemeInfo()
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def onResetThemeButtonClicked(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
self.ThemeComboBox.blockSignals(True)
|
||||||
|
if self.__original_custom_theme:
|
||||||
|
idx = self.ThemeComboBox.findText(self.__original_custom_theme)
|
||||||
|
if idx >= 0:
|
||||||
|
self.ThemeComboBox.setCurrentIndex(idx)
|
||||||
|
else:
|
||||||
|
self.ThemeComboBox.setCurrentIndex(0)
|
||||||
|
else:
|
||||||
|
self.ThemeComboBox.setCurrentIndex(0)
|
||||||
|
self.ThemeComboBox.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)
|
||||||
|
_applyCustomTheme(self.__original_custom_theme, self.__original_theme)
|
||||||
|
self.updateThemeStatus()
|
||||||
|
self.updateThemeInfo()
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def onCancelButtonClicked(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def onApplyButtonClicked(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
_, style, _ = self.collectSettings()
|
||||||
|
style_changed = self.__original_style != style
|
||||||
|
self.saveAndApply()
|
||||||
|
if style_changed:
|
||||||
|
self.maybeRestart()
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def onConfirmButtonClicked(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
|
||||||
|
_, style, _ = self.collectSettings()
|
||||||
|
style_changed = self.__original_style != style
|
||||||
|
self.saveAndApply()
|
||||||
|
if style_changed:
|
||||||
|
self.maybeRestart()
|
||||||
|
self.close()
|
||||||
Binary file not shown.
@@ -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;
|
||||||
|
}
|
||||||
@@ -1956,13 +1956,13 @@
|
|||||||
<widget class="QPushButton" name="ExportConfigButton">
|
<widget class="QPushButton" name="ExportConfigButton">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>100</width>
|
<width>120</width>
|
||||||
<height>25</height>
|
<height>25</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="maximumSize">
|
<property name="maximumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>100</width>
|
<width>120</width>
|
||||||
<height>25</height>
|
<height>25</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
|
|||||||
@@ -281,6 +281,12 @@ font: 700 9pt;</string>
|
|||||||
<property name="nativeMenuBar">
|
<property name="nativeMenuBar">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
|
<widget class="QMenu" name="ToolsMenu">
|
||||||
|
<property name="title">
|
||||||
|
<string>工具</string>
|
||||||
|
</property>
|
||||||
|
<addaction name="SettingsAction"/>
|
||||||
|
</widget>
|
||||||
<widget class="QMenu" name="HelpMenu">
|
<widget class="QMenu" name="HelpMenu">
|
||||||
<property name="mouseTracking">
|
<property name="mouseTracking">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
@@ -291,6 +297,7 @@ font: 700 9pt;</string>
|
|||||||
<addaction name="ManualAction"/>
|
<addaction name="ManualAction"/>
|
||||||
<addaction name="AboutAction"/>
|
<addaction name="AboutAction"/>
|
||||||
</widget>
|
</widget>
|
||||||
|
<addaction name="ToolsMenu"/>
|
||||||
<addaction name="HelpMenu"/>
|
<addaction name="HelpMenu"/>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QStatusBar" name="StatusBar">
|
<widget class="QStatusBar" name="StatusBar">
|
||||||
@@ -308,6 +315,11 @@ font: 700 9pt;</string>
|
|||||||
<string>关于</string>
|
<string>关于</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="SettingsAction">
|
||||||
|
<property name="text">
|
||||||
|
<string>全局设置</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<resources/>
|
<resources/>
|
||||||
<connections/>
|
<connections/>
|
||||||
|
|||||||
@@ -0,0 +1,536 @@
|
|||||||
|
<?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>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<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>450</width>
|
||||||
|
<height>380</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="CustomQssGroupBox">
|
||||||
|
<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="CustomQssHintLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>选择一个主题,或导入新的主题文件:</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="QssPathLayout">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>5</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="ThemeComboBox">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>160</width>
|
||||||
|
<height>25</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="QssPathEdit">
|
||||||
|
<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="BrowseQssButton">
|
||||||
|
<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="ThemeInfoLabel">
|
||||||
|
<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::AlignTop</set>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="QssActionLayout">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>5</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="ApplyQssButton">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>80</width>
|
||||||
|
<height>25</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="visible">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>应用样式</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="ResetThemeButton">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>80</width>
|
||||||
|
<height>25</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>重置主题</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="QssActionSpacer">
|
||||||
|
<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="QssStatusLabel">
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
@@ -7,13 +7,13 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>350</width>
|
<width>350</width>
|
||||||
<height>400</height>
|
<height>500</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>350</width>
|
<width>350</width>
|
||||||
<height>460</height>
|
<height>500</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="maximumSize">
|
<property name="maximumSize">
|
||||||
|
|||||||
@@ -66,6 +66,12 @@ class CfgKey:
|
|||||||
CURRENT = ConfigPath(ConfigType.GLOBAL, "automation.user_path.current")
|
CURRENT = ConfigPath(ConfigType.GLOBAL, "automation.user_path.current")
|
||||||
PATHS = ConfigPath(ConfigType.GLOBAL, "automation.user_path.paths")
|
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 TIMERTASK:
|
class TIMERTASK:
|
||||||
ROOT = ConfigPath(ConfigType.TIMERTASK, "")
|
ROOT = ConfigPath(ConfigType.TIMERTASK, "")
|
||||||
TIMER_TASKS = ConfigPath(ConfigType.TIMERTASK, "timer_tasks")
|
TIMER_TASKS = ConfigPath(ConfigType.TIMERTASK, "timer_tasks")
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ class ConfigTemplate:
|
|||||||
"current": 0,
|
"current": 0,
|
||||||
"paths": []
|
"paths": []
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"theme": "system",
|
||||||
|
"style": "Fusion",
|
||||||
|
"custom_theme": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case ConfigType.BULLETIN:
|
case ConfigType.BULLETIN:
|
||||||
|
|||||||
@@ -360,10 +360,6 @@ class WebDriverDownloader:
|
|||||||
break
|
break
|
||||||
if not driver_file:
|
if not driver_file:
|
||||||
raise FileNotFoundError(f"未找到 web driver 文件 : {expected_name}")
|
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, "解压完成")
|
progress_callback(100, 100, 0.0, "解压完成")
|
||||||
self.download_path.unlink()
|
self.download_path.unlink()
|
||||||
self._cleanup(driver_file)
|
self._cleanup(driver_file)
|
||||||
|
|||||||
@@ -111,10 +111,6 @@ class WebDriverManager:
|
|||||||
for driver_info in self.__driver_infos:
|
for driver_info in self.__driver_infos:
|
||||||
driver_path = self._getDriverPath(driver_info)
|
driver_path = self._getDriverPath(driver_info)
|
||||||
if driver_path and driver_path.exists() and driver_path.is_file():
|
if driver_path and driver_path.exists() and driver_path.is_file():
|
||||||
# Repair missing execute permission on Unix
|
|
||||||
# (zip-extracted drivers from older versions).
|
|
||||||
if os.name != 'nt' and not os.access(str(driver_path), os.X_OK):
|
|
||||||
os.chmod(str(driver_path), 0o755)
|
|
||||||
driver_info.driver_path = driver_path
|
driver_info.driver_path = driver_path
|
||||||
driver_info.driver_status = WebDriverStatus.INSTALLED
|
driver_info.driver_status = WebDriverStatus.INSTALLED
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,321 @@
|
|||||||
|
# -*- 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 shutil
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
from PySide6.QtCore import Qt
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QApplication,
|
||||||
|
QStyleFactory
|
||||||
|
)
|
||||||
|
|
||||||
|
from interfaces.ConfigProvider import CfgKey
|
||||||
|
from managers.config.ConfigManager import instance as configInstance
|
||||||
|
from managers.log.LogManager import instance as logInstance
|
||||||
|
from utils.ThemeUtils import (
|
||||||
|
packTheme,
|
||||||
|
readThemeInfo,
|
||||||
|
unpackTheme,
|
||||||
|
wrapQssToAtheme
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_active_style_name = "Fusion"
|
||||||
|
|
||||||
|
|
||||||
|
def setActiveStyle(
|
||||||
|
style_name: str
|
||||||
|
):
|
||||||
|
|
||||||
|
global _active_style_name
|
||||||
|
_active_style_name = style_name
|
||||||
|
|
||||||
|
def getActiveStyle(
|
||||||
|
) -> str:
|
||||||
|
|
||||||
|
return _active_style_name
|
||||||
|
|
||||||
|
|
||||||
|
class ThemeManager:
|
||||||
|
"""
|
||||||
|
Theme manager class.
|
||||||
|
|
||||||
|
Manages the themes storage directory, providing import,
|
||||||
|
list, remove, and apply operations for .altheme theme files.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
themes_dir (str): Path to the themes storage directory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
themes_dir: str
|
||||||
|
):
|
||||||
|
|
||||||
|
self.__themes_dir = os.path.abspath(themes_dir)
|
||||||
|
self.__lock = threading.Lock()
|
||||||
|
self.__current_theme_name = ""
|
||||||
|
os.makedirs(self.__themes_dir, exist_ok=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _colorSchemeFor(
|
||||||
|
theme: str
|
||||||
|
) -> Qt.ColorScheme:
|
||||||
|
"""
|
||||||
|
Map a theme identifier to the corresponding Qt color scheme.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if theme == "dark":
|
||||||
|
return Qt.ColorScheme.Dark
|
||||||
|
elif theme == "light":
|
||||||
|
return Qt.ColorScheme.Light
|
||||||
|
else:
|
||||||
|
return Qt.ColorScheme.Unknown
|
||||||
|
|
||||||
|
def themesDir(
|
||||||
|
self
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Get the themes directory path.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The absolute path to the themes storage directory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.__themes_dir
|
||||||
|
|
||||||
|
def importTheme(
|
||||||
|
self,
|
||||||
|
source_path: str
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Import a theme file into the themes directory.
|
||||||
|
|
||||||
|
Supports .altheme (zip archive) and bare .qss files.
|
||||||
|
Bare .qss files are automatically wrapped into .altheme format.
|
||||||
|
For .altheme files, validates that theme.qss exists in the archive
|
||||||
|
and sanitises the theme name to prevent path traversal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source_path (str): Path to the .altheme or .qss file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The imported theme name.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If source_path does not exist.
|
||||||
|
ValueError: If the file type is unsupported or the .altheme is invalid.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not os.path.isfile(source_path):
|
||||||
|
raise FileNotFoundError(source_path)
|
||||||
|
ext = os.path.splitext(source_path)[1].lower()
|
||||||
|
with self.__lock:
|
||||||
|
if ext == ".qss":
|
||||||
|
name = os.path.splitext(os.path.basename(source_path))[0]
|
||||||
|
dest_path = os.path.join(self.__themes_dir, name + ".altheme")
|
||||||
|
if os.path.exists(dest_path):
|
||||||
|
raise ValueError(f"主题 '{name}' 已存在")
|
||||||
|
wrapQssToAtheme(source_path, dest_path, "both")
|
||||||
|
return name
|
||||||
|
elif ext == ".altheme":
|
||||||
|
with zipfile.ZipFile(source_path, "r") as zf:
|
||||||
|
if "theme.qss" not in zf.namelist():
|
||||||
|
raise ValueError("无效的 .altheme: 缺少 theme.qss")
|
||||||
|
info = readThemeInfo(source_path)
|
||||||
|
name = info.get("name", os.path.splitext(os.path.basename(source_path))[0])
|
||||||
|
safe_name = os.path.basename(name)
|
||||||
|
dest_path = os.path.join(self.__themes_dir, safe_name + ".altheme")
|
||||||
|
if os.path.exists(dest_path):
|
||||||
|
raise ValueError(f"主题 '{safe_name}' 已存在")
|
||||||
|
# Check for name collision with existing themes by the same author
|
||||||
|
new_author = info.get("author", "")
|
||||||
|
for existing in self.listThemes():
|
||||||
|
if (existing.get("name", "") == safe_name
|
||||||
|
and existing.get("author", "") == new_author):
|
||||||
|
raise ValueError(
|
||||||
|
f"主题名称 '{safe_name}' (作者 '{new_author}') 已存在"
|
||||||
|
)
|
||||||
|
shutil.copy2(source_path, dest_path)
|
||||||
|
return safe_name
|
||||||
|
else:
|
||||||
|
raise ValueError(f"不支持的文件类型: {ext}")
|
||||||
|
|
||||||
|
def listThemes(
|
||||||
|
self
|
||||||
|
) -> list:
|
||||||
|
"""
|
||||||
|
List all available themes in the themes directory.
|
||||||
|
|
||||||
|
Scans the themes directory for .altheme files and reads
|
||||||
|
their info.json metadata.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[dict]: A list of theme info dictionaries.
|
||||||
|
"""
|
||||||
|
|
||||||
|
themes = []
|
||||||
|
seen_keys = set()
|
||||||
|
if not os.path.isdir(self.__themes_dir):
|
||||||
|
return themes
|
||||||
|
for filename in sorted(os.listdir(self.__themes_dir)):
|
||||||
|
if filename.endswith(".altheme"):
|
||||||
|
filepath = os.path.join(self.__themes_dir, filename)
|
||||||
|
try:
|
||||||
|
info = readThemeInfo(filepath)
|
||||||
|
with zipfile.ZipFile(filepath, "r") as zf:
|
||||||
|
if "theme.qss" not in zf.namelist():
|
||||||
|
raise ValueError("缺少 theme.qss")
|
||||||
|
name = info.get("name", "")
|
||||||
|
author = info.get("author", "")
|
||||||
|
key = (name, author)
|
||||||
|
if key in seen_keys:
|
||||||
|
logInstance().getLogger("ThemeManager").warning(
|
||||||
|
f"主题名称 '{name}' (作者 '{author}') 重复 (文件 '{filename}') 已跳过"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
seen_keys.add(key)
|
||||||
|
themes.append(info)
|
||||||
|
except Exception as e:
|
||||||
|
logInstance().getLogger("ThemeManager").warning(
|
||||||
|
f"无法读取主题文件 '{filename}',已跳过: {e}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logInstance().getLogger("ThemeManager").warning(
|
||||||
|
f"未知文件类型 '{filename}',已跳过"
|
||||||
|
)
|
||||||
|
return themes
|
||||||
|
|
||||||
|
def removeTheme(
|
||||||
|
self,
|
||||||
|
name: str
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Remove a theme by name.
|
||||||
|
|
||||||
|
If the removed theme is currently active, clears the QSS
|
||||||
|
stylesheet from the application.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The theme name to remove.
|
||||||
|
"""
|
||||||
|
|
||||||
|
filepath = os.path.join(self.__themes_dir, name + ".altheme")
|
||||||
|
with self.__lock:
|
||||||
|
if os.path.isfile(filepath):
|
||||||
|
os.remove(filepath)
|
||||||
|
if self.__current_theme_name == name:
|
||||||
|
self.__current_theme_name = ""
|
||||||
|
saved_theme = configInstance().get(
|
||||||
|
CfgKey.GLOBAL.APPEARANCE.THEME, "system"
|
||||||
|
)
|
||||||
|
self.clearTheme(saved_theme)
|
||||||
|
|
||||||
|
def applyTheme(
|
||||||
|
self,
|
||||||
|
name: str
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Apply a theme by name.
|
||||||
|
|
||||||
|
Extracts the QSS from the .altheme file, applies it to
|
||||||
|
QApplication, and sets the Qt color scheme based on
|
||||||
|
the theme's need_theme metadata.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The theme name to apply.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If the theme .altheme file does not exist.
|
||||||
|
"""
|
||||||
|
|
||||||
|
filepath = os.path.join(self.__themes_dir, name + ".altheme")
|
||||||
|
if not os.path.isfile(filepath):
|
||||||
|
raise FileNotFoundError(filepath)
|
||||||
|
with self.__lock:
|
||||||
|
info = readThemeInfo(filepath)
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
unpackTheme(filepath, tmpdir)
|
||||||
|
qss_path = os.path.join(tmpdir, "theme.qss")
|
||||||
|
if os.path.isfile(qss_path):
|
||||||
|
with open(qss_path, "r", encoding="utf-8") as fh:
|
||||||
|
qss = fh.read()
|
||||||
|
app = QApplication.instance()
|
||||||
|
if app:
|
||||||
|
app.setStyleSheet(qss)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"主题 '{name}' 的 .altheme 文件中缺少 theme.qss"
|
||||||
|
)
|
||||||
|
app = QApplication.instance()
|
||||||
|
if app:
|
||||||
|
need_theme = info.get("need_theme", "both")
|
||||||
|
app.styleHints().setColorScheme(
|
||||||
|
ThemeManager._colorSchemeFor(need_theme)
|
||||||
|
)
|
||||||
|
app.setStyle(QStyleFactory.create(_active_style_name))
|
||||||
|
self.__current_theme_name = name
|
||||||
|
|
||||||
|
def clearTheme(
|
||||||
|
self,
|
||||||
|
theme: str
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Clear the current QSS stylesheet and apply the given color scheme.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
theme (str): The color scheme to apply after clearing
|
||||||
|
("light", "dark", or "system").
|
||||||
|
"""
|
||||||
|
|
||||||
|
app = QApplication.instance()
|
||||||
|
if not app:
|
||||||
|
return
|
||||||
|
app.setStyleSheet("")
|
||||||
|
app.styleHints().setColorScheme(
|
||||||
|
ThemeManager._colorSchemeFor(theme)
|
||||||
|
)
|
||||||
|
app.setStyle(QStyleFactory.create(_active_style_name))
|
||||||
|
|
||||||
|
|
||||||
|
# ThemeManager singleton instance.
|
||||||
|
_theme_manager_instance = None
|
||||||
|
|
||||||
|
# Singleton instance lock.
|
||||||
|
_instance_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def instance(
|
||||||
|
themes_dir: str = ""
|
||||||
|
) -> ThemeManager:
|
||||||
|
"""
|
||||||
|
Get the ThemeManager singleton instance.
|
||||||
|
|
||||||
|
On first call, initialises the ThemeManager with the themes
|
||||||
|
directory derived from ConfigManager's config directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
themes_dir (str): Optional themes directory path.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ThemeManager: The singleton ThemeManager instance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
global _theme_manager_instance
|
||||||
|
with _instance_lock:
|
||||||
|
if _theme_manager_instance is None:
|
||||||
|
if not themes_dir:
|
||||||
|
cfg = configInstance()
|
||||||
|
themes_dir = os.path.join(cfg.configDir(), "themes")
|
||||||
|
_theme_manager_instance = ThemeManager(themes_dir)
|
||||||
|
return _theme_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.
|
||||||
|
"""
|
||||||
+13
-15
@@ -10,7 +10,6 @@ See the LICENSE file for details.
|
|||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
from selenium import webdriver
|
from selenium import webdriver
|
||||||
from selenium.webdriver.remote.webdriver import WebDriver
|
|
||||||
from selenium.common.exceptions import (
|
from selenium.common.exceptions import (
|
||||||
TimeoutException,
|
TimeoutException,
|
||||||
WebDriverException,
|
WebDriverException,
|
||||||
@@ -38,11 +37,11 @@ class AutoLib(MsgBase):
|
|||||||
output_queue: queue.Queue,
|
output_queue: queue.Queue,
|
||||||
run_config: dict,
|
run_config: dict,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
super().__init__(input_queue, output_queue)
|
super().__init__(input_queue, output_queue)
|
||||||
|
|
||||||
self.__run_config: dict = run_config
|
self.__run_config: dict = run_config
|
||||||
self.__user_config: dict | None = None
|
self.__user_config: dict | None = None
|
||||||
self.__driver: WebDriver | None = None
|
self.__driver = None
|
||||||
self.__driver_type: str = ""
|
self.__driver_type: str = ""
|
||||||
self.__driver_path: str = ""
|
self.__driver_path: str = ""
|
||||||
self.__login_page: LoginPage = None
|
self.__login_page: LoginPage = None
|
||||||
@@ -59,7 +58,7 @@ class AutoLib(MsgBase):
|
|||||||
else:
|
else:
|
||||||
if not self.__initDriverUrl():
|
if not self.__initDriverUrl():
|
||||||
self.close()
|
self.close()
|
||||||
raise Exception("浏览器驱动 URL 初始化失败 !")
|
raise Exception("浏览器驱动URL初始化失败 !")
|
||||||
self.__initPagesServices()
|
self.__initPagesServices()
|
||||||
self.__initPagesFlows()
|
self.__initPagesFlows()
|
||||||
|
|
||||||
@@ -68,10 +67,9 @@ class AutoLib(MsgBase):
|
|||||||
) -> bool:
|
) -> bool:
|
||||||
|
|
||||||
self._showTrace("正在初始化浏览器驱动......", no_log=True)
|
self._showTrace("正在初始化浏览器驱动......", no_log=True)
|
||||||
driver_config: dict = self.__run_config.get("web_driver", None)
|
web_driver_config: dict = self.__run_config.get("web_driver", None)
|
||||||
self.__driver_type = driver_config.get("driver_type", "none")
|
self.__driver_type = web_driver_config.get("driver_type", "none")
|
||||||
self.__driver_type = self.__driver_type.lower()
|
match self.__driver_type.lower():
|
||||||
match self.__driver_type:
|
|
||||||
case "edge":
|
case "edge":
|
||||||
driver_options = webdriver.EdgeOptions()
|
driver_options = webdriver.EdgeOptions()
|
||||||
case "chrome":
|
case "chrome":
|
||||||
@@ -84,10 +82,10 @@ class AutoLib(MsgBase):
|
|||||||
self.TraceLevel.WARNING,
|
self.TraceLevel.WARNING,
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
if not driver_config:
|
if not web_driver_config:
|
||||||
self._showTrace("未配置浏览器驱动参数 !", self.TraceLevel.ERROR)
|
self._showTrace("未配置浏览器驱动参数 !", self.TraceLevel.ERROR)
|
||||||
return False
|
return False
|
||||||
if driver_config.get("headless", False):
|
if web_driver_config.get("headless", False):
|
||||||
driver_options.add_argument("--headless")
|
driver_options.add_argument("--headless")
|
||||||
driver_options.add_argument("--disable-gpu")
|
driver_options.add_argument("--disable-gpu")
|
||||||
driver_options.add_argument("--no-sandbox")
|
driver_options.add_argument("--no-sandbox")
|
||||||
@@ -112,11 +110,11 @@ class AutoLib(MsgBase):
|
|||||||
"AppleWebKit/537.36 (KHTML, like Gecko) "\
|
"AppleWebKit/537.36 (KHTML, like Gecko) "\
|
||||||
"Chrome/120.0.0.0 "\
|
"Chrome/120.0.0.0 "\
|
||||||
"Safari/537.36"
|
"Safari/537.36"
|
||||||
if self.__driver_type == "edge":
|
if self.__driver_type.lower() == "edge":
|
||||||
user_agent += " Edg/120.0.0.0"
|
user_agent += " Edg/120.0.0.0"
|
||||||
|
|
||||||
# set options for firefox
|
# set options for firefox
|
||||||
elif self.__driver_type == "firefox":
|
elif self.__driver_type.lower() == "firefox":
|
||||||
driver_options.set_preference("dom.webdriver.enabled", False)
|
driver_options.set_preference("dom.webdriver.enabled", False)
|
||||||
driver_options.set_preference("useAutomationExtension", False)
|
driver_options.set_preference("useAutomationExtension", False)
|
||||||
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) "\
|
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) "\
|
||||||
@@ -124,14 +122,14 @@ class AutoLib(MsgBase):
|
|||||||
driver_options.add_argument(f"user-agent={user_agent}")
|
driver_options.add_argument(f"user-agent={user_agent}")
|
||||||
|
|
||||||
# init browser driver
|
# init browser driver
|
||||||
self.__driver_path = driver_config.get("driver_path", "")
|
self.__driver_path = web_driver_config.get("driver_path", "")
|
||||||
if not self.__driver_path:
|
if not self.__driver_path:
|
||||||
self._showTrace("未配置浏览器驱动路径 !", self.TraceLevel.WARNING)
|
self._showTrace("未配置浏览器驱动路径 !", self.TraceLevel.WARNING)
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
self.__driver_path = os.path.abspath(self.__driver_path)
|
self.__driver_path = os.path.abspath(self.__driver_path)
|
||||||
service = None
|
service = None
|
||||||
match self.__driver_type:
|
match self.__driver_type.lower():
|
||||||
case "edge":
|
case "edge":
|
||||||
service = EdgeService(executable_path=self.__driver_path)
|
service = EdgeService(executable_path=self.__driver_path)
|
||||||
self.__driver = webdriver.Edge(service=service, options=driver_options)
|
self.__driver = webdriver.Edge(service=service, options=driver_options)
|
||||||
@@ -163,7 +161,7 @@ class AutoLib(MsgBase):
|
|||||||
self._showTrace("未配置图书馆参数 !", self.TraceLevel.ERROR)
|
self._showTrace("未配置图书馆参数 !", self.TraceLevel.ERROR)
|
||||||
return False
|
return False
|
||||||
url: str = lib_config.get("host_url") + lib_config.get("login_url")
|
url: str = lib_config.get("host_url") + lib_config.get("login_url")
|
||||||
self.__login_page = LoginPage(self._input_queue, self._output_queue, self.__driver)
|
self.__login_page = LoginPage(self.__driver, tracer=self._showTrace)
|
||||||
self.__driver.set_page_load_timeout(5)
|
self.__driver.set_page_load_timeout(5)
|
||||||
try:
|
try:
|
||||||
self.__driver.get(url)
|
self.__driver.get(url)
|
||||||
|
|||||||
+19
-13
@@ -7,8 +7,7 @@ This software is provided "as is", without any warranty of any kind.
|
|||||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||||
See the LICENSE file for details.
|
See the LICENSE file for details.
|
||||||
"""
|
"""
|
||||||
import queue
|
from typing import Callable, Optional
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
from selenium.common.exceptions import (
|
from selenium.common.exceptions import (
|
||||||
ElementNotInteractableException,
|
ElementNotInteractableException,
|
||||||
@@ -20,10 +19,8 @@ from selenium.webdriver.remote.webdriver import WebDriver
|
|||||||
from selenium.webdriver.support.ui import WebDriverWait
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
from selenium.webdriver.support import expected_conditions as EC
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
|
||||||
from base.MsgBase import MsgBase
|
|
||||||
|
|
||||||
|
class LoginPage:
|
||||||
class LoginPage(MsgBase):
|
|
||||||
|
|
||||||
USERNAME_INPUT = (By.NAME, "username")
|
USERNAME_INPUT = (By.NAME, "username")
|
||||||
PASSWORD_INPUT = (By.NAME, "password")
|
PASSWORD_INPUT = (By.NAME, "password")
|
||||||
@@ -39,13 +36,22 @@ class LoginPage(MsgBase):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
input_queue: queue.Queue,
|
|
||||||
output_queue: queue.Queue,
|
|
||||||
driver: WebDriver,
|
driver: WebDriver,
|
||||||
|
tracer: Optional[Callable[..., None]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
super().__init__(input_queue, output_queue)
|
|
||||||
self._driver: WebDriver = driver
|
self._driver: WebDriver = driver
|
||||||
|
self._tracer: Optional[Callable[..., None]] = tracer
|
||||||
|
|
||||||
|
def _trace(
|
||||||
|
self,
|
||||||
|
msg: str,
|
||||||
|
level: int = 20,
|
||||||
|
no_log: bool = False,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
if self._tracer:
|
||||||
|
self._tracer(msg, level, no_log)
|
||||||
|
|
||||||
def navigate(
|
def navigate(
|
||||||
self,
|
self,
|
||||||
@@ -179,7 +185,7 @@ class LoginPage(MsgBase):
|
|||||||
) -> bool:
|
) -> bool:
|
||||||
|
|
||||||
for attempt in range(max_attempts):
|
for attempt in range(max_attempts):
|
||||||
self._showTrace(
|
self._trace(
|
||||||
f"用户 {username} 第 {attempt + 1} 次尝试登录......",
|
f"用户 {username} 第 {attempt + 1} 次尝试登录......",
|
||||||
no_log=True,
|
no_log=True,
|
||||||
)
|
)
|
||||||
@@ -190,16 +196,16 @@ class LoginPage(MsgBase):
|
|||||||
continue
|
continue
|
||||||
if not self.fillCaptcha(captcha_text):
|
if not self.fillCaptcha(captcha_text):
|
||||||
continue
|
continue
|
||||||
self._showTrace("尝试登录...", no_log=True)
|
self._trace("尝试登录...", no_log=True)
|
||||||
if not self.clickLogin():
|
if not self.clickLogin():
|
||||||
continue
|
continue
|
||||||
if self.waitLoginSuccess():
|
if self.waitLoginSuccess():
|
||||||
self._showTrace(f"用户 {username} 第 {attempt + 1} 次登录成功 !")
|
self._trace(f"用户 {username} 第 {attempt + 1} 次登录成功 !")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
self._showTrace(
|
self._trace(
|
||||||
"登录页面加载失败 ! : "
|
"登录页面加载失败 ! : "
|
||||||
"用户账号或者密码错误/验证码错误, 具体以页面提示为准",
|
"用户账号或者密码错误/验证码错误, 具体以页面提示为准",
|
||||||
level=self.TraceLevel.ERROR,
|
level=40,
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class CheckinFlow(MsgBase):
|
|||||||
self._driver: WebDriver = driver
|
self._driver: WebDriver = driver
|
||||||
self._shell: MainShell = shell
|
self._shell: MainShell = shell
|
||||||
|
|
||||||
def _ensureCheckinButton(
|
def execute(
|
||||||
self,
|
self,
|
||||||
username: str,
|
username: str,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@@ -49,13 +49,7 @@ class CheckinFlow(MsgBase):
|
|||||||
self._showTrace(f"签到按钮启用失败 !", self.TraceLevel.ERROR)
|
self._showTrace(f"签到按钮启用失败 !", self.TraceLevel.ERROR)
|
||||||
return False
|
return False
|
||||||
self._showTrace("签到按钮已启用")
|
self._showTrace("签到按钮已启用")
|
||||||
return True
|
self._shell.clickCheckinButton()
|
||||||
|
|
||||||
def _processCheckinDialog(
|
|
||||||
self,
|
|
||||||
username: str,
|
|
||||||
) -> bool:
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with CheckinResultDialog(self._driver) as dialog:
|
with CheckinResultDialog(self._driver) as dialog:
|
||||||
result_msg = dialog.getResultMessage()
|
result_msg = dialog.getResultMessage()
|
||||||
@@ -93,13 +87,3 @@ class CheckinFlow(MsgBase):
|
|||||||
except (TimeoutException, NoSuchElementException, ElementNotInteractableException):
|
except (TimeoutException, NoSuchElementException, ElementNotInteractableException):
|
||||||
self._showTrace("签到时发生未知错误 !", self.TraceLevel.ERROR)
|
self._showTrace("签到时发生未知错误 !", self.TraceLevel.ERROR)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def execute(
|
|
||||||
self,
|
|
||||||
username: str,
|
|
||||||
) -> bool:
|
|
||||||
|
|
||||||
if not self._ensureCheckinButton(username):
|
|
||||||
return False
|
|
||||||
self._shell.clickCheckinButton()
|
|
||||||
return self._processCheckinDialog(username)
|
|
||||||
|
|||||||
@@ -61,23 +61,19 @@ class RenewFlow(MsgBase):
|
|||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _computeRenewTarget(
|
def execute(
|
||||||
self,
|
self,
|
||||||
|
username: str,
|
||||||
record: dict,
|
record: dict,
|
||||||
renew_info: dict,
|
renew_info: dict,
|
||||||
):
|
) -> bool:
|
||||||
|
|
||||||
|
max_diff = renew_info.get("max_diff", 30)
|
||||||
|
prefer_earlier = renew_info.get("prefer_early", True)
|
||||||
end_time = record["time"]["end"]
|
end_time = record["time"]["end"]
|
||||||
target_renew_mins = timeStrToMins(end_time) + renew_info.get("expect_duration", 2) * 60
|
target_renew_mins = timeStrToMins(end_time) + renew_info.get("expect_duration", 2) * 60
|
||||||
if not self._validateRenewTime(end_time, target_renew_mins):
|
if not self._validateRenewTime(end_time, target_renew_mins):
|
||||||
return None
|
return False
|
||||||
return target_renew_mins
|
|
||||||
|
|
||||||
def _ensureExtendButton(
|
|
||||||
self,
|
|
||||||
username: str,
|
|
||||||
) -> bool:
|
|
||||||
|
|
||||||
if not self._shell.waitExtendButton():
|
if not self._shell.waitExtendButton():
|
||||||
self._showTrace(f"用户 {username} 续约界面加载失败 !", self.TraceLevel.ERROR)
|
self._showTrace(f"用户 {username} 续约界面加载失败 !", self.TraceLevel.ERROR)
|
||||||
return False
|
return False
|
||||||
@@ -87,17 +83,7 @@ class RenewFlow(MsgBase):
|
|||||||
f"请连接图书馆网络后重试"
|
f"请连接图书馆网络后重试"
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
return True
|
self._shell.clickExtendButton()
|
||||||
|
|
||||||
def _processRenewDialog(
|
|
||||||
self,
|
|
||||||
username: str,
|
|
||||||
record: dict,
|
|
||||||
target_renew_mins: int,
|
|
||||||
max_diff: int,
|
|
||||||
prefer_earlier: bool,
|
|
||||||
) -> bool:
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with RenewDialog(self._driver) as dialog:
|
with RenewDialog(self._driver) as dialog:
|
||||||
if not dialog.waitUntilReady():
|
if not dialog.waitUntilReady():
|
||||||
@@ -146,22 +132,3 @@ class RenewFlow(MsgBase):
|
|||||||
self._showTrace(f"用户 {username} 续约失败 ! : {e}", self.TraceLevel.ERROR)
|
self._showTrace(f"用户 {username} 续约失败 ! : {e}", self.TraceLevel.ERROR)
|
||||||
self._shell.refresh()
|
self._shell.refresh()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def execute(
|
|
||||||
self,
|
|
||||||
username: str,
|
|
||||||
record: dict,
|
|
||||||
renew_info: dict,
|
|
||||||
) -> bool:
|
|
||||||
|
|
||||||
max_diff = renew_info.get("max_diff", 30)
|
|
||||||
prefer_earlier = renew_info.get("prefer_early", True)
|
|
||||||
target_renew_mins = self._computeRenewTarget(record, renew_info)
|
|
||||||
if target_renew_mins is None:
|
|
||||||
return False
|
|
||||||
if not self._ensureExtendButton(username):
|
|
||||||
return False
|
|
||||||
self._shell.clickExtendButton()
|
|
||||||
return self._processRenewDialog(
|
|
||||||
username, record, target_renew_mins, max_diff, prefer_earlier,
|
|
||||||
)
|
|
||||||
|
|||||||
+32
-100
@@ -58,70 +58,59 @@ class ReserveFlow(MsgBase):
|
|||||||
self._driver: WebDriver = driver
|
self._driver: WebDriver = driver
|
||||||
self._shell: MainShell = shell
|
self._shell: MainShell = shell
|
||||||
|
|
||||||
def _loadReserveView(
|
def execute(
|
||||||
self,
|
self,
|
||||||
) -> ReserveView | None:
|
|
||||||
|
|
||||||
try:
|
|
||||||
return self._shell.gotoReserveView()
|
|
||||||
except (TimeoutException, ElementNotInteractableException) as e:
|
|
||||||
self._showTrace(f"加载预约选座页面失败 ! : {e}", self.TraceLevel.ERROR)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _selectDate(
|
|
||||||
self,
|
|
||||||
view: ReserveView,
|
|
||||||
ctx: ReserveContext,
|
ctx: ReserveContext,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|
||||||
|
submit_reserve = False
|
||||||
|
reserve_success = False
|
||||||
|
have_hover_on_page = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
view = self._shell.gotoReserveView()
|
||||||
|
except (TimeoutException, ElementNotInteractableException) as e:
|
||||||
|
self._showTrace(f"加载预约选座页面失败 ! : {e}", self.TraceLevel.ERROR)
|
||||||
|
return False
|
||||||
if not view.selectDate(ctx.date):
|
if not view.selectDate(ctx.date):
|
||||||
self._showTrace(f"选择日期失败 ! : {ctx.date} 不可用", self.TraceLevel.ERROR)
|
self._showTrace(f"选择日期失败 ! : {ctx.date} 不可用", self.TraceLevel.ERROR)
|
||||||
return False
|
return False
|
||||||
self._showTrace(f"日期 {ctx.date} 选择成功 !")
|
self._showTrace(f"日期 {ctx.date} 选择成功 !")
|
||||||
return True
|
|
||||||
|
|
||||||
def _selectPlace(
|
|
||||||
self,
|
|
||||||
view: ReserveView,
|
|
||||||
) -> bool:
|
|
||||||
|
|
||||||
if not view.selectPlace("1"):
|
if not view.selectPlace("1"):
|
||||||
self._showTrace("选择预约场所失败 ! : 图书馆 不可用", self.TraceLevel.ERROR)
|
self._showTrace("选择预约场所失败 ! : 图书馆 不可用", self.TraceLevel.ERROR)
|
||||||
return False
|
return False
|
||||||
self._showTrace("预约场所 图书馆 选择成功 !")
|
self._showTrace("预约场所 图书馆 选择成功 !")
|
||||||
return True
|
|
||||||
|
|
||||||
def _selectFloor(
|
|
||||||
self,
|
|
||||||
view: ReserveView,
|
|
||||||
ctx: ReserveContext,
|
|
||||||
) -> bool:
|
|
||||||
|
|
||||||
if not view.selectFloor(ctx.floor):
|
if not view.selectFloor(ctx.floor):
|
||||||
display_floor = ReserveView.FLOOR_MAP.get(ctx.floor, ctx.floor)
|
display_floor = ReserveView.FLOOR_MAP.get(ctx.floor, ctx.floor)
|
||||||
self._showTrace(f"选择楼层失败 ! : {display_floor} 不可用", self.TraceLevel.ERROR)
|
self._showTrace(f"选择楼层失败 ! : {display_floor} 不可用", self.TraceLevel.ERROR)
|
||||||
return False
|
return False
|
||||||
self._showTrace(f"楼层 {ReserveView.FLOOR_MAP.get(ctx.floor)} 选择成功 !")
|
self._showTrace(f"楼层 {ReserveView.FLOOR_MAP.get(ctx.floor)} 选择成功 !")
|
||||||
return True
|
|
||||||
|
|
||||||
def _selectRoom(
|
|
||||||
self,
|
|
||||||
view: ReserveView,
|
|
||||||
ctx: ReserveContext,
|
|
||||||
):
|
|
||||||
|
|
||||||
seat_map = view.selectRoom(ctx.room)
|
seat_map = view.selectRoom(ctx.room)
|
||||||
if seat_map is None:
|
if seat_map is None:
|
||||||
display_room = ReserveView.ROOM_MAP.get(ctx.room, ctx.room)
|
display_room = ReserveView.ROOM_MAP.get(ctx.room, ctx.room)
|
||||||
self._showTrace(f"选择房间失败 ! : {display_room} 不可用", self.TraceLevel.ERROR)
|
self._showTrace(f"选择房间失败 ! : {display_room} 不可用", self.TraceLevel.ERROR)
|
||||||
return None
|
return False
|
||||||
self._showTrace(f"房间 {ReserveView.ROOM_MAP.get(ctx.room)} 选择成功 !")
|
self._showTrace(f"房间 {ReserveView.ROOM_MAP.get(ctx.room)} 选择成功 !")
|
||||||
return seat_map
|
have_hover_on_page = True
|
||||||
|
seat_status = seat_map.selectSeat(ctx.seat_id)
|
||||||
def _processReserveResult(
|
if seat_status is None:
|
||||||
self,
|
self._showTrace(
|
||||||
) -> bool:
|
f"座位 {ctx.seat_id} 在该楼层区域中不存在, 请检查座位号是否正确",
|
||||||
|
self.TraceLevel.WARNING,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._showTrace(f"座位 {ctx.seat_id} 选择成功 ! : 当前状态 - '{seat_status}'")
|
||||||
|
try:
|
||||||
|
time_dialog = TimeSelectDialog(self._driver, tracer=self._showTrace)
|
||||||
|
except TimeoutException:
|
||||||
|
self._showTrace("时间选择面板未出现 !", self.TraceLevel.ERROR)
|
||||||
|
else:
|
||||||
|
if not time_dialog.selectSeatTime(ctx):
|
||||||
|
self._showTrace("选择时间失败 !", self.TraceLevel.ERROR)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
view.submitReserve()
|
||||||
|
submit_reserve = True
|
||||||
with ReserveResultDialog(self._driver) as result:
|
with ReserveResultDialog(self._driver) as result:
|
||||||
if result.isFailure():
|
if result.isFailure():
|
||||||
self._showTrace("预约失败", self.TraceLevel.ERROR)
|
self._showTrace("预约失败", self.TraceLevel.ERROR)
|
||||||
@@ -142,68 +131,11 @@ class ReserveFlow(MsgBase):
|
|||||||
" 预约成功 !\n"
|
" 预约成功 !\n"
|
||||||
" 未找获取到详细信息"
|
" 未找获取到详细信息"
|
||||||
)
|
)
|
||||||
return True
|
reserve_success = True
|
||||||
else:
|
else:
|
||||||
self._showTrace("预约结果加载失败 !", self.TraceLevel.ERROR)
|
self._showTrace("预约结果加载失败 !", self.TraceLevel.ERROR)
|
||||||
return False
|
|
||||||
|
|
||||||
def _selectSeatAndSubmit(
|
|
||||||
self,
|
|
||||||
view: ReserveView,
|
|
||||||
seat_map,
|
|
||||||
ctx: ReserveContext,
|
|
||||||
) -> tuple[bool, bool]:
|
|
||||||
|
|
||||||
submit_reserve = False
|
|
||||||
reserve_success = False
|
|
||||||
|
|
||||||
seat_status = seat_map.selectSeat(ctx.seat_id)
|
|
||||||
if seat_status is None:
|
|
||||||
self._showTrace(
|
|
||||||
f"座位 {ctx.seat_id} 在该楼层区域中不存在, 请检查座位号是否正确",
|
|
||||||
self.TraceLevel.WARNING,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self._showTrace(f"座位 {ctx.seat_id} 选择成功 ! : 当前状态 - '{seat_status}'")
|
|
||||||
try:
|
|
||||||
time_dialog = TimeSelectDialog(self._driver, tracer=self._showTrace)
|
|
||||||
except TimeoutException:
|
|
||||||
self._showTrace("时间选择面板未出现 !", self.TraceLevel.ERROR)
|
|
||||||
else:
|
|
||||||
if not time_dialog.selectSeatTime(ctx):
|
|
||||||
self._showTrace("选择时间失败 !", self.TraceLevel.ERROR)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
view.submitReserve()
|
|
||||||
submit_reserve = True
|
|
||||||
reserve_success = self._processReserveResult()
|
|
||||||
except (TimeoutException, ElementNotInteractableException):
|
except (TimeoutException, ElementNotInteractableException):
|
||||||
self._showTrace("预约提交失败 !", self.TraceLevel.ERROR)
|
self._showTrace("预约提交失败 !", self.TraceLevel.ERROR)
|
||||||
return submit_reserve, reserve_success
|
|
||||||
|
|
||||||
def execute(
|
|
||||||
self,
|
|
||||||
ctx: ReserveContext,
|
|
||||||
) -> bool:
|
|
||||||
|
|
||||||
# reserve flow pipeline:
|
|
||||||
# date > place > floor > room > seat (begin/end time) > submit > result
|
|
||||||
view = self._loadReserveView()
|
|
||||||
if view is None:
|
|
||||||
return False
|
|
||||||
if not self._selectDate(view, ctx):
|
|
||||||
return False
|
|
||||||
if not self._selectPlace(view):
|
|
||||||
return False
|
|
||||||
if not self._selectFloor(view, ctx):
|
|
||||||
return False
|
|
||||||
seat_map = self._selectRoom(view, ctx)
|
|
||||||
if seat_map is None:
|
|
||||||
return False
|
|
||||||
have_hover_on_page = True
|
|
||||||
submit_reserve, reserve_success = self._selectSeatAndSubmit(
|
|
||||||
view, seat_map, ctx,
|
|
||||||
)
|
|
||||||
if not submit_reserve and have_hover_on_page:
|
if not submit_reserve and have_hover_on_page:
|
||||||
view.refresh()
|
view.refresh()
|
||||||
if reserve_success:
|
if reserve_success:
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# -*- 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 json
|
||||||
|
import os
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
|
||||||
|
def packTheme(
|
||||||
|
qss_path: str,
|
||||||
|
info: dict,
|
||||||
|
output_path: str
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Pack a .qss file and info dict into a .altheme file.
|
||||||
|
|
||||||
|
The .altheme file is a zip archive containing info.json and theme.qss.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
qss_path (str): Path to the .qss stylesheet file.
|
||||||
|
info (dict): Theme metadata dict with keys name, author, need_theme, brief.
|
||||||
|
output_path (str): Destination path for the .altheme file.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If qss_path does not exist.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not os.path.isfile(qss_path):
|
||||||
|
raise FileNotFoundError(qss_path)
|
||||||
|
with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
zf.writestr("info.json", json.dumps(info, ensure_ascii=False, indent=4))
|
||||||
|
zf.write(qss_path, "theme.qss")
|
||||||
|
|
||||||
|
|
||||||
|
def unpackTheme(
|
||||||
|
altheme_path: str,
|
||||||
|
output_dir: str
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Extract a .altheme file to a directory.
|
||||||
|
|
||||||
|
Performs Zip Slip validation before extraction.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
altheme_path (str): Path to the .altheme file.
|
||||||
|
output_dir (str): Directory to extract contents into.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If altheme_path does not exist.
|
||||||
|
ValueError: If a zip entry contains an unsafe path.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not os.path.isfile(altheme_path):
|
||||||
|
raise FileNotFoundError(altheme_path)
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
with zipfile.ZipFile(altheme_path, "r") as zf:
|
||||||
|
for name in zf.namelist():
|
||||||
|
if name.startswith("/") or ".." in name:
|
||||||
|
raise ValueError(f"不安全的 .altheme 入口: {name}")
|
||||||
|
zf.extractall(output_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def readThemeInfo(
|
||||||
|
altheme_path: str
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Read only the info.json metadata from a .altheme file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
altheme_path (str): Path to the .altheme file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: The theme metadata dictionary.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If altheme_path does not exist.
|
||||||
|
ValueError: If the .altheme does not contain info.json.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not os.path.isfile(altheme_path):
|
||||||
|
raise FileNotFoundError(altheme_path)
|
||||||
|
with zipfile.ZipFile(altheme_path, "r") as zf:
|
||||||
|
if "info.json" not in zf.namelist():
|
||||||
|
raise ValueError("无效的 .altheme: 缺少 info.json")
|
||||||
|
with zf.open("info.json") as fh:
|
||||||
|
info = json.loads(fh.read().decode("utf-8"))
|
||||||
|
if "name" not in info:
|
||||||
|
raise ValueError("无效的 .altheme: info.json 缺少 'name' 字段")
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
def wrapQssToAtheme(
|
||||||
|
qss_path: str,
|
||||||
|
output_path: str,
|
||||||
|
current_theme: str
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Wrap a bare .qss file into a .altheme file with auto-generated metadata.
|
||||||
|
|
||||||
|
The generated info.json uses the filename as the theme name
|
||||||
|
and sets default values for author and brief.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
qss_path (str): Path to the bare .qss stylesheet file.
|
||||||
|
output_path (str): Destination path for the .altheme file.
|
||||||
|
current_theme (str): The need_theme value to embed in metadata
|
||||||
|
("light", "dark", or "both").
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If qss_path does not exist.
|
||||||
|
"""
|
||||||
|
|
||||||
|
filename = os.path.splitext(os.path.basename(qss_path))[0]
|
||||||
|
info = {
|
||||||
|
"name": filename,
|
||||||
|
"author": "未知",
|
||||||
|
"need_theme": current_theme,
|
||||||
|
"brief": "没有相关简介"
|
||||||
|
}
|
||||||
|
packTheme(qss_path, info, output_path)
|
||||||
Reference in New Issue
Block a user