mirror of
https://github.com/KenanZhu/AutoLibrary.git
synced 2026-06-18 23:43:02 +08:00
Compare commits
9 Commits
79e5b43498
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d05d4c7cb | |||
| 01f4ccaa0e | |||
| 0bad34d7a8 | |||
| 61d1b44402 | |||
| 72301c63fd | |||
| 609850ab60 | |||
| 8e14d45b71 | |||
| f6ef9af39d | |||
| 72dce83c55 |
@@ -4,6 +4,9 @@ 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
|
||||||
@@ -209,13 +212,283 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload build summary
|
- name: Upload build summary
|
||||||
run: |
|
run: |
|
||||||
Write-Host "## Build Test Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
|
"## Build Test Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
|
||||||
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
|
"========================================" | 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
|
"✓ Pull request build test completed successfully!" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||||
Write-Host "- Version: ${{ steps.get_version.outputs.VERSION }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
"- Version: ${{ steps.get_version.outputs.VERSION }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||||
Write-Host "- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
"- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||||
Write-Host "- Pull Request #${{ github.event.pull_request.number || 'N/A' }}" | 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 "- Branch: ${{ github.event.pull_request.head.ref || github.ref }}" | 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 "- Event: ${{ github.event_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
|
||||||
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
|
||||||
|
|||||||
+311
-10
@@ -1,7 +1,10 @@
|
|||||||
name: Build
|
name: Build
|
||||||
|
|
||||||
# This workflow compiles the application for Windows platform using PyInstaller, and
|
# This workflow compiles the application for Windows and macOS platforms using
|
||||||
# archives the built artifacts as 'AutoLibrary.<tag_name>-windows-x86_64.zip'.
|
# PyInstaller, and archives the built artifacts.
|
||||||
|
#
|
||||||
|
# - Windows: AutoLibrary.<tag_name>-windows-x86_64.zip
|
||||||
|
# - macOS: AutoLibrary.<tag_name>-macos-arm64.dmg
|
||||||
#
|
#
|
||||||
# It is triggered when called by the release workflow.
|
# It is triggered when called by the release workflow.
|
||||||
|
|
||||||
@@ -251,12 +254,310 @@ jobs:
|
|||||||
- name: Upload build summary
|
- name: Upload build summary
|
||||||
if: ${{ inputs.is_test == 'true' }}
|
if: ${{ inputs.is_test == 'true' }}
|
||||||
run: |
|
run: |
|
||||||
Write-Host "## Build Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
|
"## Build Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
|
||||||
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
|
"========================================" | 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
|
"✓ Build test completed successfully!" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||||
Write-Host "- Version: ${{ steps.get_version.outputs.VERSION }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
"- Version: ${{ steps.get_version.outputs.VERSION }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||||
Write-Host "- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
"- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||||
Write-Host "- Event: ${{ github.event_name }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
"- Event: ${{ github.event_name }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
||||||
Write-Host "- Ref: ${{ github.ref }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|
"- 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,8 +21,10 @@ 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 platform using PyInstaller, and
|
# Compiles the application for Windows and macOS platforms using PyInstaller, and
|
||||||
# archives the built artifacts as 'AutoLibrary.<tag_name>-windows-x86_64.zip'.
|
# archives the built artifacts.
|
||||||
|
# - 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
|
||||||
@@ -181,12 +183,18 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download artifacts
|
- name: Download Windows 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
|
||||||
@@ -195,6 +203,7 @@ 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,6 +24,7 @@ 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,16 +10,10 @@ 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 interfaces.ConfigProvider import CfgKey
|
from managers.log.LogManager import instance as logInstance
|
||||||
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(
|
||||||
@@ -70,35 +64,13 @@ 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 -> Appearance
|
LogManager -> ConfigManager -> WebDriverManager
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not _initializeLogManager():
|
if not _initializeLogManager():
|
||||||
@@ -107,5 +79,4 @@ def initializeApp(
|
|||||||
return False
|
return False
|
||||||
if not _initializeWebDriverManager():
|
if not _initializeWebDriverManager():
|
||||||
return False
|
return False
|
||||||
_initializeAppearance()
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ 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,
|
||||||
@@ -22,6 +20,7 @@ from PySide6.QtCore import (
|
|||||||
from PySide6.QtGui import (
|
from PySide6.QtGui import (
|
||||||
QColor,
|
QColor,
|
||||||
QFont,
|
QFont,
|
||||||
|
QIcon,
|
||||||
QSyntaxHighlighter,
|
QSyntaxHighlighter,
|
||||||
QTextCharFormat,
|
QTextCharFormat,
|
||||||
)
|
)
|
||||||
@@ -216,18 +215,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(qta.icon("fa5s.undo", color=self._iconColor()))
|
self.ZoomResetBtn.setIcon(QIcon(":/res/icons/Reset.svg"))
|
||||||
self.ZoomResetBtn.setIconSize(QSize(14, 14))
|
self.ZoomResetBtn.setIconSize(QSize(20, 20))
|
||||||
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.setFixedSize(80, 25)
|
self.OrchBtn.setFixedHeight(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.setFixedSize(80, 25)
|
self.DebugBtn.setFixedHeight(25)
|
||||||
self.DebugBtn.setToolTip("使用右侧模拟数据执行脚本,查看目标变量变化")
|
self.DebugBtn.setToolTip("使用右侧模拟数据执行脚本,查看目标变量变化")
|
||||||
ToolbarLayout.addWidget(self.DebugBtn)
|
ToolbarLayout.addWidget(self.DebugBtn)
|
||||||
Sep = QFrame()
|
Sep = QFrame()
|
||||||
@@ -241,8 +240,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(qta.icon("fa5s.copy", color=self._iconColor()))
|
self.CopyBtn.setIcon(QIcon(":/res/icons/Copy.svg"))
|
||||||
self.CopyBtn.setIconSize(QSize(14, 14))
|
self.CopyBtn.setIconSize(QSize(20, 20))
|
||||||
self.CopyBtn.setFixedSize(25, 25)
|
self.CopyBtn.setFixedSize(25, 25)
|
||||||
self.CopyBtn.setToolTip("复制脚本")
|
self.CopyBtn.setToolTip("复制脚本")
|
||||||
ToolbarLayout.addWidget(self.CopyBtn)
|
ToolbarLayout.addWidget(self.CopyBtn)
|
||||||
@@ -265,9 +264,7 @@ 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(
|
||||||
@@ -540,14 +537,6 @@ 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
|
||||||
):
|
):
|
||||||
|
|||||||
+1
-31
@@ -33,7 +33,6 @@ 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
|
||||||
@@ -61,7 +60,6 @@ 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
|
||||||
@@ -83,7 +81,6 @@ 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:
|
||||||
@@ -128,6 +125,7 @@ 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)
|
||||||
@@ -192,9 +190,6 @@ 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)
|
||||||
|
|
||||||
@@ -307,31 +302,6 @@ 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,
|
||||||
|
|||||||
@@ -1,408 +0,0 @@
|
|||||||
# -*- 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.
@@ -1,553 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 6px;
|
|
||||||
color: #d0daf0;
|
|
||||||
}
|
|
||||||
QMenuBar::item {
|
|
||||||
padding: 4px 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 {
|
|
||||||
/* image: none; */
|
|
||||||
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 {
|
|
||||||
image: none;
|
|
||||||
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: 8px;
|
|
||||||
color: #d0daf0;
|
|
||||||
}
|
|
||||||
QCheckBox::indicator,
|
|
||||||
QRadioButton::indicator {
|
|
||||||
border-style: solid;
|
|
||||||
border-color: #334478;
|
|
||||||
border-width: 2px;
|
|
||||||
border-radius: 3px;
|
|
||||||
background-color: #0a1020;
|
|
||||||
}
|
|
||||||
QCheckBox::indicator:hover,
|
|
||||||
QRadioButton::indicator:hover {
|
|
||||||
border-color: #2dd4bf;
|
|
||||||
}
|
|
||||||
QCheckBox::indicator:checked {
|
|
||||||
background-color: #2dd4bf;
|
|
||||||
border-color: #2dd4bf;
|
|
||||||
}
|
|
||||||
QRadioButton::indicator {
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
border-style: solid;
|
|
||||||
border-color: #253250;
|
|
||||||
border-width: 1px;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-top: 12px;
|
|
||||||
padding-top: 14px;
|
|
||||||
color: #d0daf0;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
QGroupBox::title {
|
|
||||||
subcontrol-origin: margin;
|
|
||||||
left: 12px;
|
|
||||||
padding: 0 6px;
|
|
||||||
color: #8b9ad0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- 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 10px;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
QListWidget::item:selected,
|
|
||||||
QTreeWidget::item:selected,
|
|
||||||
QTableWidget::item:selected {
|
|
||||||
background-color: #2dd4bf;
|
|
||||||
color: #0f1119;
|
|
||||||
}
|
|
||||||
QHeaderView::section {
|
|
||||||
background-color: #0f1628;
|
|
||||||
border: none;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,557 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 6px;
|
|
||||||
color: #1a2740;
|
|
||||||
}
|
|
||||||
QMenuBar::item {
|
|
||||||
padding: 4px 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 {
|
|
||||||
/* image: none; */
|
|
||||||
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 {
|
|
||||||
image: none;
|
|
||||||
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: 8px;
|
|
||||||
color: #1a2740;
|
|
||||||
}
|
|
||||||
QCheckBox::indicator,
|
|
||||||
QRadioButton::indicator {
|
|
||||||
border-style: solid;
|
|
||||||
border-color: #90a4c4;
|
|
||||||
border-width: 2px;
|
|
||||||
border-radius: 3px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
QCheckBox::indicator:hover,
|
|
||||||
QRadioButton::indicator:hover {
|
|
||||||
border-color: #0ea58a;
|
|
||||||
}
|
|
||||||
QCheckBox::indicator:checked {
|
|
||||||
background-color: #0ea58a;
|
|
||||||
border-color: #0ea58a;
|
|
||||||
}
|
|
||||||
QRadioButton::indicator {
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
border-style: solid;
|
|
||||||
border-color: #c0cdda;
|
|
||||||
border-width: 1px;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-top: 12px;
|
|
||||||
padding-top: 14px;
|
|
||||||
color: #1a2740;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
QGroupBox::title {
|
|
||||||
subcontrol-origin: margin;
|
|
||||||
left: 12px;
|
|
||||||
padding: 0 6px;
|
|
||||||
color: #4a6080;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- 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;
|
|
||||||
}
|
|
||||||
QTabBar::tab:hover:!selected {
|
|
||||||
background-color: #d5dde8;
|
|
||||||
color: #1a2740;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- 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 10px;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
QListWidget::item:selected,
|
|
||||||
QTreeWidget::item:selected,
|
|
||||||
QTableWidget::item:selected {
|
|
||||||
background-color: #0ea58a;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
QHeaderView::section {
|
|
||||||
background-color: #dce4ee;
|
|
||||||
border: none;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -281,12 +281,6 @@ 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>
|
||||||
@@ -297,7 +291,6 @@ 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">
|
||||||
@@ -315,11 +308,6 @@ 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/>
|
||||||
|
|||||||
@@ -1,536 +0,0 @@
|
|||||||
<?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>500</height>
|
<height>400</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>350</width>
|
<width>350</width>
|
||||||
<height>500</height>
|
<height>460</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="maximumSize">
|
<property name="maximumSize">
|
||||||
|
|||||||
@@ -66,12 +66,6 @@ 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,11 +54,6 @@ class ConfigTemplate:
|
|||||||
"current": 0,
|
"current": 0,
|
||||||
"paths": []
|
"paths": []
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"appearance": {
|
|
||||||
"theme": "system",
|
|
||||||
"style": "Fusion",
|
|
||||||
"custom_theme": ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case ConfigType.BULLETIN:
|
case ConfigType.BULLETIN:
|
||||||
|
|||||||
@@ -360,6 +360,10 @@ 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,6 +111,10 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -1,321 +0,0 @@
|
|||||||
# -*- 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
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# -*- 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.
|
|
||||||
"""
|
|
||||||
+15
-13
@@ -10,6 +10,7 @@ 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,
|
||||||
@@ -37,11 +38,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 = None
|
self.__driver: WebDriver | None = 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
|
||||||
@@ -58,7 +59,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()
|
||||||
|
|
||||||
@@ -67,9 +68,10 @@ class AutoLib(MsgBase):
|
|||||||
) -> bool:
|
) -> bool:
|
||||||
|
|
||||||
self._showTrace("正在初始化浏览器驱动......", no_log=True)
|
self._showTrace("正在初始化浏览器驱动......", no_log=True)
|
||||||
web_driver_config: dict = self.__run_config.get("web_driver", None)
|
driver_config: dict = self.__run_config.get("web_driver", None)
|
||||||
self.__driver_type = web_driver_config.get("driver_type", "none")
|
self.__driver_type = driver_config.get("driver_type", "none")
|
||||||
match self.__driver_type.lower():
|
self.__driver_type = self.__driver_type.lower()
|
||||||
|
match self.__driver_type:
|
||||||
case "edge":
|
case "edge":
|
||||||
driver_options = webdriver.EdgeOptions()
|
driver_options = webdriver.EdgeOptions()
|
||||||
case "chrome":
|
case "chrome":
|
||||||
@@ -82,10 +84,10 @@ class AutoLib(MsgBase):
|
|||||||
self.TraceLevel.WARNING,
|
self.TraceLevel.WARNING,
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
if not web_driver_config:
|
if not driver_config:
|
||||||
self._showTrace("未配置浏览器驱动参数 !", self.TraceLevel.ERROR)
|
self._showTrace("未配置浏览器驱动参数 !", self.TraceLevel.ERROR)
|
||||||
return False
|
return False
|
||||||
if web_driver_config.get("headless", False):
|
if 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")
|
||||||
@@ -110,11 +112,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.lower() == "edge":
|
if self.__driver_type == "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.lower() == "firefox":
|
elif self.__driver_type == "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) "\
|
||||||
@@ -122,14 +124,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 = web_driver_config.get("driver_path", "")
|
self.__driver_path = 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.lower():
|
match self.__driver_type:
|
||||||
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)
|
||||||
@@ -161,7 +163,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.__driver, tracer=self._showTrace)
|
self.__login_page = LoginPage(self._input_queue, self._output_queue, self.__driver)
|
||||||
self.__driver.set_page_load_timeout(5)
|
self.__driver.set_page_load_timeout(5)
|
||||||
try:
|
try:
|
||||||
self.__driver.get(url)
|
self.__driver.get(url)
|
||||||
|
|||||||
+13
-19
@@ -7,7 +7,8 @@ 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.
|
||||||
"""
|
"""
|
||||||
from typing import Callable, Optional
|
import queue
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
from selenium.common.exceptions import (
|
from selenium.common.exceptions import (
|
||||||
ElementNotInteractableException,
|
ElementNotInteractableException,
|
||||||
@@ -19,8 +20,10 @@ 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")
|
||||||
@@ -36,22 +39,13 @@ class LoginPage:
|
|||||||
|
|
||||||
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,
|
||||||
@@ -185,7 +179,7 @@ class LoginPage:
|
|||||||
) -> bool:
|
) -> bool:
|
||||||
|
|
||||||
for attempt in range(max_attempts):
|
for attempt in range(max_attempts):
|
||||||
self._trace(
|
self._showTrace(
|
||||||
f"用户 {username} 第 {attempt + 1} 次尝试登录......",
|
f"用户 {username} 第 {attempt + 1} 次尝试登录......",
|
||||||
no_log=True,
|
no_log=True,
|
||||||
)
|
)
|
||||||
@@ -196,16 +190,16 @@ class LoginPage:
|
|||||||
continue
|
continue
|
||||||
if not self.fillCaptcha(captcha_text):
|
if not self.fillCaptcha(captcha_text):
|
||||||
continue
|
continue
|
||||||
self._trace("尝试登录...", no_log=True)
|
self._showTrace("尝试登录...", no_log=True)
|
||||||
if not self.clickLogin():
|
if not self.clickLogin():
|
||||||
continue
|
continue
|
||||||
if self.waitLoginSuccess():
|
if self.waitLoginSuccess():
|
||||||
self._trace(f"用户 {username} 第 {attempt + 1} 次登录成功 !")
|
self._showTrace(f"用户 {username} 第 {attempt + 1} 次登录成功 !")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
self._trace(
|
self._showTrace(
|
||||||
"登录页面加载失败 ! : "
|
"登录页面加载失败 ! : "
|
||||||
"用户账号或者密码错误/验证码错误, 具体以页面提示为准",
|
"用户账号或者密码错误/验证码错误, 具体以页面提示为准",
|
||||||
level=40,
|
level=self.TraceLevel.ERROR,
|
||||||
)
|
)
|
||||||
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 execute(
|
def _ensureCheckinButton(
|
||||||
self,
|
self,
|
||||||
username: str,
|
username: str,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@@ -49,7 +49,13 @@ class CheckinFlow(MsgBase):
|
|||||||
self._showTrace(f"签到按钮启用失败 !", self.TraceLevel.ERROR)
|
self._showTrace(f"签到按钮启用失败 !", self.TraceLevel.ERROR)
|
||||||
return False
|
return False
|
||||||
self._showTrace("签到按钮已启用")
|
self._showTrace("签到按钮已启用")
|
||||||
self._shell.clickCheckinButton()
|
return True
|
||||||
|
|
||||||
|
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()
|
||||||
@@ -87,3 +93,13 @@ 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,19 +61,23 @@ class RenewFlow(MsgBase):
|
|||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def execute(
|
def _computeRenewTarget(
|
||||||
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 False
|
return None
|
||||||
|
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
|
||||||
@@ -83,7 +87,17 @@ class RenewFlow(MsgBase):
|
|||||||
f"请连接图书馆网络后重试"
|
f"请连接图书馆网络后重试"
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
self._shell.clickExtendButton()
|
return True
|
||||||
|
|
||||||
|
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():
|
||||||
@@ -132,3 +146,22 @@ 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,
|
||||||
|
)
|
||||||
|
|||||||
+100
-32
@@ -58,59 +58,70 @@ class ReserveFlow(MsgBase):
|
|||||||
self._driver: WebDriver = driver
|
self._driver: WebDriver = driver
|
||||||
self._shell: MainShell = shell
|
self._shell: MainShell = shell
|
||||||
|
|
||||||
def execute(
|
def _loadReserveView(
|
||||||
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 False
|
return None
|
||||||
self._showTrace(f"房间 {ReserveView.ROOM_MAP.get(ctx.room)} 选择成功 !")
|
self._showTrace(f"房间 {ReserveView.ROOM_MAP.get(ctx.room)} 选择成功 !")
|
||||||
have_hover_on_page = True
|
return seat_map
|
||||||
seat_status = seat_map.selectSeat(ctx.seat_id)
|
|
||||||
if seat_status is None:
|
def _processReserveResult(
|
||||||
self._showTrace(
|
self,
|
||||||
f"座位 {ctx.seat_id} 在该楼层区域中不存在, 请检查座位号是否正确",
|
) -> bool:
|
||||||
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)
|
||||||
@@ -131,11 +142,68 @@ class ReserveFlow(MsgBase):
|
|||||||
" 预约成功 !\n"
|
" 预约成功 !\n"
|
||||||
" 未找获取到详细信息"
|
" 未找获取到详细信息"
|
||||||
)
|
)
|
||||||
reserve_success = True
|
return 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:
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
# -*- 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