diff --git a/.github/workflows/macOSBuild.yml b/.github/workflows/macOSBuild.yml index 9c03d618..da9b9138 100644 --- a/.github/workflows/macOSBuild.yml +++ b/.github/workflows/macOSBuild.yml @@ -1,72 +1,374 @@ name: macOS deployment -#on: [push, pull_request] - on: workflow_dispatch: push: - branches: - - master + branches: + - master + tags: + - 'v*' jobs: macos-build: - name: MacOS Build - strategy: - matrix: - os: [macos-12, macos-13] - - runs-on: ${{ matrix.os }} - - steps: - - name: Install Dependencies - run: | - unset HOMEBREW_NO_INSTALL_FROM_API - brew update - brew upgrade || true - brew install qt6 - brew install qt6-webengine - brew link qt6 --force - brew link qt6-webengine --force - brew install hamlib - brew link hamlib --force - brew install qtkeychain - brew install dbus-glib - brew install brotli - brew install icu4c - brew install pkg-config - - name: Checkout Code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Get version from tag - run : | - TAGVERSION=$(git describe --tags) - echo "TAGVERSION=${TAGVERSION:1}" >> $GITHUB_ENV - - - name: Configure and compile - run: | - mkdir build - cd build - qmake -config release .. - make -j4 - - name: Build dmg - run: | - cd build - macdeployqt qlog.app -executable=./qlog.app/Contents/MacOS/qlog - cp `brew --prefix`/lib/libhamlib.dylib qlog.app/Contents/Frameworks/libhamlib.dylib - cp `brew --prefix`/lib/libqt6keychain.dylib qlog.app/Contents/Frameworks/libqt6keychain.dylib - cp `brew --prefix`/lib/libdbus-1.dylib qlog.app/Contents/Frameworks/libdbus-1.dylib - cp `brew --prefix brotli`/lib/libbrotlicommon.1.dylib qlog.app/Contents/Frameworks/libbrotlicommon.1.dylib - cp `brew --prefix`/opt/icu4c/lib/libicui18n.74.dylib qlog.app/Contents/Frameworks/libicui18n.74.dylib - install_name_tool -change `brew --prefix`/lib/libhamlib.dylib @executable_path/../Frameworks/libhamlib.dylib qlog.app/Contents/MacOS/qlog - install_name_tool -change `brew --prefix`/lib/libqt6keychain.dylib @executable_path/../Frameworks/libqt6keychain.dylib qlog.app/Contents/MacOS/qlog - install_name_tool -change @loader_path/libbrotlicommon.1.dylib @executable_path/../Frameworks/libbrotlicommon.1.dylib qlog.app/Contents/MacOS/qlog - install_name_tool -change /usr/local/opt/icu4c/lib/libicui18n.74.dylib @executable_path/../Frameworks/libicui18n.74.dylib qlog.app/Contents/MacOS/qlog - otool -L qlog.app/Contents/MacOS/qlog - macdeployqt qlog.app -dmg - - name: Copy artifact - uses: actions/upload-artifact@v4 - with: - name: QLog-${{ env.TAGVERSION }}-${{ matrix.os }} - path: /Users/runner/work/QLog/QLog/build/qlog.dmg + name: macOS Build + runs-on: macos-latest + + env: + APP_NAME: QLog + BUNDLE_BIN: qlog + QT_PREFIX: /opt/homebrew + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + - name: Install Dependencies + run: | + set -euo pipefail + unset HOMEBREW_NO_INSTALL_FROM_API + brew update + brew upgrade || true + brew install \ + qt6 \ + hamlib \ + qtkeychain \ + dbus-glib \ + brotli \ + icu4c \ + pkg-config \ + autoconf \ + automake \ + libtool \ + libusb + # qt6 is keg-only; many Qt-aware tools rely on the linkage being visible + brew link qt6 --force || true + brew link hamlib --force || true + + - name: Get version from tag + run: | + set -euo pipefail + # If we're on a tag like v0.50.0, use that; otherwise fall back to describe. + if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then + TAGVERSION="${GITHUB_REF#refs/tags/v}" + else + RAW="$(git describe --tags --always 2>/dev/null || echo dev)" + TAGVERSION="${RAW#v}" + fi + echo "TAGVERSION=${TAGVERSION}" >> "$GITHUB_ENV" + echo "Version: ${TAGVERSION}" + + - name: Ensure QtSvg module is enabled + run: | + set -euo pipefail + PRO="qlog.pro" + if ! grep -q ' svg' "$PRO"; then + echo "Adding svg to QT modules" + awk ' + BEGIN { added=0 } + /^[[:space:]]*QT[[:space:]]/ && !added { + print $0 " svg"; added=1; next + } + { print } + ' "$PRO" > "$PRO.tmp" && mv "$PRO.tmp" "$PRO" + else + echo "QtSvg already present" + fi + + - name: Configure and compile + run: | + set -euo pipefail + mkdir -p build + cd build + qmake CONFIG+=release .. + make -j"$(sysctl -n hw.ncpu)" + + - name: Run macdeployqt + run: | + set -euo pipefail + cd build + APP="$APP_NAME.app" + # macdeployqt builds Contents/Frameworks and rewrites Qt links. + macdeployqt "$APP" \ + -always-overwrite \ + -verbose=2 \ + -executable="$APP/Contents/MacOS/$BUNDLE_BIN" \ + -libpath="$QT_PREFIX/opt/qtbase/lib" \ + -libpath="$QT_PREFIX/opt/qtdeclarative/lib" \ + -libpath="$QT_PREFIX/opt/qtwebengine/lib" \ + -libpath="$QT_PREFIX/opt/qtsvg/lib" + + - name: Bundle rigctld and fix linkage + run: | + set -euo pipefail + cd build + APP="$APP_NAME.app" + MACOS_DIR="$APP/Contents/MacOS" + FRAMEWORKS_DIR="$APP/Contents/Frameworks" + mkdir -p "$MACOS_DIR" "$FRAMEWORKS_DIR" + + RIGCTLD_SRC="$(command -v rigctld)" + [ -n "$RIGCTLD_SRC" ] || { echo "ERROR: rigctld not found"; exit 1; } + echo "Using rigctld from: $RIGCTLD_SRC" + cp -f "$RIGCTLD_SRC" "$MACOS_DIR/rigctld" + chmod 0755 "$MACOS_DIR/rigctld" + + install_name_tool -add_rpath "@executable_path/../Frameworks" \ + "$MACOS_DIR/rigctld" 2>/dev/null || true + + # Recursively bundle any /opt/homebrew or /usr/local dependency, fix + # install IDs to @rpath, and rewrite the parent's link. + bundle_deps() { + local target="$1" + local dep base + otool -L "$target" | awk 'NR>1 {print $1}' | while read -r dep; do + case "$dep" in + /opt/homebrew/*|/usr/local/*) + base="$(basename "$dep")" + if [ ! -f "$FRAMEWORKS_DIR/$base" ]; then + echo " -> bundling $base (from $dep)" + cp -f "$dep" "$FRAMEWORKS_DIR/$base" + chmod u+w "$FRAMEWORKS_DIR/$base" + install_name_tool -id "@rpath/$base" "$FRAMEWORKS_DIR/$base" + bundle_deps "$FRAMEWORKS_DIR/$base" + fi + install_name_tool -change "$dep" "@rpath/$base" "$target" + ;; + esac + done + } + + bundle_deps "$MACOS_DIR/rigctld" + bundle_deps "$MACOS_DIR/$BUNDLE_BIN" + + echo "rigctld linkage:" + otool -L "$MACOS_DIR/rigctld" + echo "$BUNDLE_BIN linkage:" + otool -L "$MACOS_DIR/$BUNDLE_BIN" + + - name: Fix QtWebEngineProcess rpaths + run: | + set -euo pipefail + cd build + APP="$APP_NAME.app" + QWEP="$APP/Contents/Frameworks/QtWebEngineCore.framework/Versions/A/Helpers/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess" + [ -f "$QWEP" ] || { echo "QtWebEngineProcess not found at $QWEP"; exit 1; } + + install_name_tool -add_rpath \ + "@executable_path/../../../../Frameworks" "$QWEP" 2>/dev/null || true + + # Rewrite any remaining hardcoded Qt framework references to @rpath. + otool -L "$QWEP" | awk 'NR>1 {print $1}' \ + | grep '^/opt/homebrew/.*\.framework/' \ + | while read -r dep; do + fw="$(basename "$dep")" + fwdir="$(basename "$(dirname "$(dirname "$(dirname "$dep")")")")" + install_name_tool -change "$dep" \ + "@rpath/$fwdir/Versions/A/$fw" "$QWEP" || true + done + + echo "QtWebEngineProcess after fixup:" + otool -L "$QWEP" + + - name: Scan for hardcoded Homebrew paths + run: | + set -euo pipefail + cd build + APP="$APP_NAME.app" + BAD=0 + while IFS= read -r -d '' BIN; do + if otool -L "$BIN" 2>/dev/null \ + | awk 'NR>1 {print $1}' \ + | grep -E '^(/opt/homebrew/|/usr/local/Cellar/|/usr/local/opt/)' >/dev/null; then + echo "Hardcoded Homebrew path in: $BIN" + otool -L "$BIN" | awk 'NR>1 {print " " $1}' \ + | grep -E '^[[:space:]]+(/opt/homebrew/|/usr/local/Cellar/|/usr/local/opt/)' + BAD=1 + fi + done < <(find "$APP" -type f -perm +111 -print0) + if [ "$BAD" -eq 1 ]; then + echo "ERROR: hardcoded Homebrew paths remain. Bundle would fail on user machines." + exit 1 + fi + + - name: Codesign app bundle + env: + MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }} + MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} + MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }} + run: | + set -euo pipefail + APP="$GITHUB_WORKSPACE/build/$APP_NAME.app" + ENT="$GITHUB_WORKSPACE/entitlements.xml" + KEYCHAIN="$RUNNER_TEMP/build.keychain-db" + + test -f "$ENT" || { echo "Missing entitlements.xml at $ENT"; exit 1; } + + security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" "$KEYCHAIN" + security set-keychain-settings -lut 21600 "$KEYCHAIN" + security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" "$KEYCHAIN" + security list-keychains -d user -s "$KEYCHAIN" + security default-keychain -d user -s "$KEYCHAIN" + + echo "$MACOS_CERTIFICATE" | base64 --decode > "$RUNNER_TEMP/cert.p12" + security import "$RUNNER_TEMP/cert.p12" -k "$KEYCHAIN" \ + -P "$MACOS_CERTIFICATE_PWD" \ + -T /usr/bin/codesign -T /usr/bin/security + security set-key-partition-list -S apple-tool:,apple: \ + -s -k "$MACOS_CI_KEYCHAIN_PWD" "$KEYCHAIN" + + security find-identity -v -p codesigning "$KEYCHAIN" || true + + # 1) Sign every Mach-O inside the bundle (no entitlements), skipping + # QtWebEngineProcess which we sign with entitlements below. + while IFS= read -r -d '' F; do + if file "$F" | grep -q "Mach-O"; then + if [[ "$F" == *"/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess" ]]; then + continue + fi + /usr/bin/codesign --force --timestamp --options runtime \ + -s "$MACOS_CERTIFICATE_NAME" "$F" + fi + done < <(find "$APP" -type f -print0) + + # 2) Sign QtWebEngineProcess binaries WITH entitlements + while IFS= read -r -d '' BIN; do + echo "Signing WebEngine helper: $BIN" + /usr/bin/codesign --force --timestamp --options runtime \ + --entitlements "$ENT" \ + -s "$MACOS_CERTIFICATE_NAME" "$BIN" + done < <(find "$APP" -path "*QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess" -type f -print0) + + # 3) Sign QtWebEngineProcess.app bundles WITH entitlements + while IFS= read -r -d '' HAPP; do + echo "Signing WebEngine helper app: $HAPP" + /usr/bin/codesign --force --timestamp --options runtime \ + --entitlements "$ENT" \ + -s "$MACOS_CERTIFICATE_NAME" "$HAPP" + done < <(find "$APP" -path "*QtWebEngineProcess.app" -type d -print0) + + # 4) Re-seal every framework so the framework's seal includes any + # nested helper apps we just re-signed. Without this, signing the + # outer app fails with "nested code is modified or invalid". + while IFS= read -r -d '' FW; do + echo "Re-signing framework: $FW" + /usr/bin/codesign --force --timestamp --options runtime \ + -s "$MACOS_CERTIFICATE_NAME" "$FW" + done < <(find "$APP/Contents/Frameworks" -maxdepth 1 -type d -name "*.framework" -print0) + + # 5) Sign the outer app bundle WITH entitlements + /usr/bin/codesign --force --timestamp --options runtime \ + --entitlements "$ENT" \ + -s "$MACOS_CERTIFICATE_NAME" "$APP" + + /usr/bin/codesign --verify --deep --strict --verbose=2 "$APP" + spctl -a -t exec -vv "$APP" || true + + - name: Notarize app bundle + env: + PROD_MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} + PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} + PROD_MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }} + run: | + set -euo pipefail + APP="$GITHUB_WORKSPACE/build/$APP_NAME.app" + + xcrun notarytool store-credentials "notarytool-profile" \ + --apple-id "$PROD_MACOS_NOTARIZATION_APPLE_ID" \ + --team-id "$PROD_MACOS_NOTARIZATION_TEAM_ID" \ + --password "$PROD_MACOS_NOTARIZATION_PWD" + + ditto -c -k --sequesterRsrc --keepParent "$APP" "notarization.zip" + + xcrun notarytool submit "notarization.zip" \ + --keychain-profile "notarytool-profile" \ + --wait --output-format json > notarization_log.json + cat notarization_log.json + + NOTARY_ID=$(python3 -c 'import json; print(json.load(open("notarization_log.json"))["id"])') + NOTARY_STATUS=$(python3 -c 'import json; print(json.load(open("notarization_log.json"))["status"])') + + xcrun notarytool log "$NOTARY_ID" --keychain-profile "notarytool-profile" || true + + if [ "$NOTARY_STATUS" != "Accepted" ]; then + echo "Notarization failed: $NOTARY_STATUS" + exit 1 + fi + + xcrun stapler staple "$APP" + xcrun stapler validate "$APP" + + - name: Build DMG + run: | + set -euo pipefail + VERSION="${TAGVERSION%%-*}" + DMG_NAME="$APP_NAME.v${VERSION}.dmg" + echo "DMG_NAME=$DMG_NAME" >> "$GITHUB_ENV" + echo "VERSION=$VERSION" >> "$GITHUB_ENV" + + STAGING="$GITHUB_WORKSPACE/build/dmg-staging" + rm -rf "$STAGING" + mkdir -p "$STAGING" + cp -R "$GITHUB_WORKSPACE/build/$APP_NAME.app" "$STAGING/" + ln -s /Applications "$STAGING/Applications" + + hdiutil create \ + -volname "$APP_NAME Installer" \ + -srcfolder "$STAGING" \ + -ov -format UDZO \ + "$GITHUB_WORKSPACE/build/$DMG_NAME" + ls -lh "$GITHUB_WORKSPACE/build/$DMG_NAME" + + - name: Codesign DMG + env: + MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} + MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }} + run: | + set -euo pipefail + DMG="$GITHUB_WORKSPACE/build/$DMG_NAME" + KEYCHAIN="$RUNNER_TEMP/build.keychain-db" + security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" "$KEYCHAIN" + /usr/bin/codesign --force --timestamp \ + -s "$MACOS_CERTIFICATE_NAME" "$DMG" + /usr/bin/codesign --verify --verbose=2 "$DMG" + + - name: Notarize DMG + env: + PROD_MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} + PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} + PROD_MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }} + run: | + set -euo pipefail + DMG="$GITHUB_WORKSPACE/build/$DMG_NAME" + + xcrun notarytool store-credentials "notarytool-profile" \ + --apple-id "$PROD_MACOS_NOTARIZATION_APPLE_ID" \ + --team-id "$PROD_MACOS_NOTARIZATION_TEAM_ID" \ + --password "$PROD_MACOS_NOTARIZATION_PWD" + + xcrun notarytool submit "$DMG" \ + --keychain-profile "notarytool-profile" \ + --wait --output-format json > notarization_dmg.json + cat notarization_dmg.json + + NOTARY_ID=$(python3 -c 'import json; print(json.load(open("notarization_dmg.json"))["id"])') + NOTARY_STATUS=$(python3 -c 'import json; print(json.load(open("notarization_dmg.json"))["status"])') + xcrun notarytool log "$NOTARY_ID" --keychain-profile "notarytool-profile" || true + + if [ "$NOTARY_STATUS" != "Accepted" ]; then + echo "DMG notarization failed: $NOTARY_STATUS" + exit 1 + fi + + xcrun stapler staple "$DMG" + xcrun stapler validate "$DMG" + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: QLog-${{ env.TAGVERSION }}-macos + path: ${{ github.workspace }}/build/${{ env.DMG_NAME }} + if-no-files-found: error diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml new file mode 100644 index 00000000..f13cc4e7 --- /dev/null +++ b/.github/workflows/windows.yml @@ -0,0 +1,343 @@ +# ============================================================ +# QLog Windows Build — GitHub Actions +# +# Orignal from HB9VQQ +# +# Builds QLog for Windows (MSVC 2022 x64) and creates: +# - A portable ZIP (QLog-Portable-Windows) +# - A Qt IFW installer (QLog-Installer-Windows) +# +# Triggers: +# - push to master → build + artifacts (for testing) +# - push a tag v* → build + artifacts + GitHub Release +# - manual via Actions tab +# ============================================================ +name: Windows Build + +on: + push: + branches: [master] + tags: ['v*'] + paths-ignore: + - '*.md' + - 'doc/**' + - 'LICENSE' + - '.gitignore' + workflow_dispatch: # manual trigger button in Actions tab + +env: + QT_VERSION: '6.10.2' + HAMLIB_VERSION: '4.7.1' + +jobs: + build: + runs-on: windows-2022 + timeout-minutes: 60 + + steps: + # —— 1. Checkout source ——————————————————————————————————— + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # —— 2. MSVC 2022 x64 environment ———————————————————————— + - name: Setup MSVC + uses: TheMrMilchmann/setup-msvc-dev@v4 + with: + arch: x64 + + # —— 3. Install Qt 6 with required modules ——————————————— + - name: Install Qt + uses: jurplel/install-qt-action@v4 + with: + version: ${{ env.QT_VERSION }} + host: windows + target: desktop + arch: win64_msvc2022_64 + modules: >- + qtwebengine qtcharts qtserialport + qtwebsockets qtwebchannel qtpositioning + tools: tools_ifw + source: true + src-archives: qtbase + cache: true + + # —— 4. Download Hamlib w64 binary ———————————————————————— + - name: Download Hamlib + shell: pwsh + run: | + $zip = "hamlib-w64-${{ env.HAMLIB_VERSION }}.zip" + $url = "https://github.com/Hamlib/Hamlib/releases/download/${{ env.HAMLIB_VERSION }}/$zip" + Invoke-WebRequest $url -OutFile $zip + Expand-Archive $zip -DestinationPath C:\ + Rename-Item "C:\hamlib-w64-${{ env.HAMLIB_VERSION }}" C:\hamlib + + $msvc = "C:\hamlib\lib\msvc" + if (!(Test-Path $msvc)) { New-Item -ItemType Directory $msvc | Out-Null } + if (!(Test-Path "$msvc\libhamlib-4.lib")) { + $def = Get-ChildItem C:\hamlib -Recurse -Filter "libhamlib-4.def" | Select -First 1 + if ($def) { + $dest = "$msvc\libhamlib-4.def" + if ($def.FullName -ne $dest) { Copy-Item $def.FullName $dest } + Push-Location $msvc + lib /machine:X64 /def:libhamlib-4.def /out:libhamlib-4.lib + Pop-Location + } + } + + # —— 5. pthreads + zlib via fresh vcpkg clone ————————————— + - name: Install vcpkg dependencies + shell: cmd + run: | + git clone --depth 1 https://github.com/microsoft/vcpkg.git C:\vcpkg + cd /d C:\vcpkg + call bootstrap-vcpkg.bat + vcpkg install pthreads:x64-windows zlib:x64-windows openssl:x64-windows + + # —— 6. Build QtKeychain from source —————————————————————— + - name: Build QtKeychain + shell: cmd + run: | + git clone --depth 1 https://github.com/frankosterfeld/qtkeychain.git C:\qtkeychain-src + cd /d C:\qtkeychain-src + cmake -B build -G "NMake Makefiles" ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DBUILD_WITH_QT6=ON ^ + -DCMAKE_PREFIX_PATH="%QT_ROOT_DIR%" ^ + -DCMAKE_INSTALL_PREFIX=C:\qtkeychain + cmake --build build --config Release + cmake --install build + + # —— 7. Locate OpenSSL ———————————————————————————————————— + - name: Locate OpenSSL + shell: pwsh + run: | + $candidates = @( + "C:\Program Files\OpenSSL-Win64", + "C:\Program Files\OpenSSL", + "C:\OpenSSL-Win64" + ) + $ssl = $candidates | Where-Object { Test-Path $_ } | Select -First 1 + if (!$ssl) { + choco install openssl -y --no-progress + $ssl = $candidates | Where-Object { Test-Path $_ } | Select -First 1 + } + if ($ssl) { + echo "OPENSSL_DIR=$ssl" >> $env:GITHUB_ENV + } else { + echo "OPENSSL_DIR=" >> $env:GITHUB_ENV + } + + # —— 7c. Install OmniRig v1 + v2 ————————————————————————— + - name: Install OmniRig + shell: pwsh + run: | + # --- OmniRig v1: InnoSetup installer (works silently) --- + Write-Host "=== OmniRig v1 ===" + Invoke-WebRequest "http://www.dxatlas.com/OmniRig/Files/OmniRig.zip" -OutFile OmniRig.zip + Expand-Archive OmniRig.zip -DestinationPath C:\omnirig-v1-tmp + $setup = Get-ChildItem C:\omnirig-v1-tmp -Recurse -Filter "OmniRigSetup.exe" | Select -First 1 + if ($setup) { + Start-Process -FilePath $setup.FullName -ArgumentList "/VERYSILENT","/SUPPRESSMSGBOXES","/NORESTART" -Wait + Start-Sleep -Seconds 2 + } + $v1path = "C:\Program Files (x86)\Afreet\OmniRig\OmniRig.exe" + if (Test-Path $v1path) { + Write-Host "v1 OK: $v1path" + } else { + Write-Error "v1 FAILED" + } + + # --- OmniRig v2: try installer with timeout, fall back to v1 .tlb --- + Write-Host "=== OmniRig v2 ===" + $v2installed = $false + Invoke-WebRequest "https://www.hb9ryz.ch/downloads/install_omnirigv21.zip" -OutFile omnirigv2.zip + Expand-Archive omnirigv2.zip -DestinationPath C:\omnirig-v2-tmp + + $installer = Get-ChildItem C:\omnirig-v2-tmp -Recurse -Filter "*.exe" | Select -First 1 + if ($installer) { + Write-Host "Trying /S (NSIS) with 30s timeout..." + $proc = Start-Process -FilePath $installer.FullName -ArgumentList "/S" -PassThru + $finished = $proc.WaitForExit(30000) + if (!$finished) { + Write-Host "Installer timed out — killing" + $proc.Kill() + } + $v2path = "C:\Program Files (x86)\Omni-Rig V2\omnirig2.exe" + if (Test-Path $v2path) { + Write-Host "v2 installed via /S" + $v2installed = $true + } + } + + if (!$v2installed) { + # Fallback: use v1 .tlb and patch source to remove v2-only features + Write-Host "v2 installer failed — using v1 .tlb fallback with Rig3/Rig4 patch" + Invoke-WebRequest "https://raw.githubusercontent.com/VE3NEA/OmniRig/master/OmniRig.tlb" -OutFile "C:\omnirig-v1.tlb" + + $v2file = "rig\drivers\Omnirigv2RigDrv.cpp" + $content = Get-Content $v2file -Raw + + # Patch #import to use v1 .tlb + $content = $content.Replace( + 'C:\\Program Files (x86)\\Omni-Rig V2\\omnirig2.exe', + 'C:\\omnirig-v1.tlb') + + # Remove get_Rig3/get_Rig4 calls (v1 only has Rig1+Rig2) + # Change case 3/4 to fall through to default (E_INVALIDARG) + $content = $content.Replace( + 'case 3: hr = omniInterface->get_Rig3(&rig); break;', + 'case 3: /* Rig3 not available in v1 fallback */') + $content = $content.Replace( + 'case 4: hr = omniInterface->get_Rig4(&rig); break;', + 'case 4: /* Rig4 not available in v1 fallback */') + + Set-Content $v2file $content -NoNewline + + Write-Host "Patched v2 source:" + Select-String '#import' $v2file | ForEach-Object { $_.Line.Trim() } + Select-String 'case 3:|case 4:' $v2file | ForEach-Object { $_.Line.Trim() } + } + + # —— 8. Build QLog ———————————————————————————————————————— + - name: Build QLog + shell: cmd + run: | + set "QTKC_INC=C:\qtkeychain\include" + set "VCPKG_INC=C:\vcpkg\installed\x64-windows\include" + set "VCPKG_LIB=C:\vcpkg\installed\x64-windows\lib" + + mkdir build + cd build + qmake ..\QLog.pro -spec win32-msvc ^ + "CONFIG+=release" ^ + "HAMLIBINCLUDEPATH=C:\hamlib\include" ^ + "HAMLIBLIBPATH=C:\hamlib\lib\msvc" ^ + "HAMLIBVERSION_MAJOR=4" ^ + "HAMLIBVERSION_MINOR=7" ^ + "HAMLIBVERSION_PATCH=1" ^ + "QTKEYCHAININCLUDEPATH=%QTKC_INC%" ^ + "QTKEYCHAINLIBPATH=C:\qtkeychain\lib" ^ + "PTHREADINCLUDEPATH=%VCPKG_INC%" ^ + "PTHREADLIBPATH=%VCPKG_LIB%" ^ + "ZLIBINCLUDEPATH=%VCPKG_INC%" ^ + "ZLIBLIBPATH=%VCPKG_LIB%" ^ + "OPENSSLINCLUDEPATH=%VCPKG_INC%" ^ + "OPENSSLLIBPATH=%VCPKG_LIB%" + nmake + + # —— 9. Package with windeployqt —————————————————————————— + - name: Deploy + shell: pwsh + run: | + $deploy = "C:\qlog-deploy" + New-Item -ItemType Directory $deploy -Force | Out-Null + + $exe = Get-ChildItem build -Recurse -Filter "qlog.exe" | Select -First 1 + if (!$exe) { Write-Error "qlog.exe not found!"; exit 1 } + Copy-Item $exe.FullName $deploy\ + + # Copy qt6keychain.dll to Qt bin dir so windeployqt can resolve it + $qtBin = Join-Path $env:QT_ROOT_DIR "bin" + Copy-Item C:\qtkeychain\bin\qt6keychain.dll "$qtBin\" -Force -ErrorAction SilentlyContinue + Copy-Item C:\qtkeychain\lib\qt6keychain.dll "$qtBin\" -Force -ErrorAction SilentlyContinue + + Copy-Item C:\hamlib\bin\*.dll $deploy\ + Copy-Item C:\qtkeychain\bin\*.dll $deploy\ -ErrorAction SilentlyContinue + Copy-Item C:\qtkeychain\lib\*.dll $deploy\ -ErrorAction SilentlyContinue + + $vcpkgBin = "C:\vcpkg\installed\x64-windows\bin" + if (Test-Path $vcpkgBin) { + Copy-Item "$vcpkgBin\*.dll" $deploy\ -ErrorAction SilentlyContinue + } + + if ($env:OPENSSL_DIR -and (Test-Path $env:OPENSSL_DIR)) { + $sslBin = Join-Path $env:OPENSSL_DIR "bin" + if (Test-Path $sslBin) { + Copy-Item "$sslBin\libssl*.dll" $deploy\ -ErrorAction SilentlyContinue + Copy-Item "$sslBin\libcrypto*.dll" $deploy\ -ErrorAction SilentlyContinue + } + } + + Push-Location $deploy + windeployqt --release --no-translations qlog.exe + Pop-Location + + Write-Host "Deploy contents:" + Get-ChildItem $deploy | Format-Table Name, Length + + # —— 10. Create Qt IFW installer —————————————————————————— + - name: Create Installer + shell: pwsh + run: | + $bc = $null + if ($env:IQTA_TOOLS) { + $bc = Get-ChildItem $env:IQTA_TOOLS -Recurse -Filter "binarycreator.exe" -ErrorAction SilentlyContinue | Select -First 1 + } + if (!$bc) { + $bc = Get-ChildItem "$env:RUNNER_TOOL_CACHE" -Recurse -Filter "binarycreator.exe" -ErrorAction SilentlyContinue | Select -First 1 + } + if (!$bc) { + Write-Warning "binarycreator not found — skipping installer" + exit 0 + } + + Copy-Item installer -Destination installer-build -Recurse + $pkgData = "installer-build\packages\de.dl2ic.qlog\data" + New-Item -ItemType Directory $pkgData -Force | Out-Null + Copy-Item C:\qlog-deploy\* $pkgData\ -Recurse + + & $bc.FullName -f ` + -c installer-build\config\config.xml ` + -p installer-build\packages ` + qlog-installer.exe + + if (Test-Path qlog-installer.exe) { + Write-Host "Installer created: qlog-installer.exe" + } + + # —— 11. Create portable ZIP —————————————————————————————— + - name: Create Portable ZIP + if: startsWith(github.ref, 'refs/tags/v') + shell: pwsh + run: | + $tag = "${{ github.ref_name }}" + Compress-Archive -Path C:\qlog-deploy\* -DestinationPath "QLog-Portable-Windows-${tag}.zip" + Write-Host "Portable ZIP: QLog-Portable-Windows-${tag}.zip" + + # —— 12. Upload artifacts (always — for testing) —————————— + - name: Upload Installer + if: ${{ hashFiles('qlog-installer.exe') != '' }} + uses: actions/upload-artifact@v4 + with: + name: QLog-Installer-Windows + path: qlog-installer.exe + + - name: Upload Portable + uses: actions/upload-artifact@v4 + with: + name: QLog-Portable-Windows + path: C:\qlog-deploy\ + + # —— 13. Create GitHub Release (only on tag push) ————————— + - name: Create GitHub Release + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v2 + with: + name: "QLog ${{ github.ref_name }}" + body: | + ## QLog ${{ github.ref_name }} + + **Downloads:** + - **Installer** (recommended) — run `qlog-installer.exe` + - **Portable ZIP** — extract anywhere and run `qlog.exe` + + Built with Qt ${{ env.QT_VERSION }}, Hamlib ${{ env.HAMLIB_VERSION }}, MSVC 2022 x64. + draft: false + prerelease: false + files: | + qlog-installer.exe + QLog-Portable-Windows-${{ github.ref_name }}.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 1f63de72..d68f1a07 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,14 @@ *.so *.dll *.dylib - *.patch +devtools/timezones/builder/builder +devtools/timezones/builder/db.zip +devtools/timezones/builder/out/ +devtools/timezones/builder/out_v1/ +.claude/ +AGENT.md +CLAUDE.md # Qt-es object_script.*.Release diff --git a/Changelog b/Changelog index 9a1effae..6d0e4476 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,30 @@ +TBC - 0.51.0 +- [NEW] - Added a simple QSL Card printing +- [NEW] - Added virtual column Mode/Submode to Logbook (issue #798) +- [NEW] - BandMap shows IBP frequencies (issue #1014) +- [NEW] - BandMap - Added Bandmap Guide (issue #1039) +- [NEW] - BandMap - Tune rig from IBP and SOS markers +- [NEW] - Settings - Added Bandmap Guide Setting +- [NEW] - Settings - Added user defined colors for DXCC Statuses +- [NEW] - Settings - Added StartUp ADI file Import (issue #941 PR #992 @aa5sh @foldynl) +- [NEW] - Rig Widget - Added Emergency, IBP and Bandmap Guide Labels +- [NEW} - Activity Manager - Added Bandmap Guide selection +- [NEW] - Import - Added QSL Sent Status default (issue #1029) +- [NEW] - Awards - Added WAAC and WAIP (issue #1013 PR #1022 @aa5sh) +- [NEW] - Awards - Added Open Rule Button +- [NEW] - Download - Added LoTW DXCC credits import (based on PR #965 @aa5sh @foldynl) +- [NEW] - Online Map - Added civil, nautical, and astronomical twilight +- [CHANGED] - Added more built-in macOS TQSL search paths (PR #1041 @aa5sh) +- [CHANGED] - Clock - Improved timing +- [CHANGED] - IBP - Updated Beacon statuses, enabled ZL6B 5Z4B, disabled 4S7B OH2B YV5B +- Workaround for #1017: FT-950 was reported to jump to VFO-B +- Fixed ADIF Export of Gridsqaure too long (issue #1028) +- Fixed Shutdown: synchronize station-device teardown (PR #1035 @VA3THP @foldynl) +- Fixed QRZ callbook lookups stall for certain calls (issue #1040) +- Fixed ADIF multiline fields are incorrectly parsed (issue #1046) +- Fixed ADX app-fields and grid normalization +- Github - Source code is signed (#1018) + 2026/04/26 - 0.50.0 - [NEW] - Added Split detection - [NEW] - Added Developer and Support tools (PR #991 @aa5sh @foldynl) diff --git a/QLog.pro b/QLog.pro index 508cdc1f..be890b2f 100644 --- a/QLog.pro +++ b/QLog.pro @@ -10,7 +10,7 @@ greaterThan(QT_MAJOR_VERSION, 5): QT += widgets TARGET = qlog TEMPLATE = app -VERSION = 0.50.0 +VERSION = 0.51.0dev DEFINES += VERSION=\\\"$$VERSION\\\" @@ -69,6 +69,8 @@ SOURCES += \ awards/AwardPOTAActivator.cpp \ awards/AwardPOTAHunter.cpp \ awards/AwardSOTA.cpp \ + awards/AwardWAAC.cpp \ + awards/AwardWAIP.cpp \ awards/SecondarySubdivisionAward.cpp \ awards/AwardWAC.cpp \ awards/AwardWAS.cpp \ @@ -77,6 +79,7 @@ SOURCES += \ awards/AwardWWFF.cpp \ awards/BandTableAward.cpp \ core/AlertEvaluator.cpp \ + core/AdifRecovery.cpp \ core/AppGuard.cpp \ core/CallbookManager.cpp \ core/CredentialStore.cpp \ @@ -99,6 +102,7 @@ SOURCES += \ core/WsjtxUDPReceiver.cpp \ core/debug.cpp \ core/EmergencyFrequency.cpp \ + core/IBPBeacon.cpp \ core/main.cpp \ core/zonedetect.c \ cwkey/CWKeyer.cpp \ @@ -110,6 +114,7 @@ SOURCES += \ cwkey/drivers/CWWinKey.cpp \ data/ActivityProfile.cpp \ data/AntProfile.cpp \ + data/BandmapGuide.cpp \ data/BandPlan.cpp \ data/Accents.cpp \ data/CWKeyProfile.cpp \ @@ -158,6 +163,7 @@ SOURCES += \ service/GenericCallbook.cpp \ service/GenericQSLDownloader.cpp \ service/GenericQSOUploader.cpp \ + service/emailqsl/EmailQSLService.cpp \ service/cloudlog/Cloudlog.cpp \ service/clublog/ClubLog.cpp \ service/eqsl/Eqsl.cpp \ @@ -168,12 +174,14 @@ SOURCES += \ service/potaapp/PotaApp.cpp \ service/qrzcom/QRZ.cpp \ ui/ActivityEditor.cpp \ + ui/AdifRecoveryManager.cpp \ ui/AlertRuleDetail.cpp \ ui/AlertSettingDialog.cpp \ ui/AlertWidget.cpp \ ui/AwardsDialog.cpp \ ui/DXCCSubmissionDialog.cpp \ ui/BandmapWidget.cpp \ + ui/BandmapGuideDialog.cpp \ ui/CWConsoleWidget.cpp \ ui/ChatWidget.cpp \ ui/ClockWidget.cpp \ @@ -200,11 +208,13 @@ SOURCES += \ ui/KSTHighlighterSettingDialog.cpp \ ui/LogbookWidget.cpp \ ui/MainWindow.cpp \ - ui/MapWebChannelHandler.cpp \ + ui/MapPageController.cpp \ ui/MapWidget.cpp \ ui/ModeSelectionController.cpp \ ui/NewContactWidget.cpp \ ui/OnlineMapWidget.cpp \ + ui/EmailQSLDialog.cpp \ + ui/EmailQSLSettingsWidget.cpp \ ui/PaperQSLDialog.cpp \ ui/ProfileImageWidget.cpp \ ui/QSLImportStatDialog.cpp \ @@ -222,8 +232,11 @@ SOURCES += \ ui/WsjtxFilterDialog.cpp \ ui/WsjtxWidget.cpp \ ui/component/BaseDoubleSpinBox.cpp \ + ui/component/CardEditorWidget.cpp \ ui/component/EditLine.cpp \ ui/component/FreqQSpinBox.cpp \ + ui/component/ModeSubmodeDelegate.cpp \ + ui/component/LogbookFieldComboBox.cpp \ ui/component/MultiselectCompleter.cpp \ ui/component/RepeatButton.cpp \ ui/component/SmartSearchBox.cpp \ @@ -245,6 +258,8 @@ HEADERS += \ awards/AwardPOTAActivator.h \ awards/AwardPOTAHunter.h \ awards/AwardSOTA.h \ + awards/AwardWAAC.h \ + awards/AwardWAIP.h \ awards/SecondarySubdivisionAward.h \ awards/AwardWAC.h \ awards/AwardWAS.h \ @@ -253,6 +268,7 @@ HEADERS += \ awards/AwardWWFF.h \ awards/BandTableAward.h \ core/AlertEvaluator.h \ + core/AdifRecovery.h \ core/AppGuard.h \ core/CallbookManager.h \ core/CredentialStore.h \ @@ -277,6 +293,7 @@ HEADERS += \ core/csv.hpp \ core/debug.h \ core/EmergencyFrequency.h \ + core/IBPBeacon.h \ core/zonedetect.h \ cwkey/CWKeyer.h \ cwkey/drivers/CWCatKey.h \ @@ -288,6 +305,7 @@ HEADERS += \ data/ActivityProfile.h \ data/AntProfile.h \ data/Band.h \ + data/BandmapGuide.h \ data/BandPlan.h \ data/CWKeyProfile.h \ data/CWShortcutProfile.h \ @@ -352,6 +370,7 @@ HEADERS += \ service/GenericCallbook.h \ service/GenericQSLDownloader.h \ service/GenericQSOUploader.h \ + service/emailqsl/EmailQSLService.h \ service/cloudlog/Cloudlog.h \ service/clublog/ClubLog.h \ service/eqsl/Eqsl.h \ @@ -362,12 +381,14 @@ HEADERS += \ service/potaapp/PotaApp.h \ service/qrzcom/QRZ.h \ ui/ActivityEditor.h \ + ui/AdifRecoveryManager.h \ ui/AlertRuleDetail.h \ ui/AlertSettingDialog.h \ ui/AlertWidget.h \ ui/AwardsDialog.h \ ui/DXCCSubmissionDialog.h \ ui/BandmapWidget.h \ + ui/BandmapGuideDialog.h \ ui/CWConsoleWidget.h \ ui/ChatWidget.h \ ui/ClockWidget.h \ @@ -375,6 +396,8 @@ HEADERS += \ ui/DevToolsDialog.h \ ui/DownloadQSLDialog.h \ ui/DxFilterDialog.h \ + ui/EmailQSLDialog.h \ + ui/EmailQSLSettingsWidget.h \ ui/DxWidget.h \ ui/DxccTableWidget.h \ ui/EditActivitiesDialog.h \ @@ -394,7 +417,8 @@ HEADERS += \ ui/KSTHighlighterSettingDialog.h \ ui/LogbookWidget.h \ ui/MainWindow.h \ - ui/MapWebChannelHandler.h \ + ui/MapLayer.h \ + ui/MapPageController.h \ ui/MapWidget.h \ ui/ModeSelectionController.h \ ui/NewContactWidget.h \ @@ -420,8 +444,11 @@ HEADERS += \ i18n/datastrings.tri \ ui/component/BaseDoubleSpinBox.h \ ui/component/ButtonStyle.h \ + ui/component/CardEditorWidget.h \ ui/component/EditLine.h \ ui/component/FreqQSpinBox.h \ + ui/component/ModeSubmodeDelegate.h \ + ui/component/LogbookFieldComboBox.h \ ui/component/MultiselectCompleter.h \ ui/component/RepeatButton.h \ ui/component/ShutdownAwareWidget.h \ @@ -438,6 +465,7 @@ FORMS += \ ui/AwardsDialog.ui \ ui/DXCCSubmissionDialog.ui \ ui/BandmapWidget.ui \ + ui/BandmapGuideDialog.ui \ ui/CWConsoleWidget.ui \ ui/ChatWidget.ui \ ui/ClockWidget.ui \ @@ -446,6 +474,8 @@ FORMS += \ ui/DevToolsDialog.ui \ ui/DownloadQSLDialog.ui \ ui/DxFilterDialog.ui \ + ui/EmailQSLDialog.ui \ + ui/EmailQSLSettingsWidget.ui \ ui/DxWidget.ui \ ui/EditActivitiesDialog.ui \ ui/CabrilloExportDialog.ui \ @@ -620,7 +650,17 @@ macx: { LIBS += -L/usr/local/lib -L/opt/homebrew/lib -lhamlib -lsqlite3 -lz -L/opt/local/lib -lssl -lcrypto equals(QT_MAJOR_VERSION, 6): LIBS += -lqt6keychain equals(QT_MAJOR_VERSION, 5): LIBS += -lqt5keychain - DISTFILES += + + # Custom Info.plist — sets a real bundle identifier + # (io.github.foldynl.qlog) and declares NSLocalNetworkUsageDescription + # so macOS 14+ will actually prompt for / grant LAN access (needed + # for WSJT-X UDP, network rigs, rotators, etc.). Without this, qmake + # generates a default Info.plist with CFBundleIdentifier = + # com.yourcompany.qlog and no local-network key. + QMAKE_INFO_PLIST = $$PWD/res/macos/Info.plist + QMAKE_TARGET_BUNDLE_PREFIX = io.github.foldynl + + DISTFILES += res/macos/Info.plist } win32: { diff --git a/awards/AwardDXCC.cpp b/awards/AwardDXCC.cpp index f7c7dc32..36c7583b 100644 --- a/awards/AwardDXCC.cpp +++ b/awards/AwardDXCC.cpp @@ -6,6 +6,11 @@ QString AwardDXCC::displayName() const return QCoreApplication::translate("AwardsDialog", "DXCC"); } +QString AwardDXCC::rulesUrl() const +{ + return QStringLiteral("https://www.arrl.org/dxcc-rules"); +} + QString AwardDXCC::headersColumns(const QString &) const { return QStringLiteral("translate_to_locale(d.name) col1, d.prefix col2 "); diff --git a/awards/AwardDXCC.h b/awards/AwardDXCC.h index e5665668..a8e4e0a0 100644 --- a/awards/AwardDXCC.h +++ b/awards/AwardDXCC.h @@ -8,6 +8,7 @@ class AwardDXCC : public BandTableAward public: QString key() const override { return QStringLiteral("dxcc"); } QString displayName() const override; + QString rulesUrl() const override; protected: QString headersColumns(const QString &entity) const override; diff --git a/awards/AwardDefinition.cpp b/awards/AwardDefinition.cpp index 875d073a..bff93b07 100644 --- a/awards/AwardDefinition.cpp +++ b/awards/AwardDefinition.cpp @@ -10,6 +10,11 @@ bool AwardDefinition::notWorkedEnabled() const return true; } +QString AwardDefinition::rulesUrl() const +{ + return QString(); +} + AwardDefinition::ConditionResult AwardDefinition::getConditionSelected(const QModelIndex &) const { return ConditionResult(); diff --git a/awards/AwardDefinition.h b/awards/AwardDefinition.h index 4f9aa234..192a8de5 100644 --- a/awards/AwardDefinition.h +++ b/awards/AwardDefinition.h @@ -37,6 +37,9 @@ class AwardDefinition /*Translatable display name shown in the Award combo box. */ virtual QString displayName() const = 0; + /* URL with award rules/conditions. Empty means no known rules URL. */ + virtual QString rulesUrl() const; + /*Whether the "My DXCC Entity" combo is shown. Default: true. */ virtual bool entityInputEnabled() const; diff --git a/awards/AwardGridsquare.cpp b/awards/AwardGridsquare.cpp index 128437d6..78b67dc7 100644 --- a/awards/AwardGridsquare.cpp +++ b/awards/AwardGridsquare.cpp @@ -22,6 +22,11 @@ QString AwardGridsquare::displayName() const } } +QString AwardGridsquare::rulesUrl() const +{ + return {}; +} + QString AwardGridsquare::headersColumns(const QString &) const { return QString("substr(c.gridsquare, 1, %1) col1, NULL col2 ").arg(m_chars); diff --git a/awards/AwardGridsquare.h b/awards/AwardGridsquare.h index 9c753e5e..aa1fbd6d 100644 --- a/awards/AwardGridsquare.h +++ b/awards/AwardGridsquare.h @@ -10,6 +10,7 @@ class AwardGridsquare : public BandTableAward QString key() const override; QString displayName() const override; + QString rulesUrl() const override; bool notWorkedEnabled() const override { return false; } protected: diff --git a/awards/AwardIOTA.cpp b/awards/AwardIOTA.cpp index 1cdb7acb..086b09bf 100644 --- a/awards/AwardIOTA.cpp +++ b/awards/AwardIOTA.cpp @@ -6,6 +6,11 @@ QString AwardIOTA::displayName() const return QCoreApplication::translate("AwardsDialog", "IOTA"); } +QString AwardIOTA::rulesUrl() const +{ + return QStringLiteral("https://www.iota-world.org/info/directory/rules-en.pdf"); +} + QString AwardIOTA::headersColumns(const QString &) const { return QStringLiteral("c.iota col1, NULL col2 "); diff --git a/awards/AwardIOTA.h b/awards/AwardIOTA.h index 862ff82a..86451068 100644 --- a/awards/AwardIOTA.h +++ b/awards/AwardIOTA.h @@ -8,6 +8,7 @@ class AwardIOTA : public BandTableAward public: QString key() const override { return QStringLiteral("iota"); } QString displayName() const override; + QString rulesUrl() const override; bool notWorkedEnabled() const override { return false; } protected: diff --git a/awards/AwardITU.cpp b/awards/AwardITU.cpp index f8f65980..4112ced2 100644 --- a/awards/AwardITU.cpp +++ b/awards/AwardITU.cpp @@ -6,6 +6,11 @@ QString AwardITU::displayName() const return QCoreApplication::translate("AwardsDialog", "ITU"); } +QString AwardITU::rulesUrl() const +{ + return QStringLiteral("https://www.rsgbshop.org/acatalog/PDF/Worked_ITU_Zones_Award_Information.pdf"); +} + QString AwardITU::headersColumns(const QString &) const { return QStringLiteral("d.n col1, null col2 "); diff --git a/awards/AwardITU.h b/awards/AwardITU.h index c3c7d5b5..12846049 100644 --- a/awards/AwardITU.h +++ b/awards/AwardITU.h @@ -8,6 +8,7 @@ class AwardITU : public BandTableAward public: QString key() const override { return QStringLiteral("itu"); } QString displayName() const override; + QString rulesUrl() const override; protected: QString headersColumns(const QString &entity) const override; diff --git a/awards/AwardJapan.cpp b/awards/AwardJapan.cpp index 6d07b7a7..0805857b 100644 --- a/awards/AwardJapan.cpp +++ b/awards/AwardJapan.cpp @@ -2,7 +2,9 @@ #include "AwardJapan.h" AwardJapan::AwardJapan() - : SecondarySubdivisionAward(QStringLiteral("japan"), QStringLiteral("339")) + : SecondarySubdivisionAward(QStringLiteral("japan"), + QStringLiteral("339"), + QStringLiteral("https://www.jarl.org/English/4_Library/A-4-2_Awards/Award_Main.htm")) { } diff --git a/awards/AwardNZ.cpp b/awards/AwardNZ.cpp index 0cefef26..fe9c189a 100644 --- a/awards/AwardNZ.cpp +++ b/awards/AwardNZ.cpp @@ -2,7 +2,9 @@ #include "AwardNZ.h" AwardNZ::AwardNZ() - : SecondarySubdivisionAward(QStringLiteral("nz"), QStringLiteral("170")) + : SecondarySubdivisionAward(QStringLiteral("nz"), + QStringLiteral("170"), + QStringLiteral("https://nzart.org.nz/activities/awards/")) { } diff --git a/awards/AwardPOTAActivator.cpp b/awards/AwardPOTAActivator.cpp index 74db90da..085bed34 100644 --- a/awards/AwardPOTAActivator.cpp +++ b/awards/AwardPOTAActivator.cpp @@ -6,6 +6,11 @@ QString AwardPOTAActivator::displayName() const return QCoreApplication::translate("AwardsDialog", "POTA Activator"); } +QString AwardPOTAActivator::rulesUrl() const +{ + return QStringLiteral("https://docs.pota.app/docs/awards.html"); +} + QString AwardPOTAActivator::headersColumns(const QString &) const { return QStringLiteral("p.reference col1, p.name col2 "); diff --git a/awards/AwardPOTAActivator.h b/awards/AwardPOTAActivator.h index b5a414b3..2e705bc7 100644 --- a/awards/AwardPOTAActivator.h +++ b/awards/AwardPOTAActivator.h @@ -8,6 +8,7 @@ class AwardPOTAActivator : public BandTableAward public: QString key() const override { return QStringLiteral("potaa"); } QString displayName() const override; + QString rulesUrl() const override; bool entityInputEnabled() const override { return false; } bool notWorkedEnabled() const override { return false; } diff --git a/awards/AwardPOTAHunter.cpp b/awards/AwardPOTAHunter.cpp index e0190107..ee7ff05f 100644 --- a/awards/AwardPOTAHunter.cpp +++ b/awards/AwardPOTAHunter.cpp @@ -6,6 +6,11 @@ QString AwardPOTAHunter::displayName() const return QCoreApplication::translate("AwardsDialog", "POTA Hunter"); } +QString AwardPOTAHunter::rulesUrl() const +{ + return QStringLiteral("https://docs.pota.app/docs/awards.html"); +} + QString AwardPOTAHunter::headersColumns(const QString &) const { return QStringLiteral("p.reference col1, p.name col2 "); diff --git a/awards/AwardPOTAHunter.h b/awards/AwardPOTAHunter.h index 1d28ecac..c4d53516 100644 --- a/awards/AwardPOTAHunter.h +++ b/awards/AwardPOTAHunter.h @@ -8,6 +8,7 @@ class AwardPOTAHunter : public BandTableAward public: QString key() const override { return QStringLiteral("potah"); } QString displayName() const override; + QString rulesUrl() const override; bool entityInputEnabled() const override { return false; } bool notWorkedEnabled() const override { return false; } diff --git a/awards/AwardRDA.cpp b/awards/AwardRDA.cpp index f127e8c9..d300f528 100644 --- a/awards/AwardRDA.cpp +++ b/awards/AwardRDA.cpp @@ -2,7 +2,9 @@ #include "AwardRDA.h" AwardRDA::AwardRDA() - : SecondarySubdivisionAward(QStringLiteral("rda"), QStringLiteral("15, 54, 61, 126, 151")) + : SecondarySubdivisionAward(QStringLiteral("rda"), + QStringLiteral("15, 54, 61, 126, 151"), + QStringLiteral("https://rdaward.org/rda_rules_eng.htm")) { } diff --git a/awards/AwardSOTA.cpp b/awards/AwardSOTA.cpp index fe87e24a..1619eb82 100644 --- a/awards/AwardSOTA.cpp +++ b/awards/AwardSOTA.cpp @@ -6,6 +6,11 @@ QString AwardSOTA::displayName() const return QCoreApplication::translate("AwardsDialog", "SOTA"); } +QString AwardSOTA::rulesUrl() const +{ + return QStringLiteral("https://www.sota.org.uk/Joining-In/General-Rules"); +} + QString AwardSOTA::headersColumns(const QString &) const { return QStringLiteral("s.summit_code col1, NULL col2 "); diff --git a/awards/AwardSOTA.h b/awards/AwardSOTA.h index 0464d656..cebf43b9 100644 --- a/awards/AwardSOTA.h +++ b/awards/AwardSOTA.h @@ -8,6 +8,7 @@ class AwardSOTA : public BandTableAward public: QString key() const override { return QStringLiteral("sota"); } QString displayName() const override; + QString rulesUrl() const override; bool entityInputEnabled() const override { return false; } bool notWorkedEnabled() const override { return false; } diff --git a/awards/AwardSpanishDME.cpp b/awards/AwardSpanishDME.cpp index 3f68e687..dd4e273c 100644 --- a/awards/AwardSpanishDME.cpp +++ b/awards/AwardSpanishDME.cpp @@ -2,7 +2,9 @@ #include "AwardSpanishDME.h" AwardSpanishDME::AwardSpanishDME() - : SecondarySubdivisionAward(QStringLiteral("spanishdme"), QStringLiteral("21, 29, 32, 281")) + : SecondarySubdivisionAward(QStringLiteral("spanishdme"), + QStringLiteral("21, 29, 32, 281"), + QStringLiteral("https://www.ure.es/dme-award-english-version/")) { } diff --git a/awards/AwardUKD.cpp b/awards/AwardUKD.cpp index 0806d08a..6603f0bb 100644 --- a/awards/AwardUKD.cpp +++ b/awards/AwardUKD.cpp @@ -2,7 +2,9 @@ #include "AwardUKD.h" AwardUKD::AwardUKD() - : SecondarySubdivisionAward(QStringLiteral("ukd"), QStringLiteral("288")) + : SecondarySubdivisionAward(QStringLiteral("ukd"), + QStringLiteral("288"), + "") { } diff --git a/awards/AwardUSCounty.cpp b/awards/AwardUSCounty.cpp index 2f07daa0..26f5dace 100644 --- a/awards/AwardUSCounty.cpp +++ b/awards/AwardUSCounty.cpp @@ -2,7 +2,9 @@ #include "AwardUSCounty.h" AwardUSCounty::AwardUSCounty() - : SecondarySubdivisionAward(QStringLiteral("uscounty"), QStringLiteral("291, 6, 110")) + : SecondarySubdivisionAward(QStringLiteral("uscounty"), + QStringLiteral("291, 6, 110"), + QStringLiteral("https://countyhunter.com/cq.htm")) { } diff --git a/awards/AwardWAAC.cpp b/awards/AwardWAAC.cpp new file mode 100644 index 00000000..417380e1 --- /dev/null +++ b/awards/AwardWAAC.cpp @@ -0,0 +1,34 @@ +#include +#include "AwardWAAC.h" + +QString AwardWAAC::displayName() const +{ + return QCoreApplication::translate("AwardsDialog", "WAAC"); +} + +QString AwardWAAC::rulesUrl() const +{ + return QStringLiteral("https://sites.google.com/site/ik7nxm/IK7NXM"); +} + +QString AwardWAAC::headersColumns(const QString &) const +{ + return QStringLiteral("d.name col1, d.id col2 "); +} + +QString AwardWAAC::sqlDetailTable(const QString &entity) const +{ + return " FROM dxcc_entities_ad1c d" + " LEFT OUTER JOIN source_contacts c ON d.id = c.dxcc AND c.my_dxcc = '" + entity + "' AND d.cont = 'AF' " + " LEFT OUTER JOIN modes m on c.mode = m.name"; +} + +QString AwardWAAC::additionalWhere(const QString &) const +{ + return " AND d.cont = 'AF' "; +} + +QString AwardWAAC::clickFilter(const QString &, const QString &col2Value) const +{ + return QString("dxcc = '%1' ").arg(col2Value); +} diff --git a/awards/AwardWAAC.h b/awards/AwardWAAC.h new file mode 100644 index 00000000..95ae6680 --- /dev/null +++ b/awards/AwardWAAC.h @@ -0,0 +1,20 @@ +#ifndef QLOG_AWARDS_AWARDWAAC_H +#define QLOG_AWARDS_AWARDWAAC_H + +#include "BandTableAward.h" + +class AwardWAAC : public BandTableAward +{ +public: + QString key() const override { return QStringLiteral("WAAC"); } + QString displayName() const override; + QString rulesUrl() const override; + +protected: + QString headersColumns(const QString &entity) const override; + QString sqlDetailTable(const QString &entity) const override; + QString additionalWhere(const QString &entity) const override; + QString clickFilter(const QString &col1Value, const QString &col2Value) const override; +}; + +#endif // QLOG_AWARDS_AWARDWAAC_H diff --git a/awards/AwardWAC.cpp b/awards/AwardWAC.cpp index ea6ecbda..4d7761e4 100644 --- a/awards/AwardWAC.cpp +++ b/awards/AwardWAC.cpp @@ -9,6 +9,11 @@ QString AwardWAC::displayName() const return QCoreApplication::translate("AwardsDialog", "WAC"); } +QString AwardWAC::rulesUrl() const +{ + return QStringLiteral("https://www.arrl.org/wac"); +} + QString AwardWAC::headersColumns(const QString &) const { return QStringLiteral("d.column2 col1, d.column1 col2 "); diff --git a/awards/AwardWAC.h b/awards/AwardWAC.h index 0e265bac..800d120f 100644 --- a/awards/AwardWAC.h +++ b/awards/AwardWAC.h @@ -8,6 +8,7 @@ class AwardWAC : public BandTableAward public: QString key() const override { return QStringLiteral("wac"); } QString displayName() const override; + QString rulesUrl() const override; protected: QString headersColumns(const QString &entity) const override; diff --git a/awards/AwardWAIP.cpp b/awards/AwardWAIP.cpp new file mode 100644 index 00000000..82d4f45b --- /dev/null +++ b/awards/AwardWAIP.cpp @@ -0,0 +1,34 @@ +#include +#include "AwardWAIP.h" + +QString AwardWAIP::displayName() const +{ + return QCoreApplication::translate("AwardsDialog", "WAIP"); +} + +QString AwardWAIP::rulesUrl() const +{ + return QStringLiteral("https://www.ari.it/english-area/awards/1734-waip-worked-all-italian-provinces.html"); +} + +QString AwardWAIP::headersColumns(const QString &) const +{ + return QStringLiteral("d.subdivision_name col1, d.code col2 "); +} + +QString AwardWAIP::sqlDetailTable(const QString &entity) const +{ + return " FROM adif_enum_primary_subdivision d" + " LEFT OUTER JOIN source_contacts c ON d.dxcc = c.dxcc AND d.code = c.state AND c.my_dxcc = '" + entity + "' AND d.dxcc in (225, 248)" + " LEFT OUTER JOIN modes m on c.mode = m.name"; +} + +QString AwardWAIP::additionalWhere(const QString &) const +{ + return " AND d.dxcc in (225, 248) "; +} + +QString AwardWAIP::clickFilter(const QString &, const QString &col2Value) const +{ + return QString("state = '%1' and dxcc in (225, 248)").arg(col2Value); +} diff --git a/awards/AwardWAIP.h b/awards/AwardWAIP.h new file mode 100644 index 00000000..398f1d12 --- /dev/null +++ b/awards/AwardWAIP.h @@ -0,0 +1,20 @@ +#ifndef QLOG_AWARDS_AWARDWAIP_H +#define QLOG_AWARDS_AWARDWAIP_H + +#include "BandTableAward.h" + +class AwardWAIP : public BandTableAward +{ +public: + QString key() const override { return QStringLiteral("WAIP"); } + QString displayName() const override; + QString rulesUrl() const override; + +protected: + QString headersColumns(const QString &entity) const override; + QString sqlDetailTable(const QString &entity) const override; + QString additionalWhere(const QString &entity) const override; + QString clickFilter(const QString &col1Value, const QString &col2Value) const override; +}; + +#endif // QLOG_AWARDS_AWARDWAIP_H diff --git a/awards/AwardWAS.cpp b/awards/AwardWAS.cpp index 6d1bd9ec..b279ee71 100644 --- a/awards/AwardWAS.cpp +++ b/awards/AwardWAS.cpp @@ -6,6 +6,11 @@ QString AwardWAS::displayName() const return QCoreApplication::translate("AwardsDialog", "WAS"); } +QString AwardWAS::rulesUrl() const +{ + return QStringLiteral("https://www.arrl.org/was"); +} + QString AwardWAS::headersColumns(const QString &) const { return QStringLiteral("d.subdivision_name col1, d.code col2 "); diff --git a/awards/AwardWAS.h b/awards/AwardWAS.h index f5e09ae5..4816889f 100644 --- a/awards/AwardWAS.h +++ b/awards/AwardWAS.h @@ -8,6 +8,7 @@ class AwardWAS : public BandTableAward public: QString key() const override { return QStringLiteral("was"); } QString displayName() const override; + QString rulesUrl() const override; protected: QString headersColumns(const QString &entity) const override; diff --git a/awards/AwardWAZ.cpp b/awards/AwardWAZ.cpp index 42b7a8b5..72fa289e 100644 --- a/awards/AwardWAZ.cpp +++ b/awards/AwardWAZ.cpp @@ -6,6 +6,11 @@ QString AwardWAZ::displayName() const return QCoreApplication::translate("AwardsDialog", "WAZ"); } +QString AwardWAZ::rulesUrl() const +{ + return QStringLiteral("https://www.k0nr.com/wordpress/wp-content/uploads/2024/02/cq_waz_rules_english.pdf"); +} + QString AwardWAZ::headersColumns(const QString &) const { return QStringLiteral("d.n col1, null col2 "); diff --git a/awards/AwardWAZ.h b/awards/AwardWAZ.h index eef34b1e..79a4500c 100644 --- a/awards/AwardWAZ.h +++ b/awards/AwardWAZ.h @@ -8,6 +8,7 @@ class AwardWAZ : public BandTableAward public: QString key() const override { return QStringLiteral("waz"); } QString displayName() const override; + QString rulesUrl() const override; protected: QString headersColumns(const QString &entity) const override; diff --git a/awards/AwardWPX.cpp b/awards/AwardWPX.cpp index fdf84276..285b5ee0 100644 --- a/awards/AwardWPX.cpp +++ b/awards/AwardWPX.cpp @@ -6,6 +6,11 @@ QString AwardWPX::displayName() const return QCoreApplication::translate("AwardsDialog", "WPX"); } +QString AwardWPX::rulesUrl() const +{ + return QStringLiteral("https://sites.google.com/site/cqwpxawards/"); +} + QString AwardWPX::headersColumns(const QString &) const { return QStringLiteral("c.pfx col1, null col2 "); diff --git a/awards/AwardWPX.h b/awards/AwardWPX.h index 32905766..9f126429 100644 --- a/awards/AwardWPX.h +++ b/awards/AwardWPX.h @@ -8,6 +8,7 @@ class AwardWPX : public BandTableAward public: QString key() const override { return QStringLiteral("wpx"); } QString displayName() const override; + QString rulesUrl() const override; bool notWorkedEnabled() const override { return false; } protected: diff --git a/awards/AwardWWFF.cpp b/awards/AwardWWFF.cpp index 49b51444..1eba401b 100644 --- a/awards/AwardWWFF.cpp +++ b/awards/AwardWWFF.cpp @@ -6,6 +6,11 @@ QString AwardWWFF::displayName() const return QCoreApplication::translate("AwardsDialog", "WWFF"); } +QString AwardWWFF::rulesUrl() const +{ + return QStringLiteral("https://wwff.co/awards/"); +} + QString AwardWWFF::headersColumns(const QString &) const { return QStringLiteral("w.reference col1, w.name col2 "); diff --git a/awards/AwardWWFF.h b/awards/AwardWWFF.h index 9894347c..585321c2 100644 --- a/awards/AwardWWFF.h +++ b/awards/AwardWWFF.h @@ -8,6 +8,7 @@ class AwardWWFF : public BandTableAward public: QString key() const override { return QStringLiteral("wwff"); } QString displayName() const override; + QString rulesUrl() const override; bool entityInputEnabled() const override { return false; } bool notWorkedEnabled() const override { return false; } diff --git a/awards/SecondarySubdivisionAward.cpp b/awards/SecondarySubdivisionAward.cpp index d831151f..2ed9c85b 100644 --- a/awards/SecondarySubdivisionAward.cpp +++ b/awards/SecondarySubdivisionAward.cpp @@ -1,11 +1,17 @@ #include "SecondarySubdivisionAward.h" -SecondarySubdivisionAward::SecondarySubdivisionAward(const QString &key, const QString &dxccFilter) +SecondarySubdivisionAward::SecondarySubdivisionAward(const QString &key, const QString &dxccFilter, const QString &rulesUrl) : m_key(key), - m_dxccFilter(dxccFilter) + m_dxccFilter(dxccFilter), + m_rulesUrl(rulesUrl) { } +QString SecondarySubdivisionAward::rulesUrl() const +{ + return m_rulesUrl; +} + QString SecondarySubdivisionAward::headersColumns(const QString &) const { return QStringLiteral("d.subdivision_name col1, d.code col2 "); diff --git a/awards/SecondarySubdivisionAward.h b/awards/SecondarySubdivisionAward.h index abc4e3be..6cd8fc09 100644 --- a/awards/SecondarySubdivisionAward.h +++ b/awards/SecondarySubdivisionAward.h @@ -11,9 +11,10 @@ class SecondarySubdivisionAward : public BandTableAward { public: - SecondarySubdivisionAward(const QString &key, const QString &dxccFilter); + SecondarySubdivisionAward(const QString &key, const QString &dxccFilter, const QString &rulesUrl); QString key() const override { return m_key; } + QString rulesUrl() const override; bool entityInputEnabled() const override { return false; } protected: @@ -25,6 +26,7 @@ class SecondarySubdivisionAward : public BandTableAward private: QString m_key; QString m_dxccFilter; + QString m_rulesUrl; }; #endif // QLOG_AWARDS_SECONDARYSUBDIVISIONAWARD_H diff --git a/core/AdifRecovery.cpp b/core/AdifRecovery.cpp new file mode 100644 index 00000000..a5339942 --- /dev/null +++ b/core/AdifRecovery.cpp @@ -0,0 +1,252 @@ +#include "AdifRecovery.h" + +#include +#include +#include +#include +#include +#include + +#include "core/debug.h" + +MODULE_IDENTIFICATION("qlog.core.adifrecovery"); + +QString AdifRecovery::normalizePath(const QString &path) +{ + QFileInfo info(path); + const QString canonicalPath = info.canonicalFilePath(); + return canonicalPath.isEmpty() ? info.absoluteFilePath() : canonicalPath; +} + +QString AdifRecovery::fileKey(const QString &path, const QString &stationProfileName) +{ + const QString key = normalizePath(path) + QChar(0x1f) + stationProfileName.trimmed(); + return QString::fromLatin1(QCryptographicHash::hash(key.toUtf8(), + QCryptographicHash::Sha256).toHex()); +} + +QString AdifRecovery::serializeConfigList(const QList &configs) +{ + QJsonArray array; + for ( const AdifRecoveryConfig &config : configs ) + { + if ( config.path.trimmed().isEmpty() ) + continue; + + QJsonObject object; + object.insert("enabled", config.enabled); + object.insert("stationProfileName", config.stationProfileName); + object.insert("qslSentStatusDefault", config.qslSentStatusDefault.isEmpty() ? QStringLiteral("Q") + : config.qslSentStatusDefault); + object.insert("path", config.path); + array.append(object); + } + + return QString::fromUtf8(QJsonDocument(array).toJson(QJsonDocument::Compact)); +} + +QList AdifRecovery::deserializeConfigList(const QString &data) +{ + QList configs; + const QJsonDocument document = QJsonDocument::fromJson(data.toUtf8()); + + if ( !document.isArray() ) + return configs; + + for ( const QJsonValue &value : static_cast(document.array()) ) + { + const QJsonObject object = value.toObject(); + AdifRecoveryConfig config; + config.enabled = object.value("enabled").toBool(true); + config.stationProfileName = object.value("stationProfileName").toString(); + config.qslSentStatusDefault = object.value("qslSentStatusDefault").toString("Q"); + config.path = object.value("path").toString(); + if ( !config.path.trimmed().isEmpty() ) + configs.append(config); + } + + return configs; +} + +QString AdifRecovery::serializeState(const AdifRecoveryState &state) +{ + QJsonObject object; + + object.insert("path", state.path); + object.insert("offset", state.offset); + object.insert("lastRecoveryAt", state.lastRecoveryAt.toUTC().toString(Qt::ISODate)); + object.insert("lastMessage", state.lastMessage); + + return QString::fromUtf8(QJsonDocument(object).toJson(QJsonDocument::Compact)); +} + +AdifRecoveryState AdifRecovery::deserializeState(const QString &data) +{ + AdifRecoveryState state; + const QJsonDocument document = QJsonDocument::fromJson(data.toUtf8()); + + if ( !document.isObject() ) + return state; + + const QJsonObject object = document.object(); + state.path = object.value("path").toString(); + state.offset = static_cast(object.value("offset").toDouble(-1)); + state.lastRecoveryAt = QDateTime::fromString(object.value("lastRecoveryAt").toString(), Qt::ISODate); + state.lastMessage = object.value("lastMessage").toString(); + + return state; +} + +AdifRecoveryReaderWorker::AdifRecoveryReaderWorker(QObject *parent) : + QObject(parent) +{ + FCT_IDENTIFICATION; +} + +void AdifRecoveryReaderWorker::readTail(const AdifRecoveryConfig &config, + const AdifRecoveryState &state, + int maxContacts) +{ + FCT_IDENTIFICATION; + + AdifRecoveryScanResult result; + + result.path = config.path; + result.fileKey = AdifRecovery::fileKey(config.path, config.stationProfileName); + result.previousOffset = state.offset; + result.nextOffset = state.offset; + + qCDebug(runtime) << "processing file" << config.path; + + if ( config.path.trimmed().isEmpty() ) + { + result.message = tr("Startup ADI filename is empty"); + qCDebug(runtime) << "Startup ADI filename is empty"; + emit scanFinished(result); + return; + } + + QFileInfo fileInfo(config.path); + if ( !fileInfo.exists() ) + { + result.message = tr("Startup ADI file does not exist: %1").arg(config.path); + qCDebug(runtime) << "Startup ADI file does not exist"; + emit scanFinished(result); + return; + } + + result.fileSize = fileInfo.size(); + + if ( state.offset < 0 ) + { + result.nextOffset = result.fileSize; + result.message = tr("Startup ADI initialized at the end of file"); + qCDebug(runtime) << "Startup ADI initialized at the end of file"; + emit scanFinished(result); + return; + } + + if ( state.offset > result.fileSize ) + { + result.reset = true; + result.nextOffset = result.fileSize; + result.message = tr("Startup ADI file was reset; load point moved to the end"); + qCDebug(runtime) << "Startup ADI file was reset; load point moved to the end"; + emit scanFinished(result); + return; + } + + if ( state.offset == result.fileSize ) + { + qCDebug(runtime) << "The same size - nothing to do"; + emit scanFinished(result); + return; + } + + QFile file(config.path); + if ( !file.open(QIODevice::ReadOnly) ) + { + result.message = tr("Cannot open Startup ADI file: %1").arg(config.path); + qCDebug(runtime) << "Cannot open Startup ADI file"; + emit scanFinished(result); + return; + } + + const qint64 readOffset = state.offset > 0 ? state.offset - 1 : state.offset; + + if ( !file.seek(readOffset) ) + { + result.message = tr("Cannot seek Startup ADI file: %1").arg(config.path); + qCDebug(runtime) << "Cannot seek Startup ADI file"; + emit scanFinished(result); + return; + } + + const QByteArray tail = file.readAll(); + if ( file.error() != QFile::NoError ) + { + result.message = tr("Cannot read Startup ADI file: %1").arg(file.errorString()); + qCDebug(runtime) << "Cannot read Startup ADI file"; + emit scanFinished(result); + return; + } + + int adifStart = static_cast(state.offset - readOffset); + + if ( adifStart > 0 + && adifStart < tail.size() + && tail.at(adifStart - 1) == ADIF_TAG_START + && tail.at(adifStart) != ADIF_TAG_START + && !isAdifWhitespace(tail.at(adifStart)) ) + { + adifStart--; + } + + while ( adifStart < tail.size() && isAdifWhitespace(tail.at(adifStart)) ) + adifStart++; + + if ( adifStart >= tail.size() || tail.at(adifStart) != ADIF_TAG_START ) + { + qCDebug(runtime) << "Maybe another file"; // TODO what should we do here + emit scanFinished(result); + return; + } + + const QByteArray adifTail = tail.mid(adifStart); + const qint64 adifTailOffset = readOffset + adifStart; + + const QByteArray lowerTail = adifTail.toLower(); + int searchFrom = 0; + int lastRecordEnd = -1; + + while ( true ) + { + const int eorIndex = lowerTail.indexOf(EOR_TAG, searchFrom); + if ( eorIndex < 0 ) + break; + + result.contactCount++; + lastRecordEnd = eorIndex + static_cast(qstrlen(EOR_TAG)); + + if ( maxContacts > 0 && result.contactCount > maxContacts ) + { + result.tooMany = true; + result.nextOffset = result.fileSize; + result.message = tr("Too many ADIF records for automatic recovery"); + qCDebug(runtime) << "Too many ADIF records for automatic recovery"; + emit scanFinished(result); + return; + } + + searchFrom = lastRecordEnd; + } + + if ( lastRecordEnd > 0 ) + { + result.adifText = QString::fromLatin1(adifTail.left(lastRecordEnd)); + result.nextOffset = adifTailOffset + lastRecordEnd; + } + + qCDebug(runtime) << "Finished"; + emit scanFinished(result); +} diff --git a/core/AdifRecovery.h b/core/AdifRecovery.h new file mode 100644 index 00000000..70a1a863 --- /dev/null +++ b/core/AdifRecovery.h @@ -0,0 +1,78 @@ +#ifndef QLOG_CORE_ADIFRECOVERY_H +#define QLOG_CORE_ADIFRECOVERY_H + +#include +#include + +struct AdifRecoveryConfig +{ + bool enabled = true; + QString stationProfileName; + QString qslSentStatusDefault = "Q"; + QString path; +}; + +struct AdifRecoveryState +{ + QString path; + qint64 offset = -1; + QDateTime lastRecoveryAt; + QString lastMessage; +}; + +struct AdifRecoveryScanResult +{ + QString fileKey; + QString path; + QString adifText; + qint64 previousOffset = -1; + qint64 nextOffset = -1; + qint64 fileSize = -1; + int contactCount = 0; + bool reset = false; + bool tooMany = false; + QString message; +}; + +class AdifRecovery +{ + +public: + static QString fileKey(const QString &path, const QString &stationProfileName); + static QString normalizePath(const QString &path); + + static QString serializeConfigList(const QList &configs); + static QList deserializeConfigList(const QString &data); + + static QString serializeState(const AdifRecoveryState &state); + static AdifRecoveryState deserializeState(const QString &data); +}; + +class AdifRecoveryReaderWorker : public QObject +{ + Q_OBJECT + +public: + explicit AdifRecoveryReaderWorker(QObject *parent = nullptr); + +public slots: + void readTail(const AdifRecoveryConfig &config, + const AdifRecoveryState &state, + int maxContacts); +private: + const char ADIF_TAG_START = '<'; + const char *EOR_TAG = ""; + + bool isAdifWhitespace(char ch) + { + return ch > 0 && ch <= ' '; + } +signals: + void scanFinished(const AdifRecoveryScanResult &result); +}; + +Q_DECLARE_METATYPE(AdifRecoveryConfig) +Q_DECLARE_METATYPE(AdifRecoveryState) +Q_DECLARE_METATYPE(AdifRecoveryScanResult) + +#endif // QLOG_CORE_ADIFRECOVERY_H diff --git a/core/IBPBeacon.cpp b/core/IBPBeacon.cpp new file mode 100644 index 00000000..70193a20 --- /dev/null +++ b/core/IBPBeacon.cpp @@ -0,0 +1,46 @@ +#include "IBPBeacon.h" + +#include "core/debug.h" + +MODULE_IDENTIFICATION("qlog.core.ibpbeacon"); + +const QList &IBPBeacon::bands() +{ + FCT_IDENTIFICATION; + + static const QList bands = QList() + << Band(QStringLiteral("20m"), 14.1) + << Band(QStringLiteral("17m"), 18.11) + << Band(QStringLiteral("15m"), 21.15) + << Band(QStringLiteral("12m"), 24.93) + << Band(QStringLiteral("10m"), 28.2); + + return bands; +} + +const QList &IBPBeacon::beacons() +{ + FCT_IDENTIFICATION; + + static const QList beacons = QList() + << Station(QStringLiteral("4U1UN"), 40.7501, -73.9682, true) + << Station(QStringLiteral("VE8AT"), 79.9949, -85.8451, true) + << Station(QStringLiteral("W6WX"), 37.1599, -121.9083, true) + << Station(QStringLiteral("KH6RS"), 20.7652, -156.3502, true) + << Station(QStringLiteral("ZL6B"), -41.04350, 175.5952, true) + << Station(QStringLiteral("VK6RBP"), -32.1093, 116.0712, true) + << Station(QStringLiteral("JA2IGY"), 34.4613, 136.7818, true) + << Station(QStringLiteral("RR9O"), 55.0484, 82.9227, true) + << Station(QStringLiteral("VR2B"), 22.2705, 114.1507, true) + << Station(QStringLiteral("4S7B"), 6.8915, 79.8559, false) + << Station(QStringLiteral("ZS6DN"), -26.6531, 27.9474, true) + << Station(QStringLiteral("5Z4B"), -1.2687, 36.8094, true) + << Station(QStringLiteral("4X6TU"), 32.0622, 34.8069, true) + << Station(QStringLiteral("OH2B"), 60.2920, 24.3942, false) + << Station(QStringLiteral("CS3B"), 32.8217, -17.2325, true) + << Station(QStringLiteral("LU4AA"), -34.6439, -58.4138, true) + << Station(QStringLiteral("OA4B"), -12.0940, -77.0165, true) + << Station(QStringLiteral("YV5B"), 9.0964, -67.8239, false); + + return beacons; +} diff --git a/core/IBPBeacon.h b/core/IBPBeacon.h new file mode 100644 index 00000000..c9e0476e --- /dev/null +++ b/core/IBPBeacon.h @@ -0,0 +1,48 @@ +#ifndef QLOG_CORE_IBPBEACON_H +#define QLOG_CORE_IBPBEACON_H + +#include +#include + +class IBPBeacon +{ +public: + struct Band + { + Band() = default; + Band(const QString &inName, + double inFrequency) + : name(inName), + frequency(inFrequency) + { + } + + QString name; + double frequency = 0.0; + }; + + struct Station + { + Station() = default; + Station(const QString &inCallsign, + double inLatitude, + double inLongitude, + bool inActive) + : callsign(inCallsign), + latitude(inLatitude), + longitude(inLongitude), + active(inActive) + { + } + + QString callsign; + double latitude = 0.0; + double longitude = 0.0; + bool active = false; + }; + + static const QList &bands(); + static const QList &beacons(); +}; + +#endif // QLOG_CORE_IBPBEACON_H diff --git a/core/LogParam.cpp b/core/LogParam.cpp index c0466503..9ef6a591 100644 --- a/core/LogParam.cpp +++ b/core/LogParam.cpp @@ -1,8 +1,16 @@ #include #include +#include +#include +#include +#include +#include +#include +#include #include #include "LogParam.h" +#include "AdifRecovery.h" #include "debug.h" #include "data/Data.h" #include "models/LogbookModel.h" @@ -179,7 +187,9 @@ void LogParam::removeDXCTrendContinent() QStringList LogParam::bandmapsWidgets() { - return getKeys("bandmap/"); + QStringList keys = getKeys("bandmap/"); + keys.removeAll("guide"); + return keys; } void LogParam::removeBandmapWidgetGroup(const QString &group) @@ -237,6 +247,46 @@ bool LogParam::getBandmapShowEmergency(const QString &widgetID) return getParam("bandmap/" + widgetID + "/showemergency", true).toBool(); } +bool LogParam::setBandmapShowIBP(const QString &widgetID, bool show) +{ + return setParam("bandmap/" + widgetID + "/showibp", show); +} + +bool LogParam::getBandmapShowIBP(const QString &widgetID) +{ + return getParam("bandmap/" + widgetID + "/showibp", true).toBool(); +} + +bool LogParam::setBandmapGuideProfiles(const QString &json) +{ + return setParam("bandmap/guide/profiles", json); +} + +QString LogParam::getBandmapGuideProfiles() +{ + return getParam("bandmap/guide/profiles", QString()).toString(); +} + +bool LogParam::setBandmapGuideCurrentProfile(const QString &id) +{ + return setParam("bandmap/guide/currentprofile", id); +} + +QString LogParam::getBandmapGuideCurrentProfile() +{ + return getParam("bandmap/guide/currentprofile", QString()).toString(); +} + +bool LogParam::setBandmapGuideEnabled(bool state) +{ + return setParam("bandmap/guide/enabled", state); +} + +bool LogParam::getBandmapGuideEnabled() +{ + return getParam("bandmap/guide/enabled", false).toBool(); +} + QString LogParam::getUploadQSOLastCall() { return getParam("uploadqso/lastcall").toString(); @@ -307,6 +357,111 @@ void LogParam::setUploadLoTWLocation(const QString &location) setParam("uploadqso/lotw/last_location", location); } +QString LogParam::getImportQslSentStatusPaper() +{ + return getParam("import/qsl_sent_status/paper", "Q").toString(); +} + +void LogParam::setImportQslSentStatusPaper(const QString &status) +{ + setParam("import/qsl_sent_status/paper", status); +} + +QString LogParam::getImportQslSentStatusLoTW() +{ + return getParam("import/qsl_sent_status/lotw", "Q").toString(); +} + +void LogParam::setImportQslSentStatusLoTW(const QString &status) +{ + setParam("import/qsl_sent_status/lotw", status); +} + +QString LogParam::getImportQslSentStatusEQSL() +{ + return getParam("import/qsl_sent_status/eqsl", "Q").toString(); +} + +void LogParam::setImportQslSentStatusEQSL(const QString &status) +{ + setParam("import/qsl_sent_status/eqsl", status); +} + +QString LogParam::getImportQslSentStatusDCL() +{ + return getParam("import/qsl_sent_status/dcl", "Q").toString(); +} + +void LogParam::setImportQslSentStatusDCL(const QString &status) +{ + setParam("import/qsl_sent_status/dcl", status); +} + +QList LogParam::getAdifRecoveryFiles() +{ + return AdifRecovery::deserializeConfigList(getParam("adifrecovery/files").toString()); +} + +void LogParam::setAdifRecoveryFiles(const QList &files) +{ + setParam("adifrecovery/files", AdifRecovery::serializeConfigList(files)); +} + +AdifRecoveryState LogParam::getAdifRecoveryState(const QString &fileKey) +{ + return AdifRecovery::deserializeState(getParam("adifrecovery/state/" + fileKey).toString()); +} + +void LogParam::setAdifRecoveryState(const QString &fileKey, const AdifRecoveryState &state) +{ + setParam("adifrecovery/state/" + fileKey, AdifRecovery::serializeState(state)); +} + +void LogParam::removeAdifRecoveryState(const QString &fileKey) +{ + removeParamGroup("adifrecovery/state/" + fileKey); +} + +QString LogParam::getAdifRecoveryQslSentStatusPaper() +{ + return getParam("adifrecovery/qsl_sent_status/paper", "Q").toString(); +} + +void LogParam::setAdifRecoveryQslSentStatusPaper(const QString &status) +{ + setParam("adifrecovery/qsl_sent_status/paper", status); +} + +QString LogParam::getAdifRecoveryQslSentStatusLoTW() +{ + return getParam("adifrecovery/qsl_sent_status/lotw", "Q").toString(); +} + +void LogParam::setAdifRecoveryQslSentStatusLoTW(const QString &status) +{ + setParam("adifrecovery/qsl_sent_status/lotw", status); +} + +QString LogParam::getAdifRecoveryQslSentStatusEQSL() +{ + return getParam("adifrecovery/qsl_sent_status/eqsl", "Q").toString(); +} + +void LogParam::setAdifRecoveryQslSentStatusEQSL(const QString &status) +{ + setParam("adifrecovery/qsl_sent_status/eqsl", status); +} + +QString LogParam::getAdifRecoveryQslSentStatusDCL() +{ + return getParam("adifrecovery/qsl_sent_status/dcl", "Q").toString(); +} + +void LogParam::setAdifRecoveryQslSentStatusDCL(const QString &status) +{ + setParam("adifrecovery/qsl_sent_status/dcl", status); +} + bool LogParam::getDownloadQSLServiceState(const QString &name) { return getParam("downloadqsl/" + name + "/enabled", false).toBool(); @@ -467,6 +622,106 @@ void LogParam::setKSTChatUsername(const QString &username) setParam("services/kst/chat/username", username); } +QString LogParam::getEmailQSLSmtpHost() +{ + return getParam("services/emailqsl/smtpHost").toString(); +} + +void LogParam::setEmailQSLSmtpHost(const QString &host) +{ + setParam("services/emailqsl/smtpHost", host); +} + +int LogParam::getEmailQSLSmtpPort() +{ + return getParam("services/emailqsl/smtpPort", 587).toInt(); +} + +void LogParam::setEmailQSLSmtpPort(int port) +{ + setParam("services/emailqsl/smtpPort", port); +} + +int LogParam::getEmailQSLSmtpEncryption() +{ + return getParam("services/emailqsl/smtpEncryption", 2).toInt(); +} + +void LogParam::setEmailQSLSmtpEncryption(int enc) +{ + setParam("services/emailqsl/smtpEncryption", enc); +} + +QString LogParam::getEmailQSLSmtpUsername() +{ + return getParam("services/emailqsl/smtpUsername").toString(); +} + +void LogParam::setEmailQSLSmtpUsername(const QString &username) +{ + setParam("services/emailqsl/smtpUsername", username); +} + +QString LogParam::getEmailQSLFromAddress() +{ + return getParam("services/emailqsl/fromAddress").toString(); +} + +void LogParam::setEmailQSLFromAddress(const QString &addr) +{ + setParam("services/emailqsl/fromAddress", addr); +} + +QString LogParam::getEmailQSLFromName() +{ + return getParam("services/emailqsl/fromName").toString(); +} + +void LogParam::setEmailQSLFromName(const QString &name) +{ + setParam("services/emailqsl/fromName", name); +} + +QString LogParam::getEmailQSLSubjectTemplate(const QString &defaultValue) +{ + return getParam("services/emailqsl/subjectTemplate", defaultValue).toString(); +} + +void LogParam::setEmailQSLSubjectTemplate(const QString &tmpl) +{ + setParam("services/emailqsl/subjectTemplate", tmpl); +} + +QString LogParam::getEmailQSLBodyTemplate(const QString &defaultValue) +{ + return getParam("services/emailqsl/bodyTemplate", defaultValue).toString(); +} + +void LogParam::setEmailQSLBodyTemplate(const QString &tmpl) +{ + setParam("services/emailqsl/bodyTemplate", tmpl); +} + +QString LogParam::getEmailQSLCardImagePath() +{ + return getParam("services/emailqsl/cardImagePath").toString(); +} + +void LogParam::setEmailQSLCardImagePath(const QString &path) +{ + setParam("services/emailqsl/cardImagePath", path); +} + +QByteArray LogParam::getEmailQSLCardOverlays() +{ + return getParam("services/emailqsl/cardOverlays").toByteArray(); +} + +void LogParam::setEmailQSLCardOverlays(const QByteArray &json) +{ + setParam("services/emailqsl/cardOverlays", json); +} + QString LogParam::getLoTWCallbookUsername() { return getParam("services/lotw/callbook/username").toString().trimmed(); @@ -1212,6 +1467,23 @@ void LogParam::setMainWindowDarkMode(int state) setParam("mainwindow/darkmode", state); } +QVariantMap LogParam::getQsoStatusColors() +{ + const QByteArray json = getParam("gui/qso_status_colors", QByteArray()).toByteArray(); + const QJsonDocument doc = QJsonDocument::fromJson(json); + + return doc.isObject() ? doc.object().toVariantMap() : QVariantMap(); +} + +void LogParam::setQsoStatusColors(const QVariantMap &colors) +{ + QJsonObject object; + for ( auto i = colors.constBegin(); i != colors.constEnd(); ++i ) + object.insert(i.key(), i.value().toString()); + + setParam("gui/qso_status_colors", QJsonDocument(object).toJson(QJsonDocument::Compact)); +} + QByteArray LogParam::getMainWindowGeometry() { return QByteArray::fromBase64(getParam("mainwindow/geometry").toByteArray()); @@ -1297,6 +1569,37 @@ void LogParam::setQslLabelZoom(int zoom) setParam("qsllabel/zoom", zoom); } +int LogParam::getQslLabelPrintMode() +{ + return getParam("qsllabel/print_mode", 0).toInt(); +} + +void LogParam::setQslLabelPrintMode(int mode) +{ + setParam("qsllabel/print_mode", mode); +} + +int LogParam::getQslLabelPageSize() +{ + return getParam("qsllabel/page_size", static_cast(QPageSize::A4)).toInt(); +} + +void LogParam::setQslLabelPageSize(int pageSize) +{ + setParam("qsllabel/page_size", pageSize); +} + +QString LogParam::getQslLabelImageExportPath(const QString &defaultPath) +{ + const QString path = getParam("qsllabel/image_export_path", defaultPath).toString(); + return path.isEmpty() ? defaultPath : path; +} + +void LogParam::setQslLabelImageExportPath(const QString &path) +{ + setParam("qsllabel/image_export_path", path); +} + int LogParam::getQslLabelCustomPageSize() { return getParam("qsllabel/custom_pagesize", 0).toInt(); @@ -1387,6 +1690,127 @@ void LogParam::setQslLabelCustomVSpacing(double spacing) setParam("qsllabel/custom_vspacing", spacing); } +double LogParam::getQslLabelCardWidth() +{ + return getParam("qsllabel/card_width", 140.0).toDouble(); +} + +void LogParam::setQslLabelCardWidth(double width) +{ + setParam("qsllabel/card_width", width); +} + +double LogParam::getQslLabelCardHeight() +{ + return getParam("qsllabel/card_height", 90.0).toDouble(); +} + +void LogParam::setQslLabelCardHeight(double height) +{ + setParam("qsllabel/card_height", height); +} + +double LogParam::getQslLabelCardGap() +{ + return getParam("qsllabel/card_gap", 2.0).toDouble(); +} + +void LogParam::setQslLabelCardGap(double gap) +{ + setParam("qsllabel/card_gap", gap); +} + +double LogParam::getQslLabelCardLabelWidth() +{ + return getParam("qsllabel/card_label_width", 70.0).toDouble(); +} + +void LogParam::setQslLabelCardLabelWidth(double width) +{ + setParam("qsllabel/card_label_width", width); +} + +double LogParam::getQslLabelCardLabelHeight() +{ + return getParam("qsllabel/card_label_height", 35.0).toDouble(); +} + +void LogParam::setQslLabelCardLabelHeight(double height) +{ + setParam("qsllabel/card_label_height", height); +} + +double LogParam::getQslLabelCardLabelOffsetX() +{ + return getParam("qsllabel/card_label_offset_x", 5.0).toDouble(); +} + +void LogParam::setQslLabelCardLabelOffsetX(double offset) +{ + setParam("qsllabel/card_label_offset_x", offset); +} + +double LogParam::getQslLabelCardLabelOffsetY() +{ + return getParam("qsllabel/card_label_offset_y", 5.0).toDouble(); +} + +void LogParam::setQslLabelCardLabelOffsetY(double offset) +{ + setParam("qsllabel/card_label_offset_y", offset); +} + +bool LogParam::getQslLabelCardLabelOpaqueBackground() +{ + return getParam("qsllabel/card_label_opaque_background", true).toBool(); +} + +void LogParam::setQslLabelCardLabelOpaqueBackground(bool enabled) +{ + setParam("qsllabel/card_label_opaque_background", enabled); +} + +QColor LogParam::getQslLabelCardLabelBackgroundColor() +{ + QColor color(getParam("qsllabel/card_label_background_color", QStringLiteral("#ffffffff")).toString()); + + return color.isValid() ? color : QColor(Qt::white); +} + +void LogParam::setQslLabelCardLabelBackgroundColor(const QColor &color) +{ + setParam("qsllabel/card_label_background_color", + color.isValid() ? color.name(QColor::HexArgb) : QColor(Qt::white).name(QColor::HexArgb)); +} + +QImage LogParam::getQslLabelCardBackgroundImage() +{ + QImage image; + const QString base64 = getParam("qsllabel/card_background_image", QString()).toString(); + + if ( !base64.isEmpty() ) + image.loadFromData(QByteArray::fromBase64(base64.toLatin1())); + + return image; +} + +void LogParam::setQslLabelCardBackgroundImage(const QImage &image) +{ + if ( image.isNull() ) + { + setParam("qsllabel/card_background_image", QString()); + return; + } + + QByteArray imageData; + QBuffer buffer(&imageData); + + if ( !buffer.open(QIODevice::WriteOnly) || !image.save(&buffer, "PNG") ) + return; + + setParam("qsllabel/card_background_image", QString::fromLatin1(imageData.toBase64())); +} + bool LogParam::getQslLabelPrintBorders() { return getParam("qsllabel/print_borders", false).toBool(); @@ -1427,6 +1851,19 @@ void LogParam::setQslLabelMonoFont(const QString &family) setParam("qsllabel/mono_font", family); } +QColor LogParam::getQslLabelTextColor() +{ + QColor color(getParam("qsllabel/text_color", QStringLiteral("#ff000000")).toString()); + + return color.isValid() ? color : QColor(Qt::black); +} + +void LogParam::setQslLabelTextColor(const QColor &color) +{ + setParam("qsllabel/text_color", + color.isValid() ? color.name(QColor::HexArgb) : QColor(Qt::black).name(QColor::HexArgb)); +} + QString LogParam::getQslLabelExtraColumn() { return getParam("qsllabel/extra_column", QString()).toString(); diff --git a/core/LogParam.h b/core/LogParam.h index cd811083..70070686 100644 --- a/core/LogParam.h +++ b/core/LogParam.h @@ -3,8 +3,14 @@ #include #include +#include +#include #include #include +#include + +struct AdifRecoveryConfig; +struct AdifRecoveryState; class LogParam : public QObject { @@ -75,6 +81,14 @@ class LogParam : public QObject static bool getBandmapCenterRX(const QString& widgetID); static bool setBandmapShowEmergency(const QString& widgetID, bool show); static bool getBandmapShowEmergency(const QString& widgetID); + static bool setBandmapShowIBP(const QString& widgetID, bool show); + static bool getBandmapShowIBP(const QString& widgetID); + static bool setBandmapGuideProfiles(const QString &json); + static QString getBandmapGuideProfiles(); + static bool setBandmapGuideCurrentProfile(const QString &id); + static QString getBandmapGuideCurrentProfile(); + static bool setBandmapGuideEnabled(bool state); + static bool getBandmapGuideEnabled(); /******************* * UploadQSO Dialog @@ -94,6 +108,35 @@ class LogParam : public QObject static QString getUploadLoTWLocation(); static void setUploadLoTWLocation(const QString &location); + /***************** + * Import Dialog + *****************/ + static QString getImportQslSentStatusPaper(); + static void setImportQslSentStatusPaper(const QString &status); + static QString getImportQslSentStatusLoTW(); + static void setImportQslSentStatusLoTW(const QString &status); + static QString getImportQslSentStatusEQSL(); + static void setImportQslSentStatusEQSL(const QString &status); + static QString getImportQslSentStatusDCL(); + static void setImportQslSentStatusDCL(const QString &status); + + /***************** + * Startup ADI + *****************/ + static QList getAdifRecoveryFiles(); + static void setAdifRecoveryFiles(const QList &files); + static AdifRecoveryState getAdifRecoveryState(const QString &fileKey); + static void setAdifRecoveryState(const QString &fileKey, const AdifRecoveryState &state); + static void removeAdifRecoveryState(const QString &fileKey); + static QString getAdifRecoveryQslSentStatusPaper(); + static void setAdifRecoveryQslSentStatusPaper(const QString &status); + static QString getAdifRecoveryQslSentStatusLoTW(); + static void setAdifRecoveryQslSentStatusLoTW(const QString &status); + static QString getAdifRecoveryQslSentStatusEQSL(); + static void setAdifRecoveryQslSentStatusEQSL(const QString &status); + static QString getAdifRecoveryQslSentStatusDCL(); + static void setAdifRecoveryQslSentStatusDCL(const QString &status); + /********************* * DownloadQSL Dialog *********************/ @@ -158,6 +201,30 @@ class LogParam : public QObject static QString getKSTChatUsername(); static void setKSTChatUsername(const QString& username); + /********* + * Email QSL + ********/ + static QString getEmailQSLSmtpHost(); + static void setEmailQSLSmtpHost(const QString &host); + static int getEmailQSLSmtpPort(); + static void setEmailQSLSmtpPort(int port); + static int getEmailQSLSmtpEncryption(); + static void setEmailQSLSmtpEncryption(int enc); + static QString getEmailQSLSmtpUsername(); + static void setEmailQSLSmtpUsername(const QString &username); + static QString getEmailQSLFromAddress(); + static void setEmailQSLFromAddress(const QString &addr); + static QString getEmailQSLFromName(); + static void setEmailQSLFromName(const QString &name); + static QString getEmailQSLSubjectTemplate(const QString &defaultValue); + static void setEmailQSLSubjectTemplate(const QString &tmpl); + static QString getEmailQSLBodyTemplate(const QString &defaultValue); + static void setEmailQSLBodyTemplate(const QString &tmpl); + static QString getEmailQSLCardImagePath(); + static void setEmailQSLCardImagePath(const QString &path); + static QByteArray getEmailQSLCardOverlays(); + static void setEmailQSLCardOverlays(const QByteArray &json); + /********* * LoTW ********/ @@ -364,6 +431,8 @@ class LogParam : public QObject static void setMainWindowAlertBeep(bool state); static int getMainWindowDarkMode(); static void setMainWindowDarkMode(int state); + static QVariantMap getQsoStatusColors(); + static void setQsoStatusColors(const QVariantMap &colors); static QByteArray getMainWindowGeometry(); static void setMainWindowGeometry(const QByteArray &state); static QByteArray getMainWindowState(); @@ -385,6 +454,12 @@ class LogParam : public QObject static void setQslLabelSkip(int count); static int getQslLabelZoom(); static void setQslLabelZoom(int zoom); + static int getQslLabelPrintMode(); + static void setQslLabelPrintMode(int mode); + static int getQslLabelPageSize(); + static void setQslLabelPageSize(int pageSize); + static QString getQslLabelImageExportPath(const QString &defaultPath); + static void setQslLabelImageExportPath(const QString &path); static int getQslLabelCustomPageSize(); static void setQslLabelCustomPageSize(int pageSizeIndex); static int getQslLabelCustomCols(); @@ -403,6 +478,26 @@ class LogParam : public QObject static void setQslLabelCustomHSpacing(double spacing); static double getQslLabelCustomVSpacing(); static void setQslLabelCustomVSpacing(double spacing); + static double getQslLabelCardWidth(); + static void setQslLabelCardWidth(double width); + static double getQslLabelCardHeight(); + static void setQslLabelCardHeight(double height); + static double getQslLabelCardGap(); + static void setQslLabelCardGap(double gap); + static double getQslLabelCardLabelWidth(); + static void setQslLabelCardLabelWidth(double width); + static double getQslLabelCardLabelHeight(); + static void setQslLabelCardLabelHeight(double height); + static double getQslLabelCardLabelOffsetX(); + static void setQslLabelCardLabelOffsetX(double offset); + static double getQslLabelCardLabelOffsetY(); + static void setQslLabelCardLabelOffsetY(double offset); + static bool getQslLabelCardLabelOpaqueBackground(); + static void setQslLabelCardLabelOpaqueBackground(bool enabled); + static QColor getQslLabelCardLabelBackgroundColor(); + static void setQslLabelCardLabelBackgroundColor(const QColor &color); + static QImage getQslLabelCardBackgroundImage(); + static void setQslLabelCardBackgroundImage(const QImage &image); static bool getQslLabelPrintBorders(); static void setQslLabelPrintBorders(bool enabled); static QString getQslLabelDateFormat(); @@ -411,6 +506,8 @@ class LogParam : public QObject static void setQslLabelSansFont(const QString &family); static QString getQslLabelMonoFont(); static void setQslLabelMonoFont(const QString &family); + static QColor getQslLabelTextColor(); + static void setQslLabelTextColor(const QColor &color); static QString getQslLabelExtraColumn(); static void setQslLabelExtraColumn(const QString &column); static QString getQslLabelExtraColumnHeader(); diff --git a/core/Migration.cpp b/core/Migration.cpp index 0c2efc64..93bc9339 100644 --- a/core/Migration.cpp +++ b/core/Migration.cpp @@ -329,6 +329,9 @@ bool DBSchemaMigration::functionMigration(int version) case 35: ret = removeSettings2DB(); break; + case 40: + ret = emailQSLSettings2DB(); + break; default: ret = true; } @@ -907,6 +910,37 @@ bool DBSchemaMigration::removeSettings2DB() return true; } +bool DBSchemaMigration::emailQSLSettings2DB() +{ + FCT_IDENTIFICATION; + + QSettings settings; + + if (settings.contains("emailqsl/smtpHost")) + LogParam::setEmailQSLSmtpHost(settings.value("emailqsl/smtpHost").toString()); + if (settings.contains("emailqsl/smtpPort")) + LogParam::setEmailQSLSmtpPort(settings.value("emailqsl/smtpPort").toInt()); + if (settings.contains("emailqsl/smtpEncryption")) + LogParam::setEmailQSLSmtpEncryption(settings.value("emailqsl/smtpEncryption").toInt()); + if (settings.contains("emailqsl/smtpUsername")) + LogParam::setEmailQSLSmtpUsername(settings.value("emailqsl/smtpUsername").toString()); + if (settings.contains("emailqsl/fromAddress")) + LogParam::setEmailQSLFromAddress(settings.value("emailqsl/fromAddress").toString()); + if (settings.contains("emailqsl/fromName")) + LogParam::setEmailQSLFromName(settings.value("emailqsl/fromName").toString()); + if (settings.contains("emailqsl/subjectTemplate")) + LogParam::setEmailQSLSubjectTemplate(settings.value("emailqsl/subjectTemplate").toString()); + if (settings.contains("emailqsl/bodyTemplate")) + LogParam::setEmailQSLBodyTemplate(settings.value("emailqsl/bodyTemplate").toString()); + if (settings.contains("emailqsl/cardImagePath")) + LogParam::setEmailQSLCardImagePath(settings.value("emailqsl/cardImagePath").toString()); + if (settings.contains("emailqsl/cardOverlays")) + LogParam::setEmailQSLCardOverlays(settings.value("emailqsl/cardOverlays").toByteArray()); + + settings.remove("emailqsl"); + return true; +} + bool DBSchemaMigration::settings2DB() { FCT_IDENTIFICATION; diff --git a/core/Migration.h b/core/Migration.h index c789550c..b688da0b 100644 --- a/core/Migration.h +++ b/core/Migration.h @@ -14,7 +14,7 @@ class DBSchemaMigration : public QObject bool run(bool force = false); static bool backupAllQSOsToADX(bool force = false); - static constexpr int latestVersion = 38; + static constexpr int latestVersion = 40; private: bool functionMigration(int version); @@ -39,6 +39,7 @@ class DBSchemaMigration : public QObject bool profiles2DB(); bool settings2DB(); bool removeSettings2DB(); + bool emailQSLSettings2DB(); bool setSelectedProfile(const QString &tablename, const QString &profileName); QString fixIntlField(const QSqlQuery &query, const QString &columName, const QString &columnNameIntl); bool refreshUploadStatusTrigger(); diff --git a/core/QSLPrintLabelRenderer.cpp b/core/QSLPrintLabelRenderer.cpp index 96effe6f..bb074ce8 100644 --- a/core/QSLPrintLabelRenderer.cpp +++ b/core/QSLPrintLabelRenderer.cpp @@ -3,12 +3,16 @@ #include #include #include +#include +#include #include "QSLPrintLabelRenderer.h" #include "core/debug.h" MODULE_IDENTIFICATION("qlog.core.qslprintlabelrenderer"); +const double QSLPrintLabelRenderer::MILLIMETERS_PER_INCH = 25.4; + QSLPrintLabelRenderer::QSLPrintLabelRenderer() { FCT_IDENTIFICATION; @@ -25,7 +29,7 @@ void QSLPrintLabelRenderer::setTemplate(const LabelTemplate &tmpl) void QSLPrintLabelRenderer::setLabels(const QList &inLabels) { FCT_IDENTIFICATION; - qCDebug(function_parameters) << labels.size(); + qCDebug(function_parameters) << inLabels.size(); labels = inLabels; } @@ -69,10 +73,44 @@ void QSLPrintLabelRenderer::setStyleOptions(const LabelStyleOptions &opts) styleOptions = opts; } +void QSLPrintLabelRenderer::setPrintMode(QSLPrintMode mode) +{ + FCT_IDENTIFICATION; + + printMode = mode; +} + +void QSLPrintLabelRenderer::setPageSize(QPageSize::PageSizeId pageSize) +{ + FCT_IDENTIFICATION; + + outputPageSize = pageSize; +} + +void QSLPrintLabelRenderer::setCardLayout(const QSLCardLayout &layout) +{ + FCT_IDENTIFICATION; + + cardLayout = layout; +} + +void QSLPrintLabelRenderer::setCardBackgroundImage(const QImage &image) +{ + FCT_IDENTIFICATION; + + cardBackgroundImage = image; +} + int QSLPrintLabelRenderer::labelsPerPage() const { FCT_IDENTIFICATION; + if ( printMode == QSLPrintMode::DirectCard ) + { + const DirectCardGrid grid = directCardGrid(); + return grid.cols * grid.rows; + } + return labelTemplate.cols * labelTemplate.rows; } @@ -93,13 +131,28 @@ int QSLPrintLabelRenderer::pageCount() const return ( perPage > 0 ) ? (totalSlots + perPage - 1) / perPage : 0; } +qreal QSLPrintLabelRenderer::mmToUnits(const qreal mm, const qreal dpi) const +{ + return mm * dpi / MILLIMETERS_PER_INCH; +} + qreal QSLPrintLabelRenderer::mmToUnits(const qreal mm, const QPaintDevice *device, bool yAxis) const { FCT_IDENTIFICATION; - return mm * (yAxis ? device->logicalDpiY() : device->logicalDpiX()) / 25.4; // to inch - DPI (px/inch) + return mmToUnits(mm, yAxis ? device->logicalDpiY() : device->logicalDpiX()); +} + +int QSLPrintLabelRenderer::mmToPixels(const qreal mm, int dpi) const +{ + return qRound(mmToUnits(mm, dpi)); +} + +int QSLPrintLabelRenderer::dotsPerMeter(int dpi) const +{ + return mmToPixels(1000.0, dpi); } void QSLPrintLabelRenderer::drawLabel(QPainter *painter, @@ -174,7 +227,7 @@ void QSLPrintLabelRenderer::drawLabel(QPainter *painter, // --- Line 1: "To Radio" + Callsign --- painter->setFont(fontToRadio); - painter->setPen(Qt::black); + painter->setPen(styleOptions.textColor.isValid() ? styleOptions.textColor : QColor(Qt::black)); const QRectF toRadioRect(contentRect.left(), currentY, contentRect.width(), line1Height); const QString toRadioText = styleOptions.toRadioText.isEmpty() ? "To Radio" : styleOptions.toRadioText; @@ -325,10 +378,20 @@ void QSLPrintLabelRenderer::drawPage(QPainter *painter, int pageIndex) FCT_IDENTIFICATION; qCDebug(function_parameters) << pageIndex; + if ( printMode == QSLPrintMode::DirectCard ) + drawDirectCardPage(painter, pageIndex); + else + drawLabelSheetPage(painter, pageIndex); +} + +void QSLPrintLabelRenderer::drawLabelSheetPage(QPainter *painter, int pageIndex) +{ + FCT_IDENTIFICATION; + if ( !painter ) return; - QPaintDevice *device = painter->device(); + const QPaintDevice *device = painter->device(); if ( !device ) return; @@ -371,6 +434,221 @@ void QSLPrintLabelRenderer::drawPage(QPainter *painter, int pageIndex) } } +void QSLPrintLabelRenderer::drawDirectCardPage(QPainter *painter, int pageIndex) +{ + FCT_IDENTIFICATION; + + if ( !painter ) + return; + + QPaintDevice *device = painter->device(); + + if ( !device ) + return; + + const DirectCardGrid grid = directCardGrid(); + const QSizeF pageSize = grid.pageSizeMm; + const QSizeF printableSize = directCardPrintableSize(pageSize); + const int cols = grid.cols; + const int rows = grid.rows; + const int perPage = cols * rows; + + if ( perPage <= 0 ) + return; + + const qreal cardSlotW = mmToUnits(grid.cardSlotWidthMm, device); + const qreal cardSlotH = mmToUnits(grid.cardSlotHeightMm, device, true); + const qreal gapX = mmToUnits(cardLayout.cardGapMm, device); + const qreal gapY = mmToUnits(cardLayout.cardGapMm, device, true); + const qreal gridW = mmToUnits(grid.usedWidthMm, device); + const qreal gridH = mmToUnits(grid.usedHeightMm, device, true); + const qreal pageMarginX = mmToUnits(directCardPageMarginMm, device); + const qreal pageMarginY = mmToUnits(directCardPageMarginMm, device, true); + const qreal printableW = mmToUnits(printableSize.width(), device); + const qreal printableH = mmToUnits(printableSize.height(), device, true); + const qreal startX = pageMarginX + qMax(0.0, (printableW - gridW) / 2.0); + const qreal startY = pageMarginY + qMax(0.0, (printableH - gridH) / 2.0); + + const int slotStart = pageIndex * perPage; + + for ( int row = 0; row < rows; ++row ) + { + for ( int col = 0; col < cols; ++col ) + { + const int slotIndex = slotStart + row * cols + col; + const int labelIndex = slotIndex - skipLabels; + + if ( labelIndex < 0 ) + continue; + + if ( labelIndex >= labels.size() ) + return; + + const QRectF slotRect(startX + col * (cardSlotW + gapX), + startY + row * (cardSlotH + gapY), + cardSlotW, + cardSlotH); + + painter->save(); + if ( grid.rotateCard ) + { + painter->translate(slotRect.left() + slotRect.width(), slotRect.top()); + painter->rotate(90.0); + } + else + { + painter->translate(slotRect.left(), slotRect.top()); + } + + drawDirectCard(painter, labels.at(labelIndex)); + painter->restore(); + } + } +} + +void QSLPrintLabelRenderer::drawDirectCard(QPainter *painter, const QSLLabelData &label) +{ + FCT_IDENTIFICATION; + + if ( !painter ) + return; + + QPaintDevice *device = painter->device(); + + if ( !device ) + return; + + const QRectF cardRect(0, 0, + mmToUnits(cardLayout.cardWidthMm, device), + mmToUnits(cardLayout.cardHeightMm, device, true)); + + if ( !cardBackgroundImage.isNull() ) + painter->drawImage(cardRect, cardBackgroundImage); + + if ( printBorders ) + { + painter->save(); + painter->setPen(QPen(Qt::black, 0.5)); + painter->setBrush(Qt::NoBrush); + painter->drawRect(cardRect); + painter->restore(); + } + + const QRectF labelRect(cardRect.left() + mmToUnits(cardLayout.labelOffsetXMm, device), + cardRect.top() + mmToUnits(cardLayout.labelOffsetYMm, device, true), + mmToUnits(cardLayout.labelWidthMm, device), + mmToUnits(cardLayout.labelHeightMm, device, true)); + + if ( cardLayout.labelOpaqueBackground ) + { + painter->save(); + painter->setPen(Qt::NoPen); + painter->setBrush(cardLayout.labelBackgroundColor); + painter->drawRect(labelRect); + painter->restore(); + } + + drawLabel(painter, labelRect, label); +} + +QSizeF QSLPrintLabelRenderer::pageSizeMm() const +{ + FCT_IDENTIFICATION; + + if ( printMode == QSLPrintMode::DirectCard ) + return directCardGrid().pageSizeMm; + + QSizeF size = QPageSize(outputPageSize).size(QPageSize::Millimeter); + + if ( labelTemplate.orientation == QPageLayout::Landscape ) + size.transpose(); + + return size; +} + +QSizeF QSLPrintLabelRenderer::directCardPrintableSize(const QSizeF &pageSize) const +{ + FCT_IDENTIFICATION; + + return QSizeF(qMax(0.0, pageSize.width() - 2.0 * directCardPageMarginMm), + qMax(0.0, pageSize.height() - 2.0 * directCardPageMarginMm)); +} + +QSLPrintLabelRenderer::DirectCardGrid QSLPrintLabelRenderer::directCardGrid() const +{ + FCT_IDENTIFICATION; + + DirectCardGrid best; + + const QSizeF portrait = QPageSize(outputPageSize).size(QPageSize::Millimeter); + QSizeF landscape = portrait; + landscape.transpose(); + const QList> pageOptions = + { + {portrait, QPageLayout::Portrait}, + {landscape, QPageLayout::Landscape} + }; + + for ( const auto &pageOption : pageOptions ) + { + const QSizeF printableSize = directCardPrintableSize(pageOption.first); + + for ( bool rotateCard : {false, true} ) + { + DirectCardGrid candidate; + candidate.pageSizeMm = pageOption.first; + candidate.pageOrientation = pageOption.second; + candidate.rotateCard = rotateCard; + candidate.cardSlotWidthMm = rotateCard ? cardLayout.cardHeightMm : cardLayout.cardWidthMm; + candidate.cardSlotHeightMm = rotateCard ? cardLayout.cardWidthMm : cardLayout.cardHeightMm; + candidate.cols = directCardCols(printableSize, rotateCard); + candidate.rows = directCardRows(printableSize, rotateCard); + candidate.usedWidthMm = candidate.cols > 0 + ? candidate.cols * candidate.cardSlotWidthMm + + (candidate.cols - 1) * cardLayout.cardGapMm + : 0.0; + candidate.usedHeightMm = candidate.rows > 0 + ? candidate.rows * candidate.cardSlotHeightMm + + (candidate.rows - 1) * cardLayout.cardGapMm + : 0.0; + + const int bestCount = best.cols * best.rows; + const int candidateCount = candidate.cols * candidate.rows; + const QSizeF bestPrintableSize = directCardPrintableSize(best.pageSizeMm); + const double bestWaste = bestPrintableSize.width() * bestPrintableSize.height() + - best.usedWidthMm * best.usedHeightMm; + const double candidateWaste = printableSize.width() * printableSize.height() + - candidate.usedWidthMm * candidate.usedHeightMm; + + if ( candidateCount > bestCount + || (candidateCount == bestCount && candidateWaste < bestWaste) ) + { + best = candidate; + } + } + } + + return best; +} + +int QSLPrintLabelRenderer::directCardCols(const QSizeF &printableSize, bool rotateCard) const +{ + FCT_IDENTIFICATION; + + const double cardWidth = rotateCard ? cardLayout.cardHeightMm : cardLayout.cardWidthMm; + return cardWidth > 0.0 ? qFloor((printableSize.width() + cardLayout.cardGapMm) + / (cardWidth + cardLayout.cardGapMm)) : 0; +} + +int QSLPrintLabelRenderer::directCardRows(const QSizeF &printableSize, bool rotateCard) const +{ + FCT_IDENTIFICATION; + + const double cardHeight = rotateCard ? cardLayout.cardWidthMm : cardLayout.cardHeightMm; + return cardHeight > 0.0 ? qFloor((printableSize.height() + cardLayout.cardGapMm) + / (cardHeight + cardLayout.cardGapMm)) : 0; +} + QImage QSLPrintLabelRenderer::renderPage(int pageIndex, int dpi) { FCT_IDENTIFICATION; @@ -388,17 +666,14 @@ QImage QSLPrintLabelRenderer::renderPage(int pageIndex, int dpi) return QImage(); } - QSizeF pageSizeMm = QPageSize(labelTemplate.pageSize).size(QPageSize::Millimeter); + QSizeF pageSizeMm = this->pageSizeMm(); - if ( labelTemplate.orientation == QPageLayout::Landscape ) - pageSizeMm.transpose(); - - const int widthPx = qRound(pageSizeMm.width() * dpi / 25.4); - const int heightPx = qRound(pageSizeMm.height() * dpi / 25.4); + const int widthPx = mmToPixels(pageSizeMm.width(), dpi); + const int heightPx = mmToPixels(pageSizeMm.height(), dpi); QImage image(widthPx, heightPx, QImage::Format_ARGB32_Premultiplied); - image.setDotsPerMeterX(qRound(dpi / 25.4 * 1000.0)); - image.setDotsPerMeterY(qRound(dpi / 25.4 * 1000.0)); + image.setDotsPerMeterX(dotsPerMeter(dpi)); + image.setDotsPerMeterY(dotsPerMeter(dpi)); image.fill(Qt::white); QPainter painter(&image); @@ -411,6 +686,41 @@ QImage QSLPrintLabelRenderer::renderPage(int pageIndex, int dpi) return image; } +QImage QSLPrintLabelRenderer::renderDirectCard(int labelIndex, int dpi) +{ + FCT_IDENTIFICATION; + qCDebug(function_parameters) << labelIndex << dpi; + + if ( dpi <= 0 ) + { + qCWarning(runtime) << "Invalid DPI" << dpi; + return QImage(); + } + + if ( labelIndex < 0 || labelIndex >= labels.size() ) + { + qCWarning(runtime) << "Invalid label index" << labelIndex; + return QImage(); + } + + const int widthPx = mmToPixels(cardLayout.cardWidthMm, dpi); + const int heightPx = mmToPixels(cardLayout.cardHeightMm, dpi); + + QImage image(widthPx, heightPx, QImage::Format_ARGB32_Premultiplied); + image.setDotsPerMeterX(dotsPerMeter(dpi)); + image.setDotsPerMeterY(dotsPerMeter(dpi)); + image.fill(Qt::white); + + QPainter painter(&image); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setRenderHint(QPainter::TextAntialiasing, true); + + drawDirectCard(&painter, labels.at(labelIndex)); + + painter.end(); + return image; +} + void QSLPrintLabelRenderer::printAll(QPrinter *printer) { FCT_IDENTIFICATION; @@ -431,8 +741,14 @@ void QSLPrintLabelRenderer::printAll(QPrinter *printer) printer->setResolution(PRINTER_RESOLUTION); - const QPageLayout layout(QPageSize(labelTemplate.pageSize), - labelTemplate.orientation, + const QPageSize pageSize(outputPageSize); + QPageLayout::Orientation orientation = labelTemplate.orientation; + + if ( printMode == QSLPrintMode::DirectCard ) + orientation = directCardGrid().pageOrientation; + + const QPageLayout layout(pageSize, + orientation, QMarginsF(0, 0, 0, 0), QPageLayout::Millimeter); printer->setPageLayout(layout); diff --git a/core/QSLPrintLabelRenderer.h b/core/QSLPrintLabelRenderer.h index 615051af..832f3699 100644 --- a/core/QSLPrintLabelRenderer.h +++ b/core/QSLPrintLabelRenderer.h @@ -3,7 +3,9 @@ #include #include +#include #include +#include #include #include #include @@ -27,10 +29,30 @@ struct LabelTemplate double vSpacingMm; }; +enum class QSLPrintMode +{ + LabelSheet = 0, + DirectCard = 1 +}; + +struct QSLCardLayout +{ + double cardWidthMm = 140.0; + double cardHeightMm = 90.0; + double cardGapMm = 2.0; + double labelWidthMm = 70.0; + double labelHeightMm = 35.0; + double labelOffsetXMm = 5.0; + double labelOffsetYMm = 5.0; + bool labelOpaqueBackground = true; + QColor labelBackgroundColor = Qt::white; +}; + struct LabelStyleOptions { QString sansFontFamily; // empty = system default QString monoFontFamily; // empty = system fixed font + QColor textColor = Qt::black; qreal toRadioFontSize = 7.5; qreal callsignFontSize = 14.0; qreal headerFontSize = 7.0; @@ -74,19 +96,48 @@ class QSLPrintLabelRenderer void setSkipLabels(int count); void setPrintBorders(bool enabled); void setStyleOptions(const LabelStyleOptions &opts); + void setPrintMode(QSLPrintMode mode); + void setPageSize(QPageSize::PageSizeId pageSize); + void setCardLayout(const QSLCardLayout &layout); + void setCardBackgroundImage(const QImage &image); int pageCount() const; int labelCount() const; QImage renderPage(int pageIndex, int dpi = 150); + QImage renderDirectCard(int labelIndex, int dpi = 300); void printAll(QPrinter *printer); static QList predefinedTemplates(); private: + struct DirectCardGrid + { + QSizeF pageSizeMm; + QPageLayout::Orientation pageOrientation = QPageLayout::Portrait; + bool rotateCard = false; + int cols = 0; + int rows = 0; + double cardSlotWidthMm = 0.0; + double cardSlotHeightMm = 0.0; + double usedWidthMm = 0.0; + double usedHeightMm = 0.0; + }; + void drawLabel(QPainter *painter, const QRectF &labelRect, const QSLLabelData &label); void drawPage(QPainter *painter, int pageIndex); + void drawLabelSheetPage(QPainter *painter, int pageIndex); + void drawDirectCardPage(QPainter *painter, int pageIndex); + void drawDirectCard(QPainter *painter, const QSLLabelData &label); + QSizeF pageSizeMm() const; + QSizeF directCardPrintableSize(const QSizeF &pageSize) const; + DirectCardGrid directCardGrid() const; + int directCardCols(const QSizeF &printableSize, bool rotateCard) const; + int directCardRows(const QSizeF &printableSize, bool rotateCard) const; + qreal mmToUnits(const qreal mm, const qreal dpi) const; qreal mmToUnits(const qreal mm, const QPaintDevice *device, bool yAxis = false) const; + int mmToPixels(const qreal mm, int dpi) const; + int dotsPerMeter(int dpi) const; int labelsPerPage() const; LabelTemplate labelTemplate; @@ -96,8 +147,14 @@ class QSLPrintLabelRenderer int skipLabels = 0; bool printBorders = false; LabelStyleOptions styleOptions; + QSLPrintMode printMode = QSLPrintMode::LabelSheet; + QPageSize::PageSizeId outputPageSize = QPageSize::A4; + QSLCardLayout cardLayout; + QImage cardBackgroundImage; + static const double MILLIMETERS_PER_INCH; const int PRINTER_RESOLUTION = 300; + const double directCardPageMarginMm = 5.0; }; #endif // QLOG_CORE_QSLPRINTLABELRENDERER_H diff --git a/core/WsjtxUDPReceiver.cpp b/core/WsjtxUDPReceiver.cpp index 6f07d4a1..c10a7f5d 100644 --- a/core/WsjtxUDPReceiver.cpp +++ b/core/WsjtxUDPReceiver.cpp @@ -649,8 +649,18 @@ void WsjtxUDPReceiver::sendHighlightCallsign(const WsjtxEntry &entry) { FCT_IDENTIFICATION; + const QColor background = Data::statusToColor(entry.status, false, QColor()); + // QColor() means that WSJTX clears the color - sendHighlightCallsignColor(entry, Qt::black, Data::statusToColor(entry.status, false, QColor())); + if ( !background.isValid() || background.alpha() == 0 ) + { + sendHighlightCallsignColor(entry, QColor(), QColor()); + return; + } + + sendHighlightCallsignColor(entry, + Data::textColorForBackground(background), + background); } void WsjtxUDPReceiver::sendClearHighlightCallsign(const WsjtxEntry &entry) diff --git a/core/main.cpp b/core/main.cpp index 8803127f..ea538fbc 100644 --- a/core/main.cpp +++ b/core/main.cpp @@ -32,6 +32,9 @@ MODULE_IDENTIFICATION("qlog.core.main"); static QMutex debug_mutex; static QFile debugLogFile; static QTextStream debugLogStream; +static QThread *rigThreadHandle = nullptr; +static QThread *rotThreadHandle = nullptr; +static QThread *cwKeyerThreadHandle = nullptr; QTemporaryDir tempDir #ifdef QLOG_FLATPAK @@ -156,32 +159,50 @@ static void createDataDirectory() { static void startRigThread() { FCT_IDENTIFICATION; - QThread* rigThread = new QThread; + rigThreadHandle = new QThread; Rig* rig = Rig::instance(); - rig->moveToThread(rigThread); - QObject::connect(rigThread, SIGNAL(started()), rig, SLOT(start())); - rigThread->start(); + rig->moveToThread(rigThreadHandle); + QObject::connect(rigThreadHandle, SIGNAL(started()), rig, SLOT(start())); + rigThreadHandle->start(); } static void startRotThread() { FCT_IDENTIFICATION; - QThread* rotThread = new QThread; + rotThreadHandle = new QThread; Rotator* rot = Rotator::instance(); - rot->moveToThread(rotThread); - QObject::connect(rotThread, SIGNAL(started()), rot, SLOT(start())); - rotThread->start(); + rot->moveToThread(rotThreadHandle); + QObject::connect(rotThreadHandle, SIGNAL(started()), rot, SLOT(start())); + rotThreadHandle->start(); } static void startCWKeyerThread() { FCT_IDENTIFICATION; - QThread* cwKeyerThread = new QThread; + cwKeyerThreadHandle = new QThread; CWKeyer* cwKeyer = CWKeyer::instance(); - cwKeyer->moveToThread(cwKeyerThread); - QObject::connect(cwKeyerThread, SIGNAL(started()), cwKeyer, SLOT(start())); - cwKeyerThread->start(); + cwKeyer->moveToThread(cwKeyerThreadHandle); + QObject::connect(cwKeyerThreadHandle, SIGNAL(started()), cwKeyer, SLOT(start())); + cwKeyerThreadHandle->start(); +} + +static void stopWorkerThread(QThread *&threadHandle, const QString &threadName) +{ + FCT_IDENTIFICATION; + + if ( !threadHandle ) + return; + + threadHandle->quit(); + if ( !threadHandle->wait(5000) ) + { + qCWarning(runtime) << threadName << "thread did not stop within timeout"; + return; + } + + delete threadHandle; + threadHandle = nullptr; } void closeDebugLogFile() @@ -481,19 +502,29 @@ int main(int argc, char* argv[]) startRotThread(); startCWKeyerThread(); - MainWindow w; - QIcon icon(":/res/qlog.png"); + int rc = 0; + + { + MainWindow w; + QIcon icon(":/res/qlog.png"); - w.setWindowIcon(icon); + w.setWindowIcon(icon); - splash.finish(&w); - w.show(); + splash.finish(&w); + w.show(); - w.setLayoutGeometry(); + w.setLayoutGeometry(); - // check version only for Windows and MacOS. Linux has own distribution points + // check version only for Windows and MacOS. Linux has own distribution points #if defined(Q_OS_WIN) || defined(Q_OS_MAC) - w.checkNewVersion(); + w.checkNewVersion(); #endif - return app.exec(); + rc = app.exec(); + } + + stopWorkerThread(cwKeyerThreadHandle, "CWKeyer"); + stopWorkerThread(rotThreadHandle, "Rotator"); + stopWorkerThread(rigThreadHandle, "Rig"); + + return rc; } diff --git a/cwkey/CWKeyer.cpp b/cwkey/CWKeyer.cpp index c57462e0..dda1a0f9 100644 --- a/cwkey/CWKeyer.cpp +++ b/cwkey/CWKeyer.cpp @@ -7,6 +7,9 @@ #include "cwkey/drivers/CWFldigiKey.h" #include "core/debug.h" #include "data/CWKeyProfile.h" +#include +#include +#include MODULE_IDENTIFICATION("qlog.cwkey.cwkeyer"); @@ -28,6 +31,42 @@ void CWKeyer::stopTimer() Q_ASSERT( check ); } +void CWKeyer::shutdown() +{ + FCT_IDENTIFICATION; + + if ( QThread::currentThread() == thread() ) + { + shutdownImpl(); + return; + } + + if ( !thread() || !thread()->isRunning() ) + { + qCWarning(runtime) << "Cannot shut down CWKeyer because owner thread is not running"; + return; + } + + QSharedPointer shutdownDone = QSharedPointer::create(); + bool check = QMetaObject::invokeMethod(this, [this, shutdownDone]() + { + shutdownImpl(); + shutdownDone->release(); + }, Qt::QueuedConnection); + + if ( !check ) + { + qCWarning(runtime) << "Cannot queue CWKeyer shutdown"; + return; + } + + if ( !shutdownDone->tryAcquire(1, SHUTDOWN_TIMEOUT_MS) ) + { + qCWarning(runtime) << "CWKeyer shutdown did not finish within" + << SHUTDOWN_TIMEOUT_MS << "ms"; + } +} + void CWKeyer::update() { FCT_IDENTIFICATION; @@ -74,9 +113,8 @@ void CWKeyer::openImpl() { FCT_IDENTIFICATION; - cwKeyLock.lock(); + QMutexLocker locker(&cwKeyLock); __openCWKey(); - cwKeyLock.unlock(); } void CWKeyer::__openCWKey() @@ -261,10 +299,20 @@ void CWKeyer::closeImpl() FCT_IDENTIFICATION; qCDebug(runtime) << "Waiting for cwkey mutex"; - cwKeyLock.lock(); + QMutexLocker locker(&cwKeyLock); qCDebug(runtime) << "Using Key"; __closeCWKey(); - cwKeyLock.unlock(); +} + +void CWKeyer::shutdownImpl() +{ + FCT_IDENTIFICATION; + + qCDebug(runtime) << "Waiting for cwkey mutex"; + QMutexLocker locker(&cwKeyLock); + qCDebug(runtime) << "Using Key"; + __closeCWKey(); + stopTimerImplt(); } void CWKeyer::__closeCWKey() @@ -276,7 +324,7 @@ void CWKeyer::__closeCWKey() if ( cwKey ) { cwKey->close(); - cwKey->deleteLater(); + delete cwKey; cwKey = nullptr; } @@ -419,8 +467,14 @@ CWKeyer::~CWKeyer() { FCT_IDENTIFICATION; - if ( cwKey ) + if ( !cwKey && !timer ) + return; + + if ( QThread::currentThread() != thread() ) { - cwKey->deleteLater(); + qCWarning(runtime) << "Skipping CWKeyer shutdown from non-owner thread"; + return; } + + shutdownImpl(); } diff --git a/cwkey/CWKeyer.h b/cwkey/CWKeyer.h index 1deb92f0..72ed0e97 100644 --- a/cwkey/CWKeyer.h +++ b/cwkey/CWKeyer.h @@ -32,6 +32,7 @@ public slots: void update(); void open(); void close(); + void shutdown(); bool canStopSending(); bool canEchoChar(); bool rigMustConnected(); @@ -44,6 +45,7 @@ public slots: private slots: void openImpl(); void closeImpl(); + void shutdownImpl(); void setSpeedImpl(const qint16 wpm); void sendTextImpl(const QString&); void immediatelyStopImpl(); @@ -57,6 +59,8 @@ private slots: void cwKeyHWButton4PressedHandler(); private: + static const int SHUTDOWN_TIMEOUT_MS = 5000; + explicit CWKeyer(QObject *parent = nullptr); ~CWKeyer(); diff --git a/data/Accents.cpp b/data/Accents.cpp index cc5ce4b8..89f6bf1c 100644 --- a/data/Accents.cpp +++ b/data/Accents.cpp @@ -13302,6 +13302,12 @@ QString Data::removeAccents(const QString &input) { const char16_t charInt = character.unicode(); + if ( charInt == '\r' || charInt == '\n' ) + { + ret.append(character); + continue; + } + // skip the non-printable chars if ( charInt < 32 ) continue; diff --git a/data/ActivityProfile.cpp b/data/ActivityProfile.cpp index 4366dd05..e654b425 100644 --- a/data/ActivityProfile.cpp +++ b/data/ActivityProfile.cpp @@ -2,6 +2,7 @@ #include "core/debug.h" #include "data/ProfileManager.h" #include "data/AntProfile.h" +#include "data/BandmapGuide.h" #include "data/MainLayoutProfile.h" #include "data/RigProfile.h" #include "data/RotProfile.h" @@ -212,13 +213,34 @@ void ActivityProfilesManager::setAllProfiles() case ActivityProfile::MAIN_LAYOUT_PROFILE: MainLayoutProfilesManager::instance()->setCurProfile1(i.value().name); break; + case ActivityProfile::BANDMAP_GUIDE_PROFILE: + applyBandmapGuideProfile(i.value().name); + break; default: qWarning() << "Unknown Activity profile" << i.key(); } } + emit changeFinished(currActivity.profileName); } +void ActivityProfilesManager::applyBandmapGuideProfile(const QString &profileId) +{ + FCT_IDENTIFICATION; + + if ( profileId.isEmpty() ) + { + BandmapGuide::setEnabled(false); + return; + } + + if ( !BandmapGuide::profileExists(profileId) ) + return; + + BandmapGuide::setCurrentProfileId(profileId); + BandmapGuide::setEnabled(true); +} + ActivityProfilesManager::ActivityProfilesManager() : ProfileManagerSQL("activity_profiles") { diff --git a/data/ActivityProfile.h b/data/ActivityProfile.h index 4a33cf5f..2cc29139 100644 --- a/data/ActivityProfile.h +++ b/data/ActivityProfile.h @@ -14,7 +14,8 @@ class ActivityProfile STATION_PROFILE = 1, RIG_PROFILE = 2, ROT_PROFILE = 3, - MAIN_LAYOUT_PROFILE = 4 + MAIN_LAYOUT_PROFILE = 4, + BANDMAP_GUIDE_PROFILE = 5 }; enum ProfileParamType @@ -85,6 +86,8 @@ public slots: }; void save(); +private: + void applyBandmapGuideProfile(const QString &profileId); }; #endif // Q_LOG_DATA_ACTIVITYPROFILE_H diff --git a/data/BandPlan.cpp b/data/BandPlan.cpp index 6d8a4188..088e3306 100644 --- a/data/BandPlan.cpp +++ b/data/BandPlan.cpp @@ -6,16 +6,10 @@ MODULE_IDENTIFICATION("qlog.data.bandplan"); -struct BandModeRange { - double start; - double end; - BandPlan::BandPlanMode mode; -}; - // currectly only IARU Region 1 is implemented // https://www.iaru-r1.org/wp-content/uploads/2019/08/hf_r1_bandplan.pdf // https://www.oevsv.at/export/shared/.content/.galleries/pdf-Downloads/OVSV-Bandplan_05-2019.pdf -static const BandModeRange r1BandModeTable[] = +static const BandPlan::BandModeRange r1BandModeTable[] = { // 2200m {0.1357, 0.1378, BandPlan::BAND_MODE_CW}, @@ -164,6 +158,19 @@ static const BandModeRange r1BandModeTable[] = static int bandModeTableSize = sizeof(r1BandModeTable) / sizeof(r1BandModeTable[0]); +const QList BandPlan::r1BandModeRanges() +{ + FCT_IDENTIFICATION; + + QList ret; + ret.reserve(bandModeTableSize); + + for ( int i = 0; i < bandModeTableSize; i++ ) + ret.append(r1BandModeTable[i]); + + return ret; +} + BandPlan::BandPlanMode BandPlan::freq2BandMode(const double freq) { FCT_IDENTIFICATION; diff --git a/data/BandPlan.h b/data/BandPlan.h index d8defc59..8e357424 100644 --- a/data/BandPlan.h +++ b/data/BandPlan.h @@ -26,7 +26,15 @@ class BandPlan BAND_MODE_PHONE }; + struct BandModeRange + { + double start; + double end; + BandPlanMode mode; + }; + static BandPlanMode freq2BandMode(const double freq); + static const QList r1BandModeRanges(); static const QString bandMode2BandModeGroupString(const BandPlan::BandPlanMode &bandPlanMode); static const QString freq2BandModeGroupString(const double freq); static const QString bandPlanMode2ExpectedMode(const BandPlan::BandPlanMode &bandPlanMode, diff --git a/data/BandmapGuide.cpp b/data/BandmapGuide.cpp new file mode 100644 index 00000000..894e1610 --- /dev/null +++ b/data/BandmapGuide.cpp @@ -0,0 +1,403 @@ +#include "BandmapGuide.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "data/BandPlan.h" +#include "core/LogParam.h" +#include "core/debug.h" + +MODULE_IDENTIFICATION("qlog.data.bandmapguide"); + +BandmapGuide::BandmapGuide(QObject *parent) : + QObject(parent) +{ +} + +BandmapGuide *BandmapGuide::instance() +{ + static BandmapGuide guide; + + return &guide; +} + +void BandmapGuide::notifyChanged() +{ + FCT_IDENTIFICATION; + + emit instance()->changed(); +} + +BandmapGuide::Range::Range(double rangeFrom, + double rangeTo, + const QColor &rangeColor, + const QString &rangeLabel) : + from(rangeFrom), + to(rangeTo), + color(rangeColor), + label(rangeLabel) +{ +} + +bool BandmapGuide::Range::isValid() const +{ + return from < to && color.isValid(); +} + +bool BandmapGuide::Profile::isValid() const +{ + return !name.trimmed().isEmpty(); +} + +QList BandmapGuide::profiles() +{ + FCT_IDENTIFICATION; + + const QString storedProfiles = LogParam::getBandmapGuideProfiles(); + QList profileList = profilesFromJson(storedProfiles); + + if ( profileList.isEmpty() && storedProfiles.trimmed().isEmpty() ) + profileList.append(exampleProfile()); + + return profileList; +} + +void BandmapGuide::saveProfiles(const QList &profiles) +{ + FCT_IDENTIFICATION; + + LogParam::setBandmapGuideProfiles(profilesToJson(profiles)); + notifyChanged(); +} + +QString BandmapGuide::currentProfileId() +{ + FCT_IDENTIFICATION; + + return LogParam::getBandmapGuideCurrentProfile(); +} + +void BandmapGuide::setCurrentProfileId(const QString &id) +{ + FCT_IDENTIFICATION; + + if ( LogParam::getBandmapGuideCurrentProfile() == id ) + return; + + LogParam::setBandmapGuideCurrentProfile(id); + notifyChanged(); +} + +BandmapGuide::Profile BandmapGuide::currentProfile() +{ + FCT_IDENTIFICATION; + + const QString currentId = currentProfileId(); + const QList profileList = profiles(); + + for ( const Profile &profile : profileList ) + if ( profile.id == currentId ) + return profile; + + if ( !profileList.isEmpty() ) + return profileList.first(); + + return Profile(); +} + +bool BandmapGuide::profileExists(const QString &id) +{ + FCT_IDENTIFICATION; + + if ( id.isEmpty() ) + return false; + + const QList profileList = profiles(); + for ( const Profile &profile : profileList ) + if ( profile.id == id ) + return true; + + return false; +} + +bool BandmapGuide::isEnabled() +{ + FCT_IDENTIFICATION; + + return LogParam::getBandmapGuideEnabled(); +} + +void BandmapGuide::setEnabled(bool state) +{ + FCT_IDENTIFICATION; + + if ( LogParam::getBandmapGuideEnabled() == state ) + return; + + LogParam::setBandmapGuideEnabled(state); + notifyChanged(); +} + +QString BandmapGuide::newProfileId() +{ + FCT_IDENTIFICATION; + + return QUuid::createUuid().toString(QUuid::WithoutBraces); +} + +BandmapGuide::Range BandmapGuide::defaultRange() +{ + FCT_IDENTIFICATION; + + return Range(14.000, 14.100, QColor::fromRgb(0x4d8ef7)); +} + +BandmapGuide::Profile BandmapGuide::exampleProfile() +{ + FCT_IDENTIFICATION; + + const QColor cwColor = QColor::fromRgb(0x4d8ef7); + const QColor digitalColor = QColor::fromRgb(0xa6a435); + const QColor phoneColor = QColor::fromRgb(0xd92f2f); + + Profile profile; + profile.id = QStringLiteral("iaru-region-1"); + profile.name = QObject::tr("IARU Region 1"); + + const QList bandRanges = BandPlan::r1BandModeRanges(); + for ( const BandPlan::BandModeRange &bandRange : bandRanges ) + { + QColor color; + QString label; + + switch ( bandRange.mode ) + { + case BandPlan::BAND_MODE_CW: + color = cwColor; + label = BandPlan::MODE_GROUP_STRING_CW; + break; + + case BandPlan::BAND_MODE_DIGITAL: + case BandPlan::BAND_MODE_FT8: + case BandPlan::BAND_MODE_FT4: + case BandPlan::BAND_MODE_FT2: + color = digitalColor; + label = BandPlan::MODE_GROUP_STRING_DIGITAL; + break; + + case BandPlan::BAND_MODE_PHONE: + case BandPlan::BAND_MODE_LSB: + case BandPlan::BAND_MODE_USB: + color = phoneColor; + label = BandPlan::MODE_GROUP_STRING_PHONE; + break; + + case BandPlan::BAND_MODE_UNKNOWN: + continue; + } + + const Range range(bandRange.start, bandRange.end, color, label); + if ( !profile.ranges.isEmpty() ) + { + Range &lastRange = profile.ranges.last(); + if ( lastRange.to == range.from + && lastRange.color == range.color + && lastRange.label == range.label ) + { + lastRange.to = range.to; + continue; + } + } + + profile.ranges.append(range); + } + + return profile; +} + +bool BandmapGuide::writeProfileToFile(const Profile &profile, + const QString &filePath, + QString *error) +{ + FCT_IDENTIFICATION; + + QFile file(filePath); + + if ( !file.open(QIODevice::WriteOnly | QIODevice::Truncate) ) + { + if ( error ) *error = QObject::tr("Failed to write file: %1").arg(filePath); + return false; + } + + const QJsonDocument document(profileToJsonObject(profile, false)); + if ( file.write(document.toJson(QJsonDocument::Indented)) < 0 ) + { + if ( error ) *error = QObject::tr("Failed to write file: %1").arg(filePath); + return false; + } + + return true; +} + +BandmapGuide::Profile BandmapGuide::readProfileFromFile(const QString &filePath, + QString *error) +{ + FCT_IDENTIFICATION; + + QFile file(filePath); + + if ( !file.open(QIODevice::ReadOnly) ) + { + if ( error ) *error = QObject::tr("Cannot open file: %1").arg(filePath); + return Profile(); + } + + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &parseError); + if ( parseError.error != QJsonParseError::NoError || !document.isObject() ) + { + if ( error ) *error = QObject::tr("Invalid guide file: %1").arg(parseError.errorString()); + return Profile(); + } + + Profile profile; + const QJsonObject root = document.object(); + + if ( root.value("profiles").isArray() ) + { + const QJsonArray profileArray = root.value("profiles").toArray(); + if ( !profileArray.isEmpty() ) + profile = profileFromJsonObject(profileArray.first().toObject()); + } + else + { + profile = profileFromJsonObject(root); + } + + if ( profile.name.trimmed().isEmpty() ) + profile.name = QFileInfo(filePath).completeBaseName(); + + profile.id = newProfileId(); + + if ( !profile.isValid() ) + { + if ( error ) *error = QObject::tr("Invalid guide file: missing title"); + return Profile(); + } + + return profile; +} + +QString BandmapGuide::profilesToJson(const QList &profiles) +{ + FCT_IDENTIFICATION; + + QJsonArray profileArray; + + for ( const Profile &profile : profiles ) + if ( profile.isValid() ) + profileArray.append(profileToJsonObject(profile)); + + QJsonObject root; + root.insert("version", 1); + root.insert("profiles", profileArray); + + return QString::fromUtf8(QJsonDocument(root).toJson(QJsonDocument::Compact)); +} + +QList BandmapGuide::profilesFromJson(const QString &json) +{ + FCT_IDENTIFICATION; + + QList result; + if ( json.trimmed().isEmpty() ) + return result; + + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(json.toUtf8(), &parseError); + if ( parseError.error != QJsonParseError::NoError || !document.isObject() ) + { + qCWarning(runtime) << "Cannot parse bandmap guide profiles" << parseError.errorString(); + return result; + } + + const QJsonArray profileArray = document.object().value("profiles").toArray(); + for ( const QJsonValue &value : profileArray ) + { + Profile profile = profileFromJsonObject(value.toObject()); + if ( profile.id.isEmpty() ) + profile.id = newProfileId(); + if ( profile.isValid() ) + result.append(profile); + } + + return result; +} + +QJsonObject BandmapGuide::profileToJsonObject(const Profile &profile, + bool includeId) +{ + FCT_IDENTIFICATION; + + QJsonArray rangeArray; + for ( const Range &range : profile.ranges ) + if ( range.isValid() ) + rangeArray.append(rangeToJsonObject(range)); + + QJsonObject object; + object.insert("version", 1); + if ( includeId ) + object.insert("id", profile.id); + object.insert("title", profile.name); + object.insert("ranges", rangeArray); + return object; +} + +BandmapGuide::Profile BandmapGuide::profileFromJsonObject(const QJsonObject &object) +{ + FCT_IDENTIFICATION; + + Profile profile; + profile.id = object.value("id").toString(); + profile.name = object.value("title").toString(object.value("name").toString()).trimmed(); + + const QJsonArray rangeArray = object.value("ranges").toArray(); + for ( const QJsonValue &value : rangeArray ) + { + Range range = rangeFromJsonObject(value.toObject()); + if ( range.isValid() ) + profile.ranges.append(range); + } + + return profile; +} + +QJsonObject BandmapGuide::rangeToJsonObject(const Range &range) +{ + FCT_IDENTIFICATION; + + QJsonObject object; + object.insert("from", range.from); + object.insert("to", range.to); + + object.insert("color", range.color.name(QColor::HexRgb)); + object.insert("label", range.label); + return object; +} + +BandmapGuide::Range BandmapGuide::rangeFromJsonObject(const QJsonObject &object) +{ + FCT_IDENTIFICATION; + + Range range; + range.from = object.value("from").toDouble(); + range.to = object.value("to").toDouble(); + range.color = QColor(object.value("color").toString()); + range.label = object.value("label").toString(); + return range; +} diff --git a/data/BandmapGuide.h b/data/BandmapGuide.h new file mode 100644 index 00000000..d3b267f5 --- /dev/null +++ b/data/BandmapGuide.h @@ -0,0 +1,81 @@ +#ifndef QLOG_DATA_BANDMAPGUIDE_H +#define QLOG_DATA_BANDMAPGUIDE_H + +#include +#include +#include +#include + +class QJsonObject; + +class BandmapGuide : public QObject +{ + Q_OBJECT + +public: + static BandmapGuide *instance(); + + struct Range + { + double from = 0.0; + double to = 0.0; + QColor color; + QString label; + + Range() = default; + Range(double rangeFrom, + double rangeTo, + const QColor &rangeColor, + const QString &rangeLabel = QString()); + + bool isValid() const; + }; + + struct Profile + { + QString id; + QString name; + QList ranges; + + bool isValid() const; + }; + + static QList profiles(); + static void saveProfiles(const QList &profiles); + + static QString currentProfileId(); + static void setCurrentProfileId(const QString &id); + static Profile currentProfile(); + static bool profileExists(const QString &id); + + static bool isEnabled(); + static void setEnabled(bool state); + + static QString newProfileId(); + static Range defaultRange(); + static Profile exampleProfile(); + + static bool writeProfileToFile(const Profile &profile, + const QString &filePath, + QString *error = nullptr); + static Profile readProfileFromFile(const QString &filePath, + QString *error = nullptr); + +signals: + void changed(); + +private: + explicit BandmapGuide(QObject *parent = nullptr); + static void notifyChanged(); + + static QString profilesToJson(const QList &profiles); + static QList profilesFromJson(const QString &json); + static QJsonObject profileToJsonObject(const Profile &profile, + bool includeId = true); + static Profile profileFromJsonObject(const QJsonObject &object); + + static QJsonObject rangeToJsonObject(const Range &range); + static Range rangeFromJsonObject(const QJsonObject &object); +}; + +#endif // QLOG_DATA_BANDMAPGUIDE_H diff --git a/data/Data.cpp b/data/Data.cpp index f430362c..947c2170 100644 --- a/data/Data.cpp +++ b/data/Data.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include "Data.h" #include "data/Callsign.h" #include "core/debug.h" @@ -12,6 +14,216 @@ MODULE_IDENTIFICATION("qlog.data.data"); +const QString Data::QSO_STATUS_COLOR_DUPE_KEY = QStringLiteral("dupe"); +const QString Data::QSO_STATUS_COLOR_NEW_ENTITY_KEY = QStringLiteral("newEntity"); +const QString Data::QSO_STATUS_COLOR_NEW_BAND_MODE_KEY = QStringLiteral("newBandMode"); +const QString Data::QSO_STATUS_COLOR_NEW_SLOT_KEY = QStringLiteral("newSlot"); +const QString Data::QSO_STATUS_COLOR_WORKED_KEY = QStringLiteral("worked"); +const QString Data::QSO_STATUS_COLOR_CONFIRMED_KEY = QStringLiteral("confirmed"); + +Data::StatusColorRuntime::StatusColorRuntime() +{ + reset(); +} + +void Data::StatusColorRuntime::reset() +{ + colors.clear(); + + dupeColor = defaultQsoStatusColor(QSO_STATUS_COLOR_DUPE_KEY).rgba(); + const QColor bandModeColor = defaultQsoStatusColor(QSO_STATUS_COLOR_NEW_BAND_MODE_KEY); + + setStatusColor(DxccStatus::NewEntity, defaultQsoStatusColor(QSO_STATUS_COLOR_NEW_ENTITY_KEY)); + setStatusColor(DxccStatus::NewBand, bandModeColor); + setStatusColor(DxccStatus::NewMode, bandModeColor); + setStatusColor(DxccStatus::NewBandMode, bandModeColor); + setStatusColor(DxccStatus::NewSlot, defaultQsoStatusColor(QSO_STATUS_COLOR_NEW_SLOT_KEY)); + setStatusColor(DxccStatus::Worked, defaultQsoStatusColor(QSO_STATUS_COLOR_WORKED_KEY)); + setStatusColor(DxccStatus::Confirmed, defaultQsoStatusColor(QSO_STATUS_COLOR_CONFIRMED_KEY)); +} + +void Data::StatusColorRuntime::setStatusColor(DxccStatus status, const QColor &color) +{ + if ( color.isValid() && color.alpha() > 0 ) + colors.insert(static_cast(status), color.rgba()); + else + colors.remove(static_cast(status)); +} + +Data::StatusColorRuntime &Data::statusColorRuntime() +{ + static StatusColorRuntime runtime; + return runtime; +} + +void Data::setRuntimeStatusColor(DxccStatus status, const QColor &color) +{ + statusColorRuntime().setStatusColor(status, color); +} + +void Data::setRuntimeBandModeStatusColor(const QColor &color) +{ + setRuntimeStatusColor(DxccStatus::NewBand, color); + setRuntimeStatusColor(DxccStatus::NewMode, color); + setRuntimeStatusColor(DxccStatus::NewBandMode, color); +} + +void Data::setRuntimeDupeColor(const QColor &color) +{ + if ( !color.isValid() ) + return; + + statusColorRuntime().dupeColor = color.rgba(); +} + +QColor Data::defaultQsoStatusColor(const QString &key) +{ + if ( key == QSO_STATUS_COLOR_DUPE_KEY ) return QColor(109, 109, 109, 100); + if ( key == QSO_STATUS_COLOR_NEW_ENTITY_KEY ) return QColor(255, 58, 9); + if ( key == QSO_STATUS_COLOR_NEW_BAND_MODE_KEY ) return QColor(76, 200, 80); + if ( key == QSO_STATUS_COLOR_NEW_SLOT_KEY ) return QColor(30, 180, 230); + if ( key == QSO_STATUS_COLOR_WORKED_KEY ) return QColor(255, 165, 0); + + return QColor(); +} + +QColor Data::effectiveBackgroundColor(const QColor &background, const QColor &baseColor) +{ + const QColor base = baseColor.isValid() + ? baseColor + : QGuiApplication::palette().color(QPalette::Base); + + if ( !background.isValid() ) + return base; + + if ( background.alpha() == 255 ) + return background; + + const int alpha = background.alpha(); + return QColor((background.red() * alpha + base.red() * (255 - alpha)) / 255, + (background.green() * alpha + base.green() * (255 - alpha)) / 255, + (background.blue() * alpha + base.blue() * (255 - alpha)) / 255); +} + + +QColor Data::textColorForBackground(const QColor &background, + const QColor &defaultColor, + const QColor &baseColor) +{ + static const QColor textDark(0x11, 0x11, 0x11); + static const QColor textLight(0xF0, 0xF0, 0xF0); + + if ( !background.isValid() || background.alpha() == 0 ) + return defaultColor.isValid() + ? defaultColor + : QGuiApplication::palette().color(QPalette::Text); + + // Cache: keyed by effective background color, invalidated on palette change. + // Most views repeat a small set of row colors, so the hit rate is very high. + static QHash cache; + static QRgb baseCacheKey = 0; + + const QColor effective = effectiveBackgroundColor(background, baseColor); + const QRgb effectiveRgb = effective.rgba(); + + // Invalidate cache if the base color changes (theme/palette switch). + const QRgb currentBaseKey = baseColor.isValid() ? baseColor.rgba() + : QGuiApplication::palette().color(QPalette::Base).rgba(); + if ( currentBaseKey != baseCacheKey ) + { + cache.clear(); + baseCacheKey = currentBaseKey; + } + + if ( cache.contains(effectiveRgb) ) + return cache.value(effectiveRgb); + + // Converts a single sRGB channel [0.0, 1.0] to linear light intensity. + // sRGB is gamma-compressed — using raw values directly would underestimate + // the brightness of dark shades. The threshold 0.04045 and coefficients + // 12.92 / 1.055 / 0.055 / 2.4 are defined by the sRGB standard IEC 61966-2-1. + auto relativeLuminance = [](const QColor &color) + { + auto linearize = [](double c) + { + return (c <= 0.04045) ? c / 12.92 + : std::pow((c + 0.055) / 1.055, 2.4); + }; + // Weights 0.2126 / 0.7152 / 0.0722 reflect the human eye's sensitivity + // to red, green, and blue respectively (green dominates perceived brightness). + // Defined by WCAG 2.1 / ITU-R BT.709. + return 0.2126 * linearize(color.redF()) + + 0.7152 * linearize(color.greenF()) + + 0.0722 * linearize(color.blueF()); + }; + + // Computes the WCAG 2.1 contrast ratio between two colors, range [1, 21]. + // The +0.05 offsets prevent division by zero and model the ambient light + // reflected by a real display (black is never perfect black). + auto contrastRatio = [&relativeLuminance](const QColor &a, const QColor &b) + { + const double la = relativeLuminance(a); + const double lb = relativeLuminance(b); + return (std::max(la, lb) + 0.05) / (std::min(la, lb) + 0.05); + }; + + // Pick whichever of white or black yields the higher contrast ratio + // against the effective (alpha-composited) background color. + // WCAG AA requires >= 4.5:1 for normal text; this always picks the best available. + const QColor result = (contrastRatio(effective, textLight) >= contrastRatio(effective, textDark)) + ? textLight + : textDark; + + cache.insert(effectiveRgb, result); + return result; +} + +QColor Data::qsoStatusColorFromConfig(const QVariantMap &colors, const QString &key) +{ + FCT_IDENTIFICATION; + + const QString value = colors.value(key).toString().trimmed(); + if ( value.isEmpty() ) + return QColor(); + + const QColor color(value); + return color.isValid() ? color : QColor(); +} + +void Data::applyConfiguredStatusColor(const QVariantMap &colors, const QString &key, DxccStatus status) +{ + FCT_IDENTIFICATION; + + const QColor color = qsoStatusColorFromConfig(colors, key); + if ( color.isValid() ) + setRuntimeStatusColor(status, color); +} + +void Data::applyConfiguredBandModeStatusColor(const QVariantMap &colors) +{ + FCT_IDENTIFICATION; + + const QColor color = qsoStatusColorFromConfig(colors, QSO_STATUS_COLOR_NEW_BAND_MODE_KEY); + if ( color.isValid() ) + setRuntimeBandModeStatusColor(color); +} + +void Data::reloadQsoStatusColors() +{ + FCT_IDENTIFICATION; + + StatusColorRuntime &runtime = statusColorRuntime(); + runtime.reset(); + + const QVariantMap colors = LogParam::getQsoStatusColors(); + setRuntimeDupeColor(qsoStatusColorFromConfig(colors, QSO_STATUS_COLOR_DUPE_KEY)); + applyConfiguredStatusColor(colors, QSO_STATUS_COLOR_NEW_ENTITY_KEY, DxccStatus::NewEntity); + applyConfiguredBandModeStatusColor(colors); + applyConfiguredStatusColor(colors, QSO_STATUS_COLOR_NEW_SLOT_KEY, DxccStatus::NewSlot); + applyConfiguredStatusColor(colors, QSO_STATUS_COLOR_WORKED_KEY, DxccStatus::Worked); + applyConfiguredStatusColor(colors, QSO_STATUS_COLOR_CONFIRMED_KEY, DxccStatus::Confirmed); +} + Data::Data(QObject *parent) : QObject(parent), zd(nullptr), @@ -19,6 +231,7 @@ Data::Data(QObject *parent) : { FCT_IDENTIFICATION; + reloadQsoStatusColors(); loadContests(); loadPropagationModes(); loadLegacyModes(); @@ -527,28 +740,20 @@ qulonglong Data::dupeNewCountWhenQSODelected(qulonglong oldCounter, #undef RETURNCODE -QColor Data::statusToColor(const DxccStatus &status, bool isDupe, const QColor &defaultColor) { - FCT_IDENTIFICATION; - - qCDebug(function_parameters) << status << isDupe; +QColor Data::statusToColor(const DxccStatus &status, bool isDupe, const QColor &defaultColor) +{ + // Keep this hot path to an indexed lookup; settings are folded into the runtime table. + const StatusColorRuntime &runtime = statusColorRuntime(); if ( isDupe ) - return QColor(109, 109, 109, 100); + return QColor::fromRgba(runtime.dupeColor); - switch (status) { - case DxccStatus::NewEntity: - return QColor(255, 58, 9); - case DxccStatus::NewBand: - case DxccStatus::NewMode: - case DxccStatus::NewBandMode: - return QColor(76, 200, 80); - case DxccStatus::NewSlot: - return QColor(30, 180, 230); - case DxccStatus::Worked: - return QColor(255,165,0); - default: - return defaultColor; - } + const int index = static_cast(status); + const auto color = runtime.colors.constFind(index); + if ( color != runtime.colors.constEnd() ) + return QColor::fromRgba(color.value()); + + return defaultColor; } QString Data::colorToHTMLColor(const QColor &in_color) @@ -675,6 +880,34 @@ QPair Data::legacyMode(const QString &mode) return legacyModes.value(mode); } +QStringList Data::submodesForMode(const QString &mode) const +{ + FCT_IDENTIFICATION; + + if ( mode.isEmpty() ) + return QStringList(); + + QSqlQuery query; + query.prepare("SELECT submodes FROM modes WHERE name = :mode"); + query.bindValue(":mode", mode); + + if ( !query.exec() || !query.next() ) + return QStringList(); + + return QJsonDocument::fromJson(query.value(0).toString().toUtf8()).toVariant().toStringList(); +} + +bool Data::isSubmodeForMode(const QString &mode, const QString &submode) const +{ + FCT_IDENTIFICATION; + + // Empty submode is a valid way to store a plain ADIF mode without subtype. + if ( submode.isEmpty() ) + return true; + + return submodesForMode(mode).contains(submode); +} + QString Data::getIANATimeZone(double lat, double lon) { FCT_IDENTIFICATION; @@ -1518,4 +1751,3 @@ WWFFEntity Data::lookupWWFF(const QString &reference) return WWFFRet; } - diff --git a/data/Data.h b/data/Data.h index ddd12e2b..2e29c75f 100644 --- a/data/Data.h +++ b/data/Data.h @@ -2,6 +2,7 @@ #define QLOG_DATA_DATA_H #include +#include #include #include "Dxcc.h" #include "SOTAEntity.h" @@ -126,6 +127,17 @@ class Data : public QObject const QString &deletedMode); static QColor statusToColor(const DxccStatus &status, bool isDupe, const QColor &defaultColor); + static const QString QSO_STATUS_COLOR_DUPE_KEY; + static const QString QSO_STATUS_COLOR_NEW_ENTITY_KEY; + static const QString QSO_STATUS_COLOR_NEW_BAND_MODE_KEY; + static const QString QSO_STATUS_COLOR_NEW_SLOT_KEY; + static const QString QSO_STATUS_COLOR_WORKED_KEY; + static const QString QSO_STATUS_COLOR_CONFIRMED_KEY; + static QColor defaultQsoStatusColor(const QString &key); + static QColor textColorForBackground(const QColor &background, + const QColor &defaultColor = QColor(), + const QColor &baseColor = QColor()); + static void reloadQsoStatusColors(); static QString colorToHTMLColor(const QColor&); static QString statusToText(const DxccStatus &status); static QString removeAccents(const QString &input); @@ -163,6 +175,8 @@ class Data : public QObject const QString dxccName(int dxcc) const {return dxccEntityStaticInfo.value(dxcc).value("name").toString();}; int dxccITUZ(int dxcc) const {return dxccEntityStaticInfo.value(dxcc).value("ituz").toInt();}; QPair legacyMode(const QString &mode); + QStringList submodesForMode(const QString &mode) const; + bool isSubmodeForMode(const QString &mode, const QString &submode) const; QStringList satModeList() { return satModes.values();} QStringList satModesIDList() { return satModes.keys(); } QString satModeTextToID(const QString &satModeText) { return satModes.key(satModeText);} @@ -224,6 +238,26 @@ public slots: static const char translitTab[]; static const int tranlitIndexMap[]; + + struct StatusColorRuntime + { + StatusColorRuntime(); + void reset(); + void setStatusColor(DxccStatus status, const QColor &color); + + QHash colors; + QRgb dupeColor; + }; + + static StatusColorRuntime &statusColorRuntime(); + static void setRuntimeStatusColor(DxccStatus status, const QColor &color); + static void setRuntimeBandModeStatusColor(const QColor &color); + static void setRuntimeDupeColor(const QColor &color); + static QColor qsoStatusColorFromConfig(const QVariantMap &colors, const QString &key); + static void applyConfiguredStatusColor(const QVariantMap &colors, const QString &key, DxccStatus status); + static void applyConfiguredBandModeStatusColor(const QVariantMap &colors); + static QColor effectiveBackgroundColor(const QColor &background, const QColor &baseColor); + static int colorBrightness(const QColor &color); }; #endif // QLOG_DATA_DATA_H diff --git a/data/Gridsquare.cpp b/data/Gridsquare.cpp index 7b2e889a..423aafc6 100644 --- a/data/Gridsquare.cpp +++ b/data/Gridsquare.cpp @@ -7,8 +7,54 @@ MODULE_IDENTIFICATION("qlog.core.gridsquare"); -Gridsquare::Gridsquare(const QString &in_grid) : - validGrid(false), lat(qQNaN()), lon(qQNaN()) +static const double DEG_TO_RAD = M_PI / 180.0; +static const double RAD_TO_DEG = 180.0 / M_PI; +static const double IARU_EARTH_RADIUS_KM = 40032.0 / (2.0 * M_PI); + +static bool gridCharInRange(const QString &grid, int index, char min, char max) +{ + const char ch = grid.at(index).toLatin1(); + return ch >= min && ch <= max; +} + +static bool gridCharIsDigit(const QString &grid, int index) +{ + return gridCharInRange(grid, index, '0', '9'); +} + +static bool isValidGrid(const QString &grid) +{ + const int size = grid.size(); + + if ( size != 2 && size != 4 && size != 6 && size != 8 ) + return false; + + if ( !gridCharInRange(grid, 0, 'A', 'R') + || !gridCharInRange(grid, 1, 'A', 'R') ) + return false; + + if ( size == 2 ) + return true; + + if ( !gridCharIsDigit(grid, 2) + || !gridCharIsDigit(grid, 3) ) + return false; + + if ( size == 4 ) + return true; + + if ( !gridCharInRange(grid, 4, 'A', 'X') + || !gridCharInRange(grid, 5, 'A', 'X') ) + return false; + + if ( size == 6 ) + return true; + + return gridCharIsDigit(grid, 6) + && gridCharIsDigit(grid, 7); +} + +Gridsquare::Gridsquare(const QString &in_grid) { FCT_IDENTIFICATION; @@ -16,7 +62,7 @@ Gridsquare::Gridsquare(const QString &in_grid) : { grid = in_grid.toUpper(); - if ( gridRegEx().match(grid).hasMatch() ) + if ( isValidGrid(grid) ) { lon = (grid.at(0).toLatin1() - 'A') * 20 - 180; lat = (grid.at(1).toLatin1() - 'A') * 10 - 90; @@ -71,8 +117,9 @@ Gridsquare::Gridsquare(const QString &in_grid) : } } -Gridsquare::Gridsquare(const double inlat, const double inlon) : - validGrid(false), lat(inlat), lon(inlon) +Gridsquare::Gridsquare(double inlat, double inlon) : + lat(inlat), + lon(inlon) { FCT_IDENTIFICATION; @@ -105,27 +152,52 @@ Gridsquare::Gridsquare(const double inlat, const double inlon) : } } -const QRegularExpression Gridsquare::gridRegEx() +Gridsquare Gridsquare::mapDisplayGrid(const QString &grid) +{ + FCT_IDENTIFICATION; + + const Gridsquare strictGrid(grid); + + return (strictGrid.isValid() || grid.size() <= 8) ? strictGrid + : Gridsquare(grid.left(8)); +} + +const QRegularExpression &Gridsquare::gridRegEx() { FCT_IDENTIFICATION; - return QRegularExpression("^[A-Ra-r]{2}(?:[0-9]{2}|[0-9]{2}[A-Xa-x]{2}|[0-9]{2}[A-Xa-x]{2}[0-9]{2})?$"); + static const QRegularExpression regex(QStringLiteral("^[A-Ra-r]{2}(?:[0-9]{2}|" + "[0-9]{2}[A-Xa-x]{2}|" + "[0-9]{2}[A-Xa-x]{2}[0-9]{2})?$")); + return regex; //return QRegularExpression("^[A-Ra-r]{2}[0-9]{2}([A-Xa-x]{2})?([0-9]{2})?$"); } -const QRegularExpression Gridsquare::gridVUCCRegEx() +const QRegularExpression &Gridsquare::gridVUCCRegEx() { FCT_IDENTIFICATION; - return QRegularExpression("^[A-Ra-r]{2}(?:[0-9]{2}|[0-9]{2}[A-Xa-x]{2}),[ ]*[A-Ra-r]{2}(?:[0-9]{2}|[0-9]{2}[A-Xa-x]{2})$|" - "^[A-Ra-r]{2}(?:[0-9]{2}|[0-9]{2}[A-Xa-x]{2}),[ ]*[A-Ra-r]{2}(?:[0-9]{2}|[0-9]{2}[A-Xa-x]{2}),[ ]*[A-Ra-r]{2}(?:[0-9]{2}|[0-9]{2}[A-Xa-x]{2}),[ ]*[A-Ra-r]{2}(?:[0-9]{2}|[0-9]{2}[A-Xa-x]{2})$"); + static const QRegularExpression regex(QStringLiteral("^[A-Ra-r]{2}(?:[0-9]{2}|" + "[0-9]{2}[A-Xa-x]{2}),[ ]*" + "[A-Ra-r]{2}(?:[0-9]{2}|" + "[0-9]{2}[A-Xa-x]{2})$|" + "^[A-Ra-r]{2}(?:[0-9]{2}|" + "[0-9]{2}[A-Xa-x]{2}),[ ]*" + "[A-Ra-r]{2}(?:[0-9]{2}|" + "[0-9]{2}[A-Xa-x]{2}),[ ]*" + "[A-Ra-r]{2}(?:[0-9]{2}|" + "[0-9]{2}[A-Xa-x]{2}),[ ]*" + "[A-Ra-r]{2}(?:[0-9]{2}|" + "[0-9]{2}[A-Xa-x]{2})$")); + return regex; } -const QRegularExpression Gridsquare::gridExtRegEx() +const QRegularExpression &Gridsquare::gridExtRegEx() { FCT_IDENTIFICATION; - return QRegularExpression("^[A-Xa-x]{2}?([0-9]{2})?$"); + static const QRegularExpression regex(QStringLiteral("^[A-Xa-x]{2}?([0-9]{2})?$")); + return regex; } double Gridsquare::distance2localeUnitDistance(double km, @@ -171,16 +243,18 @@ bool Gridsquare::distanceTo(double lat, double lon, double &distance) const } /* https://www.movable-type.co.uk/scripts/latlong.html */ - double dLat = (lat - this->getLatitude()) * M_PI / 180; - double dLon = (lon - this->getLongitude()) * M_PI / 180; + const double dLat = (lat - this->lat) * DEG_TO_RAD; + const double dLon = (lon - this->lon) * DEG_TO_RAD; - double lat1 = this->getLatitude() * M_PI / 180; - double lat2 = lat * M_PI / 180; + const double lat1 = this->lat * DEG_TO_RAD; + const double lat2 = lat * DEG_TO_RAD; - double a = sin(dLat / 2) * sin(dLat / 2) + - sin(dLon / 2) * sin(dLon / 2) * cos(lat1) * cos(lat2); + const double sinDLat = sin(dLat / 2.0); + const double sinDLon = sin(dLon / 2.0); + const double a = sinDLat * sinDLat + + sinDLon * sinDLon * cos(lat1) * cos(lat2); - double c = 2 * atan2(sqrt(a), sqrt(1-a)); + const double c = 2.0 * atan2(sqrt(a), sqrt(1.0 - a)); // Based on IARU Rules // The centre of the Large Locator Square (e.g. IO84MM to IO91MM) is used for distance calculations. @@ -190,7 +264,7 @@ bool Gridsquare::distanceTo(double lat, double lon, double &distance) const // It means that 111.2km/° * 360 =40032km // It means that R = 40032 / (2*PI) - distance = (40032.0 / (2 * M_PI)) * c; + distance = IARU_EARTH_RADIUS_KM * c; return true; } @@ -217,14 +291,14 @@ bool Gridsquare::bearingTo(double lat, double lon, double &bearing) const return false; } - double dLon = (lon - this->getLongitude()) * M_PI / 180; - double lat1 = this->getLatitude() * M_PI / 180; - double lat2 = lat * M_PI / 180; + const double dLon = (lon - this->lon) * DEG_TO_RAD; + const double lat1 = this->lat * DEG_TO_RAD; + const double lat2 = lat * DEG_TO_RAD; - double y = sin(dLon) * cos(lat2); - double x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon); + const double y = sin(dLon) * cos(lat2); + const double x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon); - bearing = fmod((180.0 * atan2(y, x) / M_PI + 360.0), 360.0); + bearing = fmod((atan2(y, x) * RAD_TO_DEG + 360.0), 360.0); return true; } diff --git a/data/Gridsquare.h b/data/Gridsquare.h index 14bd06e8..e16c603c 100644 --- a/data/Gridsquare.h +++ b/data/Gridsquare.h @@ -2,6 +2,7 @@ #define QLOG_CORE_GRIDSQUARE_H #include +#include #include #include @@ -11,18 +12,19 @@ class Gridsquare { public: explicit Gridsquare(const QString &in_grid = QString()); - explicit Gridsquare(const double inlat,const double inlon); - ~Gridsquare() {}; - static const QRegularExpression gridRegEx(); - static const QRegularExpression gridVUCCRegEx(); - static const QRegularExpression gridExtRegEx(); + explicit Gridsquare(double inlat, double inlon); + ~Gridsquare() = default; + static Gridsquare mapDisplayGrid(const QString &grid); + static const QRegularExpression &gridRegEx(); + static const QRegularExpression &gridVUCCRegEx(); + static const QRegularExpression &gridExtRegEx(); static double distance2localeUnitDistance(double km, QString &unit, const LogLocale &locale); static double localeDistanceCoef(const LogLocale &locale); bool isValid() const; double getLongitude() const {return lon;}; double getLatitude() const {return lat;}; - const QString getGrid() const { return grid;}; + const QString &getGrid() const { return grid;}; bool distanceTo(const Gridsquare &in_grid, double &distance) const; bool distanceTo(double lat, double lon, double &distance) const; bool bearingTo(const Gridsquare &in_grid, double &bearing) const; @@ -32,8 +34,9 @@ class Gridsquare private: QString grid; - bool validGrid; - double lat, lon; + bool validGrid = false; + double lat = qQNaN(); + double lon = qQNaN(); }; #endif // QLOG_CORE_GRIDSQUARE_H diff --git a/devtools/deployment/verify_and_sign_github_sources.sh b/devtools/deployment/verify_and_sign_github_sources.sh new file mode 100755 index 00000000..470948a1 --- /dev/null +++ b/devtools/deployment/verify_and_sign_github_sources.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +# Download GitHub-generated source archives for a tag via gh CLI, verify +# their content against the local repository tag, then GPG-sign both archives. +# +# Usage: +# ./devtools/deployment/verify_and_sign_github_sources.sh [gpg_key_id] +# +# Example: +# ./devtools/deployment/verify_and_sign_github_sources.sh v0.50.0 +# ./devtools/deployment/verify_and_sign_github_sources.sh v0.50.0 0xDEADBEEF + +# ########## CONFIG +ARTIFACT_DIR="/home/foldynl/QLog_releases/release_artifacts" +# ########### END OF CONFIG + +set -euo pipefail + +if [[ $# -lt 1 ]]; then + echo "Usage: $0 [gpg_key_id]" + exit 1 +fi + +TAG="$1" +GPG_KEY_ID="${2:-}" + +if ! command -v gh > /dev/null 2>&1; then + echo "ERROR: gh CLI is required." + exit 1 +fi + +if ! command -v gpg > /dev/null 2>&1; then + echo "ERROR: gpg is required." + exit 1 +fi + +if ! command -v unzip > /dev/null 2>&1; then + echo "ERROR: unzip is required." + exit 1 +fi + +if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then + echo "ERROR: must be run from inside a git repository." + exit 1 +fi + +if ! git rev-parse -q --verify "refs/tags/${TAG}" > /dev/null 2>&1; then + echo "ERROR: local tag '${TAG}' does not exist." + exit 1 +fi + +if ! gh auth status > /dev/null 2>&1; then + echo "ERROR: gh is not authenticated. Run 'gh auth login' first." + exit 1 +fi + +REPO_FULL="$(gh repo view --json nameWithOwner -q .nameWithOwner)" +REPO_NAME="$(gh repo view --json name -q .name)" + +if ! gh release view "${TAG}" > /dev/null 2>&1; then + echo "ERROR: GitHub release '${TAG}' does not exist." + exit 1 +fi + +mkdir -p "${ARTIFACT_DIR}" + +TAR_FILE="${ARTIFACT_DIR}/${TAG}.tar.gz" +ZIP_FILE="${ARTIFACT_DIR}/${TAG}.zip" + +TMPDIR="$(mktemp -d)" +trap 'rm -rf "${TMPDIR}"' EXIT + +echo "Downloading GitHub source archives for ${REPO_FULL} ${TAG}..." +gh api "/repos/${REPO_FULL}/tarball/${TAG}" > "${TAR_FILE}" +gh api "/repos/${REPO_FULL}/zipball/${TAG}" > "${ZIP_FILE}" + +verify_archive() { + local archive="$1" + local extract_dir="$2" + local fmt="$3" + + mkdir -p "${extract_dir}" + + if [[ "${fmt}" == "tar" ]]; then + tar -xzf "${archive}" -C "${extract_dir}" + elif [[ "${fmt}" == "zip" ]]; then + unzip -q "${archive}" -d "${extract_dir}" + else + echo "ERROR: unknown archive format '${fmt}'." + exit 1 + fi + + local root_dir + root_dir="$(find "${extract_dir}" -mindepth 1 -maxdepth 1 -type d | head -n 1)" + if [[ -z "${root_dir}" ]]; then + echo "ERROR: could not find extracted top-level directory in ${archive}." + exit 1 + fi + + local git_files + local arc_files + git_files="${TMPDIR}/${fmt}_git_files.txt" + arc_files="${TMPDIR}/${fmt}_arc_files.txt" + + git ls-tree -r --name-only "${TAG}" | sort > "${git_files}" + find "${root_dir}" -type f | sed "s#^${root_dir}/##" | sort > "${arc_files}" + + if ! diff -u "${git_files}" "${arc_files}" > "${TMPDIR}/${fmt}_files.diff"; then + echo "ERROR: file list mismatch for ${archive}." + cat "${TMPDIR}/${fmt}_files.diff" + exit 1 + fi + + echo "File list OK for ${archive}. Checking file content hashes..." + + local mismatches=0 + local rel_path + while IFS= read -r rel_path; do + local hash_git + local hash_arc + hash_git="$(git show "${TAG}:${rel_path}" | sha256sum | awk '{print $1}')" + hash_arc="$(sha256sum "${root_dir}/${rel_path}" | awk '{print $1}')" + if [[ "${hash_git}" != "${hash_arc}" ]]; then + echo "MISMATCH: ${rel_path}" + mismatches=$((mismatches + 1)) + fi + done < "${git_files}" + + if [[ "${mismatches}" -ne 0 ]]; then + echo "ERROR: ${mismatches} file(s) differ in ${archive}." + exit 1 + fi + + echo "Content hashes OK for ${archive}." +} + +verify_archive "${TAR_FILE}" "${TMPDIR}/tar_extract" "tar" +verify_archive "${ZIP_FILE}" "${TMPDIR}/zip_extract" "zip" + +echo "All checks passed. Creating GPG signatures..." + +if [[ -n "${GPG_KEY_ID}" ]]; then + gpg --armor --detach-sign --local-user "${GPG_KEY_ID}" "${TAR_FILE}" + gpg --armor --detach-sign --local-user "${GPG_KEY_ID}" "${ZIP_FILE}" +else + gpg --armor --detach-sign "${TAR_FILE}" + gpg --armor --detach-sign "${ZIP_FILE}" +fi + +echo "Uploading signatures to release ${TAG}..." +gh release upload "${TAG}" "${TAR_FILE}.asc" "${ZIP_FILE}.asc" --clobber + +echo "" +echo "Done." +echo "Signed files:" +echo " ${TAR_FILE}.asc" +echo " ${ZIP_FILE}.asc" +echo "Uploaded to release ${TAG}:" +echo " $(basename "${TAR_FILE}.asc")" +echo " $(basename "${ZIP_FILE}.asc")" diff --git a/i18n/qlog_cs.qm b/i18n/qlog_cs.qm index 083b6c8f..446b022d 100644 Binary files a/i18n/qlog_cs.qm and b/i18n/qlog_cs.qm differ diff --git a/i18n/qlog_cs.ts b/i18n/qlog_cs.ts index 479c86ef..289cbbe2 100644 --- a/i18n/qlog_cs.ts +++ b/i18n/qlog_cs.ts @@ -199,20 +199,119 @@ + Bandmap Guide + Průvodce bandmapou + + + + Guide + Průvodce + + + Fields Pole - + Must not be empty Nesmí být prázdné - + + Leave unchanged + Ponechat beze změny + + + + Off + Vypnuto + + + Unsaved Neuloženo + + AdifRecoveryManager + + + Startup ADI found more than %1 new QSOs in %2. Use the standard Import. Load point was moved to the end of the file. + Spouštěcí ADI obsahuje více než %1 nových QSO v %2. Použijte standardní import. Bod načtení byl přesunut na konec souboru. + + + + Startup ADI Station Profile does not exist: %1 + Profil stanice pro Startup ADI neexistuje: %1 + + + + Cannot open Startup ADI records from %1 + Nelze otevřít záznamy Startup ADI z %1 + + + + Startup ADI from %1 finished with %n error(s); load point was not advanced. + + Startup ADI z %1 skončil s %n chybou/chybami; bod načtení nebyl posunut. + + + + + + + Startup ADI was disabled for %n file(s) because the assigned Station Profile no longer exists. + + Startup ADI byl zakázán pro %n souborů, protože přiřazený profil stanice již neexistuje. + + + + + + + AdifRecoveryReaderWorker + + + Startup ADI filename is empty + Název souboru Startup ADI je prázdný + + + + Startup ADI file does not exist: %1 + Soubor Startup ADI neexistuje: %1 + + + + Startup ADI initialized at the end of file + Startup ADI inicializován na konci souboru + + + + Startup ADI file was reset; load point moved to the end + Soubor Startup ADI byl resetován; bod načtení byl přesunut na konec + + + + Cannot open Startup ADI file: %1 + Nelze otevřít soubor Startup ADI: %1 + + + + Cannot seek Startup ADI file: %1 + Nelze nastavit pozici v souboru Startup ADI: %1 + + + + Cannot read Startup ADI file: %1 + Nelze číst soubor Startup ADI: %1 + + + + Too many ADIF records for automatic recovery + Příliš mnoho ADIF záznamů pro automatickou obnovu + + AlertRuleDetail @@ -495,42 +594,42 @@ AlertTableModel - + Rule Name Pravidlo - + Callsign Značka - + Frequency Frekvence - + Mode Druh provozu - + Updated Aktualizací - + Last Update Poslední - + Last Comment Zpráva - + Member Člen @@ -580,78 +679,83 @@ Awards Diplomy - - - Options - Nastavení - Award Diplom - + + 🌐 Rules + 🌐 Pravidla + + + My DXCC Entity Má DXCC Země - + User Filter Uživatelský filtr - + Confirmed by Potvrzeno - + LoTW LoTW - + eQSL eQSL - + Paper QSL - + Mode Druh provozu - + CW CW - + Phone Fóne - + Digi Digi - + Not-Worked Only Nepracováno - + Not-Confirmed Only Nepotvrzeno - + + Double-click a row/cell to show QSOs + Dvojklikem na řádek/buňku zobrazíte QSO + + + Show Zobrazit @@ -666,7 +770,7 @@ ITU - + WAC WAC @@ -731,80 +835,85 @@ Lokátor %1 znaků - + US Counties Okresy USA - + Russian Districts Ruské distrikty - + Japanese Cities/Ku/Guns Japonská města / ku / guny - + NZ Counties Okresy Nového Zélandu - + Spanish DMEs Španělské DME - + Ukrainian Districts Ukrajinské distrikty - + No User Filter Žádný uživatelský filtr - + North America Severní Amerika - + South America Jižní Amerika - + Europe Evropa - + Africa Afrika - + Asia Asie - - Antarctica - Antarktida - - - + Oceania Oceánie - + DELETED Smazáno + + + WAAC + + + + + WAIP + + AwardsTableModel @@ -829,6 +938,198 @@ Stále čeká + + BandmapGuideDialog + + + Bandmap Guide + Průvodce bandmapou + + + + Import guide + Import průvodce + + + + Import + Import + + + + Export guide + Export průvodce + + + + Export + Export + + + + New guide + Nový průvodce + + + + New + Nový + + + + Copy guide + Kopírovat průvodce + + + + Copy + Kopírovat + + + + Delete guide + Smazat průvodce + + + + Delete + Vymazat + + + + Guide Name: + Název průvodce: + + + + Ranges: + Rozsahy: + + + + From + Od + + + + To + Do + + + + Color + Barva + + + + Label + Štítek + + + + Add range + Přidat rozsah + + + + Add + Přidat + + + + Remove selected range + Odebrat vybraný rozsah + + + + Remove + Vymazat + + + + + MHz + MHz + + + + + New Guide + Nový průvodce + + + + Copy - %1 + Kopie – %1 + + + + Delete Guide + Smazat průvodce + + + + Delete guide '%1'? + Smazat průvodce „%1“? + + + + Import Guide + Import průvodce + + + + QLog Bandmap Guide (*.qbg);;JSON (*.json) + QLog Bandmap průvodce (*.qbg);;JSON (*.json) + + + + Import Failed + Import se nezdařil + + + + Export Guide + Export průvodce + + + + QLog Bandmap Guide (*.qbg) + QLog Bandmap průvodce (*.qbg) + + + + Export Failed + Export se nezdařil + + + + Guide Color + Barva průvodce + + + + + + QLog Warning + Upozornění QLog + + + + Guide name cannot be empty. + Název průvodce nesmí být prázdný. + + + + Guide name '%1' is already used. + Název průvodce „%1“ je již použit. + + + + Guide '%1' contains an invalid range. + Průvodce „%1“ obsahuje neplatný rozsah. + + BandmapWidget @@ -867,30 +1168,60 @@ min - + Bandmap Bandmap - + Show Band Zobrazit pásmo - + Center RX Centrovat RX - + Show Emergency Frequencies Zobrazit nouzové frekvence - + + Show IBP Frequencies + Zobrazit frekvence IBP + + + + Show Guide + Zobrazit průvodce + + + + Off + Vypnuto + + + + No Guide + Žádný průvodce + + + + Edit Guide... + Upravit průvodce... + + + SOS SOS + + + IBP + IBP + CWCatKey @@ -1167,27 +1498,27 @@ CWKeyer - + No CW Keyer Profile selected Není vybrán žádný profil klíče - + Initialization Error Chyba inicializace - + Internal Error Interní Chyba - + Connection Error Chyba připojení - + Cannot open the Keyer connection Nelze přijipoji klíč @@ -1818,198 +2149,198 @@ Import - + Export template Exportovat šablonu - + Export Export - + New template Nová šablona - + New Nový - + Copy existing template Kopírovat existující šablonu - + Copy Kopírovat - + Delete template Smazat šablonu - + Delete Vymazat - + Template Name: Název šablony: - + Contest Name: Název závodu: - + Default Mode: Výchozí mód: - + QSO Line Columns: Sloupce řádku QSO: - + Contest name as required by the rules. It is possible to enter a custom string if it is not included in the list. Název soutěže podle pravidel. Je možné zadat vlastní řetězec, pokud není v seznamu. - + Seq. Poř. - + QSO Field Pole QSO - + Formatter Formátovač - + Width Šířka - + Label Štítek - + Add line Přidat řádek - + Add Přidat - + Remove selected line Odstranit vybraný řádek - + Remove Vymazat - + New Template Nová šablona - + Copy - %1 Kopie – %1 - + Delete Template Smazat šablonu - + Delete template '%1'? Smazat šablonu „%1“? - + Import Template Importovat šablonu - - + + QLog Cabrillo Template (*.qct) QLog Cabrillo šablona (*.qct) - + Import Failed Import se nezdařil - + Export Template Exportovat šablonu - + Export Failed Export se nezdařil - + Failed to write file: %1 Nepodařilo se zapsat soubor: %1 - + File not found: %1 Soubor nebyl nalezen: %1 - + Cannot open file: %1 Nelze otevřít soubor: %1 - + Invalid template file: missing name Neplatný soubor šablony: chybí název - + QLog Error Chyba QLog - + Cannot start database transaction. Nelze zahájit databázovou transakci. - + QLog Warning Upozornění QLog - + Cannot save template '%1': %2 Nelze uložit šablonu „%1“: %2 @@ -2076,20 +2407,24 @@ Hodiny - - + + Sunrise Východ - - + + Sunset Západ - - + + + + + + N/A - @@ -2153,7 +2488,7 @@ Jiné - + Done Dokončit @@ -2161,12 +2496,12 @@ ColumnSettingGenericDialog - + Unselect All Odznačit vše - + Select All Vybrat vše @@ -2179,7 +2514,7 @@ Nastavení zobrazených sloupců - + Done Dokončit @@ -4229,70 +4564,60 @@ Data - + New Entity Nová země - + New Band Nové pásmo - + New Mode Nový druh provozu - + New Band&Mode Nové pásmo&druh - + New Slot Nový slot - + Confirmed Potvrzeno - + Worked Pracováno - + Hz Hz - + kHz kHz - + GHz GHz - + MHz MHz - - - - - - - - Yes - Ano - @@ -4300,136 +4625,146 @@ - No - Ne + Yes + Ano + + + + + No + Ne + + + + Requested Vyžádáno - + Queued Ve frontě - - - + + + Invalid Chybné - + Bureau Bureau - + Direct Direct - + Electronic Elektronicky - - - - - - - - + + + + + + + + Blank Nevyplněno - + Modified Upraveno - + Grayline - + Other Jiné - + Short Path - + Long Path - + Not Heard Neslyšeno - + Uncertain Nejistý - + Straight Key - + Sideswiper - + Mechanical semi-automatic keyer or Bug - + Mechanical fully-automatic keyer or Bug - + Single Paddle Single Paddle - + Dual Paddle - + Computer Driven - + Confirmed (AG) Potvrzeno (AG) - + Confirmed (no AG) Potvrzeno (non-AG) - + Unknown Neurčeno @@ -5075,57 +5410,57 @@ Example: DxTableModel - + Time Čas - + Callsign Značka - + Frequency Frekvence - + Mode Mode - + Spotter Spotter - + Comment Poznámka - + Continent Kontinent - + Spotter Continent Kontinent Spottera - + Band Pásmo - + Member Člen - + Country Země @@ -5139,7 +5474,7 @@ Example: - + Connect Připojit @@ -5304,67 +5639,67 @@ Example: Filtr DXC - + My Continent Můj Kontinent - + Auto Automaticky - + Connecting... Připojování... - + DX Cluster is temporarily unavailable DX Cluster je dočasně nedostupný - + DXC Server Error Chyba DXC Serveru - + An invalid callsign Nesprávná značka - + DX Cluster Password DX Cluster Heslo - + Security Notice Bezpečnostní upozornění - + The password can be sent via an unsecured channel Heslo může být posláno přes nezabezpečený kanál - + Server Server - + Username Uživatelské jméno - + Disconnect Odpojit - + DX Cluster Command Příkaz DX Clusteru @@ -5372,22 +5707,22 @@ Example: DxccTableModel - + Worked Pracováno - + eQSL eQSL - + LoTW LoTW - + Paper QSL @@ -5483,7 +5818,7 @@ Example: - + POTA POTA @@ -5683,42 +6018,42 @@ Example: Nelze označit exportovaná QSO jako Odeslaná - + Generic Obecný - + QSLs QSL - + QSL-specific QSL specifické - + All Vše - + Minimal Minimální - + Custom 1 Uživatelské 1 - + Custom 2 Uživatelské 2 - + Custom 3 Uživatelské 3 @@ -5847,132 +6182,132 @@ Toto heslo bude později potřeba pro jejich obnovení. Nelze nastavit auto_power_on - + Cannot set no_xchg to 1 Nelze nastavit no_xchg na 1 - + Rig Open Error Spojení nelze navázat - + Set TX Frequency Error Chyba nastavení TX frekvence - + Set Frequency Error Chyba v nastavení frekvence - + Set Split Error Chyba nastavení split - + Set Mode Error Chyba nastavení režimu - + Set Split Mode Error Chyba nastavení split režimu - + Set PTT Error Chyba v získaní stavu PTT - + Cannot sent Morse This cannot be displayed - + Cannot stop Morse This cannot be displayed - + Get PTT Error This cannot be displayed - + Get Frequency Error Chyba v získání frekvence - + Get Mode Error Chyba v získání druhu provozu - + Get VFO Error Chyba v získání VFO - + Get PWR Error This cannot be displayed - + Get PWR (power2mw) Error This cannot be displayed - + Get RIT Function Error This cannot be displayed - + Get RIT Error This cannot be displayed - + Get XIT Function Error This cannot be displayed - + Get XIT Error This cannot be displayed - + Get Split Error - + Get TX Frequency Error - + Get KeySpeed Error This cannot be displayed - + Set KeySpeed Error This cannot be displayed @@ -6010,140 +6345,260 @@ Toto heslo bude později potřeba pro jejich obnovení. ImportDialog - + Import Import - + Import all or only QSOs from the given period Importovat vše nebo vybraná QSO z vybraného časového období - + File Soubor - + Browse Procházet - + Defaults Výchozí - - + + Values are used only for fields that are missing in the import file. Existing values are preserved. + Hodnoty se použijí pouze pro pole, která v importním souboru chybí. Existující hodnoty zůstanou zachovány. + + + + <p>⚠ Missing QSL Sent fields are set to <b>"N"</b> (do not send) by default in ADIF. + <p>⚠ Chybějící pole QSL Sent jsou v ADIF ve výchozím stavu nastavena na <b>"N"</b> (neodesílat). + + + + Comment Komentář - + + Used only for missing QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT, and DCL_QSL_SENT fields where default is "N"; otherwise, the value from the input is used. + Použije se pouze pro chybějící pole QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT a DCL_QSL_SENT, kde je výchozí hodnota „N“; jinak se použije hodnota ze vstupu. + + + + QSL Sent status + + + + + Used only for missing QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT, and DCL_QSL_SENT fields where default is "N"; otherwise, the value from the input is used.<p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Použije se pouze pro chybějící pole QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT a DCL_QSL_SENT, kde je výchozí hodnota „N“; jinak se použije hodnota ze vstupu.<p><b>Ve frontě</b> (připraveno), <b>Ne</b> (neodesílat), <b>Ignorovat</b> (nesledovat), <b>Vyžádáno</b> (vyžádáno), <b>Ano</b> (již odesláno). + + + + Used only when the imported ADIF record does not contain the selected field. Explicit ADIF values are kept. + Použije se pouze tehdy, když importovaný ADIF záznam neobsahuje vybrané pole. Explicitní hodnoty ADIF zůstanou zachovány. + + + + Default value for missing DCL_QSL_SENT. <p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Výchozí hodnota pro chybějící DCL_QSL_SENT. <p><b>Ve frontě</b> (připraveno), <b>Ne</b> (neodesílat), <b>Ignorovat</b> (nesledovat), <b>Vyžádáno</b> (vyžádáno), <b>Ano</b> (již odesláno). + + + + Default value for missing EQSL_QSL_SENT. <p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Výchozí hodnota pro chybějící EQSL_QSL_SENT. <p><b>Ve frontě</b> (připraveno), <b>Ne</b> (neodesílat), <b>Ignorovat</b> (nesledovat), <b>Vyžádáno</b> (vyžádáno), <b>Ano</b> (již odesláno). + + + + Default value for missing LOTW_QSL_SENT. <p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Výchozí hodnota pro chybějící LOTW_QSL_SENT. <p><b>Ve frontě</b> (připraveno), <b>Ne</b> (neodesílat), <b>Ignorovat</b> (nesledovat), <b>Vyžádáno</b> (vyžádáno), <b>Ano</b> (již odesláno). + + + + LoTW + LoTW + + + + DCL + DCL + + + + Paper QSL + Papírové QSL + + + + eQSL + eQSL + + + + Default value for missing QSL_SENT.<p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Výchozí hodnota pro chybějící QSL_SENT.<p><b>Ve frontě</b> (připraveno), <b>Ne</b> (neodesílat), <b>Ignorovat</b> (nesledovat), <b>Vyžádáno</b> (vyžádáno), <b>Ano</b> (již odesláno). + + + If DXCC is missing in the imported record, it will be resolved from the callsign. Pokud v importovaném záznamu chybí DXCC, bude doplněno podle volacího znaku. - + Fill missing DXCC Entity Information Doplnit chybějící DXCC informace o entitě - + My Profile Můj profil - + My Rig Můj Rig - + ADX ADX - + Date Range Časové období - + All vše - + Options Nastavení - + &Import &Import - + Select File Vybrat soubor - - + + The value is used when an input record does not contain the ADIF value Hodnota je použita v případě, když importovaný záznam má příslušné ADIF pole prázdné - - + + Queued (ready to send) + Ve frontě (připraveno k odeslání) + + + + Ignored (do not track) + Ignorováno (nesledovat) + + + + Requested (requested again) + Vyžádáno (vyžádáno znovu) + + + + Yes (already sent) + Ano (již odesláno) + + + + Custom... + Vlastní... + + + + Queued + Ve frontě + + + + Requested + Vyžádáno + + + + Ignored + Ignorovat + + + + No + Ne + + + + Yes + Ano + + + + The values below will be used when an input record does not contain the ADIF values Hodnoty jsou použity v případě, když importovaný záznam má příslušné ADIF pole prázdné - + <p><b>In-Log QSO:</b></p><p> <p><b>QSO v logu:</b></p><p> - + <p><b>Importing:</b></p><p> <p><b>Importováno:</b></p><p> - + Duplicate QSO Duplicitní QSO - + <p>Do you want to import duplicate QSO?</p>%1 %2 <p>Přejete si importovat toto duplicitní QSO?</p>%1 %2 - + Save to File Uložit do souboru - + QLog Import Summary QLog Shrnutí Importu - + Import date Datum Importu - + Imported file Soubor importu - + Imported: %n contact(s) Importovano: %n kontaktů @@ -6152,7 +6607,7 @@ Toto heslo bude později potřeba pro jejich obnovení. - + Warning(s): %n Upozornění: %n @@ -6161,7 +6616,7 @@ Toto heslo bude později potřeba pro jejich obnovení. - + Error(s): %n Chyb: %n @@ -6170,17 +6625,17 @@ Toto heslo bude později potřeba pro jejich obnovení. - + Details Detaily - + Import Result Výsledek Importu - + Save Details... Uložit detaily... @@ -6608,18 +7063,53 @@ Toto heslo bude později potřeba pro jejich obnovení. Importováno - - + + missing QSO_DATE + chybí QSO_DATE + + + + missing CREDIT_GRANTED + chybí CREDIT_GRANTED + + + + missing CALL/DXCC + chybí CALL/DXCC + + + + no matching QSO + nenalezeno odpovídající QSO + + + + cannot update QSO %1: %2 + nelze aktualizovat QSO %1: %2 + + + + matched QSO: + odpovídající QSO: + + + + credit_granted: + credit_granted: + + + + DXCC State: Stav DXCC: - + Error Chyba - + Warning Upozornění @@ -6627,945 +7117,956 @@ Toto heslo bude později potřeba pro jejich obnovení. LogbookModel - + QSO ID QSO ID - + Time on Čas od - + Time off Čas do - + Call Značka - + RST Sent RST Sent - + RST Rcvd RST Rcvd - + Frequency Frekvence - + RSTs RSTs - + RSTr RSTr - - + + Band Pásmo - - + + Mode Druh provozu - + Submode Druh provozu - + QSLr QSLr - + QSLr Date Datum QSLr - + QSLs QSLs - + QSLs Date Datum QSLs - + LoTWr LoTWr - + LoTWr Date Datum LoTWr - + LoTWs LoTWs - + LoTWs Date Datum LoTWs - + TX PWR TX Výkon - + Address (ASCII) Adresa (ASCII) - + Altitude Nadmořská výška - + Gridsquare Extended Lokator rozšíření - + DOK DOK - + Distance Vzdálenost - + eQSLr Date Datu eQSLr - + eQSLs Date Datum eQSLs - + eQSLr eQSLr - + eQSLs eQSLs - + HamlogEU Upload Date Datum Nahrání HamlogEU - + HamlogEU Upload Status Stav nahrání HamlogEU - + HamQTH Upload Date Datum Nahrání HamQTH - + HamQTH Upload Status Stav nahrání HamQTH - + CW Key Info CW Klíč - + CW Key Type Typ CW Klíče - + My Altitude Má nadmořská výška - + My City (ASCII) Moje město (ASCII) - + My Country (ASCII) Moje země (ASCII) - + My Gridsquare Extended Muj lokator rozšíření - + My IOTA Island ID Moje IOTA Island ID - + My CW Key Info Můj CW klíč - + My CW Key Type Můj typ CW klíče - + My Name (ASCII) Mé jméno (ASCII) - + My Postal Code (ASCII) Mé směrovací číslo (ASCII) - + My POTA Ref Má POTA - + My Rig (ASCII) Můj Rig (ASCII) - + My Special Interest Activity (ASCII) - + My Spec. Interes Activity Info (ASCII) - + My Spec. Interest Activity Info - + Name Jméno - + Notes (ASCII) Poznámky (ASCII) - + Operator Callsign Značka Operátora - + QSLr Via QSLr Via - + QSLs Via QSLs Via - + QTH QTH - - + + Gridsquare Lokátor - + DXCC DXCC - - + + Country Země - + Continent Kontinent - + Name (ASCII) Jméno (ASCII) - + QTH (ASCII) QTH (ASCII) - + Country (ASCII) Země (ASCII) - + CQZ CQZ - + ITU ITU - + Prefix Prefix - + State Stát - + County Okres - + IOTA IOTA - + QRZ Download Date Datum stažení z QRZ - + QRZ Download Status Stav stahování QRZ - + QSLs Message (ASCII) Odchozí QSL Zpráva (ASCII) - + QSLs Message Odchozí QSL Zpráva - + QSLr Message Příchozí QSL Zpráva - + RcvPWR - + SIG (ASCII) - + SIG SIG - + SIG Info (ASCII) - + SIG Info SIG Info - + RcvNr RcvNr - + RcvExch RcvExch - + SentNr - + SentExch - + VUCC VUCC - + Web Web - + QSL Sent QSL Sent - + Additional Fields Nezařazená Pole - + Address Adresa - + Age Věk - + A-Index A-Index - + Antenna Az Azimut Antény - + Antenna El Elevace Antény - + Signal Path Cesta Signálu - + ARRL Section ARRL Section - + Award Submitted - + Award Granted - + Band RX RX Pásmo - + Contest Check - + Class Třída - + ClubLog Upload Date Datum Nahrání Clublog - + ClubLog Upload State Stav nahrání Clublog - + Comment (ASCII) Komentář (ASCII) - - + + Comment Komentář - + Region Region - + Rig (ASCII) Rig (ASCII) - + My ARRL Section - + My WWFF Můj WWFF - + WWFF WWFF - + Paper QSL - + + Mode/Submode + + + + + Mode: %1 +Submode: %2 + + + + LoTW LoTW - + eQSL eQSL - + QSL Received QSL přijato - + County Alt Okres Alt - + Contacted Operator Kontaktovaný Operátor - + Contest ID ID Závodu - + Credit Submitted - + Credit Granted - + DCLr Date Datum DCLr - + DCLs Date Datum DCLs - + DCLr DCLr - + DCLs DCLs - + Email Email - - + + Owner Callsign Vlastní značka - + eQSL AG eQSL AG - + FISTS Number - + FISTS CC - + EME Init EME Init - + Frequency RX RX Frekvence - + Guest Operator Hostující Operátor - + HRDLog Upload Date Datum Nahrání HRDLog - + HRDLog Upload Status Stav nahrání HRDLog - + IOTA Island ID IOTA Island ID - + K-Index K-Index - + Latitude Zeměpisná šířka - + Longitude Zeměpisná délka - + Max Bursts - + MS Shower Name Jméno MS roje - + My Antenna (ASCII) Moje anténa (ASCII) - + My Antenna Moje anténa - + My City Moje město - + My County Můj okres - + My County Alt Můj Okres Alt - + My Country Moje země - + My CQZ Moje CQZ - + My DARC DOK Můj DARC DOK - + My DXCC Moje DXCC - + My FISTS Můj FISTS - + My Gridsquare Můj lokátor - + My IOTA Moje IOTA - + My ITU Moje ITU - + My Latitude Moje zeměpisná šířka - + My Longitude Moje zeměpisná délka - + My Name Mé jméno - + My Postal Code Mé směrovací číslo - + My Rig Můj Rig - + My Special Interest Activity Má SIG - + My SOTA Moje SOTA - + My State Můj Stát - - + + My Street Moje ulice - + My USA-CA Counties - + My VUCC Grids Mé VUCC lokátory - - + + Notes Poznámky - + #MS Bursts #MS Burst - + #MS Pings #MS Ping - + POTA POTA - + Contest Precedence - + Propagation Mode Podmínky šíření - + Public Encryption Key Veřejný šifrovací klíč - + QRZ Upload Date Datum Nahrání QRZ - + QRZ Upload Status Stav nahrání QRZ - + QSL Message QSL zpráva - + QSL Via QSL Via - + QSO Completed Úplné QSO - + QSO Random Random QSO - + Rig Rig - + SAT Mode SAT Mode - + SAT Name Jméno SAT - + Solar Flux Solar Flux - + Silent Key Silent Key - + SKCC Member SKCC Member - + SOTA SOTA - + Logging Station Callsign Značka logující stanice - + SWL SWL - + Ten-Ten Number Ten-Ten Číslo - + UKSMG Member UKSMG Člen - + USA-CA Counties - + VE Prov @@ -7574,8 +8075,8 @@ Toto heslo bude později potřeba pro jejich obnovení. LogbookWidget - - + + Delete Vymazat @@ -7662,73 +8163,73 @@ Toto heslo bude později potřeba pro jejich obnovení. - + Callsign Značka - + Gridsquare Lokátor - + POTA POTA - + SOTA SOTA - + WWFF WWFF - + SIG SIG - + IOTA IOTA - + Delete the selected contacts? Vymazat vybraný kontakt? - + Clublog's <b>Immediately Send</b> supports only one-by-one deletion<br><br>Do you want to continue despite the fact<br>that the DELETE operation will not be sent to Clublog? Clublog <b>Okamžité odeslání</b> podporuje pouze mazání po jednom záznamu<br><br>Chcete pokračovat navzdory skutečnosti,<br>že operace DELETE nebude odeslána do Clublogu? - + Deleting QSOs Mazání QSO - + Update Aktualizace - + By updating, all selected rows will be affected.<br>The value currently edited in the column will be applied to all selected rows.<br><br>Do you want to edit them? Aktualizací budou ovlivněny všechny vybrané řádky<br>Aktuálněupravená hodnota ve sloupci se použije na všechny vybrané řádky.<br>Chcete je upravit? - + Count: %n Anzahl: %n @@ -7738,89 +8239,124 @@ Toto heslo bude později potřeba pro jejich obnovení. - + Downloading eQSL Image Stahování eQSL obrázku - - - + + + Cancel Zrušit - + All Bands Všechna pásma - + All Modes Všechny módy - + All Countries Všechny země - + No User Filter Žádný uživatelský filtr - + QLog Warning Upozornění QLog - + Each batch supports up to 100 QSOs. Každá dávka podporuje až 100 QSO. - + QSOs Update Progress Progres aktualizace - - - + + + QLog Error Chyba QLog - + Callbook login failed Selhalo přihlášení do Callbooku - + Callbook error: Chyba Callbooku: - + All Clubs Všechny kluby - + eQSL Download Image failed: Stažení eQSL obrázku selhalo: + + LotwDXCCCreditDownloader + + + Cannot open test LoTW DXCC credit file + Nelze otevřít testovací soubor LoTW DXCC kreditů + + + + + Incomplete LoTW DXCC credit response + Neúplná odpověď LoTW DXCC kreditů + + + + + Cannot open temporary file + Nelze otevřít dočasný soubor + + + + LoTW is not configured properly + LoTW není správně nakonfigurováno + + + + LoTW returned a non-ADIF response + LoTW vrátilo odpověď mimo ADIF + + + + Incorrect login or password + Nesprávné přihlašovací jméno nebo heslo + + LotwQSLDownloader - + Cannot open temporary file Nelze otevřít dočasný soubor - + Incorrect login or password Nesprávné přihlašovací jméno nebo heslo @@ -7828,73 +8364,73 @@ Toto heslo bude později potřeba pro jejich obnovení. LotwUploader - + Upload cancelled by user Nahrání přerušeno uživatelem - + Upload rejected by LoTW Nahrání odmítnuto LoTW - + Unexpected response from TQSL server Neočekávaná odpověď z TQSL - + TQSL utility error Chyba TQSL - + TQSLlib error Chyba TQSLLib - + Unable to open input file Nelze otevřít vstupní soubor - + Unable to open output file Nelze otevřít výstupní soubor - + All QSOs were duplicates or out of date range Všechna QSO byla duplicitní nebo mimo časový rozsah - + Some QSOs were duplicates or out of date range Nekterá QSO byla duplicitní nebo mimo časový rozsah - + Command syntax error Chybný příkaz - + LoTW Connection error (no network or LoTW is unreachable) Chyba připojení k LoTW - - + + Unexpected Error from TQSL Neočekávaná chyba TQSL - + TQSL not found Nenalezen TQSL - + TQSL crashed TQSL neočekávaně skončil @@ -7932,620 +8468,684 @@ Toto heslo bude později potřeba pro jejich obnovení. Sl&užby - + Toolbar Toolbar - - + + Clock Hodiny - - + + Map Mapa - - + + DX Cluster DX Cluster - + WSJTX WSJTX - - + + Rotator Rotátor - - + + Bandmap Bandmap - - + + Rig Rig - - + + Online Map Online Mapa - - + + CW Console CW Konzole - - + + Chat Chat - - + + Profile Image Profilová fotka - + &Settings &Nastavení - + &Import &Import - + &Export &Export - + + Print QS&L + Tisk QS&L + + + Mailing List... Mailing List... - + Edit Upravit - - + + Save Arrangement Uložit uspořádání - + Keep Options Zachovat nastavení - + Restore connection options after application restart Obnovit možnosti připojení po restartu aplikace - + Connect R&ig Připojit R&ig - - + + Alerts Upozornění - + Quit Ukončit - + Application - Quit Aplikace - Ukončit - - - - - - - + + + + + + + Pack Data && Settings Zabalit data a nastavení - - + + Unpack Data && Settings Rozbalit data a nastavení - - + + New QSO - Clear Nový kontakt - Vymazat - + &About &O aplikaci - - + + New QSO - Save Nový kontakt - Uložit - + S&tatistics S&tatistiky - + QSL &Gallery QSL &Galerie - + Developer Tools Vývojářské nástroje - + Run custom read-only SQL queries against the logbook database Spouštět vlastní SQL dotazy pouze pro čtení nad databází logu - - Print QSL &Labels - &Tisk QSL štítků - - - + Connect R&otator Připojit R&otátor - + QSO &Filters &Filtry QSO - + &Awards &Diplomy - + DXCC &Submission List &Seznam pro DXCC podání - + Generate a list of contacts to submit for ARRL DXCC award credit Vygenerovat seznam spojení pro předložení k uznání ARRL DXCC - + Beep Pípnutí - + Connect &CW Keyer Připojit &CW Klíč - + &Wiki &Wiki - + Report &Bug... Nahlásít &Bug... - + &Manual Entry &Ruční zadání - + Switch New Contact dialog to the manually entry mode<br/>(time, freq, profiles etc. are not taken from their common sources) Přepnout QSO okno do režimu ručního zadávání<br/>(hodnoty času, frekvence, profilů atd. nejsou přebírány z jejich běžných zdrojů) - + Logbook - Search Callsign Logbook - Vyhledat značku - - + + New QSO - Add text from Callsign field to Bandmap Nový kontakt - Přenést text z pole Callsign do Bandmap - + Rig - Band Down Rig - Pásmo dolů - + Rig - Band Up Rig - Pásmo nahoru - + New QSO - Use Callsign from the Whisperer Nový kontakt - Použít volací značku z Našeptávače - + CW Console - Key Speed Up CW Konzole - Rychlost klíče zvýšit - + CW Console - Key Speed Down CW Konzole - Rychlost klíče snížit - + CW Console - Profile Up CW Konzole - O profil výš - + CW Console - Profile Down CW Konzole - O profil níž - + Rig - PTT On/Off Rig - PTT On/Off - + All Bands Všechna pásma - + Each Band Každé pásmo - + Each Band && Mode Každé pásmo a mod - + No Check Bez kontroly - + Single Jedna pro vše - + Per Band Pro pásmo - + Stop Zastavit - + Reset Vymazat - + None Žádné - + Upload Nahrát - + Service - Upload QSOs Služba – Nahrávání QSO - + Download QSLs Stáhnout QSL - + Service - Download QSLs Služba - Stáhnout QSL - + + Download LoTW DXCC Credits + Stáhnout LoTW DXCC kredity + + + + Service - Download LoTW DXCC Credits + Služba - Stáhnout LoTW DXCC kredity + + + Theme: Native Téma: Native - + Theme: QLog Light Téma: QLog Light - + Theme: QLog Dark Téma: QLog Dark - + What's New Novinky - + Export Cabrillo Export Cabrillo - + Edit Rules Upravit pravidla - - + + Contest Contest - + Dupe Check Kontrola Duplicit - + Sequence Sekvence - + Linking Exchange With Spojit Exchange s - + Clear Vymazat - + Show Alerts Zobrazit upozornění - + About O aplikaci - + Wsjtx Wsjtx - + Color Theme Barevné téma - + Not enabled for non-Fusion style Není povoleno pro jiný styl než Fusion - + Press to tune the alert Stiskni pro naladění alertu - + + Startup ADI + Startup ADI + + + Clublog Immediately Upload Error Chyba Okamžitého nahrávání do Clublog - - - + + + <b>Error Detail:</b> <b>Detail chyby:</b> - + op: op: - + A New Version Nová verze - + A new version %1 is available. Je k dispozici nová verze %1. - + Remind Me Later Připomenout později - + Download Stáhnout - + + + QLog Warning + Upozornění QLog + + + + LoTW is not configured properly.<p>Please, use <b>Settings</b> dialog to configure it.</p> + LoTW není správně nakonfigurováno.<p>Pro konfiguraci použijte dialog <b>Nastavení</b>.</p> + + + + + QLog Error + Chyba QLog + + + + Cannot load local DXCC entities from the logbook: + Nelze načíst lokální DXCC entity z deníku: + + + + Unknown DXCC Entity + Neznámá DXCC entita + + + + Cannot determine a local DXCC entity from logbook contacts. + Nelze určit lokální DXCC entitu z kontaktů v deníku. + + + + LoTW DXCC Credits + LoTW DXCC kredity + + + + Select the local DXCC entity for which LoTW DXCC credits will be downloaded: + Vyberte lokální DXCC entitu, pro kterou budou staženy LoTW DXCC kredity: + + + + Cancel + Zrušit + + + + Downloading LoTW DXCC credits + Stahování LoTW DXCC kreditů + + + + Processing LoTW DXCC credits + Zpracování LoTW DXCC kreditů + + + + LoTW DXCC Credit Import Summary + Souhrn importu LoTW DXCC kreditů + + + + LoTW DXCC credit import failed: + Import LoTW DXCC kreditů selhal: + + + Failed to encrypt credentials. Nepodařilo se zašifrovat přihlašovací údaje. - + Database files (*.dbe);;All files (*) Soubory databáze (*.dbe);;Všechny soubory (*) - + Failed to create temporary file. Nepodařilo se vytvořit dočasný soubor. - + Failed to dump the database. Nepodařilo se exportovat databázi. - + Compressing database... Komprimuje se databáze… - + Database successfully dumped to %1 Databáze byla úspěšně exportována do %1 - + Failed to compress the database. Nepodařilo se komprimovat databázi. - + Failed to prepare database for import. Nepodařilo se připravit databázi pro import. - + Classic Klasické - + Do you want to remove the Contest filter %1? Prejete si odstranit Contest filtr %1? - + Contest: Contest: - + <h1>QLog %1</h1><p>&copy; 2019 Thomas Gatzweiler DL2IC<br/>&copy; 2021-2026 Ladislav Foldyna OK1MLG<br/>&copy; 2025-2026 Michael Morgan AA5SH<br/>&copy; 2025-2026 Kyle Boyle VE9KZ</p><p>Based on Qt %2<br/>%3<br/>%4<br/>%5</p><p>Icon by <a href='http://www.iconshock.com'>Icon Shock</a><br />Satellite images by <a href='http://www.nasa.gov'>NASA</a><br />ZoneDetect by <a href='https://github.com/BertoldVdb/ZoneDetect'>Bertold Van den Bergh</a><br />TimeZone Database by <a href='https://github.com/evansiroky/timezone-boundary-builder'>Evan Siroky</a> <h1>QLog %1</h1><p>&copy; 2019 Thomas Gatzweiler DL2IC<br/>&copy; 2021-2026 Ladislav Foldyna OK1MLG<br/>&copy; 2025-2026Michael Morgan AA5SH<br/>&copy; 2025-2026 Kyle Boyle VE9KZ</p><p>Záloženo na Qt %2<br/>%3<br/>%4<br/>%5</p><p>Ikony <a href='http://www.iconshock.com'>Icon Shock</a><br />Satelitní snímky <a href='http://www.nasa.gov'>NASA</a><br />ZoneDetect <a href='https://github.com/BertoldVdb/ZoneDetect'>Bertold Van den Bergh</a><br />TimeZone <a href='https://github.com/evansiroky/timezone-boundary-builder'>Evan Siroky</a> - + N/A - - MapWebChannelHandler + MapPageController + + + Aurora + Pol. záře + + + + Beam + Směrovat + + + + Chat + Chat + - - - + Grid Lokátor - - - + Gray-Line Noc a Den - - - - Beam - - - - - - - Aurora - Pol. záře - - - - - - MUF - MUF - - - - - + IBP IBP - - - - Chat - + + MUF + MUF - - - + WSJTX - CQ WSJTX - CQ - - - + Path Cesta @@ -8658,7 +9258,7 @@ Toto heslo bude později potřeba pro jejich obnovení. Člen: - + World Wide Flora & Fauna World Wide Flora & Fauna @@ -8696,7 +9296,7 @@ Toto heslo bude později potřeba pro jejich obnovení. QSL odeslat přes - + Blank Nevyplněno @@ -8721,7 +9321,7 @@ Toto heslo bude později potřeba pro jejich obnovení. Stanice - + the contacted station's DARC DOK (District Location Code) (ex. A01) DARC DOK (kód uzemí) (např A01) @@ -8746,7 +9346,7 @@ Toto heslo bude později potřeba pro jejich obnovení. Rig - + W W @@ -8821,87 +9421,87 @@ Toto heslo bude později potřeba pro jejich obnovení. Selhalo přihlášení do Callbooku - + LP LP - + New Entity! Nová země! - + New Band! Nové pásmo! - + New Mode! Nový druh provozu! - + New Band & Mode! Nové pásmo & druh! - + New Slot! Nový slot! - + Worked Pracováno - + Confirmed Potvrzeno - + GE GE - + GM GM - + GA GA - + m m - + Callbook search is active Hledání v Callbooku je aktivní - + Contest ID must be filled in to activate Pro aktivaci je nutné vyplnit Contest ID - + It is not the name of the contest but it is an assigned<br>Contest ID (ex. CQ-WW-CW for CQ WW DX Contest (CW)) Není to jméno Contestu ale jeho přiřazené Contest ID (např. CQ-WW-CW pro CQ WW DX Contest (CW)) - + Description of the contacted station's equipment Popis vybavení kontaktované stanice - + Callbook search is inactive Hledání v Callbooku není aktivní @@ -8911,17 +9511,17 @@ Toto heslo bude později potřeba pro jejich obnovení. Rozbalit/sbalit - + two or four adjacent Maidenhead grid locators, each four characters long, (ex. EN98,FM08,EM97,FM07) dva nebo čtyři sousední lokátory, každý o délce čtyř znaků (např. EN98,FM08,EM97,FM07) - + Special Activity Group - + Special Activity Group Information @@ -9135,7 +9735,7 @@ Pole můžete nechat prázdná a nastavit je později v Nastavení. QCoreApplication - + QLog Help QLog Help @@ -9148,31 +9748,31 @@ Pole můžete nechat prázdná a nastavit je později v Nastavení. - + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + QLog Warning Upozornění QLog @@ -9219,98 +9819,98 @@ Pole můžete nechat prázdná a nastavit je později v Nastavení.Síťová chyba. Nepovedlo se stáhnout Club List pro - - - + + + - - - - + + + + QLog Error Chyba QLog - + QLog is already running QLog již běží - + Failed to process pending database import. Nepodařilo se zpracovat čekající import databáze. - + The database was imported successfully, but the stored passwords could not be restored (decryption failed or the data is corrupted). All service passwords have been cleared and must be re-entered in Settings. Databáze byla úspěšně importována, ale uložená hesla nebylo možné obnovit (dešifrování selhalo nebo jsou data poškozena). Všechna hesla služeb byla vymazána a musí být znovu zadána v Nastavení. - + Could not connect to database. Nelze se připojit k databázi. - + Could not export a QLog database to ADIF as a backup.<p>Try to export your log to ADIF manually Nelze exportovat QLog databázi do ADIF jako backup.<p>Pokuste se ručně exportovat log do ADIF - + Database migration failed. Migrace databaze selhala. - + DXC Server Name Error Chyba jména DXC Serveru - + DXC Server address must be in format<p><b>[username@]hostname:port</b> (ex. hamqth.com:7300)</p> Adresa DXC Serveru musí být ve formátu <p><b>[uživatel@]hostname:port</b> (např. hamqth.com:7300)</p> - + DX Cluster Password DX Cluster heslo - + Invalid Password Nesprávné heslo - + DXC Server Connection Error Chyba připojení k DXC serveru - + The fields <b>%0</b> will not be saved because the <b>%1</b> is not filled. Pole <b>%0</b> nebudou uložena, protože <b>%1</b> není vyplněno. - + Your callsign is empty. Please, set your Station Profile Vaše značka není vyplněna. Prosím, nastavte Profil Stanice - + - + QLog Info QLog Info - + Activity name is already exists. Jméno aktivity již existuje. @@ -9336,104 +9936,104 @@ Pole můžete nechat prázdná a nastavit je později v Nastavení. - + Filter name is already exists. Filtr s tímto jménem již existuje. - - + + Please, define at least one Station Locations Profile Prosím, definujte alespoň jeden Profil Stanice - + WSJTX Multicast is enabled but the Address is not a multicast address. WSJTX Multicast je aktivní ale adresa není multicast IP adresa. - + Loop detected. Raw UDP forward uses the same port as the WSJT-X receiving port. Zjištěna smyčka. Raw UDP přesměrování používá stejný port jako příjem WSJT-X. - + Rig port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device Port musí být platný COM port.<br>Použijte COMxx pro Windows, pro ostatní cestu k souboru zařízení - + Rig PTT port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device PTT Port musí být platný COM port.<br>Použijte COMxx pro Windows, pro ostatní cestu k souboru zařízení - + <b>TX Range</b>: Max Frequency must not be 0. <b>Rozsah TX</b>: Koncová frekvence nesmí být 0. - + <b>TX Range</b>: Max Frequency must not be under Min Frequency. <b>Rozsah TX</b>: Koncová frekvence nesmí být menší než počáteční. - + Rotator port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device Port musí být platný COM port.<br>Použijte COMxx pro Windows, pro ostatní cestu k souboru zařízení - + CW Keyer port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device Port musí být platný COM port.<br>Použijte COMxx pro Windows, pro ostatní cestu k souboru zařízení - + Cannot change the CW Keyer Model to <b>Morse over CAT</b><br>No Morse over CAT support for Rig(s) <b>%1</b> Nelze změnit Model klíče na <b>Morse over CAT</b><br>Nasledující zařízení nepodporuji Morse over CAT support <b>%1</b> - + Cannot delete the CW Keyer Profile<br>The CW Key Profile is used by Rig(s): <b>%1</b> Nelze vymazat Profil Klíče<br>Profil je pouzívám temito zařízeními:<b>%1</b> - + Operator Callsign has an invalid format Značka operátora ma chybý format - + Gridsquare has an invalid format Lokátor má chybný formát - + VUCC Grids have an invalid format (must be 2 or 4 Gridsquares separated by ',') VUCC lokátor má neplatný formát (musí být 2 nebo 4 lokátory oddělené ',') - + Country must not be empty Zěme nesmí být prázdná - + CQZ must not be empty CQZ nesmí být prázdné - + ITU must not be empty ITU nesmí být prázdné - + Callsign has an invalid format Značka má chybný formát - + Filename is empty Jméno souboru je zprázdné @@ -9463,17 +10063,17 @@ Pole můžete nechat prázdná a nastavit je později v Nastavení. - + <b>Rig Error:</b> <b>Chyba Rig:</b> - + <b>Rotator Error:</b> <b>Chyba Rotátoru:</b> - + <b>CW Keyer Error:</b> <b>Chyba CW Klíče:</b> @@ -9483,7 +10083,7 @@ Pole můžete nechat prázdná a nastavit je později v Nastavení.Chyba v Chatu: - + Cannot update QSO Filter Conditions Nelze aktualizovat QSO Filter @@ -9491,74 +10091,74 @@ Pole můžete nechat prázdná a nastavit je později v Nastavení. QObject - + Cannot connect to DXC Server <p>Reason <b>: Nelse se připojit k DXC serveru <p>Důvod <b>: - + Connection Refused Spojení odmítnuto - + Host closed the connection Server uzavřel spojení - + Host not found Server nenalezen - + Timeout Timeout - + Network Error Chyba sítě - + Internal Error Interní Chyba - + Importing Database Importuji databázi - + Opening Database Načítání Databáze - + Backuping Database Záloha Databáze - + Migrating Database Migrace Databáze - + Starting Application Start aplikace @@ -9658,7 +10258,7 @@ Pole můžete nechat prázdná a nastavit je později v Nastavení.Moje DXCC - + <b>Imported</b>: %n contact(s) <b>Importován</b>: %n kontakt @@ -9667,7 +10267,7 @@ Pole můžete nechat prázdná a nastavit je později v Nastavení. - + <b>Warning(s)</b>: %n <b>Upozornění</b>: %n @@ -9676,7 +10276,7 @@ Pole můžete nechat prázdná a nastavit je později v Nastavení. - + <b>Error(s)</b>: %n <b>Chyb</b>: %n @@ -9685,12 +10285,12 @@ Pole můžete nechat prázdná a nastavit je později v Nastavení. - + km km - + miles mil @@ -9765,6 +10365,32 @@ Pole můžete nechat prázdná a nastavit je později v Nastavení.Worked Pracováno + + + IARU Region 1 + IARU Region 1 + + + + + Failed to write file: %1 + Nepodařilo se zapsat soubor: %1 + + + + Cannot open file: %1 + Nelze otevřít soubor: %1 + + + + Invalid guide file: %1 + Neplatný soubor průvodce: %1 + + + + Invalid guide file: missing title + Neplatný soubor průvodce: chybí název + QRZCallbook @@ -9777,7 +10403,7 @@ Pole můžete nechat prázdná a nastavit je později v Nastavení. QRZUploader - + General Error Obecná chyba @@ -9800,33 +10426,33 @@ Pole můžete nechat prázdná a nastavit je později v Nastavení.Seřadit podle: - + Export Filtered Exportovat filtrované - + Date (Newest) Datum (nejnovější) - + Date (Oldest) Datum (nejstarší) - + Callsign (A-Z) Volací značka (A-Z) - + Callsign (Z-A) Volací značka (Z-A) - - + + %n QSL card(s) %n QSL lístek @@ -9835,72 +10461,72 @@ Pole můžete nechat prázdná a nastavit je později v Nastavení. - + All QSL Cards Všechny QSL lístky - + Favorites Oblíbené - + By Country Podle země - + By Date Podle data - + By Band Podle pásma - + By Mode Podle módu - + By Continent Podle kontinentu - + Remove from Favorites Odebrat z oblíbených - + Add to Favorites Přidat mezi oblíbené - + Open Otevřít - + Save... Uložit… - + Save QSL Card Uložit QSL lístek - + Export QSL Cards Exportovat QSL lístky - + Exported %1 of %2 cards Exportováno %1 z %2 lístků @@ -9943,28 +10569,23 @@ Pole můžete nechat prázdná a nastavit je později v Nastavení.Detaily - - New QSLs: - Nové QSL: + + New QSLs: + Nové QSL: - Updated QSOs: - Aktualizované QSO: + Updated QSOs: + Aktualizovaná QSO: - - Unmatched QSLs: - Nespárované QSL: + + Unmatched QSLs: + Nespárované QSL: QSLPrintLabelDialog - - - Print QSL Labels - Tisk QSL štítků - Filter @@ -9996,235 +10617,398 @@ Pole můžete nechat prázdná a nastavit je později v Nastavení.Uživatelský filtr - + Label Template Šablona štítku - + + Page Size: Velikost stránky: - + Columns: Sloupce: - + Rows: Řádka: - + + Label Width: Šířka štítku: - + + Print QSL Labels / Cards + Tisk QSL štítků / lístků + + + + Print Mode + Režim tisku + + + + Mode: + Druh provozu: + + + + + QSL Card + QSL lístek + + + + Card Width: + Šířka lístku: + + + + Card Height: + Výška lístku: + + + + Card Gap: + Mezera mezi lístky: + + + + Label Height: Výška štítku: - + + Label X Offset: + Posun štítku X: + + + + Label Y Offset: + Posun štítku Y: + + + + Label Background: + Pozadí štítku: + + + + Fill under label + Vyplnit pod štítkem + + + + + Color + Barva + + + + Background Image: + Obrázek pozadí: + + + + Browse + Procházet + + + + Clear + Vymazat + + + Left Margin: Levý okraj: - + Top Margin: Horní okraj: - + H Spacing: Vodorovná mezera: - + V Spacing: Svislá mezera: - + Label Appearance Vzhled štítku - + Print Label Borders Tisk okrajů štítků - + QSOs per Label: QSO na štítek: - + Footer Left Text: Levý text zápatí: - + Footer Right Text: Pravý text zápatí: - + Skip Label: Přeskočit štítek: - + Sans Font: Bezpatkové písmo: - + Mono Font: Neproporcionální písmo: - + + Text Color: + Barva textu: + + + Callsign Size: Velikost volací značky: - + "To Radio" Size: Velikost „To Radio“: - + "To Radio" Text: Text „To Radio“: - + Header Size: Velikost záhlaví: - + Data Size: Velikost dat: - + Date Header Text: Text záhlaví data: - + Date Format: Formát data: - + Time Header Text: Text záhlaví času: - + Band Header Text: Text záhlaví pásma: - + Mode Header Text: Text záhlaví módu: - + QSL Header Text: Text záhlaví QSL: - + Extra Column: Další sloupec: - + Extra Column Text Text dalšího sloupce - + (DB column name) (název sloupce DB) - - + + No matching QSOs found Nebyla nalezena žádná odpovídající QSO - - + + Page 0 of 0 Stránka 0 z 0 - + Labels: 0 (0 pages) Štítky: 0 (0 stránek) - + Print Tisk - + Export as PDF Exportovat jako PDF - - + + Export as Images + Export obrázků + + + + Label Sheet + List štítků + + + + Custom Vlastní - + Empty Prázdné - + QSOs matching this station profile QSO odpovídající tomuto profilu stanice - + + Select Label Text Color + Vybrat barvu textu štítku + + + + Select Label Background Color + Vybrat barvu pozadí štítku + + + + + + Select QSL Card Background + Vybrat pozadí QSL lístku + + + + Images (*.png *.jpg *.jpeg *.bmp) + Obrázky (*.png *.jpg *.jpeg *.bmp) + + + + Cannot read selected image file. + Nelze přečíst vybraný soubor obrázku. + + + + Selected file is not a valid image. + Vybraný soubor není platný obrázek. + + + + Cards: %1 (%2 pages) + Lístky: %1 (%2 stran) + + + Labels: %1 (%2 pages) Štítky: %1 (%2 stránek) - + Page %1 of %2 Stránka %1 z %2 - + Export PDF Export PDF - + PDF Files (*.pdf) Soubory PDF (*.pdf) - + + + + Export QSL Card Images + Export obrázků QSL lístků + + + + Some image files already exist. Overwrite them? + Některé soubory obrázků již existují. Přepsat je? + + + + Exported %n QSL card image(s). + + Exportováno %n obrázků QSL lístků. + + + + + + + Exported %1 of %2 QSL card images. + Exportováno %1 z %2 obrázků QSL lístků. + + + + QSOs were not marked as sent. + QSO nebyla označena jako odeslaná. + + + Mark as Sent Označit jako odeslané - + Mark printed/exported QSOs as sent? Označit vytištěná/exportovaná QSO jako odeslaná? @@ -10275,7 +11059,7 @@ Pole můžete nechat prázdná a nastavit je později v Nastavení. - + Blank Nevyplněno @@ -10694,318 +11478,318 @@ Pole můžete nechat prázdná a nastavit je později v Nastavení.SendExch - + &Reset &Reset - + &Lookup &Vyhledat - - - + + + No Neodesílat - - - + + + Yes Odesláno - - - + + + Requested Vyžádáno - - - + + + Queued Ve frontě - - - + + + Ignored Ignorovat - + Bureau Bureau - + Direct Direct - + Electronic Elektronicky - + Submit changes Uložit změny - + Really submit all changes? Opravdu uložit všechny změny? - - - - + + + + QLog Error Chyba QLog - + Cannot save all changes - internal error Nepovedlo se uložit všechny změny - interní chyba - + Cannot save all changes - try to reset all changes Nepovedlo se uložit všechny změny - zkuste reset všech změn - + QSO Detail QSO Detail - + Edit QSO Úprava QSO - + Downloading eQSL Image Stahování eQSL obrázku - + Cancel Zrušit - + eQSL Download Image failed: Stažení eQSL obrázku selhalo: - + DX Callsign must not be empty Značka nesmí být prazdná - + DX callsign has an incorrect format Značka má chybný formát - - + + TX Frequency or Band must be filled TX Frekvence nebo Pásmo musí být vyplněno - - + + DX Grid has an incorrect format Lokátor má chybný formát - + Based on callsign, DXCC Country is different from the entered value - expecting Na základě značky DXCC Zěme nemá správné ID - očekáváno - + Based on callsign, DXCC Continent is different from the entered value - expecting Na základě značky DXCC Kontinent nemá správné ID - očekáváno - + Based on callsign, DXCC ITU is different from the entered value - expecting Na základě značky ITU nemá správné ID - očekáváno - + Based on callsign, DXCC CQZ is different from the entered value - expecting Na základě značky CQZ nemá správné ID - očekáváno - + Based on Frequencies, Sat Mode should be Satelitní Mode se na základě frekvencí liší od zadané hodnoty - očekáváno - + blank Nevyplněno - + Sat name must not be empty Jméno Satelitu nesmí být prázdné - + Own VUCC Grids have an incorrect format Vlastní VUCC má špatný formát - + Based on own callsign, own DXCC ITU is different from the entered value - expecting Vlastní DXCC ITU se na základě vlastní volací značky liší od zadané hodnoty - očekáváno - + Based on own callsign, own DXCC CQZ is different from the entered value - expecting Vlastní DXCC CQZ se na základě vlastní volací značky liší od zadané hodnoty - očekáváno - + Based on own callsign, own DXCC Country is different from the entered value - expecting Vlastní DXCC Země se na základě vlastní volací značky liší od zadané hodnoty - očekáváno - + LoTW Sent Status to <b>No</b> does not make any sense if QSL Sent Date is set. Set Date to 1.1.1900 to leave the date field blank Pole LoTW Odesláno nastavené na <b>Neodesílat</b> nedává smysl pokud je nastaveno datum odeslání QSL. Nastavte datum na 1.1.1900, aby pole datum zůstalo prázdné - + Date should be present for LoTW Sent Status <b>Yes</b> Datum by měl být nastavenen v případě LoTW Sent Status <b>Odesláno</b> - + eQSL Sent Status to <b>No</b> does not make any sense if QSL Sent Date is set. Set Date to 1.1.1900 to leave the date field blank Pole eQSL Odesláno nastavené na <b>Neodesílat</b> nedává smysl pokud je nastaveno datum odeslání QSL. Nastavte datum na 1.1.1900, aby pole datum zůstalo prázdné - + Date should be present for eQSL Sent Status <b>Yes</b> Datum by měl být nastavenen v případě eQSL Sent Status <b>Odesláno</b> - + Paper Sent Status to <b>No</b> does not make any sense if QSL Sent Date is set. Set Date to 1.1.1900 to leave the date field blank Pole Odesláno nastavené na <b>Neodesílat</b> nedává smysl pokud je nastaveno datum odeslání QSL. Nastavte datum na 1.1.1900, aby pole datum zůstalo prázdné - + Date should be present for Paper Sent Status <b>Yes</b> Datum by měl být nastavenen v případě Sent Status <b>Odesláno</b> - + VUCC has an incorrect format VUCC má chybný formát - + TX Band should be TX Pásmo by mělo být - + RX Band should be RX Pásmo by mělo být - + Own Callsign must not be empty Vlastní značka nesmí být prázdná - + Own callsign has an incorrect format Vlastní značka má špatný formát - + Based on SOTA Summit, QTH does not match SOTA Summit Name - expecting Na základě SOTA Summit, QTH neodpovídat SOTA definici - očekáváno - + Based on SOTA Summit, Grid does not match SOTA Grid - expecting Na základě SOTA Summit, Lokátor neodpovídat SOTA definici - očekáváno - + Based on POTA record, QTH does not match POTA Name - expecting Na základě POTA Summit, QTH neodpovídat POTA definici - očekáváno - + Based on POTA record, Grid does not match POTA Grid - expecting Na základě POTA Summit, Lokátor neodpovídat POTA definici - očekáváno - + Based on SOTA Summit, my QTH does not match SOTA Summit Name - expecting Na základě SOTA Summit, Mé QTH neodpovídat SOTA definici - očekáváno - + Based on SOTA Summit, my Grid does not match SOTA Grid - expecting Na základě SOTA Summit, Můj Lokátor neodpovídat SOTA definici - očekáváno - + Based on POTA record, my QTH does not match POTA Name - expecting Na základě POTA Summit, Mé QTH neodpovídat POTA definici - očekáváno - + Based on POTA record, my Grid does not match POTA Grid - expecting Na základě POTA Summit, Můj Lokátor neodpovídat POTA definici - očekáváno - + Callbook error: Chyba Callbooku: - - + + <b>Warning: </b> <b>Upozornění: </b> - + Validation Kontrola - + Yellow marked fields are invalid.<p>Nevertheless, save the changes?</p> Žlutě vyznačená pole obsahují neplatné hodnoty<p>I přes to uložit změny?</p> - + &Save &Uložit - + &Edit U&pravit @@ -11043,52 +11827,52 @@ Pole můžete nechat prázdná a nastavit je později v Nastavení.Přidat podmínku - + Equal Rovná se - + Not Equal Nerovná se - + Contains Obsahuje - + Not Contains Neobsahuje - + Greater Than Větší než - + Less Than Menší než - + Starts with Začíná - + RegExp RegExp - + Remove Vymazat - + Must not be empty Nesmí být prázdné @@ -11124,27 +11908,27 @@ Pole můžete nechat prázdná a nastavit je později v Nastavení. Rig - + No Rig Profile selected Není vybrán žádný Rig profil - + Rigctld Error Chyba Rigctld - + Initialization Error Chyba inicializace - + Internal Error Interní Chyba - + Cannot open Rig Rig nelze připojit @@ -11162,36 +11946,66 @@ Pole můžete nechat prázdná a nastavit je později v Nastavení.RX - + Disconnected Odpojeno - - + + MHz MHz - + Disable Split Vypnout split - + RIT: 0.00000 MHz RIT: 0.00000 MHz - + XIT: 0.00000 MHz XIT: 0.00000 MHz - + PWR: %1W PWR: %1W + + + OUT + + + + + Outside Bandmap Guide range + Mimo rozsah průvodce bandmapou + + + + SOS + SOS + + + + Emergency frequency: %1 MHz + Nouzová frekvence: %1 MHz + + + + IBP + IBP + + + + International Beacon Project: %1 MHz + + RigctldAdvancedDialog @@ -11281,57 +12095,57 @@ Nainstalujte prosím Hamlib nebo zadejte cestu ručně. RigctldManager - + rigctld executable not found in /app/bin/. This should not happen in Flatpak build. Spustitelný soubor rigctld nebyl nalezen v /app/bin/. V Flatpak verzi by se toto nemělo stát. - + rigctld executable not found. Please install Hamlib or specify the path in Advanced settings. Spustitelný soubor rigctld nebyl nalezen. Nainstalujte prosím Hamlib nebo zadejte cestu v Pokročilém nastavení. - + Hamlib major version mismatch: QLog was compiled with Hamlib %1 but rigctld reports version %2.%3.%4. Rig model IDs are incompatible between major versions. Nesoulad hlavní verze Hamlib: QLog byl kompilován s Hamlib %1, ale rigctld hlásí verzi %2.%3.%4. ID modelů transceiverů nejsou kompatibilní mezi hlavními verzemi. - + Port %1 is already in use. Another rigctld or application may be running on this port. Port %1 je již používán. Na tomto portu může běžet jiný rigctld nebo aplikace. - + rigctld started but not responding on port %1. rigctld byl spuštěn, ale na portu %1 neodpovídá. - + Failed to start rigctld: %1 %2 Nepodařilo se spustit rigctld: %1 %2 - + rigctld crashed. rigctld havaroval. - + rigctld timed out. rigctld vypršel časový limit. - + Write error with rigctld. Chyba zápisu do rigctld. - + Read error with rigctld. Chyba čtení z rigctld. - + Unknown rigctld error. Neznámá chyba rigctld. @@ -11339,22 +12153,22 @@ Nainstalujte prosím Hamlib nebo zadejte cestu ručně. Rotator - + No Rotator Profile selected Není vybrán žádný Rot profil - + Initialization Error Chyba inicializace - + Internal Error Interní Chyba - + Cannot open Rotator Rotátor nelze připojit @@ -11461,26 +12275,27 @@ Nainstalujte prosím Hamlib nebo zadejte cestu ručně. - - - - - - - - - - + + + + + + - - - + + + + + + + + Add Přidat - + Callsign Značka @@ -11713,6 +12528,7 @@ Nainstalujte prosím Hamlib nebo zadejte cestu ručně. + Description Popis @@ -11891,18 +12707,18 @@ Nainstalujte prosím Hamlib nebo zadejte cestu ručně. Flow Control - - + + None None - + Hardware Hardware - + Software Software @@ -11913,27 +12729,31 @@ Nainstalujte prosím Hamlib nebo zadejte cestu ručně. Parity - + + + + + No No - + Even Even - + Odd Odd - + Space Space - + Mark Mark @@ -12052,7 +12872,7 @@ Nainstalujte prosím Hamlib nebo zadejte cestu ručně. - + Serial Serial @@ -12270,7 +13090,7 @@ Nainstalujte prosím Hamlib nebo zadejte cestu ručně. - + Start rigctld daemon to share rig with other applications (e.g. WSJT-X) Spustit démon rigctld pro sdílení transceiveru s jinými aplikacemi (např. WSJT-X) @@ -12413,27 +13233,102 @@ Nainstalujte prosím Hamlib nebo zadejte cestu ručně. - + API Key Klíč API - + + Startup ADI + Startup ADI + + + + Configured ADI/ADIF files are checked only at startup. A newly added file starts at its current end, so only later appended QSOs are loaded. This is not a live watcher; if many new QSOs are found, loading stops and the standard Import should be used. + Nastavené soubory ADI/ADIF se kontrolují pouze při spuštění. Nově přidaný soubor začne na svém aktuálním konci, takže se načtou jen později přidaná QSO. Nejde o živé sledování; pokud je nalezeno mnoho nových QSO, načítání se zastaví a je třeba použít standardní import. + + + + Removing a file also forgets its recovery position. + Odebráním souboru se zapomene i jeho pozice obnovy. + + + + Remove + Vymazat + + + + Used when a file row has Missing QSL Sent set to Custom. Explicit ADIF values are kept. + Použije se, když má řádek souboru Chybějící QSL Sent nastaveno na Vlastní. Explicitní hodnoty ADIF zůstanou zachovány. + + + + Custom QSL Sent Defaults + Vlastní výchozí hodnoty QSL Sent + + + + Paper QSL + Papírové QSL + + + + DCL + DCL + + + + Select the <b>Bandmap Guide</b> profile shown as visual frequency hints. It does not affect mode identification. + Vyberte profil <b>Průvodce bandmapou</b> zobrazovaný jako vizuální frekvenční nápověda. Nemá vliv na rozpoznání módu. + + + + Manage + Spravovat + + + + Double-click cells to edit start/end frequency, enabled state, or SAT mode. Band names are fixed; new bands cannot be added here. + Dvojklikem na buňky upravíte počáteční/koncovou frekvenci, stav povolení nebo SAT mód. Názvy pásem jsou pevné; nová pásma zde nelze přidat. + + + + QSO DXCC Status Colors + Barvy stavu DXCC QSO + + + + Used for DX spots, Bandmap, WSJT-X and QSO status hints. Confirmed has no highlight by default. Click a color cell to choose a color or set No color. + Používá se pro DX spoty, bandmapu, WSJT-X a nápovědy stavu QSO. Potvrzené nemá ve výchozím stavu žádné zvýraznění. Kliknutím na barevnou buňku vyberete barvu nebo nastavíte Bez barvy. + + + + Restore Defaults + Obnovit výchozí + + + + Shortcuts + Zkratky + + + Danger Zone Nebezpečná zóna - + <b>⚠ This is a danger zone. Proceed with caution, as actions performed here cannot be undone and may have a significant impact on your log.</b> <b>⚠ Toto je nebezpečná zóna. Pokračujte opatrně, protože provedené akce nelze vrátit zpět a mohou mít zásadní dopad na váš log.</b> - + Delete All QSOs Smazat všechny QSO - + Delete All Passwords from the Secure Store Smazat všechna hesla z bezpečného úložiště @@ -12443,202 +13338,205 @@ Nainstalujte prosím Hamlib nebo zadejte cestu ručně. Koncový bod - + Others Jiné - + Status Confirmed By Potvrzeno - + Paper QSL - + Chat Chat - + <b>Security Notice:</b> QLog stores all passwords in the Secure Storage. Unfortunately, ON4KST uses a protocol where this password is sent over an unsecured channel as plaintext.</p><p>Please exercise caution when choosing your password for this service, as your password is sent over an unsecured channel in plaintext form.</p> <b>Oznámení o zabezpečení:</b> QLog ukládá všechna hesla do zabezpečeného úložiště. ON4KST bohužel používá protokol, kde je toto heslo odesíláno přes nezabezpečený kanál jako prostý text.</p><p>Při výběru hesla pro tuto službu buďte opatrní, protože vaše heslo je odesíláno přes nezabezpečený kanál v podobě prostého textu.< /p> - + The '>' character is interpreted as a marker for the initial cursor position in the Report column. <br/>Ex.: '5>9' means the cursor will be positioned on the second character Znak „>“ je interpretován jako značka pro počáteční pozici kurzoru ve sloupci Report.<br>Př.: „5>9“ znamená, že kurzor bude umístěn na druhém znaku - + Raw UDP Forward UDP Forward - + <p>List of IP addresses to which QLog forwards raw UDP WSJT-X packets.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Seznam IP adres, na které QLog přeposílá nezměněné UDP WSJTX pakety. </p>Adresy jsou odděleny mezerou a mají formát IP:PORT - + Join Multicast Použít Multicast - + Enable/Disable Multicast option for WSJTX Povolit/zakázat možnost Multicast pro WSJTX - + Multicast Address Multicast Adresa - + Specify Multicast Address. <br>On some Linux systems it may be necessary to enable multicast on the loop-back network interface. Zadejte Multicast adresu. <br>Na některých Linux systémech může být nutné povolit Multicast pro Loopback interface. - + TTL TTL - + Time-To-Live determines the range<br> over which a multicast packet is propagated in your intranet. Time-To-Live určuje vzdálenost<br>, do které se paket Multicastu v síti šíří. - + Color CQ Spots Obarvit CQ spoty - + Enable/Disable sending color-coded status indicators back to WSJT-X for each callsign calling CQ Povolit / zakázat odesílání barevně kódovaných stavových indikátorů zpět do WSJT-X pro každou - + Notifications Notifikace - + DX Spots DX Spoty - + <p> List of IP addresses to which QLog sends UDP notification packets with DX Cluster Spots.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Seznam IP adres, na které QLog přeposílá UDP notificate z DX Clusteru. </p>Adresy jsou odděleny mezerou a mají formát IP:PORT - + QSO Changes Změny QSO - + <p> List of IP addresses to which QLog sends UDP notification packets about a new/updated/deleted QSO in the log.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Seznam IP adres, na které QLog přeposílá UDP notificate o novém/aktualizovaném/vymazaném QSO. </p>Adresy jsou odděleny mezerou a mají formát IP:PORT - + Wsjtx CQ Spots Wsjtx CQ Spoty - + <p> List of IP addresses to which QLog sends UDP notification packets with WSJTX CQ Spots.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Seznam IP adres, na které QLog přeposílá UDP notificate s WSJTX CQ Spoty. </p>Adresy jsou odděleny mezerou a mají formát IP:PORT - + Rig Status Stav Rig - + <p> List of IP addresses to which QLog sends UDP notification packets when Rig State changes.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Seznam IP adres, na které QLog přeposílá UDP notifikace Stavu Rigu. </p>Adresy jsou odděleny mezerou a mají formát IP:PORT - + GUI GUI - + Time Format Formát času - + 24-hour 24hodinový - + AM/PM AM/PM - + Unit System Jednotkový systém - + Metric Metrický - + Imperial Imperiální - + Date Format Formát data - + System Systémový - + + + + Custom Vlastní - + <a href="https://doc.qt.io/qt-6/qdate.html#fromString-1">Time Format Documentation</a> <a href="https://doc.qt.io/qt-6/qdate.html#fromString-1">Dokumentace formátu času</a> - + Spot Alerts Upozornění na Spoty - + <p> List of IP addresses to which QLog sends UDP notification packets about user Spot Alerts.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Seznam IP adres, na které QLog přeposílá UDP notifikace Upozornění na Spoty. </p>Adresy jsou odděleny mezerou a mají formát IP:PORT - + LogID LogID - + <p>Assigned LogID to the current log.</p>The LogID is sent in the Network Nofitication messages as a unique instance identified.<p> The ID is generated automatically and cannot be changed</> <p>LogID pro aktuální log</p>LogID se posílá ve všech UDP notifikacích jako unikátní identifikátor.<p> ID je generováno automaticky a nemůže být změněno.</> @@ -12664,8 +13562,8 @@ Nainstalujte prosím Hamlib nebo zadejte cestu ručně. - - + + HamQTH HamQTH @@ -12674,7 +13572,7 @@ Nainstalujte prosím Hamlib nebo zadejte cestu ručně. - + Username Uživatelské jméno @@ -12684,36 +13582,37 @@ Nainstalujte prosím Hamlib nebo zadejte cestu ručně. - + Password Heslo - - + + QRZ.com QRZ.com - + Port where QLog listens an incoming traffic from WSJT-X Port, kde QLog poslouchá příchozí zprávy z WSJT-X - - - - - - + + + + + + ex. 192.168.1.1:1234 192.168.2.1:1234 např. 192.168.1.1:1234 192.168.2.1:1234 - + + eQSL eQSL @@ -12724,7 +13623,8 @@ Nainstalujte prosím Hamlib nebo zadejte cestu ručně. - + + LoTW LoTW @@ -12735,18 +13635,18 @@ Nainstalujte prosím Hamlib nebo zadejte cestu ručně. - - + + Network Síť - + Wsjtx Wsjtx - + Port @@ -12814,228 +13714,424 @@ Nainstalujte prosím Hamlib nebo zadejte cestu ručně. Používám interní TQSL - + Bands Pásma - + Modes Druhy provozu - - + + DXCC DXCC - - + + Name Jméno - + Report Report - - + + State Stav - - - + + + Disabled Vypnuto - + WinKey WinKey - + Press <b>Modify</b> to confirm the profile changes or <b>Cancel</b>. Stiskněte <b>Upravit</b> pro potvrzení změny profilu nebo <b>Zrušit</b>. - - - - - - - - - - + + + + + + + + + + Must not be empty Nesmí být prázdné - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + Modify Upravit - - + + Special - Omnirig Special - Omnirig - + Cannot be changed Nelze změnit - + Start (MHz) Pořátek (MHz) - + End (MHz) Konec (MHz) - + SAT Mode SAT Mode - + Dummy Dummy - + Morse Over CAT Morse Over CAT - + CWDaemon CWDaemon - + FLDigi FLDigi - + Single Paddle Single Paddle - + IAMBIC A IAMBIC A - + IAMBIC B IAMBIC B - + Ultimate Ultimate - + High High - + Low Low - + + Duplicate + Duplicitní + + + + Already worked QSO + Již navázané QSO + + + + New Entity + Nová země + + + + DXCC entity not worked yet + DXCC entita dosud nespojena + + + + New Band / Mode + Nové pásmo / mod + + + + New band, mode, or band and mode + Nové pásmo, modu nebo pásmo a modu + + + + New Slot + Nový slot + + + + New band and mode combination + Nová kombinace pásma a módu + + + + Worked + Pracováno + + + + Worked but not confirmed + Spojeno, ale nepotvrzeno + + + + Confirmed + Potvrzeno + + + + Confirmed QSO; no highlight by default + Potvrzené QSO; bez výchozího zvýraznění + + + + Status + Stav + + + + Color + Barva + + + + Choose Color... + Vybrat barvu... + + + + Default + Výchozí + + + + No Color + Bez barvy + + + + Status Color + Barva stavu + + + + No color + Bez barvy + + + + No highlight. Click to choose a color or set no color. + Bez zvýraznění. Kliknutím vyberete barvu nebo nastavíte bez barvy. + + + + Click to change color or set no color. + Kliknutím změníte barvu nebo nastavíte bez barvy. + + + Select File Vybrat soubor - + Auto Detect Automatická detekce - + TQSL was not found on this system. Please install TQSL or specify the path manually. TQSL nebyl na tomto systému nalezen. Nainstalujte prosím TQSL nebo zadejte cestu ručně. - + Not found Nenalezeno - + Rig sharing is only available for Hamlib driver Sdílení transceiveru je k dispozici pouze pro ovladač Hamlib - + Rig sharing is not available for network connection Sdílení transceiveru není k dispozici pro síťové připojení - + + Off + Vypnuto + + + Delete Passwords Smazat hesla - + All passwords have been deleted Všechna hesla byla smazána - + Deleting all QSOs... Mažu všechna QSO... - + Error Chyba - + Failed to delete all QSOs. Nepodařilo se smazat všechna QSO. - + + Enabled + Zapnuto + + + + Path + Cesta + + + + Station Profile + Profil stanice + + + + Missing QSL Sent + Chybějící QSL Sent + + + + Last Recovery + Poslední obnova + + + + + + Queued + Ve frontě + + + + + + + Ignored + Ignorovat + + + + + + + Requested + Vyžádáno + + + + + + + Yes + Ano + + + + Station Profile does not exist. Select another profile and enable this row again. + Profil stanice neexistuje. Vyberte jiný profil a tento řádek znovu povolte. + + + + File exists + Soubor existuje + + + + File does not exist + Soubor neexistuje + + + + Startup ADI initialized + Startup ADI inicializován + + + + Select ADIF File + Vybrat soubor ADIF + + + + ADIF Files (*.adi *.adif);;All Files (*) + Soubory ADIF (*.adi *.adif);;Všechny soubory (*) + + + members členů - + Required internet connection during application start Je vyžadováno připojení do internetu během startu aplikace @@ -13093,7 +14189,7 @@ Nainstalujte prosím TQSL nebo zadejte cestu ručně. StatisticsWidget - + Statistics Statistiky @@ -13184,151 +14280,150 @@ Nainstalujte prosím TQSL nebo zadejte cestu ručně. - + Band Pásmo - + Year rok - + Month měsíc - + Day in Week den v týdnu - + Hour hodinu - + Mode druh provozu - + Continent kontinent - + Propagation Mode podmínky šíření - + Confirmed / Not Confirmed Potvrzeno / Nepotvrzeno - + Countries Země - + Big Gridsquares Velké čtverce - + Distance Vzdálenost - + QSOs QSO - + Confirmed/Worked Grids Potvrzené / Pracováno Lokátory - + ODX ODX - + Sun Ned - + Mon Pon - + Tue Út - + Wed Stř - + Thu Čtvr - + Fri Pát - + Sat Sob - - - + + + Not specified Neurčeno - + Confirmed Potvrzeno - + Not Confirmed Nepotvrzeno - + No User Filter Žádný uživatelský filtr - + Over 50000 QSOs. Display them? Přes 50000 QSO. Zobrazit je? - - + Rendering QSOs... Vykreslování QSO… - + All Vše @@ -13382,17 +14477,17 @@ Nainstalujte prosím TQSL nebo zadejte cestu ručně. ToAllTableModel - + Time Čas - + Spotter Spotter - + Message Zpráva @@ -13665,27 +14760,27 @@ Nainstalujte prosím TQSL nebo zadejte cestu ručně. UserListModel - + Callsign Značka - + Gridsquare Lokátor - + Distance Vzdálenost - + Azimuth Azimut - + Comment Poznámka @@ -13693,47 +14788,47 @@ Nainstalujte prosím TQSL nebo zadejte cestu ručně. WCYTableModel - + Time Čas - + K K - + expK expK - + A A - + R R - + SFI SFI - + SA SA - + GMF GMF - + Au Au @@ -13741,27 +14836,27 @@ Nainstalujte prosím TQSL nebo zadejte cestu ručně. WWVTableModel - + Time Čas - + SFI SFI - + A A - + K K - + Info Info @@ -13887,37 +14982,37 @@ Nainstalujte prosím TQSL nebo zadejte cestu ručně. WsjtxTableModel - + Callsign Značka - + Gridsquare Lokátor - + Distance Vzdálenost - + SNR SNR - + Last Activity Aktivita - + Last Message Zpráva - + Member Člen @@ -13958,47 +15053,47 @@ Nainstalujte prosím TQSL nebo zadejte cestu ručně. main - + Run with the specific namespace. Spustit ve specifickém jmeném prostoru. - + namespace namespace - + Translation file - absolute or relative path and QM file name. Soubor s překladem - absolutní nebo relativní cesta a jméno QM souboru. - + path/QM-filename path/QM-filename - + Set language. <code> example: 'en' or 'en_US'. Ignore environment setting. Nastavit jazyk. Příklad <code>: 'en' nebo 'en_US'. Ignoruje OS nastavení. - + code kód - + Writes debug messages to the debug file Zapsat ladící zprávy do souboru - + Process pending database import (internal use) Zpracovat čekající import databáze (pro interní použití) - + Force update of all value lists (DXCC, SATs, etc.) Vynutit aktualizaci všech seznamů hodnot (DXCC, SATy atd.) diff --git a/i18n/qlog_de.qm b/i18n/qlog_de.qm index 441cadd5..5a14c1a8 100644 Binary files a/i18n/qlog_de.qm and b/i18n/qlog_de.qm differ diff --git a/i18n/qlog_de.ts b/i18n/qlog_de.ts index b51457fc..334ea2b9 100644 --- a/i18n/qlog_de.ts +++ b/i18n/qlog_de.ts @@ -199,18 +199,115 @@ + Bandmap Guide + Bandmap-Guide + + + + Guide + Guide + + + Fields Felder - + Must not be empty Darf nicht leer sein - + + Leave unchanged + Unverändert lassen + + + + Off + Aus + + + Unsaved - Ungespeichert + Nicht gespeichert + + + + AdifRecoveryManager + + + Startup ADI found more than %1 new QSOs in %2. Use the standard Import. Load point was moved to the end of the file. + Startup-ADI enthält mehr als %1 neue QSOs in %2. Verwenden Sie den Standardimport. Der Ladepunkt wurde an das Ende der Datei verschoben. + + + + Startup ADI Station Profile does not exist: %1 + Stationsprofil für Startup-ADI existiert nicht: %1 + + + + Cannot open Startup ADI records from %1 + Startup-ADI-Einträge aus %1 können nicht geöffnet werden + + + + Startup ADI from %1 finished with %n error(s); load point was not advanced. + + Startup-ADI aus %1 wurde mit %n Fehler(n) beendet; der Ladepunkt wurde nicht weitergesetzt. + + + + + + Startup ADI was disabled for %n file(s) because the assigned Station Profile no longer exists. + + Startup-ADI wurde für %n Datei(en) deaktiviert, da das zugewiesene Stationsprofil nicht mehr existiert. + + + + + + AdifRecoveryReaderWorker + + + Startup ADI filename is empty + Startup-ADI-Dateiname ist leer + + + + Startup ADI file does not exist: %1 + Startup-ADI-Datei existiert nicht: %1 + + + + Startup ADI initialized at the end of file + Startup-ADI am Dateiende initialisiert + + + + Startup ADI file was reset; load point moved to the end + Startup-ADI-Datei wurde zurückgesetzt; Ladepunkt ans Ende verschoben + + + + Cannot open Startup ADI file: %1 + Startup-ADI-Datei kann nicht geöffnet werden: %1 + + + + Cannot seek Startup ADI file: %1 + Position in Startup-ADI-Datei kann nicht gesetzt werden: %1 + + + + Cannot read Startup ADI file: %1 + Startup-ADI-Datei kann nicht gelesen werden: %1 + + + + Too many ADIF records for automatic recovery + Zu viele ADIF-Einträge für die automatische Wiederherstellung @@ -238,7 +335,7 @@ DX Cluster - + DX Cluster @@ -248,7 +345,7 @@ DX - + DX @@ -495,42 +592,42 @@ AlertTableModel - + Rule Name Regelname - + Callsign Rufzeichen - + Frequency Frequenz - + Mode Betriebsart - + Updated Aktualisiert - + Last Update Letzte Aktualisierung - + Last Comment Letzter Kommentar - + Member Mitglied @@ -580,115 +677,120 @@ Awards Auszeichnungen - - - Options - Optionen - Award Auszeichnung - + + 🌐 Rules + 🌐 Regeln + + + My DXCC Entity Eigener DXCC Eintrag - + User Filter Benutzer-Filter - + Confirmed by Bestätigt durch - + LoTW - + eQSL - + Paper Papier - + Mode Betriebsart - + CW - + Phone Phonie - + Digi - + Not-Worked Only nur nicht gearbeit - + Not-Confirmed Only Nur unbestätigt - + + Double-click a row/cell to show QSOs + Doppelklick auf Zeile/Zelle zeigt QSOs + + + Show Anzeigen DXCC - + DXCC ITU - + ITU - + WAC - + WAC WAZ - + WAZ WAS - + WAS WPX - + WPX IOTA - + IOTA @@ -708,7 +810,7 @@ WWFF - + WWFF @@ -731,79 +833,84 @@ Locator %1 Zeichen - + US Counties Countys der USA - + Russian Districts Russische Distrikte - + Japanese Cities/Ku/Guns Japanische Städte / Ku / Gun - + NZ Counties Countys Neuseelands - + Spanish DMEs Spanische DME - + Ukrainian Districts Ukrainische Distrikte - + No User Filter Kein Benutzerfilter - + North America Nordamerika - + South America Südamerika - + Europe Europa - + Africa Afrika - + Asia Asien - - Antarctica - Antarktis - - - + Oceania Ozeanien - + DELETED - Gelöscht + GELÖSCHT + + + + WAAC + + + + + WAIP + @@ -829,6 +936,198 @@ Wartend + + BandmapGuideDialog + + + Bandmap Guide + Bandmap-Guide + + + + Import guide + Guide importieren + + + + Import + Importieren + + + + Export guide + Guide exportieren + + + + Export + Exportieren + + + + New guide + Neuer Guide + + + + New + Neu + + + + Copy guide + Guide kopieren + + + + Copy + Kopieren + + + + Delete guide + Guide löschen + + + + Delete + Löschen + + + + Guide Name: + Guide-Name: + + + + Ranges: + Bereiche: + + + + From + Von + + + + To + Bis + + + + Color + Farbe + + + + Label + Etikett + + + + Add range + Bereich hinzufügen + + + + Add + Hinzufügen + + + + Remove selected range + Ausgewählten Bereich entfernen + + + + Remove + Entfernen + + + + + MHz + MHz + + + + + New Guide + Neuer Guide + + + + Copy - %1 + Kopie – %1 + + + + Delete Guide + Guide löschen + + + + Delete guide '%1'? + Guide „%1“ löschen? + + + + Import Guide + Guide importieren + + + + QLog Bandmap Guide (*.qbg);;JSON (*.json) + QLog Bandmap-Guide (*.qbg);;JSON (*.json) + + + + Import Failed + Import fehlgeschlagen + + + + Export Guide + Guide exportieren + + + + QLog Bandmap Guide (*.qbg) + QLog Bandmap-Guide (*.qbg) + + + + Export Failed + Export fehlgeschlagen + + + + Guide Color + Guide-Farbe + + + + + + QLog Warning + QLog Warnung + + + + Guide name cannot be empty. + Guide-Name darf nicht leer sein. + + + + Guide name '%1' is already used. + Guide-Name „%1“ wird bereits verwendet. + + + + Guide '%1' contains an invalid range. + Guide „%1“ enthält einen ungültigen Bereich. + + BandmapWidget @@ -851,6 +1150,11 @@ Never Nie + + + min(s) + + Clear All @@ -862,34 +1166,59 @@ Aktuelles Band löschen - - min(s) - - - - + Bandmap Bandplan - + Show Band Band anzeigen - + Center RX RX zentrieren - + Show Emergency Frequencies Notruffrequenzen anzeigen - + + Show IBP Frequencies + IBP-Frequenzen anzeigen + + + + Show Guide + Guide anzeigen + + + + Off + Aus + + + + No Guide + Kein Guide + + + + Edit Guide... + Guide bearbeiten... + + + SOS - + SOS + + + + IBP + @@ -897,7 +1226,7 @@ No Rig is connected - Rig nicht verbunden + Kein Rig ist verbunden @@ -925,7 +1254,7 @@ Rig is not connected - Rig nicht verbunden + Rig ist nicht verbunden @@ -986,17 +1315,17 @@ F1 - + F1 F2 - + F2 F3 - + F3 @@ -1167,27 +1496,27 @@ CWKeyer - + No CW Keyer Profile selected Kein CW-Keyer Profil ausgewählt - + Initialization Error Initialisierungsfehler - + Internal Error Interner Fehler - + Connection Error Verbindungsfehler - + Cannot open the Keyer connection Kann die Keyer-Verbindung nicht herstellen @@ -1817,198 +2146,198 @@ Importieren - + Export template Vorlage exportieren - + Export Exportieren - + New template Neue Vorlage - + New Neu - + Copy existing template Vorhandene Vorlage kopieren - + Copy Kopieren - + Delete template Vorlage löschen - + Delete Löschen - + Template Name: Vorlagenname: - + Contest Name: Contest-Name: - + Default Mode: Standardmodus: - + QSO Line Columns: QSO-Zeilen-Spalten: - + Contest name as required by the rules. It is possible to enter a custom string if it is not included in the list. Contest-Name gemäß den Regeln. Es ist möglich, eine benutzerdefinierte Zeichenkette einzugeben, falls sie nicht in der Liste enthalten ist. - + Seq. Nr. - + QSO Field QSO-Feld - + Formatter Formatierer - + Width Breite - + Label Etikett - + Add line Zeile hinzufügen - + Add Hinzufügen - + Remove selected line Ausgewählte Zeile entfernen - + Remove Entfernen - + New Template Neue Vorlage - + Copy - %1 Kopie – %1 - + Delete Template Vorlage löschen - + Delete template '%1'? Vorlage „%1“ löschen? - + Import Template Vorlage importieren - - + + QLog Cabrillo Template (*.qct) QLog Cabrillo-Vorlage (*.qct) - + Import Failed Import fehlgeschlagen - + Export Template Vorlage exportieren - + Export Failed Export fehlgeschlagen - + Failed to write file: %1 Datei konnte nicht geschrieben werden: %1 - + File not found: %1 Datei nicht gefunden: %1 - + Cannot open file: %1 Datei kann nicht geöffnet werden: %1 - + Invalid template file: missing name Ungültige Vorlagendatei: Name fehlt - + QLog Error QLog Fehler - + Cannot start database transaction. Datenbanktransaktion kann nicht gestartet werden. - + QLog Warning QLog Warnung - + Cannot save template '%1': %2 Vorlage „%1“ kann nicht gespeichert werden: %2 @@ -2075,20 +2404,24 @@ - - + + Sunrise SA - - + + Sunset SU - - + + + + + + N/A @@ -2152,7 +2485,7 @@ Andere - + Done erledigt @@ -2160,12 +2493,12 @@ ColumnSettingGenericDialog - + Unselect All Alle abwählen - + Select All Alle auswählen @@ -2178,7 +2511,7 @@ Spaltensichtbarkeit - + Done erledigt @@ -4228,70 +4561,60 @@ Data - + New Entity Neuer Eintrag - + New Band Neues Band - + New Mode Neuer Mode - + New Band&Mode Neues Band&Mode - + New Slot Neuer Slot - + Confirmed Bestätigt - + Worked Gearbeitet - + Hz Hz - + kHz kHz - + GHz GHz - + MHz MHz - - - - - - - - Yes - Ja - @@ -4299,136 +4622,146 @@ - No - Nein + Yes + Ja + + + + + No + Nein + + + + Requested Angefordert - + Queued Wartend - - - + + + Invalid Ungültig - + Bureau Büro - + Direct Direkt - + Electronic Elektronisch - + + + + + + + + + Blank + Leer + + - - - - - - - Blank - Leer - - - Modified Geändert - + Grayline Grayline - + Other Andere - + Short Path - + Long Path - + Not Heard Nicht gehört - + Uncertain Unsicher - + Straight Key Straight Key - + Sideswiper Sideswiper - + Mechanical semi-automatic keyer or Bug Mechanical semi-automatic keyer or Bug - + Mechanical fully-automatic keyer or Bug Mechanical fully-automatic keyer or Bug - + Single Paddle Single Paddle - + Dual Paddle Dual Paddle - + Computer Driven Computergesteuert - + Confirmed (AG) Bestätigt (AG) - + Confirmed (no AG) Bestätigt (non-AG) - + Unknown Unbekannt @@ -5074,57 +5407,57 @@ Example: DxTableModel - + Time Zeit - + Callsign Rufzeichen - + Frequency Frequenz - + Mode Betriebsart - + Spotter - + Comment Kommentar - + Continent Kontinent - + Spotter Continent Spotter Kontinent - + Band Band - + Member Mitglied - + Country Land @@ -5298,72 +5631,72 @@ Example: - + Connect Verbinden - + My Continent Mein Kontinent - + Auto Automatisch - + Connecting... Verbinden... - + DX Cluster is temporarily unavailable DX-Cluster vorübergehend nicht erreichbar - + DXC Server Error DXC-Serverfehler - + An invalid callsign Ungültiges Rufzeichen - + DX Cluster Password DX Cluster Passwort - + Security Notice Sicherheitshinweis - + The password can be sent via an unsecured channel Das Passwort kann über einen ungesicherten Kanal gesendet werden - + Server Server - + Username Benutzername - + Disconnect Trennen - + DX Cluster Command DX-Cluster Kommando @@ -5371,22 +5704,22 @@ Example: DxccTableModel - + Worked Gearbeitet - + eQSL - + LoTW - + Paper Papier @@ -5482,7 +5815,7 @@ Example: - + POTA POTA @@ -5682,42 +6015,42 @@ Example: Exportierte QSOs können nicht als gesendet markiert werden - + Generic Allgemein - + QSLs QSL - + QSL-specific QSL-spezifisch - + All Alle - + Minimal - + Custom 1 Benutzerdefiniert 1 - + Custom 2 Benutzerdefiniert 2 - + Custom 3 Benutzerdefiniert 3 @@ -5846,132 +6179,132 @@ Dieses Passwort wird später benötigt, um sie wiederherzustellen. auto_power_on kann nicht gesetzt werden - + Cannot set no_xchg to 1 no_xchg kann nicht auf 1 gesetzt werden - + Rig Open Error Verbindung fehlgeschlagen - + Set TX Frequency Error Fehler beim Setzen der TX-Frequenz - + Set Frequency Error Fehler bei der Frequenzeinstellung - + Set Split Error Fehler beim Setzen von Split - + Set Mode Error Fehler bei der Moduseinstellung - + Set Split Mode Error Fehler beim Setzen des Split-Modus - + Set PTT Error Fehler beim Auslösen der PTT - + Cannot sent Morse This cannot be displayed - + Cannot stop Morse This cannot be displayed - + Get PTT Error This cannot be displayed - + Get Frequency Error Fehler bei der Frequenzabfrage - + Get Mode Error Fehler bei der Mode-Abfrage - + Get VFO Error Fehler beim Abrufen des VFO - + Get PWR Error This cannot be displayed - + Get PWR (power2mw) Error This cannot be displayed - + Get RIT Function Error This cannot be displayed - + Get RIT Error This cannot be displayed - + Get XIT Function Error This cannot be displayed - + Get XIT Error This cannot be displayed - + Get Split Error - + Get TX Frequency Error - + Get KeySpeed Error This cannot be displayed - + Set KeySpeed Error This cannot be displayed @@ -6009,140 +6342,260 @@ Dieses Passwort wird später benötigt, um sie wiederherzustellen. ImportDialog - + Import Importieren - + Defaults Voreinstellung - - + + Comment Kommentar - + My Rig Eigener Rig - + Import all or only QSOs from the given period Alle oder nur QSOs aus dem angegebenen Zeitraum importieren - + File Datei - + ADX ADX - + Browse Durchsuchen - + + Values are used only for fields that are missing in the import file. Existing values are preserved. + Werte werden nur für Felder verwendet, die in der Importdatei fehlen. Vorhandene Werte bleiben erhalten. + + + + <p>⚠ Missing QSL Sent fields are set to <b>"N"</b> (do not send) by default in ADIF. + <p>⚠ Fehlende QSL-Sent-Felder werden in ADIF standardmäßig auf <b>"N"</b> gesetzt (nicht senden). + + + My Profile Eigenes Profil - + + Used only for missing QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT, and DCL_QSL_SENT fields where default is "N"; otherwise, the value from the input is used. + Wird nur für fehlende Felder QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT und DCL_QSL_SENT verwendet, bei denen der Standardwert „N“ ist; andernfalls wird der Wert aus der Eingabe verwendet. + + + + QSL Sent status + + + + + Used only for missing QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT, and DCL_QSL_SENT fields where default is "N"; otherwise, the value from the input is used.<p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Wird nur für fehlende Felder QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT und DCL_QSL_SENT verwendet, bei denen der Standardwert „N“ ist; andernfalls wird der Wert aus der Eingabe verwendet.<p><b>In Warteschlange</b> (bereit), <b>Nein</b> (nicht senden), <b>Ignorieren</b> (nicht verfolgen), <b>Angefordert</b> (angefordert), <b>Ja</b> (bereits gesendet). + + + + Used only when the imported ADIF record does not contain the selected field. Explicit ADIF values are kept. + Wird nur verwendet, wenn der importierte ADIF-Eintrag das ausgewählte Feld nicht enthält. Explizite ADIF-Werte bleiben erhalten. + + + + Default value for missing DCL_QSL_SENT. <p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Standardwert für fehlendes DCL_QSL_SENT. <p><b>In Warteschlange</b> (bereit), <b>Nein</b> (nicht senden), <b>Ignorieren</b> (nicht verfolgen), <b>Angefordert</b> (angefordert), <b>Ja</b> (bereits gesendet). + + + + Default value for missing EQSL_QSL_SENT. <p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Standardwert für fehlendes EQSL_QSL_SENT. <p><b>In Warteschlange</b> (bereit), <b>Nein</b> (nicht senden), <b>Ignorieren</b> (nicht verfolgen), <b>Angefordert</b> (angefordert), <b>Ja</b> (bereits gesendet). + + + + Default value for missing LOTW_QSL_SENT. <p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Standardwert für fehlendes LOTW_QSL_SENT. <p><b>In Warteschlange</b> (bereit), <b>Nein</b> (nicht senden), <b>Ignorieren</b> (nicht verfolgen), <b>Angefordert</b> (angefordert), <b>Ja</b> (bereits gesendet). + + + + LoTW + LoTW + + + + DCL + DCL + + + + Paper QSL + Papier-QSL + + + + eQSL + eQSL + + + + Default value for missing QSL_SENT.<p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Standardwert für fehlendes QSL_SENT.<p><b>In Warteschlange</b> (bereit), <b>Nein</b> (nicht senden), <b>Ignorieren</b> (nicht verfolgen), <b>Angefordert</b> (angefordert), <b>Ja</b> (bereits gesendet). + + + If DXCC is missing in the imported record, it will be resolved from the callsign. Wenn DXCC im importierten Datensatz fehlt, wird es aus dem Rufzeichen ermittelt. - + Fill missing DXCC Entity Information Fehlende DXCC-Entitätsinformationen ergänzen - + Date Range Zeitraum - + All Alle - + Options Optionen - + &Import &Importieren - + Select File Datei auswählen - - + + The value is used when an input record does not contain the ADIF value Der Wert wird verwendet, wenn ein importierter Datensatz den ADIF-Wert nicht enthält - - + + Queued (ready to send) + In Warteschlange (bereit zum Senden) + + + + Ignored (do not track) + Ignoriert (nicht verfolgen) + + + + Requested (requested again) + Angefordert (erneut angefordert) + + + + Yes (already sent) + Ja (bereits gesendet) + + + + Custom... + Benutzerdefiniert... + + + + Queued + Wartend + + + + Requested + Angefordert + + + + Ignored + Ignoriert + + + + No + Nein + + + + Yes + Ja + + + + The values below will be used when an input record does not contain the ADIF values Die folgenden Werte werden verwendet, wenn ein importierter Datensatz die ADIF-Werte nicht enthält - + <p><b>In-Log QSO:</b></p><p> <p><b>Logbuch QSO:</b></p><p> - + <p><b>Importing:</b></p><p> <p><b>Importieren:</b></p><p> - + Duplicate QSO Doppeltes QSO - + <p>Do you want to import duplicate QSO?</p>%1 %2 <p>Doppelte QSOs importieren?</p>%1 %2 - + Save to File In Datei speichern - + QLog Import Summary Zusammenfassung QLog Import - + Import date Datum importieren - + Imported file Datei importieren - + Imported: %n contact(s) Importiert: %n Kontakt(e) @@ -6150,7 +6603,7 @@ Dieses Passwort wird später benötigt, um sie wiederherzustellen. - + Warning(s): %n Warnungen: %n @@ -6158,7 +6611,7 @@ Dieses Passwort wird später benötigt, um sie wiederherzustellen. - + Error(s): %n Fehler: %n @@ -6166,17 +6619,17 @@ Dieses Passwort wird später benötigt, um sie wiederherzustellen. - + Details Details - + Import Result Ergebnis des Imports - + Save Details... Details speichern... @@ -6604,18 +7057,53 @@ Dieses Passwort wird später benötigt, um sie wiederherzustellen. Importiert - - + + missing QSO_DATE + QSO_DATE fehlt + + + + missing CREDIT_GRANTED + CREDIT_GRANTED fehlt + + + + missing CALL/DXCC + CALL/DXCC fehlt + + + + no matching QSO + kein passendes QSO + + + + cannot update QSO %1: %2 + QSO %1 kann nicht aktualisiert werden: %2 + + + + matched QSO: + passendes QSO: + + + + credit_granted: + + + + + DXCC State: DXCC Status: - + Error Fehler - + Warning Warnung @@ -6623,946 +7111,957 @@ Dieses Passwort wird später benötigt, um sie wiederherzustellen. LogbookModel - + QSO ID Original descriptions may be more meaningful here? QSO ID - + Time on Startzeit - + Time off Endzeit - + Call Rufzeichen - + SIG (ASCII) - + SIG - + SIG Info (ASCII) - + SIG Info - + VUCC VUCC - + Web Web - + RST Sent RST Ausgang - + RST Rcvd RST Eingang - + Frequency Frequenz - - + + Band Band - - + + Mode Betriebsart - + Submode Unterart - + My City (ASCII) Eigene Stadt (ASCII) - + My Country (ASCII) Eigenes Land (ASCII) - + My Name (ASCII) Eigener Name (ASCII) - + My Postal Code (ASCII) Eigene Postleitzahl (ASCII) - + My Rig (ASCII) Eigener Rig (ASCII) - + My Special Interest Activity (ASCII) Eigene spezielle Interessenaktivität (ASCII) - + My Spec. Interes Activity Info (ASCII) Eigene spezielle Interessenaktivität Info (ASCII) - + My Spec. Interest Activity Info Eigene spezielle Interessenaktivität Info - + Name Name - + Notes (ASCII) Anmerkungen (ASCII) - + QTH - - + + Gridsquare Locator - + DXCC DXCC - - + + Country Land - + Continent Kontinent - + RSTs RSTa - + RSTr RSTe - + Name (ASCII) Name (ASCII) - + QTH (ASCII) QTH (ASCII) - + Country (ASCII) Land (ASCII) - + CQZ CQZ - + QSLr QSLe - + QSLr Date QSLe Datum - + QSLs QSLa - + QSLs Date QSLa Datum - + LoTWr LoTWe - + LoTWr Date LoTWe Datum - + LoTWs LoTWa - + LoTWs Date LoTWa Datum - + TX PWR TX Leistung - + Additional Fields Zusatzfelder - + Address Adresse - + Age Alter - + A-Index A-Index - + Antenna Az Antenne Az - + Antenna El Antenne El - + Signal Path Signalweg - + ARRL Section ARRL Sektion - + Award Submitted Award eingereicht - + Award Granted Award erteilt - + Band RX Band RX - + Contest Check Contest Püfung - + Class Klasse - + ClubLog Upload Date Clublog Uploaddatum - + ClubLog Upload State Clublog Uploadstatus - + Comment (ASCII) Kommentar (ASCII) - - + + Comment Kommentar - + + Mode/Submode + + + + + Mode: %1 +Submode: %2 + + + + County Alt Landkreis Alt - + Contacted Operator Operator kontaktiert - + Contest ID Contest ID - + Credit Submitted Beitrag eingereicht - + Credit Granted Beitrag bewilligt - + DCLr Date DCLe Datum - + DCLs Date DCLa Datum - + DCLr DCLe - + DCLs DCLa - + Email Email - - + + Owner Callsign Eigentümer Rufzeichen - + eQSL AG eQSL AG - + FISTS Number FISTS Nummer - + FISTS CC - + EME Init EME Initialisierung - + Frequency RX Frequenz RX - + Guest Operator Gastoperator - + HRDLog Upload Date HRDLog Uploaddatum - + HRDLog Upload Status HRDLog Uploadstatus - + IOTA Island ID IOTA Insel-ID - + K-Index K-Index - + Latitude Breitengrad - + Longitude Längengrad - + Max Bursts - + MS Shower Name - + My Antenna (ASCII) Eigene Antenne (ASCII) - + My Antenna Eigene Antenne - + My County Alt Eigener Landkreis Alt - + My DARC DOK Eigenes DARC DOK - + Operator Callsign Operator Rufzeichen - + POTA POTA - + QRZ Download Date QRZ Herunterladen Datum - + QRZ Download Status QRZ Herunterladen Status - + QSLs Message (ASCII) - + QSLs Message QSL-Nachricht - + QSLr Message - + QSLr Via QSLe via - + QSLs Via QSLa via - + Region Region - + Rig (ASCII) - + RcvPWR - + RcvNr Nr. erhalten - + RcvExch Info erhalten - + SentNr Nr. gesendet - + SentExch Info gesendet - + My ARRL Section Eigene ARRL Sektion - + My WWFF Eigener WWFF - + WWFF - + Paper Papier - + LoTW LoTW - + eQSL eQSL - + QSL Received QSL Eingang - + My City Eigene Stadt - + Address (ASCII) Adresse (ASCII) - + Altitude Höhe - + Gridsquare Extended Erweitertes Gitterfeld - + DOK DOK - + Distance Entfernung - + eQSLr Date eQSLe Datum - + eQSLs Date eQSLa Datum - + eQSLr eQSLe - + eQSLs eQSLa - + HamlogEU Upload Date HamlogEU Uploaddatum - + HamlogEU Upload Status HamlogEU Uploadstatus - + HamQTH Upload Date HamQTH Uploaddatum - + HamQTH Upload Status HamQTH Uploadstatus - + CW Key Info CW Key Information - + CW Key Type CW Key Typ - + My Altitude Eigene Höhe - + My County Eigener Landkreis - + My Country Eigenes Land - + My CQZ Eigene CQZ - + My DXCC Eigenes DXCC Land - + My FISTS Eigene FISTS Nummer - + My Gridsquare Eigener Locator - + My Gridsquare Extended Eigener erweiterter Locator - + My IOTA Eigene IOTA Nummer - + My IOTA Island ID Eigene IOTA Insel-ID - + My ITU Eigene ITU Zone - + My Latitude Eigener Breitengrad - + My Longitude Eigener Längengrad - + My CW Key Info Eiene CW Key - + My CW Key Type Eigener CW Key Typ - + My Name Eigener Name - + My Postal Code Eigene Postleitzahl - + My POTA Ref Eigene POTA Ref - + My Rig Eigener Rig - + My Special Interest Activity Eigene spezielle Interessenaktivität - + My SOTA Eigene SOTA Nummer - + My State Eigener Staat - - + + My Street Eigene Strasse - + My USA-CA Counties Eigene USA-CA Bezirke - + My VUCC Grids Eigenes VUCC Gitterfeld - - + + Notes Anmerkungen - + #MS Bursts - + #MS Pings - + Contest Precedence Contest Vorrang - + Propagation Mode Ausbreitungsmodus - + Public Encryption Key Öffentlicher Chiffrierschlüssel - + QRZ Upload Date QRZ Uploaddatum - + QRZ Upload Status QRZ Uploadstatus - + QSL Message QSL-Nachricht - + QSL Via - + QSO Completed QSO abgeschlossen - + QSO Random - + Rig - + SAT Mode SAT Betriebsart - + SAT Name SAT Name - + Solar Flux - + Silent Key Verstorben - + SKCC Member SKCC Mitglied - + SOTA SOTA - + Logging Station Callsign Logging Station Rufzeichen - + SWL SWL - + Ten-Ten Number Ten-Ten Nummer - + UKSMG Member UKSMG Mitglied - + USA-CA Counties USA-CA Bezirke - + VE Prov - + ITU ITU - + Prefix Präfix - + State Staat - + County Landkreis - + IOTA IOTA - + QSL Sent QSL Ausgang @@ -7571,8 +8070,8 @@ Dieses Passwort wird später benötigt, um sie wiederherzustellen. LogbookWidget - - + + Delete Löschen @@ -7659,73 +8158,73 @@ Dieses Passwort wird später benötigt, um sie wiederherzustellen. - + Callsign Rufzeichen - + Gridsquare Gitterfeld - + POTA POTA - + SOTA SOTA - + WWFF WWFF - + SIG SIG - + IOTA IOTA - + Delete the selected contacts? Die ausgewählten Kontakte löschen? - + Clublog's <b>Immediately Send</b> supports only one-by-one deletion<br><br>Do you want to continue despite the fact<br>that the DELETE operation will not be sent to Clublog? Clublog <b>Sofort Upload</b> unterstützt nur das Löschen eines Datensatzes nach dem anderen.<br><br>Möchten Sie fortfahren, obwohl<br>der DELETE-Vorgang nicht an Clublog gesendet wird? - + Deleting QSOs QSOs Löschen - + Update Aktualisieren - + By updating, all selected rows will be affected.<br>The value currently edited in the column will be applied to all selected rows.<br><br>Do you want to edit them? Durch die Aktualisierung werden alle ausgewählten Zeilen beeinflusst.<br>Der aktuell bearbeitete Wert in der Spalte wird auf alle ausgewählten Zeilen angewendet.<br>Möchten Sie fortfahren? - + Count: %n Anzahl: %n @@ -7733,89 +8232,124 @@ Dieses Passwort wird später benötigt, um sie wiederherzustellen. - + Downloading eQSL Image eQSL-Bild herunterladen - - - + + + Cancel Abbrechen - + All Bands Alle Bänder - + All Modes Alle Betriebsarten - + All Countries Alle Länder - + No User Filter Kein Benutzerfilter - + QLog Warning QLog Warnung - + Each batch supports up to 100 QSOs. Jeder Stapel unterstützt bis zu 100 QSOs. - + QSOs Update Progress QSOs Update Fortschritt - - - + + + QLog Error QLog Fehler - + Callbook login failed Callbook-Anmeldung fehlgeschlagen - + Callbook error: Callbook-Fehler: - + All Clubs Alle Clubs - + eQSL Download Image failed: eQSL-Bild download fehlgeschlagen: + + LotwDXCCCreditDownloader + + + Cannot open test LoTW DXCC credit file + LoTW-DXCC-Test-Credit-Datei kann nicht geöffnet werden + + + + + Incomplete LoTW DXCC credit response + Unvollständige LoTW-DXCC-Credit-Antwort + + + + + Cannot open temporary file + Kann temporäre Datei nicht öffnen + + + + LoTW is not configured properly + LoTW ist nicht richtig konfiguriert + + + + LoTW returned a non-ADIF response + LoTW hat eine Nicht-ADIF-Antwort zurückgegeben + + + + Incorrect login or password + Falscher Benutzername oder falsches Passwort + + LotwQSLDownloader - + Cannot open temporary file Kann temporäre Datei nicht öffnen - + Incorrect login or password Falscher Benutzername oder falsches Passwort @@ -7823,73 +8357,73 @@ Dieses Passwort wird später benötigt, um sie wiederherzustellen. LotwUploader - + Upload cancelled by user Upload vom Benutzer abgebrochen - + Upload rejected by LoTW Upload durch LoTW abgelehnt - + Unexpected response from TQSL server Unerwartete Antwort vom TQSL-Server - + TQSL utility error TQSL-Utility Fehler - + TQSLlib error TQSLib Fehler - + Unable to open input file Kann Eingabedatei nicht öffnen - + Unable to open output file Kann Ausgabedatei nicht öffnen - + All QSOs were duplicates or out of date range Alle QSOs waren Duplikate oder außerhalb des Datumsbereichs - + Some QSOs were duplicates or out of date range Einige QSOs waren Duplikate oder außerhalb des Datumsbereichs - + Command syntax error Befehls-Syntaxfehler - + LoTW Connection error (no network or LoTW is unreachable) LoTW Verbindungsfehler (kein Netzwerk oder LoTW nicht erreichbar) - - + + Unexpected Error from TQSL Unerwarteter Fehler von TQSL - + TQSL not found TQSL nicht gefunden - + TQSL crashed TQSL abgestürzt @@ -7897,19 +8431,19 @@ Dieses Passwort wird später benötigt, um sie wiederherzustellen. MainWindow - - + + Rig - - + + Map Karte - + Toolbar Werkzeugleiste @@ -7944,603 +8478,667 @@ Dieses Passwort wird später benötigt, um sie wiederherzustellen. - - + + Clock Uhr - + WSJTX WSJTX - - + + Rotator Rotor - - + + Bandmap - - + + Online Map Online Karte - - + + CW Console - - + + Chat - - + + Profile Image Profilfoto - + &Settings Einste&llungen - + &Import &Importieren - + &Export &Exportieren - + + Print QS&L + QS&L drucken + + + Mailing List... - + Edit Bearbeiten - + Keep Options Optionen behalten - + Restore connection options after application restart Verbindungsoptionen nach dem Neustart der Anwendung wiederherstellen - + Connect R&ig Verbinde R&ig - - + + Alerts Alerts - + Quit Beenden - + Application - Quit App - Beenden - - - - - - - + + + + + + + Pack Data && Settings Daten und Einstellungen packen - - + + Unpack Data && Settings Daten und Einstellungen entpacken - - + + New QSO - Clear Neuer Kontakt – Löschen - + &About &Über - - + + New QSO - Save Neuer Kontakt – Speichern - + S&tatistics S&tatistik - + QSL &Gallery QSL &Galerie - + Developer Tools Entwickler-Tools - + Run custom read-only SQL queries against the logbook database Benutzerdefinierte SQL-Abfragen (nur lesen) für die Logbuchdatenbank ausführen - - Print QSL &Labels - QSL-Etiketten &drucken - - - + Connect R&otator Verbinde R&otor - + QSO &Filters QSO &Filter - + &Awards - + DXCC &Submission List DXCC-&Einreichungsliste - + Generate a list of contacts to submit for ARRL DXCC award credit Liste von Verbindungen zur Einreichung für ARRL DXCC-Anerkennung erstellen - + Beep - + Connect &CW Keyer Verbinde &CW Keyer - + &Wiki &Wiki - + Report &Bug... Fehler &melden... - + &Manual Entry &Manueller Eintrag - + Switch New Contact dialog to the manually entry mode<br/>(time, freq, profiles etc. are not taken from their common sources) Schalte den Dialog "Neuer Kontakt" in den manuellen Eingabemodus<br/>(Zeit, Frequenz, Profile etc. werden nicht aus ihren gemeinsamen Quellen übernommen) - - + + Save Arrangement Arrangement speichern - + Logbook - Search Callsign Logbuch - Suche - - + + New QSO - Add text from Callsign field to Bandmap Neuer Kontakt - Übertragen Sie den Text vom Rufzeichenfeld in die Bandmap - + Rig - Band Down Rig - Band (-) - + Rig - Band Up Rig - Band (+) - + New QSO - Use Callsign from the Whisperer Neuer Kontakt - Verwende Rufzeichen vom Whisperer - + CW Console - Key Speed Up CW Console - Geschwindigkeit (+) - + CW Console - Key Speed Down CW Console - Geschwindigkeit (-) - + CW Console - Profile Up CW Console - CW-Keyer Profil (+) - + CW Console - Profile Down CW Console - CW-Keyer Profil (-) - + Rig - PTT On/Off Rig - PTT On/Off - + All Bands Alle Bänder - + Each Band Jedes Band - + Each Band && Mode Jedes Band & Mode - + No Check Keine Kontrolle - + Single Eine für alles - + Per Band Band - + Stop Stoppen - + Reset Zurücksetzen - + None Keine - + Upload Hochladen - + Service - Upload QSOs Service – QSO hochladen - + Download QSLs QSLs herunterladen - + Service - Download QSLs Service - QSLs herunterladen - + + Download LoTW DXCC Credits + LoTW-DXCC-Credits herunterladen + + + + Service - Download LoTW DXCC Credits + Dienst - LoTW-DXCC-Credits herunterladen + + + Theme: Native Thema: Native - + Theme: QLog Light Thema: QLog Light - + Theme: QLog Dark Thema: QLog Dark - + What's New Was ist neu - + Export Cabrillo Cabrillo exportieren - + Wsjtx Wsjtx - - + + Contest Contest - + Dupe Check Dupe-Prüfung - + Sequence Sequenz - + Linking Exchange With Exchange mit verknüpfen - + Edit Rules Regeln bearbeiten - + Clear Löschen - + Show Alerts Alerts anzeigen - + About Über - - + + DX Cluster - + Color Theme Farbschema - + Not enabled for non-Fusion style Für keinen anderen Stil als Fusion zulässig - + Press to tune the alert Drücken zum Einstellen des Alarms - + + Startup ADI + + + + Clublog Immediately Upload Error Clublog-Sofort-Upload-Fehler - - - + + + <b>Error Detail:</b> - + op: op: - + A New Version Eine neue Version - + A new version %1 is available. Eine neue Version %1 ist verfügbar. - + Remind Me Later Später erinnern - + Download Herunterladen - - Failed to encrypt credentials. - Verschlüsseln der Zugangsdaten fehlgeschlagen. + + + QLog Warning + QLog Warnung - - Database files (*.dbe);;All files (*) - Datenbankdateien (*.dbe);;Alle Dateien (*) + + LoTW is not configured properly.<p>Please, use <b>Settings</b> dialog to configure it.</p> + LoTW ist nicht richtig konfiguriert.<p>Bitte verwenden Sie den Dialog <b>Einstellungen</b>, um es zu konfigurieren.</p> - - Failed to create temporary file. - Temporäre Datei konnte nicht erstellt werden. + + + QLog Error + QLog Fehler - - Failed to dump the database. - Datenbank-Dump fehlgeschlagen. + + Cannot load local DXCC entities from the logbook: + Lokale DXCC-Entities können nicht aus dem Logbuch geladen werden: - + + Unknown DXCC Entity + Unbekannte DXCC-Entity + + + + Cannot determine a local DXCC entity from logbook contacts. + Lokale DXCC-Entity kann aus den Logbuchkontakten nicht bestimmt werden. + + + + LoTW DXCC Credits + LoTW-DXCC-Credits + + + + Select the local DXCC entity for which LoTW DXCC credits will be downloaded: + Wählen Sie die lokale DXCC-Entity aus, für die LoTW-DXCC-Credits heruntergeladen werden: + + + + Cancel + Abbrechen + + + + Downloading LoTW DXCC credits + LoTW-DXCC-Credits werden heruntergeladen + + + + Processing LoTW DXCC credits + LoTW-DXCC-Credits werden verarbeitet + + + + LoTW DXCC Credit Import Summary + Zusammenfassung des LoTW-DXCC-Credit-Imports + + + + LoTW DXCC credit import failed: + Import der LoTW-DXCC-Credits fehlgeschlagen: + + + + Failed to encrypt credentials. + Verschlüsseln der Zugangsdaten fehlgeschlagen. + + + + Database files (*.dbe);;All files (*) + Datenbankdateien (*.dbe);;Alle Dateien (*) + + + + Failed to create temporary file. + Temporäre Datei konnte nicht erstellt werden. + + + + Failed to dump the database. + Datenbank-Dump fehlgeschlagen. + + + Compressing database... Datenbank wird komprimiert… - + Database successfully dumped to %1 Datenbank erfolgreich exportiert nach %1 - + Failed to compress the database. Datenbank konnte nicht komprimiert werden. - + Failed to prepare database for import. Datenbank konnte nicht für den Import vorbereitet werden. - + Classic Klassisch - + Do you want to remove the Contest filter %1? Möchten Sie den Contest-Filter %1 entfernen? - + Contest: Contest: - + <h1>QLog %1</h1><p>&copy; 2019 Thomas Gatzweiler DL2IC<br/>&copy; 2021-2026 Ladislav Foldyna OK1MLG<br/>&copy; 2025-2026 Michael Morgan AA5SH<br/>&copy; 2025-2026 Kyle Boyle VE9KZ</p><p>Based on Qt %2<br/>%3<br/>%4<br/>%5</p><p>Icon by <a href='http://www.iconshock.com'>Icon Shock</a><br />Satellite images by <a href='http://www.nasa.gov'>NASA</a><br />ZoneDetect by <a href='https://github.com/BertoldVdb/ZoneDetect'>Bertold Van den Bergh</a><br />TimeZone Database by <a href='https://github.com/evansiroky/timezone-boundary-builder'>Evan Siroky</a> <h1>QLog %1</h1><p>&copy; 2019 Thomas Gatzweiler DL2IC<br/>&copy; 2021-2026 Ladislav Foldyna OK1MLG<br/>&copy; 2025-2026 Michael Morgan AA5SH<br/>&copy; 2025-2026 Kyle Boyle VE9KZ</p><p>Qt %2<br/>%3<br/>%4<br/>%5</p><p>Icon by <a href='http://www.iconshock.com'>Icon Shock</a><br />Satellite images by <a href='http://www.nasa.gov'>NASA</a><br />ZoneDetect by <a href='https://github.com/BertoldVdb/ZoneDetect'>Bertold Van den Bergh</a><br />TimeZone Database by <a href='https://github.com/evansiroky/timezone-boundary-builder'>Evan Siroky</a> - + N/A - MapWebChannelHandler + MapPageController - - - - Grid - Gitterfeld + + Aurora + Aurora - - - - Gray-Line - + + Beam + - - - - Beam - + + Chat + - - - - Aurora - + + Grid + Gitterfeld - - - - MUF - + + Gray-Line + - - - + IBP - + - - - - Chat - + + MUF + - - - + WSJTX - CQ WSJTX - CQ - - - + Path Weg @@ -8563,7 +9161,7 @@ Dieses Passwort wird später benötigt, um sie wiederherzustellen. Startzeit - + W @@ -8603,7 +9201,7 @@ Dieses Passwort wird später benötigt, um sie wiederherzustellen. &Details - + the contacted station's DARC DOK (District Location Code) (ex. A01) DARC-DOK (Ortsverbandkenner) der kontaktierten Station (z.B. A01) @@ -8638,7 +9236,7 @@ Dieses Passwort wird später benötigt, um sie wiederherzustellen. Dauer - + World Wide Flora & Fauna @@ -8691,7 +9289,7 @@ Dieses Passwort wird später benötigt, um sie wiederherzustellen. <b>Stationsstatistik</b> - + Blank Leer @@ -8816,87 +9414,87 @@ Dieses Passwort wird später benötigt, um sie wiederherzustellen. Callbook-Anmeldung fehlgeschlagen - + LP LP - + New Entity! Neuer Eintrag! - + New Band! Neues Band! - + New Mode! Neue Betriebsart! - + New Band & Mode! Neues Band & Betriebsart! - + New Slot! Neuer Slot! - + Worked Gearbeitet - + Confirmed Bestätigt - + GE GA - + GM GM - + GA GT - + m - + Callbook search is active Callbook-Suche ist aktiv - + Contest ID must be filled in to activate Zur Aktivierung muss die Contest-ID ausgefüllt werden - + It is not the name of the contest but it is an assigned<br>Contest ID (ex. CQ-WW-CW for CQ WW DX Contest (CW)) - + Description of the contacted station's equipment Beschreibung der Ausrüstung der kontaktierten Station - + Callbook search is inactive Callbook-Suche ist inaktiv @@ -8906,17 +9504,17 @@ Dieses Passwort wird später benötigt, um sie wiederherzustellen. Erweitern/Zusammenklappen - + two or four adjacent Maidenhead grid locators, each four characters long, (ex. EN98,FM08,EM97,FM07) zwei oder vier nebeneinander liegende, jeweils vier Zeichen lange Gitterfelder (z. B. EN98,FM08,EM97,FM07) - + Special Activity Group Spezielle Aktivitätsgruppe - + Special Activity Group Information @@ -9130,7 +9728,7 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren. QCoreApplication - + QLog Help QLog Hilfe @@ -9138,48 +9736,48 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren. QMessageBox - - - + + + - - - - + + + + QLog Error QLog Fehler - + QLog is already running QLog wird bereits ausgeführt - + Failed to process pending database import. Ausstehenden Datenbankimport konnte nicht verarbeitet werden. - + The database was imported successfully, but the stored passwords could not be restored (decryption failed or the data is corrupted). All service passwords have been cleared and must be re-entered in Settings. Die Datenbank wurde erfolgreich importiert, aber die gespeicherten Passwörter konnten nicht wiederhergestellt werden (Entschlüsselung fehlgeschlagen oder Daten sind beschädigt). Alle Service-Passwörter wurden gelöscht und müssen in den Einstellungen erneut eingegeben werden. - + Could not connect to database. Keine Verbindung zur Datenbank möglich. - + Could not export a QLog database to ADIF as a backup.<p>Try to export your log to ADIF manually Kann QLog-Datenbank nicht als Backup nach ADIF exportieren.<p>Versuchen Sie, Ihr Log manuell nach ADIF zu exportieren - + Database migration failed. Migration der Datenbank fehlgeschlagen. @@ -9189,31 +9787,31 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren. - + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + QLog Warning QLog Warnung @@ -9260,52 +9858,52 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren.Netzwerkfehler. Clubliste kann nicht heruntergeladen werden für - + DXC Server Name Error DXC Server Namensfehler - + DXC Server address must be in format<p><b>[username@]hostname:port</b> (ex. hamqth.com:7300)</p> Format der DXC-Server-Adresse <p><b>[benutzername@]hostname:Port</b> (z. B. hamqth.com:7300)</p> - + DX Cluster Password DX Cluster Passwort - + Invalid Password Falsches Passwort - + DXC Server Connection Error DXC Server Verbindungsfehler - + The fields <b>%0</b> will not be saved because the <b>%1</b> is not filled. Die Felder <b>%0</b> werden nicht gespeichert, da <b>%1</b> nicht ausgefüllt ist. - + Your callsign is empty. Please, set your Station Profile Ihr Rufzeichen ist nicht angegeben. Bitte richten Sie Ihr Stationsprofil ein - + - + QLog Info QLog Info - + Activity name is already exists. Der Aktivitätsname existiert bereits. @@ -9331,104 +9929,104 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren. - + Filter name is already exists. Filtername ist bereits vorhanden. - - + + Please, define at least one Station Locations Profile Bitte definieren Sie mindestens ein Stationsstandortprofil - + WSJTX Multicast is enabled but the Address is not a multicast address. WSJTX Multicast ist aktiviert, aber die Adresse ist keine Multicast-Adresse. - + Loop detected. Raw UDP forward uses the same port as the WSJT-X receiving port. Schleife erkannt. Raw-UDP-Weiterleitung verwendet denselben Port wie der WSJT-X-Empfangsport. - + Rig port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device Rig port muss ein gültiger COM-Port sein.<br>Für Windows verwende COMxx, für Unix-ähnliche Betriebssysteme verwende einen Pfad zum Gerät - + Rig PTT port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device Rig-PTT-Port muss ein gültiger COM-Port sein. Für Windows verwenden Sie COMxx, für Unix-ähnliche Betriebssysteme verwenden Sie einen Pfad zum Gerät - + <b>TX Range</b>: Max Frequency must not be 0. <b>TX-Bereich</b>: Max Frequenz darf nicht 0 sein. - + <b>TX Range</b>: Max Frequency must not be under Min Frequency. <b>TX-Bereich</b>: Die Maximalfrequenz darf nicht niedriger als die Minimalfrequenz sein. - + Rotator port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device Rotor-Port muss ein gültiger COM-Port sein.<br>Für Windows verwenden Sie COMxx, für Unix-ähnliche Betriebssysteme verwenden Sie einen Pfad zum Gerät - + CW Keyer port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device CW Keyer Port muss ein gültiger COM-Port sein.<br>Für Windows verwenden Sie COMxx, für unix-ähnliche Betriebssysteme verwenden Sie einen Pfad zum Gerät - + Cannot change the CW Keyer Model to <b>Morse over CAT</b><br>No Morse over CAT support for Rig(s) <b>%1</b> Kann das CW-Keyer Modell nicht zu <b>CW-über-CAT</b> ändern.<br>Keine CW-über-CAT Unterstützung für Rig(s) <b>%1</b> - + Cannot delete the CW Keyer Profile<br>The CW Key Profile is used by Rig(s): <b>%1</b> Kann das CW-Keyer Profil nicht löschen.<br>Das Profil wird verwendet von Rig(s): <b>%1</b> - + Operator Callsign has an invalid format Das Operator-Rufzeichen hat ein ungültiges Format - + Gridsquare has an invalid format Gitterfeld hat ein falsches Format - + VUCC Grids have an invalid format (must be 2 or 4 Gridsquares separated by ',') VUCC Gitterfeld hat ein falsches Format (es müssen 2 oder 4 Gitterfelder sein, getrennt durch ',') - + Country must not be empty Das Land darf nicht leer sein - + CQZ must not be empty CQZ darf nicht leer sein - + ITU must not be empty ITU darf nicht leer sein - + Callsign has an invalid format Rufzeichen hat ein falsches Format - + Filename is empty Dateiname ist leer @@ -9462,22 +10060,22 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren.Chat Fehler: - + Cannot update QSO Filter Conditions QSO-Filter kann nicht aktualisiert werden - + <b>Rig Error:</b> - + <b>Rotator Error:</b> - + <b>CW Keyer Error:</b> @@ -9485,74 +10083,74 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren. QObject - + Cannot connect to DXC Server <p>Reason <b>: Kann keine Verbindung zum DXC Server herstellen <p>Ursache <b>: - + Connection Refused Verbindung abgelehnt - + Host closed the connection Der Host hat die Verbindung beendet - + Host not found Host nicht gefunden - + Timeout Zeitüberschreitung - + Network Error Netzwerkfehler - + Internal Error Interner Fehler - + Importing Database Datenbank wird importiert - + Opening Database Datenbank öffnen - + Backuping Database Datenbank sichern - + Migrating Database Datenbank migrieren - + Starting Application Anwendung starten @@ -9652,7 +10250,7 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren.Eigenes DXCC Land - + <b>Imported</b>: %n contact(s) <b>Importiert</b>: %n Kontakt(e) @@ -9660,7 +10258,7 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren. - + <b>Warning(s)</b>: %n <b>Warnungen</b>: %n @@ -9668,7 +10266,7 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren. - + <b>Error(s)</b>: %n <b>Fehler</b>: %n @@ -9676,12 +10274,12 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren. - + km km - + miles mil @@ -9756,6 +10354,32 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren.Worked Gearbeitet + + + IARU Region 1 + + + + + + Failed to write file: %1 + Datei konnte nicht geschrieben werden: %1 + + + + Cannot open file: %1 + Datei kann nicht geöffnet werden: %1 + + + + Invalid guide file: %1 + Ungültige Guide-Datei: %1 + + + + Invalid guide file: missing title + Ungültige Guide-Datei: Titel fehlt + QRZCallbook @@ -9768,7 +10392,7 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren. QRZUploader - + General Error Allgemeiner Fehler @@ -9791,33 +10415,33 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren.Sortieren nach: - + Export Filtered Gefilterte exportieren - + Date (Newest) Datum (neueste) - + Date (Oldest) Datum (älteste) - + Callsign (A-Z) Rufzeichen (A-Z) - + Callsign (Z-A) Rufzeichen (Z-A) - - + + %n QSL card(s) %n QSL-Karte(n) @@ -9825,72 +10449,72 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren. - + All QSL Cards Alle QSL-Karten - + Favorites Favoriten - + By Country Nach Land - + By Date Nach Datum - + By Band Nach Band - + By Mode Nach Modus - + By Continent Nach Kontinent - + Remove from Favorites Aus Favoriten entfernen - + Add to Favorites Zu Favoriten hinzufügen - + Open Öffnen - + Save... Speichern… - + Save QSL Card QSL-Karte speichern - + Export QSL Cards QSL-Karten exportieren - + Exported %1 of %2 cards %1 von %2 Karten exportiert @@ -9933,28 +10557,23 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren.Details - - New QSLs: - Neue QSLs: + + New QSLs: + Neue QSLs: - Updated QSOs: - Aktualisierte QSOs: + Updated QSOs: + Aktualisierte QSOs: - - Unmatched QSLs: - Nicht gefundene QSLs: + + Unmatched QSLs: + Nicht zugeordnete QSLs: QSLPrintLabelDialog - - - Print QSL Labels - QSL-Etiketten drucken - Filter @@ -9986,235 +10605,397 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren.Benutzer-Filter - + Label Template Etikettenvorlage - + + Page Size: Seitengröße: - + Columns: Spalte: - + Rows: Zeile: - + + Label Width: Etikettenbreite: - + + Print QSL Labels / Cards + QSL-Etiketten / Karten drucken + + + + Print Mode + Druckmodus + + + + Mode: + Betriebsart: + + + + + QSL Card + QSL-Karte + + + + Card Width: + Kartenbreite: + + + + Card Height: + Kartenhöhe: + + + + Card Gap: + Kartenabstand: + + + + Label Height: Etikettenhöhe: - + + Label X Offset: + X-Versatz des Etiketts: + + + + Label Y Offset: + Y-Versatz des Etiketts: + + + + Label Background: + Etikettenhintergrund: + + + + Fill under label + Unter Etikett füllen + + + + + Color + Farbe + + + + Background Image: + Hintergrundbild: + + + + Browse + Durchsuchen + + + + Clear + Löschen + + + Left Margin: Linker Rand: - + Top Margin: Oberer Rand: - + H Spacing: Horizontaler Abstand: - + V Spacing: Vertikaler Abstand: - + Label Appearance Erscheinungsbild des Etiketts - + Print Label Borders Etikettenränder drucken - + QSOs per Label: QSOs pro Etikett: - + Footer Left Text: Linker Fußzeilentext: - + Footer Right Text: Rechter Fußzeilentext: - + Skip Label: Etikett überspringen: - + Sans Font: Serifenlose Schrift: - + Mono Font: Monospace-Schrift: - + + Text Color: + Textfarbe: + + + Callsign Size: Größe des Rufzeichens: - + "To Radio" Size: Größe „To Radio“: - + "To Radio" Text: Text „To Radio“: - + Header Size: Kopfzeilen-Größe: - + Data Size: Datengröße: - + Date Header Text: Datum-Header-Text: - + Date Format: Datumsformat: - + Time Header Text: Zeit-Header-Text: - + Band Header Text: Band-Header-Text: - + Mode Header Text: Modus-Header-Text: - + QSL Header Text: QSL-Header-Text: - + Extra Column: Zusätzliche Spalte: - + Extra Column Text Text der zusätzlichen Spalte - + (DB column name) (Datenbank-Spaltenname) - - + + No matching QSOs found Keine passenden QSOs gefunden - - + + Page 0 of 0 Seite 0 von 0 - + Labels: 0 (0 pages) Etiketten: 0 (0 Seiten) - + Print Drucken - + Export as PDF Als PDF exportieren - - + + Export as Images + Bilder exportieren + + + + Label Sheet + Etikettenbogen + + + + Custom Benutzerdefiniert - + Empty Leer - + QSOs matching this station profile QSOs, die diesem Stationsprofil entsprechen - + + Select Label Text Color + Textfarbe des Etiketts wählen + + + + Select Label Background Color + Hintergrundfarbe des Etiketts wählen + + + + + + Select QSL Card Background + QSL-Kartenhintergrund wählen + + + + Images (*.png *.jpg *.jpeg *.bmp) + Bilder (*.png *.jpg *.jpeg *.bmp) + + + + Cannot read selected image file. + Ausgewählte Bilddatei kann nicht gelesen werden. + + + + Selected file is not a valid image. + Ausgewählte Datei ist kein gültiges Bild. + + + + Cards: %1 (%2 pages) + Karten: %1 (%2 Seiten) + + + Labels: %1 (%2 pages) Etiketten: %1 (%2 Seiten) - + Page %1 of %2 Seite %1 von %2 - + Export PDF PDF exportieren - + PDF Files (*.pdf) PDF-Dateien (*.pdf) - + + + + Export QSL Card Images + QSL-Kartenbilder exportieren + + + + Some image files already exist. Overwrite them? + Einige Bilddateien existieren bereits. Überschreiben? + + + + Exported %n QSL card image(s). + + %n QSL-Kartenbild(er) exportiert. + + + + + + Exported %1 of %2 QSL card images. + %1 von %2 QSL-Kartenbildern exportiert. + + + + QSOs were not marked as sent. + QSOs wurden nicht als gesendet markiert. + + + Mark as Sent Als gesendet markieren - + Mark printed/exported QSOs as sent? Gedruckte/exportierte QSOs als gesendet markieren? @@ -10265,7 +11046,7 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren. - + Blank Leer @@ -10621,7 +11402,7 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren. Received - Eingang + Erhalten @@ -10641,12 +11422,12 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren. QSL via - + QSL via Sent - Ausgang + Gesendet @@ -10684,318 +11465,318 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren.Info gesendet - + &Reset &Zurücksetzen - + &Lookup &Suchen - - - + + + No Nein - - - + + + Yes Ja - - - + + + Requested Angefordert - - - + + + Queued Wartend - - - + + + Ignored Ignoriert - + Bureau Büro - + Direct Direkt - + Electronic Elektronisch - + Submit changes Änderungen übermitteln - + Really submit all changes? Wirklich alle Änderungen übermitteln? - - - - + + + + QLog Error QLog Fehler - + Cannot save all changes - internal error Es können nicht alle Änderungen gespeichert werden - interner Fehler - + Cannot save all changes - try to reset all changes Es können nicht alle Änderungen gespeichert werden - versuchen Sie, alle Änderungen zurückzusetzen - + QSO Detail QSO Detail - + Edit QSO QSO Bearbeiten - + Downloading eQSL Image eQSL-Bild herunterladen - + Cancel Abbrechen - + eQSL Download Image failed: eQSL-Bild download fehlgeschlagen: - + DX Callsign must not be empty DX-Rufzeichen darf nicht leer sein - + DX callsign has an incorrect format DX-Rufzeichen hat ein falsches Format - - + + TX Frequency or Band must be filled TX-Frequenz oder -Band muss ausgefüllt sein - - + + DX Grid has an incorrect format DX Gitterfeld hat ein fehlerhaftes Format - + Based on callsign, DXCC Country is different from the entered value - expecting - + Based on callsign, DXCC Continent is different from the entered value - expecting - + Based on callsign, DXCC ITU is different from the entered value - expecting - + Based on callsign, DXCC CQZ is different from the entered value - expecting - + Based on own callsign, own DXCC ITU is different from the entered value - expecting - + Based on own callsign, own DXCC CQZ is different from the entered value - expecting - + Based on own callsign, own DXCC Country is different from the entered value - expecting - + Based on SOTA Summit, Grid does not match SOTA Grid - expecting - + Based on POTA record, QTH does not match POTA Name - expecting - + Based on POTA record, Grid does not match POTA Grid - expecting - + Based on SOTA Summit, my QTH does not match SOTA Summit Name - expecting - + Based on SOTA Summit, my Grid does not match SOTA Grid - expecting - + Based on POTA record, my QTH does not match POTA Name - expecting - + Based on POTA record, my Grid does not match POTA Grid - expecting - + VUCC has an incorrect format VUCC hat ein fehlerhaftes Format - + blank Leer - + Based on Frequencies, Sat Mode should be - + Sat name must not be empty Der Satellitenname darf nicht leer sein - + Own Callsign must not be empty Eigenes Rufzeichen darf nicht leer sein - + Own callsign has an incorrect format Eigenes Rufzeichen hat ein fehlerhaftes Format - + Own VUCC Grids have an incorrect format Eigenes VUCC Gitterfeld hat ein fehlerhaftes Format - + LoTW Sent Status to <b>No</b> does not make any sense if QSL Sent Date is set. Set Date to 1.1.1900 to leave the date field blank LoTW Sent Status <b>Nein</b> macht keinen Sinn, wenn ein QSL Sent Datum gesetzt ist. Setzen Sie das Datum auf 1.1.1900, um das Datumsfeld leer zu lassen - + Date should be present for LoTW Sent Status <b>Yes</b> Ein Datum sollte für LoTW Sent Status <b>Ja</b> angegeben werden - + eQSL Sent Status to <b>No</b> does not make any sense if QSL Sent Date is set. Set Date to 1.1.1900 to leave the date field blank eQSL Sent Status <b>Nein</b> macht keinen Sinn, wenn ein QSL Sent Datum eingestellt ist. Setzen Sie Datum auf 1.1.1900, um das Datumsfeld leer zu lassen - + Date should be present for eQSL Sent Status <b>Yes</b> Ein Datum sollte für eQSL Sent Status <b>Ja</b> angegeben werden - + Paper Sent Status to <b>No</b> does not make any sense if QSL Sent Date is set. Set Date to 1.1.1900 to leave the date field blank Papier Sent Status <b>Nein</b> macht keinen Sinn, wenn ein QSL Sent Datum gesetzt ist. Setzen Sie das Datum auf 1.1.1900, um das Datumsfeld leer zu lassen - + Date should be present for Paper Sent Status <b>Yes</b> Ein Datum sollte für Papier Sent Status <b>Ja</b> angegeben werden - + Based on SOTA Summit, QTH does not match SOTA Summit Name - expecting Auf Grundlage der SOTA-Daten stimmt das QTH nicht mit dem SOTA Gipfelname überein - erwartend - + TX Band should be TX-Band sollte sein - + RX Band should be RX-Band sollte sein - + Callbook error: Callbook-Fehler: - - + + <b>Warning: </b> <b>Warnung: </b> - + Validation Überprüfung - + Yellow marked fields are invalid.<p>Nevertheless, save the changes?</p> Gelb markierte Felder sind ungültig.<p>Dennoch die Änderungen speichern?</p> - + &Save &Speichern - + &Edit B&earbeiten @@ -11033,52 +11814,52 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren.Bedingung hinzufügen - + Equal gleich - + Not Equal nicht gleich - + Contains enthält - + Not Contains enthält nicht - + Greater Than größer als - + Less Than kleiner als - + Starts with beginnt mit - + RegExp - + Remove Entfernen - + Must not be empty Darf nicht leer sein @@ -11114,27 +11895,27 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren. Rig - + No Rig Profile selected Kein Rig-Profil ausgewählt - + Rigctld Error Rigctld-Fehler - + Initialization Error Initialisierungsfehler - + Internal Error Interner Fehler - + Cannot open Rig Kann Rig nicht öffnen @@ -11152,36 +11933,66 @@ Sie können Felder leer lassen und später in den Einstellungen konfigurieren. - + Disconnected Nicht verbunden - - + + MHz MHz - + Disable Split Split deaktivieren - + RIT: 0.00000 MHz - + XIT: 0.00000 MHz - + PWR: %1W + + + OUT + + + + + Outside Bandmap Guide range + Außerhalb des Bandmap-Guide-Bereichs + + + + SOS + SOS + + + + Emergency frequency: %1 MHz + Notfrequenz: %1 MHz + + + + IBP + + + + + International Beacon Project: %1 MHz + + RigctldAdvancedDialog @@ -11271,57 +12082,57 @@ Bitte installieren Sie Hamlib oder geben Sie den Pfad manuell an. RigctldManager - + rigctld executable not found in /app/bin/. This should not happen in Flatpak build. Rigctld-Ausführbare Datei wurde in /app/bin/ nicht gefunden. Dies sollte im Flatpak-Build nicht passieren. - + rigctld executable not found. Please install Hamlib or specify the path in Advanced settings. Rigctld-Ausführbare Datei wurde nicht gefunden. Bitte installieren Sie Hamlib oder geben Sie den Pfad in den erweiterten Einstellungen an. - + Hamlib major version mismatch: QLog was compiled with Hamlib %1 but rigctld reports version %2.%3.%4. Rig model IDs are incompatible between major versions. Hamlib-Hauptversionskonflikt: QLog wurde mit Hamlib %1 kompiliert, aber rigctld meldet Version %2.%3.%4. Rig-Modell-IDs sind zwischen Hauptversionen nicht kompatibel. - + Port %1 is already in use. Another rigctld or application may be running on this port. Port %1 wird bereits verwendet. Ein anderer rigctld oder eine Anwendung läuft möglicherweise auf diesem Port. - + rigctld started but not responding on port %1. Rigctld wurde gestartet, reagiert aber nicht auf Port %1. - + Failed to start rigctld: %1 %2 Rigctld konnte nicht gestartet werden: %1 %2 - + rigctld crashed. rigctld ist abgestürzt. - + rigctld timed out. rigctld hat die Zeitüberschreitung erreicht. - + Write error with rigctld. Schreibfehler bei rigctld. - + Read error with rigctld. Lese Fehler bei rigctld. - + Unknown rigctld error. Unbekannter rigctld-Fehler. @@ -11329,22 +12140,22 @@ Bitte installieren Sie Hamlib oder geben Sie den Pfad manuell an. Rotator - + No Rotator Profile selected Kein Rotor-Profil ausgewählt - + Initialization Error Initialisierungsfehler - + Internal Error Interner Fehler - + Cannot open Rotator Kann Rotor nicht öffnen @@ -11416,7 +12227,7 @@ Bitte installieren Sie Hamlib oder geben Sie den Pfad manuell an. - + Callsign Rufzeichen @@ -11446,20 +12257,21 @@ Bitte installieren Sie Hamlib oder geben Sie den Pfad manuell an. - - - - - - - - - - + + + + + + - - - + + + + + + + + Add Hinzufügen @@ -11630,6 +12442,7 @@ Bitte installieren Sie Hamlib oder geben Sie den Pfad manuell an. + Description Beschreibung @@ -11722,18 +12535,18 @@ Bitte installieren Sie Hamlib oder geben Sie den Pfad manuell an. - - + + None - + Hardware - + Software @@ -11744,27 +12557,31 @@ Bitte installieren Sie Hamlib oder geben Sie den Pfad manuell an. - + + + + + No - + Even - + Odd - + Space - + Mark @@ -11970,8 +12787,8 @@ Bitte installieren Sie Hamlib oder geben Sie den Pfad manuell an. - - + + QRZ.com @@ -11991,12 +12808,12 @@ Bitte installieren Sie Hamlib oder geben Sie den Pfad manuell an. Verwendung einer internen TQSL - + Port where QLog listens an incoming traffic from WSJT-X Port, an dem QLog auf eingehenden Datenverkehr von WSJT-X wartet - + Raw UDP Forward UDP Rohdaten-Weiterleitung @@ -12048,7 +12865,7 @@ Bitte installieren Sie Hamlib oder geben Sie den Pfad manuell an. - + Serial Serial @@ -12271,7 +13088,7 @@ Bitte installieren Sie Hamlib oder geben Sie den Pfad manuell an. - + Start rigctld daemon to share rig with other applications (e.g. WSJT-X) Rigctld-Daemon starten, um das Rig mit anderen Anwendungen zu teilen (z. B. WSJT-X) @@ -12415,27 +13232,102 @@ Bitte installieren Sie Hamlib oder geben Sie den Pfad manuell an. - + API Key API-Schlüssel - + + Startup ADI + + + + + Configured ADI/ADIF files are checked only at startup. A newly added file starts at its current end, so only later appended QSOs are loaded. This is not a live watcher; if many new QSOs are found, loading stops and the standard Import should be used. + Konfigurierte ADI/ADIF-Dateien werden nur beim Start geprüft. Eine neu hinzugefügte Datei beginnt an ihrem aktuellen Ende, sodass nur später angehängte QSOs geladen werden. Dies ist keine Live-Überwachung; wenn viele neue QSOs gefunden werden, wird das Laden gestoppt und der Standardimport sollte verwendet werden. + + + + Removing a file also forgets its recovery position. + Beim Entfernen einer Datei wird auch ihre Wiederherstellungsposition vergessen. + + + + Remove + Entfernen + + + + Used when a file row has Missing QSL Sent set to Custom. Explicit ADIF values are kept. + Wird verwendet, wenn in einer Dateizeile Fehlendes QSL Sent auf Benutzerdefiniert gesetzt ist. Explizite ADIF-Werte bleiben erhalten. + + + + Custom QSL Sent Defaults + Benutzerdefinierte QSL-Sent-Standardwerte + + + + Paper QSL + Papier-QSL + + + + DCL + DCL + + + + Select the <b>Bandmap Guide</b> profile shown as visual frequency hints. It does not affect mode identification. + Wählen Sie das Profil <b>Bandmap-Guide</b>, das als visuelle Frequenzhilfe angezeigt wird. Es beeinflusst die Moduserkennung nicht. + + + + Manage + Verwalten + + + + Double-click cells to edit start/end frequency, enabled state, or SAT mode. Band names are fixed; new bands cannot be added here. + Doppelklick auf Zellen bearbeitet Start-/Endfrequenz, Aktivierungsstatus oder SAT-Modus. Bandnamen sind fest; neue Bänder können hier nicht hinzugefügt werden. + + + + QSO DXCC Status Colors + QSO-DXCC-Statusfarben + + + + Used for DX spots, Bandmap, WSJT-X and QSO status hints. Confirmed has no highlight by default. Click a color cell to choose a color or set No color. + Wird für DX-Spots, Bandmap, WSJT-X und QSO-Statushinweise verwendet. Bestätigt hat standardmäßig keine Hervorhebung. Klicken Sie auf eine Farbzelle, um eine Farbe zu wählen oder Keine Farbe festzulegen. + + + + Restore Defaults + Standardwerte wiederherstellen + + + + Shortcuts + Tastenkürzel + + + Danger Zone Gefahrenzone - + <b>⚠ This is a danger zone. Proceed with caution, as actions performed here cannot be undone and may have a significant impact on your log.</b> <b>⚠ Dies ist eine Gefahrenzone. Gehen Sie vorsichtig vor, da die ausgeführten Aktionen nicht rückgängig gemacht werden können und erhebliche Auswirkungen auf Ihr Log haben können.</b> - + Delete All QSOs Alle QSOs löschen - + Delete All Passwords from the Secure Store Alle Passwörter aus dem sicheren Speicher löschen @@ -12445,213 +13337,217 @@ Bitte installieren Sie Hamlib oder geben Sie den Pfad manuell an. Endpunkt - + Others Andere - + Status Confirmed By Bestätigt durch - + Paper Papier - + Chat - + <b>Security Notice:</b> QLog stores all passwords in the Secure Storage. Unfortunately, ON4KST uses a protocol where this password is sent over an unsecured channel as plaintext.</p><p>Please exercise caution when choosing your password for this service, as your password is sent over an unsecured channel in plaintext form.</p> <b>Sicherheitshinweis:</b> QLog speichert alle Passwörter im Secure Storage. Leider verwendet ON4KST ein Protokoll, bei dem dieses Passwort über einen ungesicherten Kanal im Klartext gesendet wird.</p><p>Bitte seien Sie vorsichtig, wenn Sie Ihr Passwort für diesen Dienst wählen, da Ihr Passwort über einen ungesicherten Kanal im Klartext gesendet wird.</p> - + The '>' character is interpreted as a marker for the initial cursor position in the Report column. <br/>Ex.: '5>9' means the cursor will be positioned on the second character Das Zeichen „>“ wird als Markierung für die anfängliche Cursorposition in der Spalte „Report“ interpretiert.<br/>Beispiel: „5>9“ bedeutet, dass sich der Cursor auf dem zweiten Zeichen befindet - + <p>List of IP addresses to which QLog forwards raw UDP WSJT-X packets.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Liste der IP-Adressen, an die QLog unbearbeitete UDP WSJT-X-Pakete weiterleitet.</p>Die IP-Adressen werden durch ein Leerzeichen getrennt und haben die Form IP:PORT - + Join Multicast Multicast verbinden - + Enable/Disable Multicast option for WSJTX Aktivieren/Deaktivieren der Multicast-Option für WSJTX - + Multicast Address Multicast Adresse - + Specify Multicast Address. <br>On some Linux systems it may be necessary to enable multicast on the loop-back network interface. Geben Sie die Multicast-Adresse an. <br>Auf einigen Linux-Systemen kann es erforderlich sein, Multicast auf der Loopback-Netzwerkschnittstelle zu aktivieren. - + TTL - + Time-To-Live determines the range<br> over which a multicast packet is propagated in your intranet. Time-To-Live bestimmt den Bereich<br>, über den ein Multicast-Paket in Ihrem Intranet verbreitet wird. - + Color CQ Spots CQ-Spots einfärben - + Enable/Disable sending color-coded status indicators back to WSJT-X for each callsign calling CQ Senden farbcodierter Statusindikatoren für jede CQ-rufende Station zurück an WSJT-X aktivieren/deaktivieren - + Notifications Benachrichtigungen - + DX Spots - + <p> List of IP addresses to which QLog sends UDP notification packets with DX Cluster Spots.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Liste der IP-Adressen, an die QLog UDP-Notification-Pakete mit DX Cluster Spots sendet.</p>Die IP-Adressen sind durch ein Leerzeichen getrennt und haben das Format IP:PORT - + QSO Changes QSO Änderungen - + <p> List of IP addresses to which QLog sends UDP notification packets about a new/updated/deleted QSO in the log.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Liste der IP-Adressen, an die QLog UDP-Benachrichtigungspakete über ein neues/aktualisiertes/gelöschtes QSO im Log sendet.</p>Die IP-Adressen sind durch ein Leerzeichen getrennt und haben das Format IP:PORT - + Wsjtx CQ Spots - + <p> List of IP addresses to which QLog sends UDP notification packets with WSJTX CQ Spots.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Liste der IP-Adressen, an die QLog UDP-Benachrichtigungspakete mit WSJTX CQ Spots sendet.</p>Die IP-Adressen sind durch ein Leerzeichen getrennt und haben das Format IP:PORT - + Rig Status Rig-Status - + <p> List of IP addresses to which QLog sends UDP notification packets when Rig State changes.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Liste der IP-Adressen, an die QLog UDP-Benachrichtigungspakete mit Rig-Status sendet.</p>Die IP-Adressen sind durch ein Leerzeichen getrennt und haben das Format IP:PORT - + GUI GUI - + Time Format Zeitformat - + 24-hour 24-Stunden - + AM/PM AM/PM - + Unit System Einheitensystem - + Metric Metrisch - + Imperial Imperial - + Date Format Datumsformat - + System System - + + + + Custom Benutzerdefiniert - + <a href="https://doc.qt.io/qt-6/qdate.html#fromString-1">Time Format Documentation</a> <a href="https://doc.qt.io/qt-6/qdate.html#fromString-1">Dokumentation des Zeitformats</a> - + Spot Alerts - + <p> List of IP addresses to which QLog sends UDP notification packets about user Spot Alerts.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Liste der IP-Adressen, an die QLog UDP-Benachrichtigungspakete über User Spot Alerts sendet.</p>Die IP-Adressen sind durch ein Leerzeichen getrennt und haben das Format IP:PORT - + LogID - + <p>Assigned LogID to the current log.</p>The LogID is sent in the Network Nofitication messages as a unique instance identified.<p> The ID is generated automatically and cannot be changed</> <p>Zugeordnete LogID für das aktuelle Protokoll.</p>Die LogID wird in den Netzwerk-Notifizierungsmeldungen als eindeutige Instanzkennung gesendet.<p>Die ID wird automatisch generiert und kann nicht geändert werden</> - - - - - - + + + + + + ex. 192.168.1.1:1234 192.168.2.1:1234 - + + eQSL @@ -12662,7 +13558,8 @@ Bitte installieren Sie Hamlib oder geben Sie den Pfad manuell an. - + + LoTW LoTW @@ -12673,18 +13570,18 @@ Bitte installieren Sie Hamlib oder geben Sie den Pfad manuell an. - - + + Network Netzwerk - + Wsjtx Wsjtx - + Port @@ -12704,18 +13601,18 @@ Bitte installieren Sie Hamlib oder geben Sie den Pfad manuell an. - + Bands Bänder - + Modes Betriebsarten - - + + DXCC @@ -12776,8 +13673,8 @@ Bitte installieren Sie Hamlib oder geben Sie den Pfad manuell an. - - + + HamQTH @@ -12786,7 +13683,7 @@ Bitte installieren Sie Hamlib oder geben Sie den Pfad manuell an. - + Username Benutzername @@ -12796,217 +13693,413 @@ Bitte installieren Sie Hamlib oder geben Sie den Pfad manuell an. - + Password Passwort - - + + Name Name - + Report Bericht - - + + State Staat - + Start (MHz) Beginn (MHz) - + End (MHz) Ende (MHz) - + SAT Mode SAT Betriebsart - - - + + + Disabled Inaktiv - + Dummy - + Morse Over CAT CW-über-CAT - + CWDaemon - + FLDigi - + Single Paddle - + IAMBIC A - + IAMBIC B - + Ultimate - + High High - + Low Low - + + Duplicate + Duplikate + + + + Already worked QSO + Bereits gearbeitetes QSO + + + + New Entity + Neuer Eintrag + + + + DXCC entity not worked yet + DXCC-Entity noch nicht gearbeitet + + + + New Band / Mode + Neues Band / Modus + + + + New band, mode, or band and mode + Neues Band, neuer Modus oder beides + + + + New Slot + Neuer Slot + + + + New band and mode combination + Neue Band-Modus-Kombination + + + + Worked + Gearbeitet + + + + Worked but not confirmed + Gearbeitet, aber nicht bestätigt + + + + Confirmed + Bestätigt + + + + Confirmed QSO; no highlight by default + Bestätigtes QSO; standardmäßig keine Hervorhebung + + + + Status + Status + + + + Color + Farbe + + + + Choose Color... + Farbe wählen... + + + + Default + Standard + + + + No Color + Keine Farbe + + + + Status Color + Statusfarbe + + + + No color + Keine Farbe + + + + No highlight. Click to choose a color or set no color. + Keine Hervorhebung. Klicken, um eine Farbe zu wählen oder keine Farbe festzulegen. + + + + Click to change color or set no color. + Klicken, um die Farbe zu ändern oder keine Farbe festzulegen. + + + Press <b>Modify</b> to confirm the profile changes or <b>Cancel</b>. Drücken Sie <b>Ändern</b>, um die Profiländerungen zu bestätigen oder <b>Abbrechen</b>. - - - - - - - - - - + + + + + + + + + + Must not be empty Darf nicht leer sein - + Auto Detect Automatische Erkennung - + TQSL was not found on this system. Please install TQSL or specify the path manually. TQSL wurde auf diesem System nicht gefunden. Bitte installieren Sie TQSL oder geben Sie den Pfad manuell an. - + Not found Nicht gefunden - + Rig sharing is only available for Hamlib driver Rig-Freigabe ist nur für den Hamlib-Treiber verfügbar - + Rig sharing is not available for network connection Rig-Freigabe ist für Netzwerkverbindungen nicht verfügbar - + + Off + Aus + + + Delete Passwords Passwörter löschen - + All passwords have been deleted Alle Passwörter wurden gelöscht - + Deleting all QSOs... Alle QSOs werden gelöscht... - + Error Fehler - + Failed to delete all QSOs. Alle QSOs konnten nicht gelöscht werden. - + + Enabled + Aktiv + + + + Path + Weg + + + + Station Profile + Stationsprofil + + + + Missing QSL Sent + Fehlendes QSL Sent + + + + Last Recovery + Letzte Wiederherstellung + + + + + + Queued + Wartend + + + + + + + Ignored + Ignoriert + + + + + + + Requested + Angefordert + + + + + + + Yes + Ja + + + + Station Profile does not exist. Select another profile and enable this row again. + Stationsprofil existiert nicht. Wählen Sie ein anderes Profil und aktivieren Sie diese Zeile erneut. + + + + File exists + Datei existiert + + + + File does not exist + Datei existiert nicht + + + + Startup ADI initialized + Startup-ADI initialisiert + + + + Select ADIF File + ADIF-Datei auswählen + + + + ADIF Files (*.adi *.adif);;All Files (*) + ADIF-Dateien (*.adi *.adif);;Alle Dateien (*) + + + members Mitglieder - + Required internet connection during application start Erfordert Internetverbindung beim Start der Anwendung - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + Modify Ändern - - + + Special - Omnirig Special - Omnirig - + Cannot be changed Kann nicht geändert werden - + WinKey WinKey - + Select File Datei auswählen @@ -13064,7 +14157,7 @@ Bitte installieren Sie TQSL oder geben Sie den Pfad manuell an. StatisticsWidget - + Statistics Statistik @@ -13155,151 +14248,150 @@ Bitte installieren Sie TQSL oder geben Sie den Pfad manuell an. - + Band Band - + Confirmed - + Not Confirmed - + Year Jahr - + Month Monat - + Day in Week Wochentag - + Hour Stunde - + Mode Betriebsart - + Continent Kontinent - + Propagation Mode Ausbreitungsmodus - + Confirmed / Not Confirmed Bestätigt / nicht Bestätigt - + Countries Länder - + Big Gridsquares Grossfelder - + Distance Entfernung - + QSOs - + Confirmed/Worked Grids Bestätigte / gearbeitete Gitterfelder - + ODX ODX - + Sun Son - + Mon Mon - + Tue Die - + Wed Mit - + Thu Don - + Fri Fre - + Sat Sa - - - + + + Not specified Nicht angegeben - + No User Filter Kein Benutzerfilter - + Over 50000 QSOs. Display them? Über 50000 QSOs. Anzeigen? - - + Rendering QSOs... Rendern der QSOs… - + All Alle @@ -13353,17 +14445,17 @@ Bitte installieren Sie TQSL oder geben Sie den Pfad manuell an. ToAllTableModel - + Time Zeit - + Spotter - + Message Nachricht @@ -13636,27 +14728,27 @@ Bitte installieren Sie TQSL oder geben Sie den Pfad manuell an. UserListModel - + Callsign Rufzeichen - + Gridsquare Gitterfeld - + Distance Entfernung - + Azimuth - + Comment Kommentar @@ -13664,47 +14756,47 @@ Bitte installieren Sie TQSL oder geben Sie den Pfad manuell an. WCYTableModel - + Time Zeit - + K - + expK - + A - + R - + SFI - + SA - + GMF - + Au @@ -13712,27 +14804,27 @@ Bitte installieren Sie TQSL oder geben Sie den Pfad manuell an. WWVTableModel - + Time Zeit - + SFI - + A - + K - + Info @@ -13858,37 +14950,37 @@ Bitte installieren Sie TQSL oder geben Sie den Pfad manuell an. WsjtxTableModel - + SNR SNR - + Callsign Rufzeichen - + Gridsquare Gitterfeld - + Distance Entfernung - + Last Activity Letzte Aktivität - + Last Message Letzte Nachricht - + Member Mitglied @@ -13929,47 +15021,47 @@ Bitte installieren Sie TQSL oder geben Sie den Pfad manuell an. main - + Run with the specific namespace. Im spezifischen Namensraum ausführen. - + namespace Namensraum - + Translation file - absolute or relative path and QM file name. Übersetzungsdatei - Absoluter oder relativer Pfad und Name der QM-Datei. - + path/QM-filename path/QM-filename - + Set language. <code> example: 'en' or 'en_US'. Ignore environment setting. Sprache einstellen. Beispiel <code>: 'en' oder 'en_US'. Ignoriert Betriebssystemeinstellungen. - + code code - + Writes debug messages to the debug file Schreibt Debugmeldungen in die Debugdatei - + Process pending database import (internal use) Ausstehenden Datenbankimport verarbeiten (interne Verwendung) - + Force update of all value lists (DXCC, SATs, etc.) Aktualisierung aller Wertelisten erzwingen (DXCC, SATs usw.) diff --git a/i18n/qlog_es.qm b/i18n/qlog_es.qm index 907acaf0..3a80dcae 100644 Binary files a/i18n/qlog_es.qm and b/i18n/qlog_es.qm differ diff --git a/i18n/qlog_es.ts b/i18n/qlog_es.ts index 49030cd0..955cb640 100644 --- a/i18n/qlog_es.ts +++ b/i18n/qlog_es.ts @@ -199,20 +199,117 @@ + Bandmap Guide + Ayuda de Bandmap + + + + Guide + + + + Fields Campos - + Must not be empty No debe estar vacío - + + Leave unchanged + Dejar sin cambios + + + + Off + Desactivado + + + Unsaved Sin guardar + + AdifRecoveryManager + + + Startup ADI found more than %1 new QSOs in %2. Use the standard Import. Load point was moved to the end of the file. + El ADI de inicio contiene más de %1 QSO nuevos en %2. Use la importación estándar. El punto de carga se movió al final del archivo. + + + + Startup ADI Station Profile does not exist: %1 + El perfil de estación de Startup ADI no existe: %1 + + + + Cannot open Startup ADI records from %1 + No se pueden abrir los registros de Startup ADI desde %1 + + + + Startup ADI from %1 finished with %n error(s); load point was not advanced. + + Startup ADI desde %1 terminó con %n error(es); el punto de carga no se avanzó. + + + + + + Startup ADI was disabled for %n file(s) because the assigned Station Profile no longer exists. + + Startup ADI se desactivó para %n archivo(s) porque el perfil de estación asignado ya no existe. + + + + + + AdifRecoveryReaderWorker + + + Startup ADI filename is empty + El nombre de archivo de Startup ADI está vacío + + + + Startup ADI file does not exist: %1 + El archivo Startup ADI no existe: %1 + + + + Startup ADI initialized at the end of file + Startup ADI inicializado al final del archivo + + + + Startup ADI file was reset; load point moved to the end + El archivo Startup ADI se reinició; punto de carga movido al final + + + + Cannot open Startup ADI file: %1 + No se puede abrir el archivo Startup ADI: %1 + + + + Cannot seek Startup ADI file: %1 + No se puede posicionar en el archivo Startup ADI: %1 + + + + Cannot read Startup ADI file: %1 + No se puede leer el archivo Startup ADI: %1 + + + + Too many ADIF records for automatic recovery + Demasiados registros ADIF para la recuperación automática + + AlertRuleDetail @@ -495,42 +592,42 @@ AlertTableModel - + Rule Name Nombre Regla - + Callsign Indicativo - + Frequency Frecuencia - + Mode Modo - + Updated Actualizado - + Last Update Última Actualización - + Last Comment Último Comentario - + Member Miembro @@ -584,12 +681,6 @@ Diplomas Diplomas - - - Options - Opciones - Opciones - Award @@ -597,72 +688,82 @@ Diploma - + + 🌐 Rules + 🌐 Reglas + + + My DXCC Entity Mi entidad DXCC Mi Entidad DXCC - + User Filter Filtro de usuario - + Confirmed by Confirmado por Confirmado por - + LoTW LoTW - + eQSL eQSL - + Paper Papel Papel - + Mode Modo Modo - + CW CW - + Phone Fonía - + Digi Digi - + Not-Worked Only Sólo no trabajado Sólo no trabajado - + Not-Confirmed Only No confirmado - + + Double-click a row/cell to show QSOs + Doble clic en fila/celda para mostrar QSO + + + Show Mostrar Mostrar @@ -678,7 +779,7 @@ ITU - + WAC WAC @@ -747,85 +848,89 @@ - + US Counties Condados de EE. UU - + Russian Districts Distritos de Rusia - + Japanese Cities/Ku/Guns Ciudades japonesas / Ku / Gun - + NZ Counties Condados de Nueva Zelanda - + Spanish DMEs DMEs de España - + Ukrainian Districts Distritos de Ucrania - + No User Filter Sin filtro de usuario - + DELETED Eliminado - + North America América del Norte América del Norte - + South America América del Sur América del Sur - + Europe Europa Europa - + Africa África África - + Oceania Oceanía Oceanía - + Asia Asia - - Antarctica - Antártida - Antártida + + WAAC + + + + + WAIP + @@ -851,6 +956,198 @@ Esperando + + BandmapGuideDialog + + + Bandmap Guide + Ayuda de Bandmap + + + + Import guide + Importar guía + + + + Import + Importar + + + + Export guide + Exportar guía + + + + Export + Exportar + + + + New guide + Nueva guía + + + + New + Nuevo + + + + Copy guide + Copiar guía + + + + Copy + Copiar + + + + Delete guide + Eliminar guía + + + + Delete + + + + + Guide Name: + Nombre de guía: + + + + Ranges: + Rangos: + + + + From + Von + + + + To + Hasta + + + + Color + Color + + + + Label + Etiqueta + + + + Add range + Añadir rango + + + + Add + + + + + Remove selected range + Eliminar rango seleccionado + + + + Remove + + + + + + MHz + MHz + + + + + New Guide + Nueva guía + + + + Copy - %1 + Copia – %1 + + + + Delete Guide + Eliminar guía + + + + Delete guide '%1'? + ¿Eliminar guía «%1»? + + + + Import Guide + Importar guía + + + + QLog Bandmap Guide (*.qbg);;JSON (*.json) + Guía de Bandmap QLog (*.qbg);;JSON (*.json) + + + + Import Failed + Importación fallida + + + + Export Guide + Exportar guía + + + + QLog Bandmap Guide (*.qbg) + Guía de Bandmap QLog (*.qbg) + + + + Export Failed + Exportación fallida + + + + Guide Color + Color de guía + + + + + + QLog Warning + Alerta de QLog + + + + Guide name cannot be empty. + El nombre de guía no puede estar vacío. + + + + Guide name '%1' is already used. + El nombre de guía «%1» ya está en uso. + + + + Guide '%1' contains an invalid range. + La guía «%1» contiene un rango no válido. + + BandmapWidget @@ -889,30 +1186,60 @@ min(s) - + Bandmap Mapa de Banda - + Show Band Mostrar Banda - + Center RX Centrar RX - + Show Emergency Frequencies Mostrar frecuencias de emergencia - + + Show IBP Frequencies + Mostrar frecuencias IBP + + + + Show Guide + Mostrar guía + + + + Off + Desactivado + + + + No Guide + Sin guía + + + + Edit Guide... + Editar guía... + + + SOS + + + IBP + + CWCatKey @@ -1189,27 +1516,27 @@ CWKeyer - + No CW Keyer Profile selected No Hay Perfil de Manipulador Seleccionado - + Initialization Error Error al Inicializar - + Internal Error Error Interno - + Connection Error Error de Conexión - + Cannot open the Keyer connection No se puede conectar al Manipulador @@ -1839,198 +2166,198 @@ Importar - + Export template Exportar plantilla - + Export Exportar - + New template Nueva plantilla - + New Nuevo - + Copy existing template Copiar plantilla existente - + Copy Copiar - + Delete template Eliminar plantilla - + Delete - + Template Name: Nombre de la plantilla: - + Contest Name: Nombre del concurso: - + Default Mode: Modo predeterminado: - + QSO Line Columns: Columnas de línea QSO: - + Contest name as required by the rules. It is possible to enter a custom string if it is not included in the list. Nombre del concurso según las reglas. Es posible introducir una cadena personalizada si no está incluida en la lista. - + Seq. N.º - + QSO Field Campo QSO - + Formatter Formateador - + Width Ancho - + Label Etiqueta - + Add line Añadir línea - + Add - + Remove selected line Eliminar línea seleccionada - + Remove - + New Template Nueva plantilla - + Copy - %1 Copia – %1 - + Delete Template Eliminar plantilla - + Delete template '%1'? ¿Eliminar la plantilla «%1»? - + Import Template Importar plantilla - - + + QLog Cabrillo Template (*.qct) Plantilla Cabrillo de QLog (*.qct) - + Import Failed Importación fallida - + Export Template Exportar plantilla - + Export Failed Exportación fallida - + Failed to write file: %1 No se pudo escribir el archivo: %1 - + File not found: %1 Archivo no encontrado: %1 - + Cannot open file: %1 No se puede abrir el archivo: %1 - + Invalid template file: missing name Archivo de plantilla inválido: falta el nombre - + QLog Error Error de QLog - + Cannot start database transaction. No se puede iniciar la transacción de base de datos. - + QLog Warning Alerta de QLog - + Cannot save template '%1': %2 No se puede guardar la plantilla «%1»: %2 @@ -2097,20 +2424,24 @@ Reloj - - + + Sunrise Amanecer - - + + Sunset Ocaso - - + + + + + + N/A S/D @@ -2174,7 +2505,7 @@ Otros - + Done Hecho @@ -2182,12 +2513,12 @@ ColumnSettingGenericDialog - + Unselect All Seleccionar Nada - + Select All Seleccionar Todo @@ -2200,7 +2531,7 @@ Ajustes de Visibilidad de Columnas - + Done Hecho @@ -4250,70 +4581,60 @@ Data - + New Entity Nueva Entidad - + New Band Nueva Banda - + New Mode Nuevo Modo - + New Band&Mode Nueva Banda y Modo - + New Slot Nuevo Slot - + Confirmed Confirmado - + Worked Trabajado - + Hz Hz - + kHz kHz - + GHz GHz - + MHz MHz - - - - - - - - Yes - - @@ -4321,136 +4642,146 @@ - No - No + Yes + + + + + + No + No + + + + Requested Solicitado - + Queued En Cola - - - + + + Invalid Inválido - + Bureau Bureau - + Direct Directa - + Electronic Electrónica - - - - - - - - + + + + + + + + Blank Vacío - + Modified Modificado - + Grayline Línea Gris - + Other Otro - + Short Path Paso Corto - + Long Path Paso Largo - + Not Heard No Escuchado - + Uncertain Dudoso - + Straight Key - + Sideswiper - + Mechanical semi-automatic keyer or Bug - + Mechanical fully-automatic keyer or Bug - + Single Paddle Pala Simple - + Dual Paddle - + Computer Driven - + Confirmed (AG) Confirmado (AG) - + Confirmed (no AG) Confirmado (non-AG) - + Unknown Desconocido @@ -5097,57 +5428,57 @@ Example: DxTableModel - + Time Hora - + Callsign Indicativo - + Frequency Frecuencia - + Mode Modo - + Spotter Anunciante - + Comment Comentario - + Continent Continente - + Spotter Continent Continente del Anunciante - + Band Banda - + Member Miembro - + Country País @@ -5161,7 +5492,7 @@ Example: - + Connect Conectar @@ -5326,67 +5657,67 @@ Example: DXC - Búsqueda - + My Continent Mi continente - + Auto Automático - + Connecting... Conectando... - + DX Cluster is temporarily unavailable El Cluster DX no está disponible temporalmente - + DXC Server Error Error del Servidor del Cluster DX - + An invalid callsign Un indicativo inválido - + DX Cluster Password Contraseña del Cluster DX - + Security Notice Aviso de Seguridad - + The password can be sent via an unsecured channel La contraseña se puede enviar a través de un canal no seguro - + Server Servidor - + Username Nombre de Usuario - + Disconnect Desconectar - + DX Cluster Command Comando de DX Cluster @@ -5394,22 +5725,22 @@ Example: DxccTableModel - + Worked Trabajado - + eQSL eQSL - + LoTW LoTW - + Paper Papel @@ -5525,7 +5856,7 @@ Example: - + POTA POTA @@ -5705,42 +6036,42 @@ Example: No se pueden marcar los QSOs exportados como enviados - + Generic Genérico - + QSLs QSLs - + All Todas - + Minimal Mínimo - + QSL-specific Específico de QSL - + Custom 1 Personalizado 1 - + Custom 2 Personalizado 2 - + Custom 3 Personalizado 3 @@ -5875,132 +6206,132 @@ Esta contraseña será necesaria más adelante para restaurarlas. No se puede establecer auto_power_on - + Cannot set no_xchg to 1 No se puede establecer no_xchg en 1 - + Rig Open Error Conexión fallida - + Set TX Frequency Error Error al establecer la frecuencia TX - + Set Frequency Error Error al Establecer Frecuencia - + Set Split Error Error al establecer split - + Set Mode Error Error al establecer el modo - + Set Split Mode Error Error al establecer el modo split - + Set PTT Error Error al Establecer PTT - + Cannot sent Morse This cannot be displayed No se puede enviar Morse - + Cannot stop Morse This cannot be displayed No se puede detener Morse - + Get PTT Error No se puede visualizar Error al obtener PTT - + Get Frequency Error Error al Obtener Frecuencia - + Get Mode Error Error al Obtener Modo - + Get VFO Error Error al obtener el VFO - + Get PWR Error No se puede visualizar Error al obtener PWR - + Get PWR (power2mw) Error No se puede visualizar Error al obtener PWR (power2mw) - + Get RIT Function Error No se puede visualizar Error al obtenerfunción de RIT - + Get RIT Error No se puede visualizar Error al obtener RIT - + Get XIT Function Error No se puede visualizar Error al obtener función XIT - + Get XIT Error No se puede visualizar Error al obtener XIT - + Get Split Error - + Get TX Frequency Error - + Get KeySpeed Error No se puede visualizar Error al obtener KeySpeed - + Set KeySpeed Error No se puede visualizar Configurar error de KeySpeed @@ -6038,140 +6369,260 @@ Esta contraseña será necesaria más adelante para restaurarlas. ImportDialog - + Import Importar - + Date Range Rango de Fechas - + Import all or only QSOs from the given period Importar todos o sólo los QSOs del período determinado - + All Todos - + File Archivo - + ADX - + Browse Navegar - + Options Opciones - - + + The value is used when an input record does not contain the ADIF value El valor se utiliza cuando un registro de entrada no contiene el valor ADIF - + Defaults Valores predeterminados - + + Values are used only for fields that are missing in the import file. Existing values are preserved. + Los valores se usan solo para los campos que faltan en el archivo de importación. Los valores existentes se conservan. + + + + <p>⚠ Missing QSL Sent fields are set to <b>"N"</b> (do not send) by default in ADIF. + <p>⚠ Los campos QSL Sent faltantes se establecen en <b>"N"</b> (no enviar) por defecto en ADIF. + + + My Profile Mi Perfil - + My Rig Mi Radio - - + + Comment Comentario - + + Used only for missing QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT, and DCL_QSL_SENT fields where default is "N"; otherwise, the value from the input is used. + Se usa solo para los campos QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT y DCL_QSL_SENT faltantes, donde el valor predeterminado es «N»; de lo contrario, se usa el valor de la entrada. + + + + QSL Sent status + + + + + Used only for missing QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT, and DCL_QSL_SENT fields where default is "N"; otherwise, the value from the input is used.<p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Se usa solo para los campos QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT y DCL_QSL_SENT faltantes, donde el valor predeterminado es «N»; de lo contrario, se usa el valor de la entrada.<p><b>En cola</b> (listo), <b>No</b> (no enviar), <b>Ignorar</b> (no seguir), <b>Solicitado</b> (solicitado), <b>Sí</b> (ya enviado). + + + + Used only when the imported ADIF record does not contain the selected field. Explicit ADIF values are kept. + Se usa solo cuando el registro ADIF importado no contiene el campo seleccionado. Los valores ADIF explícitos se conservan. + + + + Default value for missing DCL_QSL_SENT. <p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Valor predeterminado para DCL_QSL_SENT faltante. <p><b>En cola</b> (listo), <b>No</b> (no enviar), <b>Ignorar</b> (no seguir), <b>Solicitado</b> (solicitado), <b>Sí</b> (ya enviado). + + + + Default value for missing EQSL_QSL_SENT. <p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Valor predeterminado para EQSL_QSL_SENT faltante. <p><b>En cola</b> (listo), <b>No</b> (no enviar), <b>Ignorar</b> (no seguir), <b>Solicitado</b> (solicitado), <b>Sí</b> (ya enviado). + + + + Default value for missing LOTW_QSL_SENT. <p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Valor predeterminado para LOTW_QSL_SENT faltante. <p><b>En cola</b> (listo), <b>No</b> (no enviar), <b>Ignorar</b> (no seguir), <b>Solicitado</b> (solicitado), <b>Sí</b> (ya enviado). + + + + LoTW + LoTW + + + + DCL + DCL + + + + Paper QSL + QSL en papel + + + + eQSL + eQSL + + + + Default value for missing QSL_SENT.<p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Valor predeterminado para QSL_SENT faltante.<p><b>En cola</b> (listo), <b>No</b> (no enviar), <b>Ignorar</b> (no seguir), <b>Solicitado</b> (solicitado), <b>Sí</b> (ya enviado). + + + If DXCC is missing in the imported record, it will be resolved from the callsign. Si falta el DXCC en el registro importado, se resolverá a partir del indicativo. - + Fill missing DXCC Entity Information Completar la información de entidad DXCC faltante + + + Queued (ready to send) + En cola (listo para enviar) + + + + Ignored (do not track) + Ignorado (no seguir) + + + + Requested (requested again) + Solicitado (solicitado de nuevo) + + + + Yes (already sent) + Sí (ya enviado) + + + + Custom... + Personalizado... + + + + Queued + En Cola + + + + Requested + Solicitado + + Ignored + Ignorado + + + + No + No + + + + Yes + + + + &Import &Importar - + Select File Seleccionar Archivo - - + + The values below will be used when an input record does not contain the ADIF values Los valores siguientes se utilizarán cuando un registro de entrada no contenga los valores ADIF - + <p><b>In-Log QSO:</b></p><p> <p><b>QSO en Log:</b></p><p> - + <p><b>Importing:</b></p><p> <p><b>Importando:</b></p><p> - + Duplicate QSO QSO Duplicado - + <p>Do you want to import duplicate QSO?</p>%1 %2 <p>¿Quiere importar QSO duplicado?</p>%1 %2 - + Save to File Guardar al Archivo - + QLog Import Summary Resumen de Importación de QLog - + Import date Fecha de Importación - + Imported file Archivo Importado - + Imported: %n contact(s) Importado: %n contacto/s @@ -6179,7 +6630,7 @@ Esta contraseña será necesaria más adelante para restaurarlas. - + Warning(s): %n Alerta/s: %n @@ -6187,7 +6638,7 @@ Esta contraseña será necesaria más adelante para restaurarlas. - + Error(s): %n Error/s: %n @@ -6195,17 +6646,17 @@ Esta contraseña será necesaria más adelante para restaurarlas. - + Details Detalles - + Import Result Resultado de Importación - + Save Details... Guardar Detalles... @@ -6632,18 +7083,53 @@ Esta contraseña será necesaria más adelante para restaurarlas. Importado - - + + missing QSO_DATE + falta QSO_DATE + + + + missing CREDIT_GRANTED + falta CREDIT_GRANTED + + + + missing CALL/DXCC + falta CALL/DXCC + + + + no matching QSO + sin QSO coincidente + + + + cannot update QSO %1: %2 + no se puede actualizar el QSO %1: %2 + + + + matched QSO: + QSO coincidente: + + + + credit_granted: + + + + + DXCC State: Estado DXCC: - + Error Error - + Warning Alerta @@ -6651,945 +7137,956 @@ Esta contraseña será necesaria más adelante para restaurarlas. LogbookModel - + QSO ID ID QSO - + Time on Hora inicio - + Time off Hora fin - + Call Indicativo - + RSTs RSTe - + RSTr RSTr - + Frequency Frecuencia - - + + Band Banda - - + + Mode Modo - + Submode Submodo - + Name (ASCII) Nombre (ASCII) - + QTH (ASCII) QTH (ASCII) - - + + Gridsquare Locator - + DXCC DXCC - + Country (ASCII) País (ASCII) - + Continent Continente - + CQZ CQZ - + ITU ITU - + Prefix Prefijo - + State Estado - + County Condado - + IOTA IOTA - + QSLr QSLr - + QSLr Date Fecha QSLr - + QSLs QSLe - + QSLs Date Fecha QSLe - + LoTWr LoTWr - + LoTWr Date Fecha LoTWr - + LoTWs LoTWe - + LoTWs Date Fecha LoTWe - + TX PWR Potencia TX - + Additional Fields Campos Adicionales - + Address (ASCII) Dirección (ASCII) - + Address Dirección - + Age Edad - + Altitude Altitud - + A-Index A-Index - + Antenna Az Antena Az - + Antenna El Antena El - + Signal Path Paso de Antena - + ARRL Section Sección ARRL - + Award Submitted Diploma Presentado - + Award Granted Diploma Otorgado - + Band RX Banda RX - + Gridsquare Extended Locator Extendido - + Contest Check Prueba de Concurso - + Class Categoría - + ClubLog Upload Date Fecha de carga en ClubLog - + ClubLog Upload State Estado de carga en ClubLog - + Comment (ASCII) Comentario (ASCII) - - + + Comment Comentario - + Contacted Operator Operador Contactado - + Contest ID ID del Concurso - - + + Country País - + + Mode/Submode + + + + + Mode: %1 +Submode: %2 + + + + County Alt Condado Alt - + Credit Submitted Crédito Recibido - + Credit Granted Crédito Otorgado - + DOK DOK - + DCLr Date Fecha DCLr - + DCLs Date Fecha DCLe - + DCLr DCLr - + DCLs DCLe - + Distance Distancia - + Email EMail - - + + Owner Callsign Indicativo del Propietario - + eQSL AG eQSL AG - + eQSLr Date Fecha eQSLr - + eQSLs Date Fecha eQSLe - + eQSLr eQSLr - + eQSLs eQSLe - + FISTS Number Número FISTS - + FISTS CC CC FISTS - + EME Init Inicio EME - + Frequency RX Frecuencia RX - + Guest Operator Operador Invitado - + HamlogEU Upload Date Fecha de carga en HamlogEU - + HamlogEU Upload Status Estado de carga en HamlogEU - + HamQTH Upload Date Fecha de carga en HamQTH - + HamQTH Upload Status Estado de carga en HamQTH - + HRDLog Upload Date Fecha de carga en HRDLog - + HRDLog Upload Status Estado de carga en HRDLog - + IOTA Island ID ID IOTA de la Isla - + K-Index K-Index - + Latitude Latitud - + Longitude Longitud - + Max Bursts Ráfagas Máximas - + MS Shower Name Nombre Lluvia Meteroros - + My Altitude Mi Altitud - + My Antenna (ASCII) Mi Antena (ASCII) - + My Antenna Mi Antena - + My City (ASCII) Mi Ciudad (ASCII) - + My City Mi Ciudad - + My County Mi Condado - + My County Alt Mi Condado Alt - + My Country (ASCII) Mi Condado (ASCII) - + My Country Mi País - + My CQZ Mi Zona CQ - + My DARC DOK Mi DARC DOK - + My DXCC Mi DXCC - + My FISTS Mi FISTS - + My Gridsquare Mi Locator - + My Gridsquare Extended Mi Locator Extendido - + My IOTA Mi IOTA - + My IOTA Island ID Mi ID IOTA de la isla - + My ITU MI ITU - + My Latitude Mi Latitud - + My Longitude Mi Longitud - + My Name (ASCII) Mi Nombre (ASCII) - + My Name Mi Nombre - + My Postal Code (ASCII) Mi Código Postal (ASCII) - + My Postal Code Mi Código Postal - + My POTA Ref Mi Ref POTA - + My Rig (ASCII) Mi Radio (ASCII) - + My Rig Mi Radio - + My Special Interest Activity (ASCII) Mi Actividad de Interés Especial (ASCII) - + My Special Interest Activity Mi Actividad de Interés Especial - + My Spec. Interes Activity Info (ASCII) Mi Info de Actividad de Interés Especial (ASCII) - + My Spec. Interest Activity Info Mi Info de Actividad de Interés Especial - + My SOTA Mi SOTA - + My State Mi Estado - - + + My Street Mi Calle - + My USA-CA Counties Mi Condado USA-CA - + My VUCC Grids Mi Locator VUCC - + Name Nombre - + Notes (ASCII) Notas (ASCII) - + QRZ Download Date Fecha de descarga de QRZ - + QRZ Download Status Estado de la descarga QRZ - + QSLs Message (ASCII) - + QSLs Message Mensaje del QSL - + QSLr Message - + RcvPWR Potencia recibida - + RcvNr Número recibido - + RcvExch Intercambio recibido - + SentNr Número enviado - + SentExch Intercambio enviado - - + + Notes Notas - + #MS Bursts - + #MS Pings - + POTA POTA - + Contest Precedence Precedencia del Concurso - + Propagation Mode Modo de Propagación - + Public Encryption Key Clave de Cifrado Pública - + QRZ Upload Date Fecha de carga en QRZ - + QRZ Upload Status Estado de carga en QRZ - + QSL Message Mensaje del QSL - + CW Key Info - + CW Key Type - + My CW Key Info - + My CW Key Type - + Operator Callsign Indicativo del operador - + QSLr Via QSLr Vía - + QSLs Via QSLe Vía - + QSL Via QSL vía - + QSO Completed QSO Completado - + QSO Random QSO Aleatorio - + QTH QTH - + Region Región - + Rig (ASCII) Radio (ASCII) - + Rig Equipo - + SAT Mode Modo del Satélite - + SAT Name Nombre del Satélite - + Solar Flux Flujo Solar - + SIG (ASCII) - + SIG - + SIG Info (ASCII) - + SIG Info - + Silent Key - + SKCC Member Miembro SKCC - + SOTA SOTA - + Logging Station Callsign Indicativo de la Estación - + SWL SWL - + Ten-Ten Number Número Ten-Ten - + UKSMG Member Miembro UKSMG - + USA-CA Counties Condado USA-CA - + VE Prov Provincia VE - + VUCC VUCC - + Web Web - + My ARRL Section Mi Sección ARRL - + My WWFF Mi WWFF - + WWFF WWFF - + RST Sent RST Enviadas - + RST Rcvd RST Recibidas - + Paper Papel - + LoTW LoTW - + eQSL eQSL - + QSL Received QSL Recibida - + QSL Sent QSL Enviada @@ -7598,8 +8095,8 @@ Esta contraseña será necesaria más adelante para restaurarlas. LogbookWidget - - + + Delete Eliminar @@ -7687,73 +8184,73 @@ Esta contraseña será necesaria más adelante para restaurarlas. - + Callsign Indicativo - + Gridsquare - + POTA POTA - + SOTA SOTA - + WWFF WWFF - + SIG SIG - + IOTA IOTA - + Delete the selected contacts? ¿Eliminar los conactos seleccionados? - + Clublog's <b>Immediately Send</b> supports only one-by-one deletion<br><br>Do you want to continue despite the fact<br>that the DELETE operation will not be sent to Clublog? - + Deleting QSOs Borrando QSOs - + Update Actualizar - + By updating, all selected rows will be affected.<br>The value currently edited in the column will be applied to all selected rows.<br><br>Do you want to edit them? Al actualizar, todas las filas seleccionadas se verán afectadas.<br>El valor actualmente editado en la columna se aplicará a todas las filas seleccionadas.<br><br>¿Quieres editarlas? - + Count: %n QSO: %n @@ -7761,89 +8258,124 @@ Esta contraseña será necesaria más adelante para restaurarlas. - + Downloading eQSL Image Descargando Imágen de eQSL - - - + + + Cancel Cancelar - + All Bands Todas las bandas - + All Modes Todos los modos - + All Countries Todos los países - + No User Filter Sin filtro de usuario - + QLog Warning Alerta de QLog - + Each batch supports up to 100 QSOs. Cada lote admite hasta 100 QSO. - + QSOs Update Progress Actualización de QSOs Progreso - - - + + + QLog Error Error de QLog - + Callbook login failed Error al iniciar sesión en el Callbook - + Callbook error: Error del Libro de Guardia: - + All Clubs Todos los clubes - + eQSL Download Image failed: La descarga de imágen de eQSL ha fallado: + + LotwDXCCCreditDownloader + + + Cannot open test LoTW DXCC credit file + No se puede abrir el archivo de prueba de créditos DXCC de LoTW + + + + + Incomplete LoTW DXCC credit response + Respuesta incompleta de créditos DXCC de LoTW + + + + + Cannot open temporary file + No se puede abrir el archivo temporal + + + + LoTW is not configured properly + LoTW no está configurado correctamente + + + + LoTW returned a non-ADIF response + LoTW devolvió una respuesta no ADIF + + + + Incorrect login or password + Usuario o contraseña incorrectos + + LotwQSLDownloader - + Cannot open temporary file No se puede abrir el archivo temporal - + Incorrect login or password Usuario o contraseña incorrectos @@ -7851,73 +8383,73 @@ Esta contraseña será necesaria más adelante para restaurarlas. LotwUploader - + Upload cancelled by user Carga cancelada por el usuario - + Upload rejected by LoTW Carga rechazada por LoTW - + Unexpected response from TQSL server Respuesta inesperada del servidor TQSL - + TQSL utility error Error de TQSL - + TQSLlib error Error de TQSLlib - + Unable to open input file No se puede abrir el archivo de entrante - + Unable to open output file No se puede abrir el archivo saliente - + All QSOs were duplicates or out of date range Todos los QSOs estaban duplicados o fuera de rango - + Some QSOs were duplicates or out of date range Algunos QSOs estaban duplicados o fuera de rango - + Command syntax error Error de sintaxis - + LoTW Connection error (no network or LoTW is unreachable) Error de conexión a LoTW (no hay red o LoTW es inaccesible) - - + + Unexpected Error from TQSL Error inesperado de TQSL - + TQSL not found TQSL no encontrado - + TQSL crashed TQSL falló @@ -7955,620 +8487,684 @@ Esta contraseña será necesaria más adelante para restaurarlas. &Servicios - + Toolbar Barra de herramientas - - + + Clock Reloj - - + + Map Mapa - - + + DX Cluster Cluster DX - + WSJTX WSJTX - - + + Rotator Rotor - - + + Bandmap Mapa de Banda - - + + Rig Radio - - + + Online Map Mapa en línea - - + + CW Console Consola CW - - + + Chat Chat - - + + Profile Image Imágen de Perfil - - + + Alerts Alertas - + &Settings &Ajustes - + &Import &Importar - + &Export &Exportar - + Connect R&ig Conectar &Radio - + &About &Acerca de - + + Print QS&L + Imprimir QS&L + + + Upload Subir - + Service - Upload QSOs Servicios - Subir QSOs - + Download QSLs Descargar QSLs - + Service - Download QSLs Servicios - Descargar QSLs - + Quit Salir - + Application - Quit Aplicación - Salir - - + + New QSO - Clear Nuevo QSO - Limpiar - - + + New QSO - Save Nuevo QSO - Guardar - + S&tatistics Es&tadísticas - + Wsjtx Wsjtx - + Connect R&otator Conectar R&otor - + QSO &Filters &Filtrar QSO - + &Awards &Diplomas - + Edit Rules Editar Reglas - + Clear Limpiar - + Show Alerts Mostrar Alertas - + Beep Campana - - + + Contest Concurso - + Dupe Check Comprobación de duplicados - + Sequence Secuencia - + Linking Exchange With Intercambio de enlaces con - - - - - - - + + + + + + + Pack Data && Settings Empaquetar datos y configuración - - + + Unpack Data && Settings Desempaquetar datos y configuración - + QSL &Gallery &Galería QSL - + Developer Tools Herramientas dev - + Run custom read-only SQL queries against the logbook database Ejecutar consultas SQL personalizadas de solo lectura sobre la base de datos del log - - Print QSL &Labels - &Imprimir etiquetas QSL - - - + DXCC &Submission List &Lista de envío DXCC - + Generate a list of contacts to submit for ARRL DXCC award credit Generar una lista de contactos para presentar para el crédito del premio ARRL DXCC - + Connect &CW Keyer Conectar &Manipulador - + &Wiki &Wiki - + Report &Bug... Reportar &Bug... - + &Manual Entry Entrada &Manual - + Switch New Contact dialog to the manually entry mode<br/>(time, freq, profiles etc. are not taken from their common sources) Cambia la pantalla de Nuevo Contacto a Entrada Manual<br/>(hora, frec, perfiles etc. no se tomarán de las fuentes habituales) - + Mailing List... Lista de Correo... - + Edit Editar - - + + Save Arrangement Guardar Disposición - + Keep Options Mantener Opciones - + Restore connection options after application restart Restaurar las opciones de conexión después de reiniciar - + Logbook - Search Callsign Libro de Guardia - Buscar Indicativo - - + + New QSO - Add text from Callsign field to Bandmap Nuevo QSO - Agregar texto del campo Indicativo al Mapa de Banda - + Rig - Band Down Radio - Bajar Banda - + Rig - Band Up Radio - Subir Banda - + New QSO - Use Callsign from the Whisperer Nuevo QSO - Usar el indicativo del Whisperer - + CW Console - Key Speed Up Consola CW - Subir Velocidad - + CW Console - Key Speed Down Consola CW - Bajar Velocidad - + CW Console - Profile Up Consola CW - Subir Perfil - + CW Console - Profile Down Consola CW - Bajar Perfil - + Rig - PTT On/Off Radio - PTT Enc./Apag - + All Bands Todas las bandas - + Each Band Cada banda - + Each Band && Mode Cada banda y modo - + No Check Sin control - + Single Único - + Per Band Por banda - + Stop Parar - + Reset Restablecer - + None Ninguno - + + Download LoTW DXCC Credits + Descargar créditos DXCC de LoTW + + + + Service - Download LoTW DXCC Credits + Servicio - Descargar créditos DXCC de LoTW + + + Theme: Native Tema: Native - + Theme: QLog Light Tema: QLog Light - + Theme: QLog Dark Tema: QLog Dark - + What's New Novedades - + Export Cabrillo Exportar Cabrillo - + Color Theme Tema de color - + Not enabled for non-Fusion style No habilitado para estilos que no sean Fusion - + Press to tune the alert Presione para sintonizar la alerta - + + Startup ADI + + + + Clublog Immediately Upload Error Error de carga inmediata de Clublog - - - + + + <b>Error Detail:</b> <b>Detalle del Error:</b> - + op: Op: - + A New Version Una nueva versión - + A new version %1 is available. Una nueva versión %1 está disponible. - + Remind Me Later Recordármelo más tarde - + Download Descargar - + + + QLog Warning + Alerta de QLog + + + + LoTW is not configured properly.<p>Please, use <b>Settings</b> dialog to configure it.</p> + LoTW no está configurado correctamente.<p>Use el diálogo <b>Configuración</b> para configurarlo.</p> + + + + + QLog Error + Error de QLog + + + + Cannot load local DXCC entities from the logbook: + No se pueden cargar las entidades DXCC locales desde el libro de guardia: + + + + Unknown DXCC Entity + Entidad DXCC desconocida + + + + Cannot determine a local DXCC entity from logbook contacts. + No se puede determinar una entidad DXCC local a partir de los contactos del libro de guardia. + + + + LoTW DXCC Credits + Créditos DXCC de LoTW + + + + Select the local DXCC entity for which LoTW DXCC credits will be downloaded: + Seleccione la entidad DXCC local para la que se descargarán los créditos DXCC de LoTW: + + + + Cancel + Cancelar + + + + Downloading LoTW DXCC credits + Descargando créditos DXCC de LoTW + + + + Processing LoTW DXCC credits + Procesando créditos DXCC de LoTW + + + + LoTW DXCC Credit Import Summary + Resumen de importación de créditos DXCC de LoTW + + + + LoTW DXCC credit import failed: + Error al importar créditos DXCC de LoTW: + + + Failed to encrypt credentials. No se pudo cifrar las credenciales. - + Database files (*.dbe);;All files (*) Archivos de base de datos (*.dbe);;Todos los archivos (*) - + Failed to create temporary file. No se pudo crear el archivo temporal. - + Failed to dump the database. No se pudo volcar la base de datos. - + Compressing database... Comprimiendo base de datos… - + Database successfully dumped to %1 Base de datos exportada con éxito a %1 - + Failed to compress the database. No se pudo comprimir la base de datos. - + Failed to prepare database for import. No se pudo preparar la base de datos para la importación. - + Classic Clásico - + Do you want to remove the Contest filter %1? ¿Desea eliminar el filtro Concurso %1? - + Contest: Concurso: - + <h1>QLog %1</h1><p>&copy; 2019 Thomas Gatzweiler DL2IC<br/>&copy; 2021-2026 Ladislav Foldyna OK1MLG<br/>&copy; 2025-2026 Michael Morgan AA5SH<br/>&copy; 2025-2026 Kyle Boyle VE9KZ</p><p>Based on Qt %2<br/>%3<br/>%4<br/>%5</p><p>Icon by <a href='http://www.iconshock.com'>Icon Shock</a><br />Satellite images by <a href='http://www.nasa.gov'>NASA</a><br />ZoneDetect by <a href='https://github.com/BertoldVdb/ZoneDetect'>Bertold Van den Bergh</a><br />TimeZone Database by <a href='https://github.com/evansiroky/timezone-boundary-builder'>Evan Siroky</a> <h1>QLog %1</h1><p>&copy; 2019 Thomas Gatzweiler DL2IC<br/>&copy; 2021-2026 Ladislav Foldyna OK1MLG<br/>&copy; 2025-2026 Michael Morgan AA5SH<br/>&copy; 2025-2026 Kyle Boyle VE9KZ</p><p>Basado en Qt %2<br/>%3<br/>%4<br/>%5</p><p>Iconos por <a href='http://www.iconshock.com'>Icon Shock</a><br />Imágenes satelitales por <a href='http://www.nasa.gov'>NASA</a><br />Detección de zonas por <a href='https://github.com/BertoldVdb/ZoneDetect'>Bertold Van den Bergh</a><br />Base de datos de zonas horarias por <a href='https://github.com/evansiroky/timezone-boundary-builder'>Evan Siroky</a> - + About Acerca de - + N/A S/D - MapWebChannelHandler - - - - - Grid - Locator - + MapPageController - - - - Gray-Line - Línea Gris + + Aurora + Aurora - - - + Beam Haz Antena - - - - Aurora - Aurora + + Chat + Chat - - - - MUF - + + Grid + Locator + + + + Gray-Line + Línea Gris - - - + IBP - + - - - - Chat - Chat + + MUF + - - - + WSJTX - CQ WSJTX - CQ - - - + Path Rutas @@ -8755,12 +9351,12 @@ Esta contraseña será necesaria más adelante para restaurarlas. Antena - + Blank Vacío - + W W @@ -8840,112 +9436,112 @@ Esta contraseña será necesaria más adelante para restaurarlas. Error al iniciar sesión en el Callbook - + LP LP - + New Entity! Nueva Entidad! - + New Band! Nueva Banda! - + New Mode! Nuevo Modo! - + New Band & Mode! Nueva Banda y Modo! - + New Slot! Nuevo Slot! - + Worked Trabajado - + Confirmed Confirmado - + GE GE - + GM GM - + GA GA - + m - + Callbook search is inactive Búsqueda en Callbook desactivada - + Callbook search is active Búsqueda en Callbook activada - + Contest ID must be filled in to activate Debe rellenarse el ID del concurso para activarlo - + two or four adjacent Maidenhead grid locators, each four characters long, (ex. EN98,FM08,EM97,FM07) dos o cuatro locators adyacentes, cada uno de cuatro caracteres (ej. EN98,FM08,EM97,FM07) - + the contacted station's DARC DOK (District Location Code) (ex. A01) el DARC DOK (código de ubicación de distrito) de la estación contactada (ej. A01) - + World Wide Flora & Fauna World Wide Flora & Fauna - + Special Activity Group Grupo de Actividades Especiales - + Special Activity Group Information Información Grupo de Actividades Especiales - + It is not the name of the contest but it is an assigned<br>Contest ID (ex. CQ-WW-CW for CQ WW DX Contest (CW)) No es el nombre del concurso, sino un ID de concurso asignado (ej. CQ-WW-CW para CQ WW DX Contest (CW)) - + Description of the contacted station's equipment Descripción del equipo de la estación contactada @@ -9159,7 +9755,7 @@ Puede dejar los campos vacíos y configurarlos más tarde en Configuración. QCoreApplication - + QLog Help Ayuda de QLog @@ -9189,31 +9785,31 @@ Puede dejar los campos vacíos y configurarlos más tarde en Configuración. - + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + QLog Warning Alerta de QLog @@ -9243,63 +9839,63 @@ Puede dejar los campos vacíos y configurarlos más tarde en Configuración.Error de red. No se puede descargar la lista de clubes para - - - + + + - - - - + + + + QLog Error Error de QLog - + QLog is already running QLog ya se está ejecutando - + Failed to process pending database import. No se pudo procesar la importación pendiente de la base de datos. - + The database was imported successfully, but the stored passwords could not be restored (decryption failed or the data is corrupted). All service passwords have been cleared and must be re-entered in Settings. La base de datos se importó correctamente, pero no se pudieron restaurar las contraseñas almacenadas (falló la descifrado o los datos están corruptos). Todas las contraseñas de los servicios se han borrado y deben volver a introducirse en Configuración. - + Could not connect to database. No se pudo conectar a la base de datos. - + Could not export a QLog database to ADIF as a backup.<p>Try to export your log to ADIF manually No se pudo exportar una base de datos QLog a ADIF como copia de seguridad.<p>Intente exportar su registro a ADIF manualmente - + Database migration failed. Error en la migración de la base de datos. - + - + QLog Info Información de Qlog - + Activity name is already exists. El nombre de la actividad ya existe. @@ -9324,33 +9920,33 @@ Puede dejar los campos vacíos y configurarlos más tarde en Configuración.No se pueden actualizar las reglas de alerta - + DXC Server Name Error Error en el nombre del servidor del Cluster DX - + DXC Server address must be in format<p><b>[username@]hostname:port</b> (ex. hamqth.com:7300)</p> La dirección del servidor del Cluster DX debe tener el formato<p><b>[nombre de usuario@]nombre de host:puerto</b> (ej. hamqth.com:7300)</p> - + DX Cluster Password Contraseña del Cluster DX - + Invalid Password Contraseña incorrecta - + DXC Server Connection Error Error en la conexión del servidor del Cluster DX - + Filename is empty El nombre del archivo está vacío @@ -9385,129 +9981,129 @@ Puede dejar los campos vacíos y configurarlos más tarde en Configuración. - + Filter name is already exists. El nombre del filtro ya existe. - + <b>Rig Error:</b> <b>Error del Equipo:</b> - + <b>Rotator Error:</b> <b>Error del Rotor:</b> - + <b>CW Keyer Error:</b> <b>Error del Manipulador:</b> - + The fields <b>%0</b> will not be saved because the <b>%1</b> is not filled. Los campos <b>%0</b> no se guardarán porque <b>%1</b> no está completado. - + Your callsign is empty. Please, set your Station Profile Tu indicativo está vacío. Por favor, configure su Perfil de Estación - - + + Please, define at least one Station Locations Profile Por favor, defina al menos un Perfil de Ubicaciones de Estaciones - + WSJTX Multicast is enabled but the Address is not a multicast address. WSJTX Multicast está habilitada pero la dirección no es una dirección de multidifusión. - + Loop detected. Raw UDP forward uses the same port as the WSJT-X receiving port. Bucle detectado. El reenvío UDP en bruto usa el mismo puerto que el puerto de recepción de WSJT-X. - + Rig port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device El puerto de la Radio debe ser un puerto COM válido.<br>Para Windows use COMxx, para sistemas operativos tipo Unix use una ruta al dispositivo - + Rig PTT port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device El puerto de control del PTT debe ser un puerto COM válido.<br>Para Windows utilice COMxx, para SO tipo unix utilice una ruta al dispositivo - + <b>TX Range</b>: Max Frequency must not be 0. <b>Rango TX</b>: la frecuencia máxima no debe ser 0. - + <b>TX Range</b>: Max Frequency must not be under Min Frequency. <b>Rango TX</b>: la frecuencia máxima no debe estar por debajo de la frecuencia mínima. - + Rotator port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device El puerto del Rotor debe ser un puerto COM válido.<br>Para Windows use COMxx, para sistemas operativos tipo Unix use una ruta al dispositivo - + CW Keyer port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device El puerto del Manipulador debe ser un puerto COM válido.<br>Para Windows use COMxx, para sistemas operativos tipo Unix use una ruta al dispositivo - + Cannot change the CW Keyer Model to <b>Morse over CAT</b><br>No Morse over CAT support for Rig(s) <b>%1</b> No se puede cambiar el modelo de Manipulador a <b>Morse sobre CAT</b><br>No se admite Morse sobre CAT para la/s Radio/s <b>%1</b> - + Cannot delete the CW Keyer Profile<br>The CW Key Profile is used by Rig(s): <b>%1</b> No se puede eliminar el Perfil del Manipulador<br>El Perfil del Manipulador es utilizado por la/s radio/s: <b>%1</b> - + Callsign has an invalid format El Indicativo tiene un formato no válido - + Operator Callsign has an invalid format El indicativo del operador tiene un formato no válido - + Gridsquare has an invalid format La cuadrícula no es válida en ese formato. El Locator tiene un formato no válido - + VUCC Grids have an invalid format (must be 2 or 4 Gridsquares separated by ',') El Locator VUCC tiene un formato no válido (debe se 2 o 4 dígitos separadas por ',') - + Country must not be empty El País no debe estar vacío - + CQZ must not be empty La Zona CQ no debe estar vacía - + ITU must not be empty ITU no debe estar vacío - + Cannot update QSO Filter Conditions No se pueden actualizar las condiciones del filtro QSO @@ -9515,79 +10111,79 @@ Puede dejar los campos vacíos y configurarlos más tarde en Configuración. QObject - + km km - + miles millas - + Connection Refused Conexión Denegada - + Host closed the connection El host cerró la conexión - + Host not found Host no encontrado - + Timeout Se agotó el tiempo de espera - + Network Error Error de red - + Internal Error Error Interno - + Importing Database Importando base de datos - + Opening Database Abriendo Base de Datos - + Backuping Database Respaldando Base de Datos - + Migrating Database Migrando Base de Datos - + Starting Application Iniciando Aplicación @@ -9687,12 +10283,12 @@ Puede dejar los campos vacíos y configurarlos más tarde en Configuración.Mi DXCC - + Cannot connect to DXC Server <p>Reason <b>: No se puede conectar al servidor del Cluster DX <p>Razón <b>: - + <b>Imported</b>: %n contact(s) <b>Importado</b>: %n contacto/s @@ -9700,7 +10296,7 @@ Puede dejar los campos vacíos y configurarlos más tarde en Configuración. - + <b>Warning(s)</b>: %n <b>Alerta/s</b>: %n @@ -9708,7 +10304,7 @@ Puede dejar los campos vacíos y configurarlos más tarde en Configuración. - + <b>Error(s)</b>: %n <b>Error/es</b>: %n @@ -9786,6 +10382,32 @@ Puede dejar los campos vacíos y configurarlos más tarde en Configuración.Worked Trabajado + + + IARU Region 1 + + + + + + Failed to write file: %1 + No se pudo escribir el archivo: %1 + + + + Cannot open file: %1 + No se puede abrir el archivo: %1 + + + + Invalid guide file: %1 + Archivo de guía no válido: %1 + + + + Invalid guide file: missing title + Archivo de guía no válido: falta el título + QRZCallbook @@ -9798,7 +10420,7 @@ Puede dejar los campos vacíos y configurarlos más tarde en Configuración. QRZUploader - + General Error Error General @@ -9821,33 +10443,33 @@ Puede dejar los campos vacíos y configurarlos más tarde en Configuración.Ordenar por: - + Export Filtered Exportar filtrados - + Date (Newest) Fecha (más reciente) - + Date (Oldest) Fecha (más antigua) - + Callsign (A-Z) Indicativo (A-Z) - + Callsign (Z-A) Indicativo (Z-A) - - + + %n QSL card(s) %n tarjeta(s) QSL @@ -9855,72 +10477,72 @@ Puede dejar los campos vacíos y configurarlos más tarde en Configuración. - + All QSL Cards Todas las tarjetas QSL - + Favorites Favoritos - + By Country Por país - + By Date Por fecha - + By Band Por banda - + By Mode Por modo - + By Continent Por continente - + Remove from Favorites Quitar de favoritos - + Add to Favorites Añadir a favoritos - + Open Abrir - + Save... Guardar… - + Save QSL Card Guardar tarjeta QSL - + Export QSL Cards Exportar tarjetas QSL - + Exported %1 of %2 cards Exportadas %1 de %2 tarjetas @@ -9963,28 +10585,23 @@ Puede dejar los campos vacíos y configurarlos más tarde en Configuración.Detalles - - New QSLs: - QSLs Nuevas: + + New QSLs: + Nuevas QSL: - Updated QSOs: - QSOs actualizados: + Updated QSOs: + QSO actualizados: - - Unmatched QSLs: - QSLs Incomparables: + + Unmatched QSLs: + QSL sin coincidencia: QSLPrintLabelDialog - - - Print QSL Labels - Imprimir etiquetas QSL - Filter @@ -10016,235 +10633,397 @@ Puede dejar los campos vacíos y configurarlos más tarde en Configuración.Filtro de usuario - + Label Template Plantilla de etiqueta - + + Page Size: Tamaño de página: - + Columns: Columna: - + Rows: Fila: - + + Label Width: Ancho de la etiqueta: - + + Print QSL Labels / Cards + Imprimir etiquetas / tarjetas QSL + + + + Print Mode + Modo de impresión + + + + Mode: + Modo: + + + + + QSL Card + Tarjeta QSL + + + + Card Width: + Ancho de tarjeta: + + + + Card Height: + Alto de tarjeta: + + + + Card Gap: + Espacio entre tarjetas: + + + + Label Height: Altura de la etiqueta: - + + Label X Offset: + Desplazamiento X de etiqueta: + + + + Label Y Offset: + Desplazamiento Y de etiqueta: + + + + Label Background: + Fondo de etiqueta: + + + + Fill under label + Rellenar bajo etiqueta + + + + + Color + Color + + + + Background Image: + Imagen de fondo: + + + + Browse + Navegar + + + + Clear + + + + Left Margin: Margen izquierdo: - + Top Margin: Margen superior: - + H Spacing: Espaciado horizontal: - + V Spacing: Espaciado vertical: - + Label Appearance Apariencia de la etiqueta - + Print Label Borders Imprimir bordes de etiquetas - + QSOs per Label: QSOs por etiqueta: - + Footer Left Text: Texto de pie de página izquierdo: - + Footer Right Text: Texto de pie de página derecho: - + Skip Label: Omitir etiqueta: - + Sans Font: Fuente sans-serif: - + Mono Font: Fuente monoespaciada: - + + Text Color: + Color de texto: + + + Callsign Size: Tamaño del indicativo: - + "To Radio" Size: Tamaño de «To Radio»: - + "To Radio" Text: Texto «To Radio»: - + Header Size: Tamaño del encabezado: - + Data Size: Tamaño de datos: - + Date Header Text: Texto del encabezado de fecha: - + Date Format: Formato de fecha: - + Time Header Text: Texto del encabezado de hora: - + Band Header Text: Texto del encabezado de banda: - + Mode Header Text: Texto del encabezado de modo: - + QSL Header Text: Texto del encabezado QSL: - + Extra Column: Columna adicional: - + Extra Column Text Texto de la columna adicional - + (DB column name) (nombre de columna de BD) - - + + No matching QSOs found No se encontraron QSOs coincidentes - - + + Page 0 of 0 Página 0 de 0 - + Labels: 0 (0 pages) Etiquetas: 0 (0 páginas) - + Print Imprimir - + Export as PDF Exportar como PDF - - + + Export as Images + Exportar imágenes + + + + Label Sheet + Hoja de etiquetas + + + + Custom Personalizado - + Empty Vacío - + QSOs matching this station profile QSOs que coinciden con este perfil de estación - + + Select Label Text Color + Seleccionar color de texto de etiqueta + + + + Select Label Background Color + Seleccionar color de fondo de etiqueta + + + + + + Select QSL Card Background + Seleccionar fondo de tarjeta QSL + + + + Images (*.png *.jpg *.jpeg *.bmp) + Imágenes (*.png *.jpg *.jpeg *.bmp) + + + + Cannot read selected image file. + No se puede leer el archivo de imagen seleccionado. + + + + Selected file is not a valid image. + El archivo seleccionado no es una imagen válida. + + + + Cards: %1 (%2 pages) + Tarjetas: %1 (%2 páginas) + + + Labels: %1 (%2 pages) Etiquetas: %1 (%2 páginas) - + Page %1 of %2 Página %1 de %2 - + Export PDF Exportar PDF - + PDF Files (*.pdf) Archivos PDF (*.pdf) - + + + + Export QSL Card Images + Exportar imágenes de tarjetas QSL + + + + Some image files already exist. Overwrite them? + Algunos archivos de imagen ya existen. ¿Sobrescribirlos? + + + + Exported %n QSL card image(s). + + %n imagen(es) de tarjeta QSL exportada(s). + + + + + + Exported %1 of %2 QSL card images. + Exportadas %1 de %2 imágenes de tarjetas QSL. + + + + QSOs were not marked as sent. + Los QSO no se marcaron como enviados. + + + Mark as Sent Marcar como enviado - + Mark printed/exported QSOs as sent? ¿Marcar QSOs impresos/exportados como enviados? @@ -10301,7 +11080,7 @@ Puede dejar los campos vacíos y configurarlos más tarde en Configuración. - + Blank Vacío @@ -10714,318 +11493,318 @@ Puede dejar los campos vacíos y configurarlos más tarde en Configuración.Miembro: - + &Reset &Reiniciar - + &Lookup &Buscar - - - + + + No No - - - + + + Yes - - - + + + Requested Solicitado - - - + + + Queued En Cola - - - + + + Ignored Ignorado - + Bureau Bureau - + Direct Directa - + Electronic Electrónica - + Submit changes Grabar cambios - + Really submit all changes? Realmente desea grabar todos los cambios? - - - - + + + + QLog Error Error de QLog - + Cannot save all changes - internal error No se pueden guardar todos los cambios - Error interno - + Cannot save all changes - try to reset all changes No se pueden guardar todos los cambios - intente reestablecer todos los cambios - + QSO Detail Detalle del QSO - + Edit QSO Editar QSO - + Downloading eQSL Image Descargando Imágen de eQSL - + Cancel Cancelar - + eQSL Download Image failed: La descarga de imágen de eQSL ha fallado: - + DX Callsign must not be empty El indicativo del DX no puede estar vacío - + DX callsign has an incorrect format El indicativo del DX tiene formato incorrecto - - + + TX Frequency or Band must be filled La Frecuencia o Bande de TX debe estar completada - + TX Band should be La Banda TX debe ser - + RX Band should be La Banda RX debe ser - - + + DX Grid has an incorrect format El Locator del DX tiene formato incorrecto - + Based on callsign, DXCC Country is different from the entered value - expecting Según el indicativo, el País es diferente del valor ingresado; se esperaba - + Based on callsign, DXCC Continent is different from the entered value - expecting Según el indicativo, el Continente es diferente del valor ingresado; se esperaba - + Based on callsign, DXCC ITU is different from the entered value - expecting Según el indicativo, la Zona ITU es diferente del valor ingresado; se esperaba - + Based on callsign, DXCC CQZ is different from the entered value - expecting Según el indicativo, la Zona CQ es diferente del valor ingresado; se esperaba - + VUCC has an incorrect format VUCC tiene formato incorrecto - + Based on Frequencies, Sat Mode should be Según las frecuencias, el modo Satélite debe ser - + blank vacío - + Sat name must not be empty El nombre del Satélite no debe estar vacío - + Own Callsign must not be empty El Indicativo propio no puede estar vacío - + Own callsign has an incorrect format El Indicativo propio tiene formato incorrecto - + Own VUCC Grids have an incorrect format El Locator VUCC propio tiene formato incorrecto - + Based on own callsign, own DXCC ITU is different from the entered value - expecting Según el indicativo propio, la Zona ITU propia es diferente del valor ingresado; se esperaba - + Based on own callsign, own DXCC CQZ is different from the entered value - expecting Según el indicativo propio, la Zona CQ propia es diferente del valor ingresado; se esperaba - + Based on own callsign, own DXCC Country is different from the entered value - expecting Según el indicativo propio, el País propio es diferente del valor ingresado; se esperaba - + Based on SOTA Summit, QTH does not match SOTA Summit Name - expecting Según la Cumbre SOTA, el QTH no coincide con el Nombre de la Cumbre SOTA - se esperaba - + Based on SOTA Summit, Grid does not match SOTA Grid - expecting Según la Cumbre SOTA, el Locator no coincide con el Locator SOTA - se esperaba - + Based on POTA record, QTH does not match POTA Name - expecting Según los registros POTA, el QTH no coincide con el Nombre POTA - se esperaba - + Based on POTA record, Grid does not match POTA Grid - expecting Según los registros POTA, el Locator no coincide con el Locator POTA - se esperaba - + Based on SOTA Summit, my QTH does not match SOTA Summit Name - expecting Según la Cumbre SOTA, mi QTH no coincide con el Nombre de la Cumbre SOTA - se esperaba - + Based on SOTA Summit, my Grid does not match SOTA Grid - expecting Según la Cumbre SOTA, mi Locator no coincide con el Locator SOTA - se esperaba - + Based on POTA record, my QTH does not match POTA Name - expecting Según los registros POTA, mi QTH no coincide con el Nombre POTA - se esperaba - + Based on POTA record, my Grid does not match POTA Grid - expecting Según los registros POTA, mi Locator no coincide con el Locator POTA - se esperaba - + LoTW Sent Status to <b>No</b> does not make any sense if QSL Sent Date is set. Set Date to 1.1.1900 to leave the date field blank El estado de envío de LoTW en <b>No</b> no tiene ningún sentido si se establece la fecha de envío de QSL. Establezca la fecha en 1.1.1900 para dejar el campo de fecha en blanco - + Date should be present for LoTW Sent Status <b>Yes</b> La fecha debe estar presente para el estado de envío de LotW <b>Sí</b> - + eQSL Sent Status to <b>No</b> does not make any sense if QSL Sent Date is set. Set Date to 1.1.1900 to leave the date field blank El estado de envío de eQSL en <b>No</b> no tiene ningún sentido si se establece la fecha de envío de QSL. Establezca la fecha en 1.1.1900 para dejar el campo de fecha en blanco - + Date should be present for eQSL Sent Status <b>Yes</b> La fecha debe estar presente para el estado de envío de eQSL <b>Sí</b> - + Paper Sent Status to <b>No</b> does not make any sense if QSL Sent Date is set. Set Date to 1.1.1900 to leave the date field blank El estado de envío de QSL Papel en <b>No</b> no tiene ningún sentido si se establece la fecha de envío de QSL. Establezca la fecha en 1.1.1900 para dejar el campo de fecha en blanco - + Date should be present for Paper Sent Status <b>Yes</b> La fecha debe estar presente para el estado de envío de Papel <b>Sí</b> - + Callbook error: Error del Libro de Guardia: - - + + <b>Warning: </b> <b>Alerta: </b> - + Validation Validación - + Yellow marked fields are invalid.<p>Nevertheless, save the changes?</p> Los campos marcados en amarillo no son válidos.<p>¿Guardar los cambios igualmente? - + &Save &Guardar - + &Edit &Editar @@ -11063,52 +11842,52 @@ Puede dejar los campos vacíos y configurarlos más tarde en Configuración.Agregar Condición - + Equal Igual - + Not Equal No es Igual - + Contains Contiene - + Not Contains No Contiene - + Greater Than Mayor que - + Less Than Menor que - + Starts with Comience con - + RegExp - + Remove Quitar - + Must not be empty No debe estar vacío @@ -11144,27 +11923,27 @@ Puede dejar los campos vacíos y configurarlos más tarde en Configuración. Rig - + No Rig Profile selected No Hay Perfil de Radio Seleccionado - + Rigctld Error Error de Rigctld - + Initialization Error Error al Inicializar - + Internal Error Error Interno - + Cannot open Rig No se puede abrir la Radio @@ -11182,36 +11961,66 @@ Puede dejar los campos vacíos y configurarlos más tarde en Configuración. - + Disconnected Desconectado - - + + MHz MHz - + Disable Split Desactivar split - + RIT: 0.00000 MHz - + XIT: 0.00000 MHz - + PWR: %1W + + + OUT + + + + + Outside Bandmap Guide range + Fuera del rango de la guía de Bandmap + + + + SOS + + + + + Emergency frequency: %1 MHz + Frecuencia de emergencia: %1 MHz + + + + IBP + + + + + International Beacon Project: %1 MHz + + RigctldAdvancedDialog @@ -11301,57 +12110,57 @@ Instale Hamlib o especifique la ruta manualmente. RigctldManager - + rigctld executable not found in /app/bin/. This should not happen in Flatpak build. No se encontró el ejecutable de rigctld en /app/bin/. Esto no debería ocurrir en la versión Flatpak. - + rigctld executable not found. Please install Hamlib or specify the path in Advanced settings. No se encontró el ejecutable de rigctld. Instale Hamlib o especifique la ruta en la configuración avanzada. - + Hamlib major version mismatch: QLog was compiled with Hamlib %1 but rigctld reports version %2.%3.%4. Rig model IDs are incompatible between major versions. Incompatibilidad de versión principal de Hamlib: QLog se compiló con Hamlib %1, pero rigctld informa la versión %2.%3.%4. Los ID de modelo de transceptor no son compatibles entre versiones principales. - + Port %1 is already in use. Another rigctld or application may be running on this port. El puerto %1 ya está en uso. Otro rigctld o aplicación puede estar ejecutándose en este puerto. - + rigctld started but not responding on port %1. rigctld se inició pero no responde en el puerto %1. - + Failed to start rigctld: %1 %2 No se pudo iniciar rigctld: %1 %2 - + rigctld crashed. rigctld se bloqueó. - + rigctld timed out. rigctld agotó el tiempo de espera. - + Write error with rigctld. Error de escritura con rigctld. - + Read error with rigctld. Error de lectura con rigctld. - + Unknown rigctld error. Error desconocido de rigctld. @@ -11359,22 +12168,22 @@ Instale Hamlib o especifique la ruta manualmente. Rotator - + No Rotator Profile selected No Hay Perfil de Rotor Seleccionado - + Initialization Error Error al Inicializar - + Internal Error Error Interno - + Cannot open Rotator No se puede abrir el Rotor @@ -11501,7 +12310,7 @@ Instale Hamlib o especifique la ruta manualmente. - + Callsign Indicativo @@ -11570,20 +12379,21 @@ Instale Hamlib o especifique la ruta manualmente. - - - - - - - - - - + + + + + + - - - + + + + + + + + Add Agregar @@ -11678,6 +12488,7 @@ Instale Hamlib o especifique la ruta manualmente. + Description Descripción @@ -12069,7 +12880,7 @@ Instale Hamlib o especifique la ruta manualmente. - + Start rigctld daemon to share rig with other applications (e.g. WSJT-X) Iniciar el demonio rigctld para compartir el transceptor con otras aplicaciones (p. ej., WSJT-X) @@ -12208,14 +13019,14 @@ Instale Hamlib o especifique la ruta manualmente. - + Serial Serie - - + + Network Red @@ -12266,8 +13077,8 @@ Instale Hamlib o especifique la ruta manualmente. - - + + HamQTH @@ -12276,7 +13087,7 @@ Instale Hamlib o especifique la ruta manualmente. - + Username Nombre de Usuario @@ -12286,15 +13097,15 @@ Instale Hamlib o especifique la ruta manualmente. - + Password Contraseña - - + + QRZ.com @@ -12354,22 +13165,97 @@ Instale Hamlib o especifique la ruta manualmente. Los QSOs serán subidos inmediatamente - + + Startup ADI + + + + + Configured ADI/ADIF files are checked only at startup. A newly added file starts at its current end, so only later appended QSOs are loaded. This is not a live watcher; if many new QSOs are found, loading stops and the standard Import should be used. + Los archivos ADI/ADIF configurados se comprueban solo al iniciar. Un archivo recién añadido comienza en su final actual, por lo que solo se cargan los QSO añadidos posteriormente. No es una supervisión en vivo; si se encuentran muchos QSO nuevos, la carga se detiene y debe usarse la importación estándar. + + + + Removing a file also forgets its recovery position. + Al eliminar un archivo también se olvida su posición de recuperación. + + + + Remove + + + + + Used when a file row has Missing QSL Sent set to Custom. Explicit ADIF values are kept. + Se usa cuando una fila de archivo tiene QSL Sent faltante establecido en Personalizado. Los valores ADIF explícitos se conservan. + + + + Custom QSL Sent Defaults + Valores predeterminados personalizados de QSL Sent + + + + Paper QSL + QSL en papel + + + + DCL + DCL + + + + Select the <b>Bandmap Guide</b> profile shown as visual frequency hints. It does not affect mode identification. + Seleccione el perfil <b>Guía de Bandmap</b> mostrado como ayuda visual de frecuencia. No afecta a la identificación del modo. + + + + Manage + Administrar + + + + Double-click cells to edit start/end frequency, enabled state, or SAT mode. Band names are fixed; new bands cannot be added here. + Doble clic en celdas para editar la frecuencia inicial/final, el estado habilitado o el modo SAT. Los nombres de banda son fijos; no se pueden añadir nuevas bandas aquí. + + + + QSO DXCC Status Colors + Colores de estado DXCC del QSO + + + + Used for DX spots, Bandmap, WSJT-X and QSO status hints. Confirmed has no highlight by default. Click a color cell to choose a color or set No color. + Se usa para DX spots, Bandmap, WSJT-X y ayudas de estado de QSO. Confirmado no tiene resaltado por defecto. Haga clic en una celda de color para elegir un color o establecer Sin color. + + + + Restore Defaults + Restaurar valores predeterminados + + + + Shortcuts + Atajos + + + Danger Zone Zona de peligro - + <b>⚠ This is a danger zone. Proceed with caution, as actions performed here cannot be undone and may have a significant impact on your log.</b> <b>⚠ Esta es una zona de peligro. Proceda con precaución, ya que las acciones realizadas no se pueden deshacer y pueden tener un impacto significativo en su log.</b> - + Delete All QSOs Eliminar todos los QSOs - + Delete All Passwords from the Secure Store Eliminar todas las contraseñas del almacén seguro @@ -12380,7 +13266,8 @@ Instale Hamlib o especifique la ruta manualmente. - + + eQSL @@ -12413,7 +13300,8 @@ Instale Hamlib o especifique la ruta manualmente. - + + LoTW @@ -12448,118 +13336,121 @@ Instale Hamlib o especifique la ruta manualmente. Usando una instancia interna de TQSL - + Others Otros - + Status Confirmed By Confirmado por - + Paper Papel - + Chat Chat - + <b>Security Notice:</b> QLog stores all passwords in the Secure Storage. Unfortunately, ON4KST uses a protocol where this password is sent over an unsecured channel as plaintext.</p><p>Please exercise caution when choosing your password for this service, as your password is sent over an unsecured channel in plaintext form.</p> <b>Aviso de seguridad:</b> QLog almacena todas las contraseñas en el almacenamiento seguro. Desafortunadamente, ON4KST utiliza un protocolo en el que esta contraseña se envía a través de un canal no seguro como texto sin formato.</p><p>Tenga cuidado al elegir su contraseña para este servicio, ya que su contraseña se envía a través de un canal no seguro en formato de texto sin formato.< /p> - + Bands Bandas - + Modes Modos - + The '>' character is interpreted as a marker for the initial cursor position in the Report column. <br/>Ex.: '5>9' means the cursor will be positioned on the second character El carácter ">" se interpreta como un marcador de la posición inicial del cursor en la columna Report.<br/>Ej.: "5>9" significa que el cursor se colocará en el segundo carácter - + Color CQ Spots Colorear los spots CQ - + Enable/Disable sending color-coded status indicators back to WSJT-X for each callsign calling CQ Habilitar/deshabilitar el envío de indicadores de estado codificados por color de vuelta a WSJT-X para cada indicativo que llama CQ - + Rig Status Radio - + GUI GUI - + Time Format Formato de tiempo - + 24-hour 24 horas - + AM/PM AM/PM - + Unit System Sistema de unidades - + Metric Métrico - + Imperial Imperial - + Date Format Formato de fecha - + System Sistema - + + + + Custom Personalizado - + <a href="https://doc.qt.io/qt-6/qdate.html#fromString-1">Time Format Documentation</a> <a href="https://doc.qt.io/qt-6/qdate.html#fromString-1">Documentación del formato de hora</a> - - + + DXCC @@ -12585,7 +13476,7 @@ Instale Hamlib o especifique la ruta manualmente. - + API Key Clave API @@ -12595,52 +13486,52 @@ Instale Hamlib o especifique la ruta manualmente. Punto final - + Wsjtx - + Raw UDP Forward Reenvío UDP sin procesar - + <p>List of IP addresses to which QLog forwards raw UDP WSJT-X packets.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Lista de direcciones IP a las que QLog reenvía paquetes UDP WSJT-X sin procesar.</p>Las direcciones IP están separadas por un espacio y tienen el formato IP:PUERTO - - - - - - + + + + + + ex. 192.168.1.1:1234 192.168.2.1:1234 ej. 192.168.1.1:1234 192.168.2.1:1234 - + Port Puerto - + Port where QLog listens an incoming traffic from WSJT-X Puerto donde QLog escucha el tráfico entrante de WSJT-X - + Join Multicast Unirse a Multidifusión - + Enable/Disable Multicast option for WSJTX Activar/desactivar la opción de multidifusión para WSJTX - + Multicast Address Dirección de multidifusión @@ -12717,328 +13608,528 @@ Instale Hamlib o especifique la ruta manualmente. Dejar vacío para detección automática - + Specify Multicast Address. <br>On some Linux systems it may be necessary to enable multicast on the loop-back network interface. Especifique la dirección de multidifusión. <br>En algunos sistemas Linux, puede ser necesario habilitar la multidifusión en la interfaz de red de bucle invertido. - + TTL TTL - + Time-To-Live determines the range<br> over which a multicast packet is propagated in your intranet. Time-To-Live determina el rango<br> sobre el cual se propaga un paquete de multidifusión en su intranet. - + Notifications Notificaciones - + LogID - + <p>Assigned LogID to the current log.</p>The LogID is sent in the Network Nofitication messages as a unique instance identified.<p> The ID is generated automatically and cannot be changed</> <p>LogID asignado al registro actual.</p>El LogID se envía en los mensajes de notificación de red como una instancia única identificada.<p>El ID se genera automáticamente y no se puede cambiar</> - + DX Spots Anuncios DX - + <p> List of IP addresses to which QLog sends UDP notification packets with DX Cluster Spots.</p>The IP addresses are separated by a space and have the form IP:PORT <p> Lista de direcciones IP a las que QLog envía paquetes de notificación UDP con anuncios de Cluster DX.</p> Las direcciones IP están separadas por un espacio y tienen la forma IP:PUERTO - + Spot Alerts Alertas de Anuncios - + <p> List of IP addresses to which QLog sends UDP notification packets about user Spot Alerts.</p>The IP addresses are separated by a space and have the form IP:PORT <p> Lista de direcciones IP a las que QLog envía paquetes de notificación UDP sobre Alertas de Anuncios de usuario.</p> Las direcciones IP están separadas por un espacio y tienen el formato IP:PUERTO - + QSO Changes Cambios de QSO - + <p> List of IP addresses to which QLog sends UDP notification packets about a new/updated/deleted QSO in the log.</p>The IP addresses are separated by a space and have the form IP:PORT <p> Lista de direcciones IP a las que QLog envía paquetes de notificación UDP sobre un QSO nuevo/actualizado/eliminado en el registro.</p>Las direcciones IP están separadas por un espacio y tienen el formato IP:PUERTO - + Wsjtx CQ Spots Anuncios CQ de Wsjtx - + <p> List of IP addresses to which QLog sends UDP notification packets with WSJTX CQ Spots.</p>The IP addresses are separated by a space and have the form IP:PORT <p> Lista de direcciones IP a las que QLog envía paquetes de notificación UDP con Anuncios de CQ de WSJTX.</p>Las direcciones IP están separadas por un espacio y tienen el formato IP:PUERTO - + <p> List of IP addresses to which QLog sends UDP notification packets when Rig State changes.</p>The IP addresses are separated by a space and have the form IP:PORT - - + + Special - Omnirig Especial - Omnirig - + Cannot be changed No se puede cambiar - - + + Name Nombre - + Report Informe - - + + State Estado - + Start (MHz) Inicio (MHz) - + End (MHz) Fin (MHz) - + SAT Mode Modo del Satélite - - - + + + Disabled Desactivado - - + + None Ninguno - + Hardware Hardware - + Software Software - + + + + + No No - + Even Par - + Odd Impar - + Mark Marca - + Space Espacio - + Dummy - + Morse Over CAT Morse Sobre CAT - + WinKey WinKey - + CWDaemon - + FLDigi - + Single Paddle Pala Simple - + IAMBIC A - + IAMBIC B - + Ultimate - + High High - + Low Low - + + Duplicate + Duplicado + + + + Already worked QSO + QSO ya trabajado + + + + New Entity + Nueva Entidad + + + + DXCC entity not worked yet + Entidad DXCC aún no trabajada + + + + New Band / Mode + Nueva banda / modo + + + + New band, mode, or band and mode + Nueva banda, modo o ambos + + + + New Slot + Nuevo Slot + + + + New band and mode combination + Nueva combinación de banda y modo + + + + Worked + Trabajado + + + + Worked but not confirmed + Trabajado, pero no confirmado + + + + Confirmed + Confirmado + + + + Confirmed QSO; no highlight by default + QSO confirmado; sin resaltado predeterminado + + + + Status + Estado + + + + Color + Color + + + + Choose Color... + Elegir color... + + + + Default + Predeterminado + + + + No Color + Sin color + + + + Status Color + Color de estado + + + + No color + Sin color + + + + No highlight. Click to choose a color or set no color. + Sin resaltado. Haga clic para elegir un color o establecer sin color. + + + + Click to change color or set no color. + Haga clic para cambiar el color o establecer sin color. + + + Press <b>Modify</b> to confirm the profile changes or <b>Cancel</b>. Presione <b>Modificar</b> para confirmar los cambios de perfil o <b>Cancelar</b>. - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + Modify Modificar - - - - - - - - - - + + + + + + + + + + Must not be empty No debe estar vacío - + Select File Seleccionar Archivo - + Auto Detect Detección automática - + TQSL was not found on this system. Please install TQSL or specify the path manually. No se encontró TQSL en este sistema. Instale TQSL o especifique la ruta manualmente. - + Not found No encontrado - + Rig sharing is only available for Hamlib driver Compartir el transceptor solo está disponible para el controlador Hamlib - + Rig sharing is not available for network connection Compartir el transceptor no está disponible para conexiones de red - + + Off + Desactivado + + + Delete Passwords Eliminar contraseñas - + All passwords have been deleted Todas las contraseñas han sido eliminadas - + Deleting all QSOs... Eliminando todos los QSOs... - + Error Error - + Failed to delete all QSOs. No se pudieron eliminar todos los QSOs. - + + Enabled + Activado + + + + Path + Rutas + + + + Station Profile + Perfil de estación + + + + Missing QSL Sent + QSL Sent faltante + + + + Last Recovery + Última recuperación + + + + + + Queued + En Cola + + + + + + + Ignored + Ignorado + + + + + + + Requested + Solicitado + + + + + + + Yes + + + + + Station Profile does not exist. Select another profile and enable this row again. + El perfil de estación no existe. Seleccione otro perfil y habilite esta fila de nuevo. + + + + File exists + El archivo existe + + + + File does not exist + El archivo no existe + + + + Startup ADI initialized + Startup ADI inicializado + + + + Select ADIF File + Seleccionar archivo ADIF + + + + ADIF Files (*.adi *.adif);;All Files (*) + Archivos ADIF (*.adi *.adif);;Todos los archivos (*) + + + members Miembros - + Required internet connection during application start Required internet connection during application start @@ -13096,7 +14187,7 @@ Instale TQSL o especifique la ruta manualmente. StatisticsWidget - + Statistics Estadísticas @@ -13167,7 +14258,7 @@ Instale TQSL o especifique la ruta manualmente. - + Band Banda @@ -13192,146 +14283,145 @@ Instale TQSL o especifique la ruta manualmente. Papel - + Year Año - + Month Mes - + Day in Week Día de la Semana - + Hour Hora - + Mode Modo - + Continent Continente - + Propagation Mode Modo de Propagación - + Confirmed / Not Confirmed Confirmado / No Confirmado - + Countries Paises - + Big Gridsquares Grandes cuadrículas - + Distance Distancia - + QSOs QSOs - + Confirmed/Worked Grids Locators Confirmados/Trabajados - + ODX ODX - + Sun Dom - + Mon Lun - + Tue Mar - + Wed Mie - + Thu Jue - + Fri Vie - + Sat Sáb - - - + + + Not specified No Especificado - + Confirmed Confirmado - + Not Confirmed No Confirmado - + No User Filter Sin filtro de usuario - + Over 50000 QSOs. Display them? Más de 50.000 QSO. ¿Mostrarlo? - - + Rendering QSOs... Renderizando los QSO… - + All Todos @@ -13385,17 +14475,17 @@ Instale TQSL o especifique la ruta manualmente. ToAllTableModel - + Time Hora - + Spotter Anunciante - + Message Mensaje @@ -13668,27 +14758,27 @@ Instale TQSL o especifique la ruta manualmente. UserListModel - + Callsign Indicativo - + Gridsquare Cuadrícula - + Distance Distancia - + Azimuth Azimut - + Comment Comentario @@ -13696,47 +14786,47 @@ Instale TQSL o especifique la ruta manualmente. WCYTableModel - + Time Hora - + K - + expK - + A - + R - + SFI - + SA - + GMF - + Au @@ -13744,27 +14834,27 @@ Instale TQSL o especifique la ruta manualmente. WWVTableModel - + Time Hora - + SFI - + A - + K - + Info Info @@ -13890,37 +14980,37 @@ Instale TQSL o especifique la ruta manualmente. WsjtxTableModel - + Callsign Indicativo - + Gridsquare Cuadrícula - + Distance Distancia - + SNR - + Last Activity Última Actividad - + Last Message Último Mensaje - + Member Miembro @@ -13961,47 +15051,47 @@ Instale TQSL o especifique la ruta manualmente. main - + Run with the specific namespace. - + namespace - + Translation file - absolute or relative path and QM file name. - + path/QM-filename - + Set language. <code> example: 'en' or 'en_US'. Ignore environment setting. - + code - + Writes debug messages to the debug file Escribe mensajes de depuración en el archivo de depuración - + Process pending database import (internal use) Procesar importación pendiente de la base de datos (uso interno) - + Force update of all value lists (DXCC, SATs, etc.) Forzar actualización de todas las listas de valores (DXCC, SATs, etc.) diff --git a/i18n/qlog_fr.qm b/i18n/qlog_fr.qm index 91afb19f..bf45f94d 100644 Binary files a/i18n/qlog_fr.qm and b/i18n/qlog_fr.qm differ diff --git a/i18n/qlog_fr.ts b/i18n/qlog_fr.ts index 5d54a657..37286ff7 100644 --- a/i18n/qlog_fr.ts +++ b/i18n/qlog_fr.ts @@ -199,20 +199,118 @@ + Bandmap Guide + Aide Bandmap + + + + Guide + + + + Fields Champs - + Must not be empty Ne doit pas être vide - + + Leave unchanged + Laisser inchangé + + + + Off + Désactivé + + + Unsaved Non enregistré + + AdifRecoveryManager + + + Startup ADI found more than %1 new QSOs in %2. Use the standard Import. Load point was moved to the end of the file. + L'ADI de démarrage contient plus de %1 nouveaux QSO dans %2. Utilisez l'importation standard. Le point de chargement a été déplacé à la fin du fichier. + + + + Startup ADI Station Profile does not exist: %1 + Le profil de station pour Startup ADI n’existe pas : %1 + + + + Cannot open Startup ADI records from %1 + Impossible d’ouvrir les enregistrements Startup ADI depuis %1 + + + + Startup ADI from %1 finished with %n error(s); load point was not advanced. + + Startup ADI depuis %1 s’est terminé avec %n erreur(s) ; le point de chargement n’a pas été avancé. + + + + + + + Startup ADI was disabled for %n file(s) because the assigned Station Profile no longer exists. + + Startup ADI a été désactivé pour %n fichier(s), car le profil de station attribué n’existe plus. + + + + + + AdifRecoveryReaderWorker + + + Startup ADI filename is empty + Le nom du fichier Startup ADI est vide + + + + Startup ADI file does not exist: %1 + Le fichier Startup ADI n’existe pas : %1 + + + + Startup ADI initialized at the end of file + Startup ADI initialisé à la fin du fichier + + + + Startup ADI file was reset; load point moved to the end + Le fichier Startup ADI a été réinitialisé ; point de chargement déplacé à la fin + + + + Cannot open Startup ADI file: %1 + Impossible d’ouvrir le fichier Startup ADI : %1 + + + + Cannot seek Startup ADI file: %1 + Impossible de se positionner dans le fichier Startup ADI : %1 + + + + Cannot read Startup ADI file: %1 + Impossible de lire le fichier Startup ADI : %1 + + + + Too many ADIF records for automatic recovery + Trop d’enregistrements ADIF pour la récupération automatique + + AlertRuleDetail @@ -495,42 +593,42 @@ AlertTableModel - + Rule Name Nom de la règle - + Callsign Indicatif - + Frequency Fréquence - + Mode Mode - + Updated Mis à jour - + Last Update Dernière MAJ - + Last Comment Dernier commentaire - + Member Membre @@ -580,81 +678,86 @@ Awards Diplômes - - - Options - Options - Award Diplôme - + + 🌐 Rules + 🌐 Règles + + + My DXCC Entity Mon entité DXCC - + User Filter Filtre utilisateur - + Confirmed by Confirmé par - + LoTW LoTW - + eQSL eQSL - + Paper Papier (QSL) - + Mode Mode - + CW CW - + Phone Phonie - + Digi Numérique - + Show Afficher - + Not-Worked Only Non contactés seulement - + Not-Confirmed Only Non confirmés seulement + + + Double-click a row/cell to show QSOs + Double-cliquez sur une ligne/cellule pour afficher les QSO + DXCC @@ -666,7 +769,7 @@ Zones ITU - + WAC WAC @@ -731,79 +834,84 @@ Locator (%1 car.) - + US Counties Comtés US - + Russian Districts Districts russes (RDA) - + Japanese Cities/Ku/Guns Villes/Ku/Guns japonais (JCC/JCG) - + NZ Counties Comtés NZ - + Spanish DMEs DME espagnols - + Ukrainian Districts Districts ukrainiens (URDA) - + No User Filter Aucun filtre utilisateur - + DELETED SUPPRIMÉ - + North America Amérique du Nord - + South America Amérique du Sud - + Europe Europe - + Africa Afrique - + Oceania Océanie - + Asia Asie - - Antarctica - Antarctique + + WAAC + + + + + WAIP + @@ -829,6 +937,198 @@ En attente + + BandmapGuideDialog + + + Bandmap Guide + Aide Bandmap + + + + Import guide + Importer le guide + + + + Import + Importation + + + + Export guide + Exporter le guide + + + + Export + Exporter + + + + New guide + Nouveau guide + + + + New + Nouveau + + + + Copy guide + Copier le guide + + + + Copy + Copier + + + + Delete guide + Supprimer le guide + + + + Delete + Supprimer + + + + Guide Name: + Nom du guide : + + + + Ranges: + Plages : + + + + From + De + + + + To + À + + + + Color + Couleur + + + + Label + Étiquette + + + + Add range + Ajouter une plage + + + + Add + Ajouter + + + + Remove selected range + Supprimer la plage sélectionnée + + + + Remove + + + + + + MHz + MHz + + + + + New Guide + Nouveau guide + + + + Copy - %1 + Copie – %1 + + + + Delete Guide + Supprimer le guide + + + + Delete guide '%1'? + Supprimer le guide « %1 » ? + + + + Import Guide + Importer le guide + + + + QLog Bandmap Guide (*.qbg);;JSON (*.json) + Guide Bandmap QLog (*.qbg);;JSON (*.json) + + + + Import Failed + Import échoué + + + + Export Guide + Exporter le guide + + + + QLog Bandmap Guide (*.qbg) + Guide Bandmap QLog (*.qbg) + + + + Export Failed + Export échoué + + + + Guide Color + Couleur du guide + + + + + + QLog Warning + Avertissement QLog + + + + Guide name cannot be empty. + Le nom du guide ne peut pas être vide. + + + + Guide name '%1' is already used. + Le nom du guide « %1 » est déjà utilisé. + + + + Guide '%1' contains an invalid range. + Le guide « %1 » contient une plage non valide. + + BandmapWidget @@ -867,30 +1167,60 @@ Effacer la bande actuelle - + Bandmap Bandmap - + Show Band Afficher la bande - + Center RX Centrer sur RX - + Show Emergency Frequencies Montrer les fréquences d'urgence - + + Show IBP Frequencies + Afficher les fréquences IBP + + + + Show Guide + Afficher le guide + + + + Off + Désactivé + + + + No Guide + Aucun guide + + + + Edit Guide... + Modifier le guide... + + + SOS SOS + + + IBP + IBP + CWCatKey @@ -1167,27 +1497,27 @@ CWKeyer - + No CW Keyer Profile selected Aucun profil de manipulateur CW sélectionné - + Initialization Error Erreur d'initialisation - + Internal Error Erreur interne - + Connection Error Erreur de connexion - + Cannot open the Keyer connection Impossible d'ouvrir la connexion au manipulateur @@ -1817,198 +2147,198 @@ Importation - + Export template Exporter un modèle - + Export Exporter - + New template Nouveau modèle - + New Nouveau - + Copy existing template Copier un modèle existant - + Copy Copier - + Delete template Supprimer le modèle - + Delete Supprimer - + Template Name: Nom du modèle: - + Contest Name: Nom du concours: - + Default Mode: Mode par défaut: - + QSO Line Columns: Colonnes de ligne QSO: - + Contest name as required by the rules. It is possible to enter a custom string if it is not included in the list. Nom du concours selon les règles. Il est possible de saisir une chaîne personnalisée si elle ne figure pas dans la liste. - + Seq. - + QSO Field Champ QSO - + Formatter Formateur - + Width Largeur - + Label Étiquette - + Add line Ajouter une ligne - + Add Ajouter - + Remove selected line Supprimer la ligne sélectionnée - + Remove - + New Template Nouveau modèle - + Copy - %1 Copie – %1 - + Delete Template Supprimer le modèle - + Delete template '%1'? Supprimer le modèle «%1» ? - + Import Template Importer un modèle - - + + QLog Cabrillo Template (*.qct) Modèle Cabrillo QLog (*.qct) - + Import Failed Import échoué - + Export Template Exporter un modèle - + Export Failed Export échoué - + Failed to write file: %1 Impossible d’écrire le fichier : %1 - + File not found: %1 Fichier introuvable : %1 - + Cannot open file: %1 Impossible d’ouvrir le fichier : %1 - + Invalid template file: missing name Fichier de modèle invalide : nom manquant - + QLog Error Erreur QLog - + Cannot start database transaction. Impossible de démarrer la transaction de base de données. - + QLog Warning Avertissement QLog - + Cannot save template '%1': %2 Impossible d’enregistrer le modèle «%1» : %2 @@ -2075,20 +2405,24 @@ Formulaire - - + + Sunrise Lever soleil - - + + Sunset Coucher soleil - - + + + + + + N/A N/D @@ -2152,7 +2486,7 @@ Autres - + Done Terminé @@ -2160,12 +2494,12 @@ ColumnSettingGenericDialog - + Unselect All Tout désélectionner - + Select All Tout sélectionner @@ -2178,7 +2512,7 @@ Réglage de visibilité des colonnes - + Done Terminé @@ -4228,70 +4562,60 @@ Data - + New Entity Nouvelle entité - + New Band Nouvelle bande - + New Mode Nouveau mode - + New Band&Mode Nouvelle bande & mode - + New Slot Nouveau créneau - + Confirmed Confirmé - + Worked Contacté - + Hz Hz - + kHz kHz - + GHz GHz - + MHz MHz - - - - - - - - Yes - Oui - @@ -4299,136 +4623,146 @@ - No - Non + Yes + Oui + + + + + No + Non + + + + Requested Demandé - + Queued En attente - - - + + + Invalid Invalide - + Bureau Bureau - + Direct Direct - + Electronic Électronique - - - - - - - - + + + + + + + + Blank Vide - + Modified Modifié - + Grayline Ligne de gris - + Other Autre - + Short Path Petit chemin - + Long Path Grand chemin - + Not Heard Non entendu - + Uncertain Incertain - + Straight Key Pioche (Clé droite) - + Sideswiper Sideswiper (Cootie) - + Mechanical semi-automatic keyer or Bug Manipulateur semi-automatique (Bug) - + Mechanical fully-automatic keyer or Bug Manipulateur entièrement automatique - + Single Paddle Simple levier - + Dual Paddle Double levier (Iambique) - + Computer Driven Piloté par ordinateur - + Confirmed (AG) Confirmé (AG) - + Confirmed (no AG) Confirmé (sans AG) - + Unknown Inconnu @@ -5073,57 +5407,57 @@ Example: DxTableModel - + Time Heure - + Callsign Indicatif - + Frequency Fréquence - + Mode Mode - + Spotter Spotteur - + Comment Commentaire - + Continent Continent - + Spotter Continent Continent du spotteur - + Band Bande - + Member Membre - + Country Pays @@ -5137,7 +5471,7 @@ Example: - + Connect Connecter @@ -5302,67 +5636,67 @@ Example: DXC - Rechercher - + My Continent Mon continent - + Auto Auto - + Connecting... Connexion... - + DX Cluster is temporarily unavailable Le DX Cluster est temporairement indisponible - + DXC Server Error Erreur serveur DXC - + An invalid callsign Un indicatif invalide - + DX Cluster Password Mot de passe DX Cluster - + Security Notice Avis de sécurité - + The password can be sent via an unsecured channel Le mot de passe peut être envoyé via un canal non sécurisé - + Server Serveur - + Username Utilisateur - + Disconnect Déconnecter - + DX Cluster Command Commande DX Cluster @@ -5370,22 +5704,22 @@ Example: DxccTableModel - + Worked Contacté - + eQSL eQSL - + LoTW LoTW - + Paper Papier @@ -5501,7 +5835,7 @@ Example: - + POTA POTA @@ -5681,42 +6015,42 @@ Example: Impossible de marquer les QSO exportés comme Envoyés - + Generic Générique - + QSLs QSL - + All Tous - + Minimal Minimal - + QSL-specific Spécifique aux QSL - + Custom 1 Personnalisé 1 - + Custom 2 Personnalisé 2 - + Custom 3 Personnalisé 3 @@ -5845,122 +6179,122 @@ Ce mot de passe sera nécessaire pour les restaurer ultérieurement.Impossible de définir auto_power_on - + Cannot set no_xchg to 1 Impossible de définir no_xchg à 1 - + Rig Open Error Erreur d'ouverture du transceiver - + Set TX Frequency Error Erreur lors du réglage de la fréquence TX - + Set Frequency Error Erreur lors du réglage de la fréquence - + Set Split Error Erreur lors du réglage du split - + Set Mode Error Erreur lors du réglage du mode - + Set Split Mode Error Erreur lors du réglage du mode split - + Set PTT Error Erreur lors du réglage du PTT - + Cannot sent Morse Impossible d'envoyer du Morse - + Cannot stop Morse Impossible d'arrêter le Morse - + Get PTT Error Erreur lors de la lecture du PTT - + Get Frequency Error Erreur lors de la lecture de la fréquence - + Get Mode Error Erreur lors de la lecture du mode - + Get VFO Error Erreur lors de la lecture du VFO - + Get PWR Error Erreur lors de la lecture de la puissance - + Get PWR (power2mw) Error Erreur lors de la lecture de puissance (mW) - + Get RIT Function Error Erreur lors de la lecture de la fonction RIT - + Get RIT Error Erreur lors de la lecture du RIT - + Get XIT Function Error Erreur lors de la lecture de la fonction XIT - + Get XIT Error Erreur lors de la lecture du XIT - + Get Split Error - + Get TX Frequency Error - + Get KeySpeed Error Erreur lors de la lecture de la vitesse de manipulation - + Set KeySpeed Error Erreur lors du réglage de la vitesse de manipulation @@ -5997,140 +6331,260 @@ Ce mot de passe sera nécessaire pour les restaurer ultérieurement. ImportDialog - + Import Importation - + Date Range Plage de dates - + Import all or only QSOs from the given period Importer tous les QSO ou seulement ceux d'une période donnée - + All Tous - + File Fichier - + ADX ADX - + Browse Parcourir - + Options Options - - + + The value is used when an input record does not contain the ADIF value La valeur est utilisée lorsqu'un enregistrement d'entrée ne contient pas la valeur ADIF - + Defaults Par défaut - + + Values are used only for fields that are missing in the import file. Existing values are preserved. + Les valeurs sont utilisées uniquement pour les champs manquants dans le fichier d’importation. Les valeurs existantes sont conservées. + + + + <p>⚠ Missing QSL Sent fields are set to <b>"N"</b> (do not send) by default in ADIF. + <p>⚠ Les champs QSL Sent manquants sont définis sur <b>"N"</b> (ne pas envoyer) par défaut dans ADIF. + + + My Profile Mon profil - + My Rig Mon transceiver - - + + Comment Commentaire - + + Used only for missing QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT, and DCL_QSL_SENT fields where default is "N"; otherwise, the value from the input is used. + Utilisé uniquement pour les champs QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT et DCL_QSL_SENT manquants, où la valeur par défaut est « N » ; sinon, la valeur de l’entrée est utilisée. + + + + QSL Sent status + + + + + Used only for missing QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT, and DCL_QSL_SENT fields where default is "N"; otherwise, the value from the input is used.<p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Utilisé uniquement pour les champs QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT et DCL_QSL_SENT manquants, où la valeur par défaut est « N » ; sinon, la valeur de l’entrée est utilisée.<p><b>En file</b> (prêt), <b>Non</b> (ne pas envoyer), <b>Ignorer</b> (ne pas suivre), <b>Demandé</b> (demandé), <b>Oui</b> (déjà envoyé). + + + + Used only when the imported ADIF record does not contain the selected field. Explicit ADIF values are kept. + Utilisé uniquement lorsque l’enregistrement ADIF importé ne contient pas le champ sélectionné. Les valeurs ADIF explicites sont conservées. + + + + Default value for missing DCL_QSL_SENT. <p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Valeur par défaut pour DCL_QSL_SENT manquant. <p><b>En file</b> (prêt), <b>Non</b> (ne pas envoyer), <b>Ignorer</b> (ne pas suivre), <b>Demandé</b> (demandé), <b>Oui</b> (déjà envoyé). + + + + Default value for missing EQSL_QSL_SENT. <p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Valeur par défaut pour EQSL_QSL_SENT manquant. <p><b>En file</b> (prêt), <b>Non</b> (ne pas envoyer), <b>Ignorer</b> (ne pas suivre), <b>Demandé</b> (demandé), <b>Oui</b> (déjà envoyé). + + + + Default value for missing LOTW_QSL_SENT. <p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Valeur par défaut pour LOTW_QSL_SENT manquant. <p><b>En file</b> (prêt), <b>Non</b> (ne pas envoyer), <b>Ignorer</b> (ne pas suivre), <b>Demandé</b> (demandé), <b>Oui</b> (déjà envoyé). + + + + LoTW + LoTW + + + + DCL + DCL + + + + Paper QSL + QSL papier + + + + eQSL + eQSL + + + + Default value for missing QSL_SENT.<p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Valeur par défaut pour QSL_SENT manquant.<p><b>En file</b> (prêt), <b>Non</b> (ne pas envoyer), <b>Ignorer</b> (ne pas suivre), <b>Demandé</b> (demandé), <b>Oui</b> (déjà envoyé). + + + If DXCC is missing in the imported record, it will be resolved from the callsign. Si le DXCC manque dans l’enregistrement importé, il sera déterminé à partir de l’indicatif. - + Fill missing DXCC Entity Information Compléter les informations d’entité DXCC manquantes + + + Queued (ready to send) + En file (prêt à envoyer) + + + + Ignored (do not track) + Ignoré (ne pas suivre) + + + + Requested (requested again) + Demandé (redemandé) + + + + Yes (already sent) + Oui (déjà envoyé) + + + + Custom... + Personnalisé... + + + + Queued + + + + + Requested + + + Ignored + + + + + No + Non + + + + Yes + Oui + + + &Import &Importer - + Select File Sélectionner un fichier - - + + The values below will be used when an input record does not contain the ADIF values Les valeurs ci-dessous seront utilisées lorsqu'un enregistrement d'entrée ne contient pas les valeurs ADIF - + <p><b>In-Log QSO:</b></p><p> <p><b>QSO dans le log :</b></p><p> - + <p><b>Importing:</b></p><p> <p><b>Importation :</b></p><p> - + Duplicate QSO QSO en double - + <p>Do you want to import duplicate QSO?</p>%1 %2 <p>Voulez-vous importer le QSO en double ?</p>%1 %2 - + Save to File Sauvegarder dans un fichier - + QLog Import Summary Résumé de l'importation QLog - + Import date Date d'importation - + Imported file Fichier importé - + Imported: %n contact(s) Importé : %n contact @@ -6138,7 +6592,7 @@ Ce mot de passe sera nécessaire pour les restaurer ultérieurement. - + Warning(s): %n Avertissement : %n @@ -6146,7 +6600,7 @@ Ce mot de passe sera nécessaire pour les restaurer ultérieurement. - + Error(s): %n Erreur : %n @@ -6154,17 +6608,17 @@ Ce mot de passe sera nécessaire pour les restaurer ultérieurement. - + Details Détails - + Import Result Résultat de l'importation - + Save Details... Enregistrer les détails... @@ -6591,18 +7045,53 @@ Ce mot de passe sera nécessaire pour les restaurer ultérieurement.Importé - - + + missing QSO_DATE + QSO_DATE manquant + + + + missing CREDIT_GRANTED + CREDIT_GRANTED manquant + + + + missing CALL/DXCC + CALL/DXCC manquant + + + + no matching QSO + aucun QSO correspondant + + + + cannot update QSO %1: %2 + impossible de mettre à jour le QSO %1 : %2 + + + + matched QSO: + QSO correspondant : + + + + credit_granted: + + + + + DXCC State: État DXCC : - + Error Erreur - + Warning Avertissement @@ -6610,945 +7099,956 @@ Ce mot de passe sera nécessaire pour les restaurer ultérieurement. LogbookModel - - + + Mode/Submode + + + + + Mode: %1 +Submode: %2 + + + + + Country Pays - - + + Band Bande - - + + Mode Mode - + RST Sent RST Envoyé - + RST Rcvd RST Reçu - - + + Gridsquare Locator - + QSL Message Message QSL - - + + Comment Commentaire - - + + Notes Notes - + Paper Papier - + LoTW LoTW - + eQSL eQSL - + QSL Received QSL Reçue - + QSL Sent QSL Envoyée - + QSO ID ID QSO - + Time on Heure début - + Time off Heure fin - + Call Indicatif - + RSTs RSTe - + RSTr RSTr - + Frequency Fréquence - + Submode Sous-mode - + Name (ASCII) Nom (ASCII) - + QTH (ASCII) QTH (ASCII) - + DXCC DXCC - + Country (ASCII) Pays (ASCII) - + Continent Continent - + CQZ CQZ - + ITU ITU - + Prefix Préfixe - + State État - + County Comté - + County Alt Comté Alt - + IOTA IOTA - + QSLr QSLr - + QSLr Date Date QSLr - + QSLs QSLs - + QSLs Date Date QSLs - + LoTWr LoTWr - + LoTWr Date Date LoTWr - + LoTWs LoTWs - + LoTWs Date Date LoTWs - + TX PWR TX PWR - + Additional Fields Champs additionnels - + Address (ASCII) Adresse (ASCII) - + Address Adresse - + Age Âge - + Altitude Altitude - + A-Index Indice A - + Antenna Az Azimut Antenne - + Antenna El Élévation Antenne - + Signal Path Chemin du Signal - + ARRL Section Section ARRL - + Award Submitted Récompense soumise - + Award Granted Récompense accordée - + Band RX Bande RX - + Gridsquare Extended Locator étendu - + Contest Check Vérification Concours - + Class Classe - + ClubLog Upload Date Date d'envoi ClubLog - + ClubLog Upload State État d'envoi ClubLog - + Comment (ASCII) Commentaire (ASCII) - + Contacted Operator Opérateur contacté - + Contest ID ID Concours - + Credit Submitted Crédit soumis - + Credit Granted Crédit accordé - + DOK DOK - + DCLr Date Date DCLr - + DCLs Date Date DCLs - + DCLr DCLr - + DCLs DCLs - + Distance Distance - + Email Email - - + + Owner Callsign Indicatif du propriétaire - + eQSL AG eQSL AG - + eQSLr Date Date eQSLr - + eQSLs Date Date eQSLs - + eQSLr eQSLr - + eQSLs eQSLs - + FISTS Number Numéro FISTS - + FISTS CC FISTS CC - + EME Init Init EME - + Frequency RX Fréquence RX - + Guest Operator Opérateur invité - + HamlogEU Upload Date Date d'envoi HamlogEU - + HamlogEU Upload Status Statut d'envoi HamlogEU - + HamQTH Upload Date Date d'envoi HamQTH - + HamQTH Upload Status Statut d'envoi HamQTH - + HRDLog Upload Date Date d'envoi HRDLog - + HRDLog Upload Status Statut d'envoi HRDLog - + IOTA Island ID ID Île IOTA - + K-Index Indice K - + Latitude Latitude - + Longitude Longitude - + Max Bursts Rafales max (Bursts) - + CW Key Info Infos clé CW - + CW Key Type Type de clé CW - + MS Shower Name Essaim MS (Météor Scatter) - + My Altitude Mon altitude - + My Antenna (ASCII) Mon antenne (ASCII) - + My Antenna Mon antenne - + My City (ASCII) Ma ville (ASCII) - + My City Ma ville - + My County Mon département/comté - + My County Alt Mon département (Alt) - + My Country (ASCII) Mon pays (ASCII) - + My Country Mon pays - + My CQZ Ma zone CQ - + My DARC DOK Mon DOK (DARC) - + My DXCC Mon DXCC - + My FISTS Mon n° FISTS - + My Gridsquare Mon Locator - + My Gridsquare Extended Mon Locator (étendu) - + My IOTA Mon IOTA - + My IOTA Island ID Mon ID d'île IOTA - + My ITU Ma zone ITU - + My Latitude Ma latitude - + My Longitude Ma longitude - + My CW Key Info Infos sur ma clé CW - + My CW Key Type Mon type de clé CW - + My Name (ASCII) Mon nom (ASCII) - + My Name Mon nom - + My Postal Code (ASCII) Mon code postal (ASCII) - + My Postal Code Mon code postal - + My POTA Ref Ma réf. POTA - + My Rig (ASCII) Ma station/TRX (ASCII) - + My Rig Ma station/TRX - + My Special Interest Activity (ASCII) Mon activité spéciale (ASCII) - + My Special Interest Activity Mon activité spéciale - + My Spec. Interes Activity Info (ASCII) Infos activité spéc. (ASCII) - + My Spec. Interest Activity Info Infos activité spéc. - + My SOTA Mon SOTA - + My State Mon État/Province - - + + My Street Ma rue - + My USA-CA Counties Mes comtés USA-CA - + My VUCC Grids Mes locators VUCC - + Name Nom - + Notes (ASCII) Notes (ASCII) - + #MS Bursts Nb rafales MS - + #MS Pings Nb pings MS - + Operator Callsign Indicatif de l'opérateur - + POTA POTA - + Contest Precedence Precedence (Contest) - + Propagation Mode Mode de propagation - + Public Encryption Key Clé de chiffrement publique - + QRZ Download Date Date de téléchargement QRZ - + QRZ Download Status Statut téléchargement QRZ - + QRZ Upload Date Date d'envoi vers QRZ - + QRZ Upload Status Statut envoi vers QRZ - + QSLs Message (ASCII) Message QSL envoyée (ASCII) - + QSLs Message Message QSL envoyée - + QSLr Message Message QSL reçue - + QSLr Via QSL reçue via - + QSLs Via QSL envoyée via - + QSL Via QSL via - + QSO Completed QSO terminé - + QSO Random QSO aléatoire - + QTH QTH - + Region Région - + Rig (ASCII) TRX (ASCII) - + Rig TRX - + RcvPWR Puissance reçue - + SAT Mode Mode Satellite - + SAT Name Nom du Satellite - + Solar Flux Flux solaire - + SIG (ASCII) SIG (ASCII) - + SIG SIG - + SIG Info (ASCII) Infos SIG (ASCII) - + SIG Info Infos SIG - + Silent Key Silent Key (SK) - + SKCC Member Membre SKCC - + SOTA SOTA - + RcvNr N° reçu - + RcvExch Échange reçu - + Logging Station Callsign Indicatif de la station de saisie - + SentNr N° envoyé - + SentExch Échange envoyé - + SWL SWL - + Ten-Ten Number Numéro Ten-Ten - + UKSMG Member Membre UKSMG - + USA-CA Counties Comtés USA-CA - + VE Prov Province VE - + VUCC VUCC - + Web Web - + My ARRL Section Ma section ARRL - + My WWFF Mon WWFF - + WWFF WWFF @@ -7557,8 +8057,8 @@ Ce mot de passe sera nécessaire pour les restaurer ultérieurement.LogbookWidget - - + + Delete Supprimer @@ -7645,137 +8145,137 @@ Ce mot de passe sera nécessaire pour les restaurer ultérieurement. - + Callsign Indicatif - + Gridsquare Locator - + POTA POTA - + SOTA SOTA - + WWFF WWFF - + SIG SIG - + IOTA IOTA - + All Bands Toutes les bandes - + All Modes Tous les modes - + All Countries Tous les pays - + No User Filter Aucun filtre utilisateur - + QLog Warning Avertissement QLog - + Each batch supports up to 100 QSOs. Chaque lot supporte jusqu'à 100 QSO. - + QSOs Update Progress Progression de la mise à jour des QSO - - - + + + Cancel Annuler - - - + + + QLog Error Erreur QLog - + Callbook login failed Échec de connexion au Callbook - + Callbook error: Erreur Callbook : - + All Clubs Tous les clubs - + Delete the selected contacts? Supprimer les contacts sélectionnés ? - + Clublog's <b>Immediately Send</b> supports only one-by-one deletion<br><br>Do you want to continue despite the fact<br>that the DELETE operation will not be sent to Clublog? L'option <b>Envoi immédiat</b> de Clublog ne supporte que la suppression unitaire.<br><br>Voulez-vous continuer sachant que l'opération de SUPPRESSION ne sera pas transmise à Clublog ? - + Deleting QSOs Suppression des QSO en cours - + Update Mettre à jour - + By updating, all selected rows will be affected.<br>The value currently edited in the column will be applied to all selected rows.<br><br>Do you want to edit them? La mise à jour affectera toutes les lignes sélectionnées.<br>La valeur éditée dans la colonne sera appliquée à toute la sélection.<br><br>Voulez-vous valider la modification ? - + Count: %n Nombre : %n @@ -7783,25 +8283,60 @@ Ce mot de passe sera nécessaire pour les restaurer ultérieurement. - + Downloading eQSL Image Téléchargement de l'image eQSL - + eQSL Download Image failed: Échec du téléchargement de l'image eQSL : + + LotwDXCCCreditDownloader + + + Cannot open test LoTW DXCC credit file + Impossible d’ouvrir le fichier de test des crédits DXCC LoTW + + + + + Incomplete LoTW DXCC credit response + Réponse incomplète des crédits DXCC LoTW + + + + + Cannot open temporary file + Impossible d'ouvrir le fichier temporaire + + + + LoTW is not configured properly + LoTW n’est pas configuré correctement + + + + LoTW returned a non-ADIF response + LoTW a renvoyé une réponse non ADIF + + + + Incorrect login or password + Identifiant ou mot de passe incorrect + + LotwQSLDownloader - + Cannot open temporary file Impossible d'ouvrir le fichier temporaire - + Incorrect login or password Identifiant ou mot de passe incorrect @@ -7809,73 +8344,73 @@ Ce mot de passe sera nécessaire pour les restaurer ultérieurement. LotwUploader - + Upload cancelled by user Envoi annulé par l'utilisateur - + Upload rejected by LoTW Envoi rejeté par LoTW - + Unexpected response from TQSL server Réponse inattendue du serveur TQSL - + TQSL utility error Erreur de l'utilitaire TQSL - + TQSLlib error Erreur TQSLlib - + Unable to open input file Impossible d'ouvrir le fichier d'entrée - + Unable to open output file Impossible d'ouvrir le fichier de sortie - + All QSOs were duplicates or out of date range Tous les QSO étaient des doublons ou hors plage de dates - + Some QSOs were duplicates or out of date range Certains QSO étaient des doublons ou hors plage de dates - + Command syntax error Erreur de syntaxe de commande - + LoTW Connection error (no network or LoTW is unreachable) Erreur de connexion LoTW (pas de réseau ou LoTW injoignable) - - + + Unexpected Error from TQSL Erreur inattendue de TQSL - + TQSL not found TQSL introuvable - + TQSL crashed TQSL a planté @@ -7913,620 +8448,684 @@ Ce mot de passe sera nécessaire pour les restaurer ultérieurement.Se&rvices - - + + Contest Concours (Contest) - + Dupe Check Vérif. Doublons - + Sequence Séquence - + Linking Exchange With Lier l'échange avec - + Toolbar Barre d'outils - - + + Clock Horloge - - + + Map Carte - - + + DX Cluster DX Cluster - + WSJTX WSJTX - - + + Rotator Rotor - - + + Bandmap Carte des bandes - - + + Rig TRX - - + + Online Map Carte en ligne - - + + CW Console Console CW - - + + Chat Chat - - + + Profile Image Image de profil - - + + Alerts Alertes - + Quit Quitter - + Application - Quit Quitter l'application - + &Settings &Réglages - - - - - - - + + + + + + + Pack Data && Settings Sauvegarder données && réglages - - + + Unpack Data && Settings Restaurer données && réglages - - + + New QSO - Clear Nouveau QSO - Effacer - + &Import &Importer - + &Export &Exporter - + Connect R&ig Connecter le T&RX - + &About À &propos - - + + New QSO - Save Nouveau QSO - Enregistrer - + S&tatistics S&tatistiques - + QSL &Gallery &Galerie QSL - + Developer Tools Outils dev - + Run custom read-only SQL queries against the logbook database Exécuter des requêtes SQL personnalisées en lecture seule sur la base de données du logbook - - Print QSL &Labels - &Imprimer les étiquettes QSL + + Print QS&L + Imprimer QS&L - + Wsjtx WSJTX - + Connect R&otator Connecter le r&otor - + QSO &Filters &Filtres de QSO - + &Awards &Diplômes - + DXCC &Submission List &Liste de soumission DXCC - + Generate a list of contacts to submit for ARRL DXCC award credit Générer une liste de contacts à soumettre pour le crédit ARRL DXCC - + Edit Rules Modifier les règles - + Beep Bip - + Connect &CW Keyer Connecter le manipulateur &CW - + &Wiki &Wiki - + Report &Bug... Signaler un &bug... - + &Manual Entry Saisie &manuelle - + Switch New Contact dialog to the manually entry mode<br/>(time, freq, profiles etc. are not taken from their common sources) Passer la saisie de contact en mode manuel<br/>(l'heure, la fréquence et les profils ne sont plus synchronisés) - + Mailing List... Liste de diffusion... - + Edit Modifier - - + + Save Arrangement Enregistrer la disposition - + Keep Options Conserver les options - + Restore connection options after application restart Restaurer les options de connexion au redémarrage - + Logbook - Search Callsign Carnet de trafic - Chercher indicatif - - + + New QSO - Add text from Callsign field to Bandmap Nouveau QSO - Ajouter l'indicatif à la carte des bandes - + Rig - Band Down TRX - Bande inférieure - + Rig - Band Up TRX - Bande supérieure - + New QSO - Use Callsign from the Whisperer Nouveau QSO - Utiliser l'indicatif du Whisperer - + CW Console - Key Speed Up Console CW - Augmenter la vitesse - + CW Console - Key Speed Down Console CW - Diminuer la vitesse - + CW Console - Profile Up Console CW - Profil suivant - + CW Console - Profile Down Console CW - Profil précédent - + Rig - PTT On/Off TRX - PTT On/Off - + Clear Effacer - + Show Alerts Afficher les alertes - + All Bands Toutes bandes - + Each Band Par bande - + Each Band && Mode Par bande && mode - + No Check Pas de vérif. - + Single Simple - + Per Band Par bande - + Stop Arrêt - + Reset Réinitialiser - + None Aucun - + Upload Envoyer - + Service - Upload QSOs Service - Envoi des QSO - + Download QSLs Récupérer QSL - + Service - Download QSLs Service - Récupération des QSL - + + Download LoTW DXCC Credits + Télécharger les crédits DXCC LoTW + + + + Service - Download LoTW DXCC Credits + Service - Télécharger les crédits DXCC LoTW + + + Theme: Native Thème : Natif - + Theme: QLog Light Thème : QLog Clair - + Theme: QLog Dark Thème : QLog Sombre - + What's New Nouveautés - + Export Cabrillo Exporter Cabrillo - + Color Theme Thème de couleur - + Not enabled for non-Fusion style Non activé pour le style hors-Fusion - + Press to tune the alert Appuyez pour régler l'alerte - + + Startup ADI + + + + Clublog Immediately Upload Error Erreur d'envoi immédiat Clublog - - - + + + <b>Error Detail:</b> <b>Détail de l'erreur :</b> - + op: op : - + A New Version Nouvelle version disponible - + A new version %1 is available. Une nouvelle version %1 est disponible. - + Remind Me Later Me le rappeler plus tard - + Download Télécharger - + + + QLog Warning + Avertissement QLog + + + + LoTW is not configured properly.<p>Please, use <b>Settings</b> dialog to configure it.</p> + LoTW n’est pas configuré correctement.<p>Utilisez la boîte de dialogue <b>Paramètres</b> pour le configurer.</p> + + + + + QLog Error + Erreur QLog + + + + Cannot load local DXCC entities from the logbook: + Impossible de charger les entités DXCC locales depuis le journal : + + + + Unknown DXCC Entity + Entité DXCC inconnue + + + + Cannot determine a local DXCC entity from logbook contacts. + Impossible de déterminer une entité DXCC locale à partir des contacts du journal. + + + + LoTW DXCC Credits + Crédits DXCC LoTW + + + + Select the local DXCC entity for which LoTW DXCC credits will be downloaded: + Sélectionnez l’entité DXCC locale pour laquelle les crédits DXCC LoTW seront téléchargés : + + + + Cancel + Annuler + + + + Downloading LoTW DXCC credits + Téléchargement des crédits DXCC LoTW + + + + Processing LoTW DXCC credits + Traitement des crédits DXCC LoTW + + + + LoTW DXCC Credit Import Summary + Résumé de l’importation des crédits DXCC LoTW + + + + LoTW DXCC credit import failed: + Échec de l’importation des crédits DXCC LoTW : + + + Failed to encrypt credentials. Échec du chiffrement des identifiants. - + Database files (*.dbe);;All files (*) Fichiers base de données (*.dbe);;Tous les fichiers (*) - + Failed to create temporary file. Échec de création du fichier temporaire. - + Failed to dump the database. Échec de l'exportation de la base de données. - + Compressing database... Compression de la base... - + Database successfully dumped to %1 Base de données exportée avec succès vers %1 - + Failed to compress the database. Échec de la compression de la base de données. - + Failed to prepare database for import. Échec de la préparation de la base pour l'importation. - + Classic Classique - + Do you want to remove the Contest filter %1? Voulez-vous supprimer le filtre de concours %1 ? - + Contest: Concours : - + <h1>QLog %1</h1><p>&copy; 2019 Thomas Gatzweiler DL2IC<br/>&copy; 2021-2026 Ladislav Foldyna OK1MLG<br/>&copy; 2025-2026 Michael Morgan AA5SH<br/>&copy; 2025-2026 Kyle Boyle VE9KZ</p><p>Based on Qt %2<br/>%3<br/>%4<br/>%5</p><p>Icon by <a href='http://www.iconshock.com'>Icon Shock</a><br />Satellite images by <a href='http://www.nasa.gov'>NASA</a><br />ZoneDetect by <a href='https://github.com/BertoldVdb/ZoneDetect'>Bertold Van den Bergh</a><br />TimeZone Database by <a href='https://github.com/evansiroky/timezone-boundary-builder'>Evan Siroky</a> <h1>QLog %1</h1><p>&copy; 2019 Thomas Gatzweiler DL2IC<br/>&copy; 2021-2026 Ladislav Foldyna OK1MLG<br/>&copy; 2025-2026 Michael Morgan AA5SH<br/>&copy; 2025-2026 Kyle Boyle VE9KZ</p><p>Basé sur Qt %2<br/>%3<br/>%4<br/>%5</p><p>Icônes par <a href='http://www.iconshock.com'>Icon Shock</a><br />Images satellites par <a href='http://www.nasa.gov'>NASA</a><br />ZoneDetect par <a href='https://github.com/BertoldVdb/ZoneDetect'>Bertold Van den Bergh</a><br />Base de données TimeZone par <a href='https://github.com/evansiroky/timezone-boundary-builder'>Evan Siroky</a> - + About À propos - + N/A N/D - MapWebChannelHandler + MapPageController - - - - Grid - Grille/Locator + + Aurora + Aurore - - - - Gray-Line - Ligne de gris (Gray-Line) + + Beam + Azimut (Beam) - - - - Beam - Rayon (Beam) + + Chat + Chat - - - - Aurora - Aurore + + Grid + Grille/Locator - - - - MUF - MUF + + Gray-Line + Ligne de gris (Gray-Line) - - - + IBP IBP - - - - Chat - Chat + + MUF + MUF - - - + WSJTX - CQ WSJTX - Appels CQ - - - + Path Chemin/Trajet @@ -8787,122 +9386,122 @@ Ce mot de passe sera nécessaire pour les restaurer ultérieurement.Échec de connexion au Callbook - + LP LP (Long Path) - + New Entity! Nouvelle Entité ! - + New Band! Nouvelle Bande ! - + New Mode! Nouveau Mode ! - + New Band & Mode! Nouveau Bande & Mode ! - + New Slot! Nouveau Slot ! - + Worked Contacté (Worked) - + Confirmed Confirmé - + GE GE (Good Evening) - + GM GM (Good Morning) - + GA GA (Good Afternoon) - + m m - + Callbook search is inactive Recherche Callbook inactive - + Callbook search is active Recherche Callbook active - + Contest ID must be filled in to activate L'ID du Concours doit être renseigné pour l'activation - + two or four adjacent Maidenhead grid locators, each four characters long, (ex. EN98,FM08,EM97,FM07) deux ou quatre locators adjacents (Maidenhead), chacun de quatre caractères (ex. JN18,JN19...) - + the contacted station's DARC DOK (District Location Code) (ex. A01) le DOK (District Location Code) DARC de la station contactée (ex. A01) - + World Wide Flora & Fauna World Wide Flora & Fauna (WWFF) - + Special Activity Group Groupe d'Activité Spéciale - + Special Activity Group Information Informations du Groupe d'Activité Spéciale - + It is not the name of the contest but it is an assigned<br>Contest ID (ex. CQ-WW-CW for CQ WW DX Contest (CW)) Ce n'est pas le nom complet du concours mais l'ID attribué<br>(ex. CQ-WW-CW pour le CQ WW DX Contest CW) - + Blank Vide - + W W - + Description of the contacted station's equipment Description de l'équipement de la station contactée @@ -9116,7 +9715,7 @@ Vous pouvez laisser les champs vides et les configurer plus tard dans les Param QCoreApplication - + QLog Help Aide de QLog @@ -9146,31 +9745,31 @@ Vous pouvez laisser les champs vides et les configurer plus tard dans les Param - + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + QLog Warning Avertissement QLog @@ -9200,63 +9799,63 @@ Vous pouvez laisser les champs vides et les configurer plus tard dans les Param Erreur réseau. Impossible de télécharger la liste des clubs pour - - - + + + - - - - + + + + QLog Error Erreur QLog - + QLog is already running QLog est déjà en cours d'exécution - + Failed to process pending database import. Échec du traitement de l'importation de la base de données en attente. - + The database was imported successfully, but the stored passwords could not be restored (decryption failed or the data is corrupted). All service passwords have been cleared and must be re-entered in Settings. La base de données a été importée avec succès, mais les mots de passe stockés n'ont pas pu être restaurés (échec du déchiffrement ou données corrompues). Tous les mots de passe des services ont été effacés et doivent être saisis à nouveau dans les Paramètres. - + Could not connect to database. Impossible de se connecter à la base de données. - + Could not export a QLog database to ADIF as a backup.<p>Try to export your log to ADIF manually Impossible d'exporter la base QLog vers ADIF pour sauvegarde.<p>Essayez d'exporter votre carnet de trafic en ADIF manuellement - + Database migration failed. La migration de la base de données a échoué. - + - + QLog Info Info QLog - + Activity name is already exists. Ce nom d'activité existe déjà. @@ -9281,33 +9880,33 @@ Vous pouvez laisser les champs vides et les configurer plus tard dans les Param Impossible de mettre à jour les règles d'alerte - + DXC Server Name Error Erreur de nom de serveur DXC - + DXC Server address must be in format<p><b>[username@]hostname:port</b> (ex. hamqth.com:7300)</p> L'adresse du serveur DXC doit être au format<p><b>[utilisateur@]hôte:port</b> (ex. hamqth.com:7300)</p> - + DX Cluster Password Mot de passe DX Cluster - + Invalid Password Mot de passe invalide - + DXC Server Connection Error Erreur de connexion au serveur DXC - + Filename is empty Le nom de fichier est vide @@ -9342,128 +9941,128 @@ Vous pouvez laisser les champs vides et les configurer plus tard dans les Param - + Filter name is already exists. Ce nom de filtre existe déjà. - + <b>Rig Error:</b> <b>Erreur Transceiver :</b> - + <b>Rotator Error:</b> <b>Erreur Rotor :</b> - + <b>CW Keyer Error:</b> <b>Erreur Keyer CW :</b> - + The fields <b>%0</b> will not be saved because the <b>%1</b> is not filled. Les champs <b>%0</b> ne seront pas sauvegardés car <b>%1</b> n'est pas renseigné. - + Your callsign is empty. Please, set your Station Profile Votre indicatif est vide. Veuillez configurer votre profil de station - + Cannot update QSO Filter Conditions Impossible de mettre à jour les conditions du filtre QSO - - + + Please, define at least one Station Locations Profile Veuillez définir au moins un profil d'emplacement de station - + WSJTX Multicast is enabled but the Address is not a multicast address. Le multicast WSJT-X est activé mais l'adresse n'est pas une adresse multicast. - + Loop detected. Raw UDP forward uses the same port as the WSJT-X receiving port. Boucle détectée. Le transfert UDP brut utilise le même port que le port de réception WSJT-X. - + Rig port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device Le port du transceiver doit être un port COM valide.<br>Pour Windows, utilisez COMxx, pour les OS de type Unix, utilisez un chemin vers le périphérique - + Rig PTT port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device Le port PTT doit être un port COM valide.<br>Pour Windows, utilisez COMxx, pour les OS de type Unix, utilisez un chemin vers le périphérique - + <b>TX Range</b>: Max Frequency must not be 0. <b>Plage TX</b> : La fréquence max ne doit pas être 0. - + <b>TX Range</b>: Max Frequency must not be under Min Frequency. <b>Plage TX</b> : La fréquence max ne doit pas être inférieure à la fréquence min. - + Rotator port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device Le port du rotor doit être un port COM valide.<br>Pour Windows, utilisez COMxx, pour les OS de type Unix, utilisez un chemin vers le périphérique - + CW Keyer port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device Le port du keyer CW doit être un port COM valide.<br>Pour Windows, utilisez COMxx, pour les OS de type Unix, utilisez un chemin vers le périphérique - + Cannot change the CW Keyer Model to <b>Morse over CAT</b><br>No Morse over CAT support for Rig(s) <b>%1</b> Impossible de changer le modèle de keyer CW en <b>Morse via CAT</b><br>Pas de support Morse via CAT pour le(s) poste(s) <b>%1</b> - + Cannot delete the CW Keyer Profile<br>The CW Key Profile is used by Rig(s): <b>%1</b> Impossible de supprimer le profil de keyer CW<br>Ce profil est utilisé par le(s) poste(s) : <b>%1</b> - + Callsign has an invalid format Le format de l'indicatif est invalide - + Operator Callsign has an invalid format L'indicatif de l'opérateur a un format invalide - + Gridsquare has an invalid format Le format du Locator est invalide - + VUCC Grids have an invalid format (must be 2 or 4 Gridsquares separated by ',') Les carrés VUCC ont un format invalide (doit être 2 ou 4 Locators séparés par des virgules) - + Country must not be empty Le pays ne doit pas être vide - + CQZ must not be empty La zone CQ ne doit pas être vide - + ITU must not be empty La zone ITU ne doit pas être vide @@ -9471,37 +10070,37 @@ Vous pouvez laisser les champs vides et les configurer plus tard dans les Param QObject - + Importing Database Importation de la base de données - + Opening Database Ouverture de la base de données - + Backuping Database Sauvegarde de la base de données - + Migrating Database Migration de la base de données - + Starting Application Démarrage de l'application - + km km - + miles milles @@ -9603,52 +10202,52 @@ Vous pouvez laisser les champs vides et les configurer plus tard dans les Param - + Connection Refused Connexion refusée - + Host closed the connection L'hôte a fermé la connexion - + Host not found Hôte introuvable - + Timeout Délai d'attente dépassé - + Network Error Erreur Réseau - + Internal Error Erreur Interne - + Cannot connect to DXC Server <p>Reason <b>: Impossible de se connecter au serveur DXC <p>Raison <b> : - + <b>Imported</b>: %n contact(s) <b>Importé</b> : %n contact @@ -9656,7 +10255,7 @@ Vous pouvez laisser les champs vides et les configurer plus tard dans les Param - + <b>Warning(s)</b>: %n <b>Avertissement</b> : %n @@ -9664,7 +10263,7 @@ Vous pouvez laisser les champs vides et les configurer plus tard dans les Param - + <b>Error(s)</b>: %n <b>Erreur</b> : %n @@ -9742,6 +10341,32 @@ Vous pouvez laisser les champs vides et les configurer plus tard dans les Param Worked + + + IARU Region 1 + + + + + + Failed to write file: %1 + Impossible d’écrire le fichier : %1 + + + + Cannot open file: %1 + Impossible d’ouvrir le fichier : %1 + + + + Invalid guide file: %1 + Fichier guide non valide : %1 + + + + Invalid guide file: missing title + Fichier guide non valide : titre manquant + QRZCallbook @@ -9754,7 +10379,7 @@ Vous pouvez laisser les champs vides et les configurer plus tard dans les Param QRZUploader - + General Error Erreur générale @@ -9777,33 +10402,33 @@ Vous pouvez laisser les champs vides et les configurer plus tard dans les Param Trier par : - + Export Filtered Exporter filtrés - + Date (Newest) Date (plus récent) - + Date (Oldest) Date (plus ancien) - + Callsign (A-Z) Indicatif (A-Z) - + Callsign (Z-A) Indicatif (Z-A) - - + + %n QSL card(s) %n carte QSL @@ -9811,72 +10436,72 @@ Vous pouvez laisser les champs vides et les configurer plus tard dans les Param - + All QSL Cards Toutes les cartes QSL - + Favorites Favoris - + By Country Par Pays - + By Date Par Date - + By Band Par Bande - + By Mode Par Mode - + By Continent Par Continent - + Remove from Favorites Retirer des favoris - + Add to Favorites Ajouter aux favoris - + Open Ouvrir - + Save... Enregistrer... - + Save QSL Card Enregistrer la carte QSL - + Export QSL Cards Exporter les cartes QSL - + Exported %1 of %2 cards %1 cartes sur %2 exportées @@ -9919,28 +10544,23 @@ Vous pouvez laisser les champs vides et les configurer plus tard dans les Param Détails - - New QSLs: - Nouvelles QSL : + + New QSLs: + Nouvelles QSL : - Updated QSOs: - QSO mis à jour : + Updated QSOs: + QSO mis à jour : - - Unmatched QSLs: - QSL non appariées : + + Unmatched QSLs: + QSL non associées : QSLPrintLabelDialog - - - Print QSL Labels - Imprimer les étiquettes QSL - Filter @@ -9972,235 +10592,397 @@ Vous pouvez laisser les champs vides et les configurer plus tard dans les Param Filtre utilisateur - + Label Template Modèle d’étiquette - + + Page Size: Taille de page: - + Columns: Colonne: - + Rows: Ligne: - + + Label Width: Largeur de l’étiquette : - + + Print QSL Labels / Cards + Imprimer les étiquettes / cartes QSL + + + + Print Mode + Mode d’impression + + + + Mode: + Mode: + + + + + QSL Card + Carte QSL + + + + Card Width: + Largeur de carte : + + + + Card Height: + Hauteur de carte : + + + + Card Gap: + Espacement des cartes : + + + + Label Height: Hauteur de l’étiquette: - + + Label X Offset: + Décalage X de l’étiquette : + + + + Label Y Offset: + Décalage Y de l’étiquette : + + + + Label Background: + Fond de l’étiquette : + + + + Fill under label + Remplir sous l’étiquette + + + + + Color + Couleur + + + + Background Image: + Image d’arrière-plan : + + + + Browse + Parcourir + + + + Clear + Effacer + + + Left Margin: Marge gauche: - + Top Margin: Marge supérieure: - + H Spacing: Espacement horizontal: - + V Spacing: Espacement vertical: - + Label Appearance Apparence de l’étiquette - + Print Label Borders Imprimer les bordures des étiquettes - + QSOs per Label: QSO par étiquette: - + Footer Left Text: Texte de pied de page gauche: - + Footer Right Text: Texte de pied de page droit: - + Skip Label: Ignorer l’étiquette: - + Sans Font: Police sans-serif: - + Mono Font: Police monospace: - + + Text Color: + Couleur du texte : + + + Callsign Size: Taille de l’indicatif: - + "To Radio" Size: Taille « To Radio » : - + "To Radio" Text: Texte « To Radio » : - + Header Size: Taille de l’en-tête: - + Data Size: Taille des données: - + Date Header Text: Texte d’en-tête de date: - + Date Format: Format de date: - + Time Header Text: Texte d’en-tête de l’heure: - + Band Header Text: Texte d’en-tête de bande: - + Mode Header Text: Texte d’en-tête du mode: - + QSL Header Text: Texte d’en-tête QSL: - + Extra Column: Colonne supplémentaire: - + Extra Column Text Texte de la colonne supplémentaire - + (DB column name) (nom de colonne BD) - - + + No matching QSOs found Aucun QSO correspondant trouvé - - + + Page 0 of 0 Page 0 sur 0 - + Labels: 0 (0 pages) Étiquettes : 0 (0 pages) - + Print Imprimer - + Export as PDF Exporter en PDF - - + + Export as Images + Exporter images + + + + Label Sheet + Feuille d’étiquettes + + + + Custom Personnalisé - + Empty Vide - + QSOs matching this station profile QSO correspondant à ce profil de station - + + Select Label Text Color + Sélectionner la couleur du texte de l’étiquette + + + + Select Label Background Color + Sélectionner la couleur de fond de l’étiquette + + + + + + Select QSL Card Background + Sélectionner l’arrière-plan de la carte QSL + + + + Images (*.png *.jpg *.jpeg *.bmp) + Images (*.png *.jpg *.jpeg *.bmp) + + + + Cannot read selected image file. + Impossible de lire le fichier image sélectionné. + + + + Selected file is not a valid image. + Le fichier sélectionné n’est pas une image valide. + + + + Cards: %1 (%2 pages) + Cartes : %1 (%2 pages) + + + Labels: %1 (%2 pages) Étiquettes : %1 (%2 pages) - + Page %1 of %2 Page %1 sur %2 - + Export PDF Exporter en PDF - + PDF Files (*.pdf) Fichiers PDF (*.pdf) - + + + + Export QSL Card Images + Exporter les images de cartes QSL + + + + Some image files already exist. Overwrite them? + Certains fichiers image existent déjà. Les remplacer ? + + + + Exported %n QSL card image(s). + + %n image(s) de carte QSL exportée(s). + + + + + + Exported %1 of %2 QSL card images. + %1 image(s) de carte QSL exportée(s) sur %2. + + + + QSOs were not marked as sent. + Les QSO n’ont pas été marqués comme envoyés. + + + Mark as Sent Marquer comme envoyé - + Mark printed/exported QSOs as sent? Marquer les QSO imprimés/exportés comme envoyés ? @@ -10257,7 +11039,7 @@ Vous pouvez laisser les champs vides et les configurer plus tard dans les Param - + Blank Vide @@ -10670,318 +11452,318 @@ Vous pouvez laisser les champs vides et les configurer plus tard dans les Param Membre : - + &Reset &Réinitialiser - + &Lookup &Recherche - - - + + + No Non - - - + + + Yes Oui - - - + + + Requested Demandée - - - + + + Queued En attente - - - + + + Ignored Ignorée - + Bureau Bureau - + Direct Direct - + Electronic Électronique - + Submit changes Soumettre les modifications - + Really submit all changes? Voulez-vous vraiment soumettre toutes les modifications ? - - - - + + + + QLog Error Erreur QLog - + Cannot save all changes - internal error Impossible de sauvegarder les modifications - erreur interne - + Cannot save all changes - try to reset all changes Impossible de sauvegarder - essayez de réinitialiser les modifications - + QSO Detail Détail du QSO - + Edit QSO Modifier le QSO - + Downloading eQSL Image Téléchargement de l'image eQSL - + Cancel Annuler - + eQSL Download Image failed: Échec du téléchargement de l'image eQSL : - + DX Callsign must not be empty L'indicatif DX ne doit pas être vide - + DX callsign has an incorrect format Le format de l'indicatif DX est incorrect - - + + TX Frequency or Band must be filled La fréquence TX ou la bande doit être renseignée - + TX Band should be La bande TX devrait être - + RX Band should be La bande RX devrait être - - + + DX Grid has an incorrect format Le format du locator DX est incorrect - + Based on callsign, DXCC Country is different from the entered value - expecting D'après l'indicatif, le pays DXCC diffère de la valeur saisie - attendu : - + Based on callsign, DXCC Continent is different from the entered value - expecting D'après l'indicatif, le continent DXCC diffère de la valeur saisie - attendu : - + Based on callsign, DXCC ITU is different from the entered value - expecting D'après l'indicatif, la zone ITU diffère de la valeur saisie - attendu : - + Based on callsign, DXCC CQZ is different from the entered value - expecting D'après l'indicatif, la zone CQ diffère de la valeur saisie - attendu : - + VUCC has an incorrect format Le format VUCC est incorrect - + Based on Frequencies, Sat Mode should be D'après les fréquences, le mode Sat devrait être - + blank vide - + Sat name must not be empty Le nom du satellite ne doit pas être vide - + Own Callsign must not be empty Votre indicatif ne doit pas être vide - + Own callsign has an incorrect format Le format de votre indicatif est incorrect - + Own VUCC Grids have an incorrect format Le format de vos locators VUCC est incorrect - + Based on own callsign, own DXCC ITU is different from the entered value - expecting D'après votre indicatif, votre zone ITU diffère de la valeur saisie - attendu : - + Based on own callsign, own DXCC CQZ is different from the entered value - expecting D'après votre indicatif, votre zone CQ diffère de la valeur saisie - attendu : - + Based on own callsign, own DXCC Country is different from the entered value - expecting D'après votre indicatif, votre pays DXCC diffère de la valeur saisie - attendu : - + Based on SOTA Summit, QTH does not match SOTA Summit Name - expecting D'après le sommet SOTA, le QTH ne correspond pas au nom du sommet - attendu : - + Based on SOTA Summit, Grid does not match SOTA Grid - expecting D'après le sommet SOTA, le locator ne correspond pas au locator SOTA - attendu : - + Based on POTA record, QTH does not match POTA Name - expecting D'après la référence POTA, le QTH ne correspond pas au nom POTA - attendu : - + Based on POTA record, Grid does not match POTA Grid - expecting D'après la référence POTA, le locator ne correspond pas au locator POTA - attendu : - + Based on SOTA Summit, my QTH does not match SOTA Summit Name - expecting D'après le sommet SOTA, mon QTH ne correspond pas au nom du sommet - attendu : - + Based on SOTA Summit, my Grid does not match SOTA Grid - expecting D'après le sommet SOTA, mon locator ne correspond pas au locator SOTA - attendu : - + Based on POTA record, my QTH does not match POTA Name - expecting D'après la référence POTA, mon QTH ne correspond pas au nom POTA - attendu : - + Based on POTA record, my Grid does not match POTA Grid - expecting D'après la référence POTA, mon locator ne correspond pas au locator POTA - attendu : - + LoTW Sent Status to <b>No</b> does not make any sense if QSL Sent Date is set. Set Date to 1.1.1900 to leave the date field blank Le statut d'envoi LoTW sur <b>Non</b> est incohérent si une date d'envoi QSL est définie. Mettez la date au 01/01/1900 pour laisser le champ vide - + Date should be present for LoTW Sent Status <b>Yes</b> Une date doit être renseignée si le statut d'envoi LoTW est <b>Oui</b> - + eQSL Sent Status to <b>No</b> does not make any sense if QSL Sent Date is set. Set Date to 1.1.1900 to leave the date field blank Le statut d'envoi eQSL sur <b>Non</b> est incohérent si une date d'envoi QSL est définie. Mettez la date au 01/01/1900 pour laisser le champ vide - + Date should be present for eQSL Sent Status <b>Yes</b> Une date doit être renseignée si le statut d'envoi eQSL est <b>Oui</b> - + Paper Sent Status to <b>No</b> does not make any sense if QSL Sent Date is set. Set Date to 1.1.1900 to leave the date field blank Le statut d'envoi Papier sur <b>Non</b> est incohérent si une date d'envoi QSL est définie. Mettez la date au 01/01/1900 pour laisser le champ vide - + Date should be present for Paper Sent Status <b>Yes</b> Une date doit être renseignée si le statut d'envoi Papier est <b>Oui</b> - + Callbook error: Erreur du callbook : - - + + <b>Warning: </b> <b>Attention : </b> - + Validation Validation - + Yellow marked fields are invalid.<p>Nevertheless, save the changes?</p> Les champs marqués en jaune sont invalides.<p>Sauvegarder tout de même les modifications ?</p> - + &Save &Sauvegarder - + &Edit &Modifier @@ -11019,52 +11801,52 @@ Vous pouvez laisser les champs vides et les configurer plus tard dans les Param Ajouter une condition - + Equal Égal à - + Not Equal Différent de - + Contains Contient - + Not Contains Ne contient pas - + Greater Than Supérieur à - + Less Than Inférieur à - + Starts with Commence par - + RegExp Exp. Régulière - + Remove Supprimer - + Must not be empty Ne doit pas être vide @@ -11100,27 +11882,27 @@ Vous pouvez laisser les champs vides et les configurer plus tard dans les Param Rig - + No Rig Profile selected Aucun profil de déca (Rig) sélectionné - + Rigctld Error Erreur Rigctld - + Initialization Error Erreur d'initialisation - + Internal Error Erreur interne - + Cannot open Rig Impossible d'ouvrir le poste @@ -11138,36 +11920,66 @@ Vous pouvez laisser les champs vides et les configurer plus tard dans les Param RX - + Disconnected Déconnecté - - + + MHz MHz - + Disable Split Désactiver le split - + RIT: 0.00000 MHz RIT : 0.00000 MHz - + XIT: 0.00000 MHz XIT : 0.00000 MHz - + PWR: %1W PWR : %1W + + + OUT + + + + + Outside Bandmap Guide range + Hors de la plage du guide Bandmap + + + + SOS + SOS + + + + Emergency frequency: %1 MHz + Fréquence d’urgence : %1 MHz + + + + IBP + IBP + + + + International Beacon Project: %1 MHz + + RigctldAdvancedDialog @@ -11257,57 +12069,57 @@ Veuillez installer Hamlib ou spécifier le chemin manuellement. RigctldManager - + rigctld executable not found in /app/bin/. This should not happen in Flatpak build. L'exécutable rigctld est introuvable dans /app/bin/. Cela ne devrait pas arriver avec un build Flatpak. - + rigctld executable not found. Please install Hamlib or specify the path in Advanced settings. L'exécutable rigctld est introuvable. Veuillez installer Hamlib ou spécifier le chemin dans les paramètres avancés. - + Hamlib major version mismatch: QLog was compiled with Hamlib %1 but rigctld reports version %2.%3.%4. Rig model IDs are incompatible between major versions. Discordance de version majeure de Hamlib : QLog a été compilé avec Hamlib %1 mais rigctld indique la version %2.%3.%4. Les ID de modèles de postes sont incompatibles entre versions majeures. - + Port %1 is already in use. Another rigctld or application may be running on this port. Le port %1 est déjà utilisé. Une autre instance de rigctld ou une autre application utilise probablement ce port. - + rigctld started but not responding on port %1. rigctld est démarré mais ne répond pas sur le port %1. - + Failed to start rigctld: %1 %2 Échec du démarrage de rigctld : %1 %2 - + rigctld crashed. rigctld a planté. - + rigctld timed out. Délai d'attente dépassé pour rigctld. - + Write error with rigctld. Erreur d'écriture avec rigctld. - + Read error with rigctld. Erreur de lecture avec rigctld. - + Unknown rigctld error. Erreur rigctld inconnue. @@ -11315,22 +12127,22 @@ Veuillez installer Hamlib ou spécifier le chemin manuellement. Rotator - + No Rotator Profile selected Aucun profil de rotor sélectionné - + Initialization Error Erreur d'initialisation - + Internal Error Erreur interne - + Cannot open Rotator Impossible d'ouvrir le rotor @@ -11575,20 +12387,21 @@ Veuillez installer Hamlib ou spécifier le chemin manuellement. - - - - - - - - - - + + + + + + - - - + + + + + + + + Add Ajouter @@ -11628,6 +12441,7 @@ Veuillez installer Hamlib ou spécifier le chemin manuellement. + Description Description @@ -11682,22 +12496,97 @@ Veuillez installer Hamlib ou spécifier le chemin manuellement. Liste de tous les manipulateurs CW disponibles - + + Startup ADI + + + + + Configured ADI/ADIF files are checked only at startup. A newly added file starts at its current end, so only later appended QSOs are loaded. This is not a live watcher; if many new QSOs are found, loading stops and the standard Import should be used. + Les fichiers ADI/ADIF configurés sont vérifiés uniquement au démarrage. Un fichier nouvellement ajouté commence à sa fin actuelle ; seuls les QSO ajoutés ensuite sont donc chargés. Il ne s’agit pas d’une surveillance en direct ; si de nombreux nouveaux QSO sont trouvés, le chargement s’arrête et l’importation standard doit être utilisée. + + + + Removing a file also forgets its recovery position. + La suppression d’un fichier oublie aussi sa position de récupération. + + + + Remove + + + + + Used when a file row has Missing QSL Sent set to Custom. Explicit ADIF values are kept. + Utilisé lorsqu’une ligne de fichier a QSL Sent manquant défini sur Personnalisé. Les valeurs ADIF explicites sont conservées. + + + + Custom QSL Sent Defaults + Valeurs QSL Sent personnalisées par défaut + + + + Paper QSL + QSL papier + + + + DCL + DCL + + + + Select the <b>Bandmap Guide</b> profile shown as visual frequency hints. It does not affect mode identification. + Sélectionnez le profil <b>Guide Bandmap</b> affiché comme aide visuelle de fréquence. Il n’affecte pas l’identification du mode. + + + + Manage + Gérer + + + + Double-click cells to edit start/end frequency, enabled state, or SAT mode. Band names are fixed; new bands cannot be added here. + Double-cliquez sur les cellules pour modifier la fréquence de début/fin, l’état activé ou le mode SAT. Les noms des bandes sont fixes ; de nouvelles bandes ne peuvent pas être ajoutées ici. + + + + QSO DXCC Status Colors + Couleurs d’état DXCC du QSO + + + + Used for DX spots, Bandmap, WSJT-X and QSO status hints. Confirmed has no highlight by default. Click a color cell to choose a color or set No color. + Utilisé pour les DX spots, la Bandmap, WSJT-X et les indications d’état QSO. Confirmé n’a aucun surlignage par défaut. Cliquez sur une cellule de couleur pour choisir une couleur ou définir Aucune couleur. + + + + Restore Defaults + Restaurer les valeurs par défaut + + + + Shortcuts + Raccourcis + + + Danger Zone Zone dangereuse - + <b>⚠ This is a danger zone. Proceed with caution, as actions performed here cannot be undone and may have a significant impact on your log.</b> <b>⚠ Ceci est une zone dangereuse. Procédez avec prudence, car les actions effectuées ne peuvent pas être annulées et peuvent avoir un impact important sur votre journal.</b> - + Delete All QSOs Supprimer tous les QSOs - + Delete All Passwords from the Secure Store Supprimer tous les mots de passe du coffre sécurisé @@ -12043,7 +12932,7 @@ Veuillez installer Hamlib ou spécifier le chemin manuellement. - + Start rigctld daemon to share rig with other applications (e.g. WSJT-X) Lancer le démon rigctld pour partager le poste avec d'autres applis (ex: WSJT-X) @@ -12181,14 +13070,14 @@ Veuillez installer Hamlib ou spécifier le chemin manuellement. - + Serial Série - - + + Network Réseau @@ -12239,8 +13128,8 @@ Veuillez installer Hamlib ou spécifier le chemin manuellement. - - + + HamQTH HamQTH @@ -12249,7 +13138,7 @@ Veuillez installer Hamlib ou spécifier le chemin manuellement. - + Username Utilisateur @@ -12259,15 +13148,15 @@ Veuillez installer Hamlib ou spécifier le chemin manuellement. - + Password Mot de passe - - + + QRZ.com QRZ.com @@ -12338,7 +13227,8 @@ Veuillez installer Hamlib ou spécifier le chemin manuellement. - + + eQSL eQSL @@ -12349,7 +13239,7 @@ Veuillez installer Hamlib ou spécifier le chemin manuellement. - + Callsign Indicatif @@ -12377,7 +13267,8 @@ Veuillez installer Hamlib ou spécifier le chemin manuellement. - + + LoTW LoTW @@ -12413,7 +13304,7 @@ Veuillez installer Hamlib ou spécifier le chemin manuellement. - + API Key Clé API @@ -12423,98 +13314,98 @@ Veuillez installer Hamlib ou spécifier le chemin manuellement. Point d'accès (Endpoint) - + Others Autres - - + + DXCC DXCC - + Status Confirmed By Statut confirmé par - + Paper Papier - + Chat Chat - + <b>Security Notice:</b> QLog stores all passwords in the Secure Storage. Unfortunately, ON4KST uses a protocol where this password is sent over an unsecured channel as plaintext.</p><p>Please exercise caution when choosing your password for this service, as your password is sent over an unsecured channel in plaintext form.</p> <b>Note de sécurité :</b> QLog stocke les mots de passe de manière sécurisée. Hélas, ON4KST utilise un protocole où le mot de passe transite en clair sur un canal non sécurisé.</p><p>Soyez prudent lors du choix de votre mot de passe pour ce service.</p> - + Bands Bandes - + Modes Modes - + The '>' character is interpreted as a marker for the initial cursor position in the Report column. <br/>Ex.: '5>9' means the cursor will be positioned on the second character Le caractère '>' sert de marqueur pour la position initiale du curseur dans la colonne Report. <br/>Ex: '5>9' positionnera le curseur sur le second caractère. - + Wsjtx WSJT-X - + Raw UDP Forward Redirection UDP brute - + <p>List of IP addresses to which QLog forwards raw UDP WSJT-X packets.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Liste des adresses IP vers lesquelles QLog redirige les paquets UDP bruts de WSJT-X.</p>Les adresses sont séparées par des espaces (format IP:PORT). - - - - - - + + + + + + ex. 192.168.1.1:1234 192.168.2.1:1234 ex: 192.168.1.1:1234 192.168.2.1:1234 - + Port Port - + Port where QLog listens an incoming traffic from WSJT-X Port sur lequel QLog écoute le trafic provenant de WSJT-X - + Join Multicast Rejoindre le Multicast - + Enable/Disable Multicast option for WSJTX Activer/Désactiver l'option Multicast pour WSJT-X - + Multicast Address Adresse Multicast @@ -12601,398 +13492,601 @@ Veuillez installer Hamlib ou spécifier le chemin manuellement. Version de TQSL - + Specify Multicast Address. <br>On some Linux systems it may be necessary to enable multicast on the loop-back network interface. Spécifier l'adresse Multicast. <br>Sur certains systèmes Linux, il peut être nécessaire d'activer le multicast sur l'interface de boucle locale (loop-back). - + TTL TTL - + Time-To-Live determines the range<br> over which a multicast packet is propagated in your intranet. Le TTL (Time-To-Live) détermine la portée de propagation<br> d'un paquet multicast sur votre intranet. - + Color CQ Spots Colorer les spots CQ - + Enable/Disable sending color-coded status indicators back to WSJT-X for each callsign calling CQ Activer/Désactiver l'envoi d'indicateurs d'état colorés à WSJT-X pour chaque indicatif appelant CQ - + Notifications Notifications - + LogID LogID - + <p>Assigned LogID to the current log.</p>The LogID is sent in the Network Nofitication messages as a unique instance identified.<p> The ID is generated automatically and cannot be changed</> <p>LogID assigné au carnet actuel.</p>Le LogID est envoyé dans les notifications réseau comme identifiant unique d'instance.<p>L'ID est généré automatiquement et ne peut être modifié.</p> - + DX Spots Spots DX - + <p> List of IP addresses to which QLog sends UDP notification packets with DX Cluster Spots.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Liste des adresses IP vers lesquelles QLog envoie les notifications UDP des spots du cluster DX.</p>Les adresses sont séparées par des espaces (format IP:PORT). - + Spot Alerts Alertes de spots - + <p> List of IP addresses to which QLog sends UDP notification packets about user Spot Alerts.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Liste des adresses IP vers lesquelles QLog envoie les notifications UDP pour les alertes de spots utilisateur.</p>Les adresses sont séparées par des espaces (format IP:PORT). - + QSO Changes Modifications de QSO - + <p> List of IP addresses to which QLog sends UDP notification packets about a new/updated/deleted QSO in the log.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Liste des adresses IP vers lesquelles QLog envoie les notifications UDP lors de l'ajout/modification/suppression d'un QSO.</p>Les adresses sont séparées par des espaces (format IP:PORT). - + Wsjtx CQ Spots Spots CQ WSJT-X - + <p> List of IP addresses to which QLog sends UDP notification packets with WSJTX CQ Spots.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Liste des adresses IP vers lesquelles QLog envoie les notifications UDP des spots CQ de WSJT-X.</p>Les adresses sont séparées par des espaces (format IP:PORT). - + Rig Status État du poste - + <p> List of IP addresses to which QLog sends UDP notification packets when Rig State changes.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Liste des adresses IP vers lesquelles QLog envoie les notifications UDP lors d'un changement d'état du poste.</p>Les adresses sont séparées par des espaces (format IP:PORT). - + GUI Interface (GUI) - + Date Format Format de date - + System Système - + + + + Custom Personnalisé - + <a href="https://doc.qt.io/qt-6/qdate.html#fromString-1">Time Format Documentation</a> <a href="https://doc.qt.io/qt-6/qdate.html#fromString-1">Documentation du format d'heure</a> - + Time Format Format d'heure - + 24-hour 24 heures - + AM/PM AM/PM - + Unit System Système d'unités - + Metric Métrique - + Imperial Impérial - - + + Special - Omnirig Spécial - Omnirig - + Cannot be changed Ne peut pas être modifié - - + + Name Nom - + Report Report - - + + State État / Province - + Start (MHz) Début (MHz) - + End (MHz) Fin (MHz) - + SAT Mode Mode SAT - - - + + + Disabled Désactivé - - + + None Aucun - + Hardware Matériel (Hard) - + Software Logiciel (Soft) - + + + + + No Non - + Even Pair - + Odd Impair - + Mark Mark - + Space Space - + Dummy Dummy (fictif) - + Morse Over CAT Morse via CAT - + WinKey WinKey - + CWDaemon CWDaemon - + FLDigi FLDigi - + Single Paddle Simple palette - + IAMBIC A IAMBIC A - + IAMBIC B IAMBIC B - + Ultimate Ultimatice - + High Haut (High) - + Low Bas (Low) - + + Duplicate + Doublon + + + + Already worked QSO + QSO déjà contacté + + + + New Entity + + + + + DXCC entity not worked yet + Entité DXCC pas encore contactée + + + + New Band / Mode + Nouvelle bande / mode + + + + New band, mode, or band and mode + Nouvelle bande, mode ou les deux + + + + New Slot + + + + + New band and mode combination + Nouvelle combinaison bande/mode + + + + Worked + + + + + Worked but not confirmed + Contacté, mais non confirmé + + + + Confirmed + Confirmé + + + + Confirmed QSO; no highlight by default + QSO confirmé ; aucun surlignage par défaut + + + + Status + État + + + + Color + Couleur + + + + Choose Color... + Choisir une couleur... + + + + Default + Par défaut + + + + No Color + Aucune couleur + + + + Status Color + Couleur d’état + + + + No color + Aucune couleur + + + + No highlight. Click to choose a color or set no color. + Aucun surlignage. Cliquez pour choisir une couleur ou définir aucune couleur. + + + + Click to change color or set no color. + Cliquez pour changer la couleur ou définir aucune couleur. + + + Press <b>Modify</b> to confirm the profile changes or <b>Cancel</b>. Appuyez sur <b>Modifier</b> pour confirmer les changements de profil ou sur <b>Annuler</b>. - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + Modify Modifier - - - - - - - - - - + + + + + + + + + + Must not be empty Ne doit pas être vide - + Select File Sélectionner un fichier - + Auto Detect Auto-détection - + TQSL was not found on this system. Please install TQSL or specify the path manually. TQSL n'a pas été trouvé sur ce système. Veuillez installer TQSL ou spécifier le chemin manuellement. - + Not found Non trouvé - + Rig sharing is only available for Hamlib driver Le partage du poste n'est disponible qu'avec le pilote Hamlib - + Rig sharing is not available for network connection Le partage du poste n'est pas disponible pour les connexions réseau - + + Off + Désactivé + + + Delete Passwords Supprimer les mots de passe - + All passwords have been deleted Tous les mots de passe ont été supprimés - + Deleting all QSOs... Suppression de tous les QSOs... - + Error Erreur - + Failed to delete all QSOs. Impossible de supprimer tous les QSOs. - + + Enabled + Activé + + + + Path + Chemin/Trajet + + + + Station Profile + Profil de station + + + + Missing QSL Sent + QSL Sent manquant + + + + Last Recovery + Dernière récupération + + + + + + Queued + + + + + + + + Ignored + + + + + + + + Requested + + + + + + + + Yes + Oui + + + + Station Profile does not exist. Select another profile and enable this row again. + Le profil de station n’existe pas. Sélectionnez un autre profil et réactivez cette ligne. + + + + File exists + Le fichier existe + + + + File does not exist + Le fichier n’existe pas + + + + Startup ADI initialized + Startup ADI initialisé + + + + Select ADIF File + Sélectionner le fichier ADIF + + + + ADIF Files (*.adi *.adif);;All Files (*) + Fichiers ADIF (*.adi *.adif);;Tous les fichiers (*) + + + members membres - + Required internet connection during application start Connexion internet requise au démarrage de l'application @@ -13050,7 +14144,7 @@ Veuillez installer TQSL ou spécifier le chemin manuellement. StatisticsWidget - + Statistics Statistiques @@ -13066,7 +14160,7 @@ Veuillez installer TQSL ou spécifier le chemin manuellement. - + Band Bande @@ -13146,146 +14240,145 @@ Veuillez installer TQSL ou spécifier le chemin manuellement. Mon antenne - + Sun Dim - + Mon Lun - + Tue Mar - + Wed Mer - + Thu Jeu - + Fri Ven - + Sat Sam - - - + + + Not specified Non spécifié - + Confirmed Confirmé - + Not Confirmed Non confirmé - + No User Filter Aucun filtre utilisateur - + Over 50000 QSOs. Display them? Plus de 50 000 QSO. Les afficher ? - - + Rendering QSOs... Génération des QSO... - + Year Année - + Month Mois - + Day in Week Jour de la semaine - + Hour Heure - + Mode Mode - + Continent Continent - + Propagation Mode Mode de propagation - + Confirmed / Not Confirmed Confirmé / Non confirmé - + Countries Pays - + Big Gridsquares Grands carrés Locator - + Distance Distance - + QSOs QSO - + Confirmed/Worked Grids Locators confirmés/contactés - + ODX ODX - + All Tout @@ -13339,17 +14432,17 @@ Veuillez installer TQSL ou spécifier le chemin manuellement. ToAllTableModel - + Time Heure - + Spotter Spotter - + Message Message @@ -13622,27 +14715,27 @@ Veuillez installer TQSL ou spécifier le chemin manuellement. UserListModel - + Callsign Indicatif - + Gridsquare Locator - + Distance Distance - + Azimuth Azimut - + Comment Commentaire @@ -13650,47 +14743,47 @@ Veuillez installer TQSL ou spécifier le chemin manuellement. WCYTableModel - + Time Heure - + K K - + expK expK - + A A - + R R - + SFI SFI - + SA SA - + GMF GMF - + Au Au @@ -13698,27 +14791,27 @@ Veuillez installer TQSL ou spécifier le chemin manuellement. WWVTableModel - + Time Heure - + SFI SFI - + A A - + K K - + Info Info @@ -13844,37 +14937,37 @@ Veuillez installer TQSL ou spécifier le chemin manuellement. WsjtxTableModel - + Callsign Indicatif - + Gridsquare Locator - + Distance Distance - + SNR SNR - + Last Activity Dernière activité - + Last Message Dernier message - + Member Membre @@ -13915,47 +15008,47 @@ Veuillez installer TQSL ou spécifier le chemin manuellement. main - + Run with the specific namespace. Exécuter avec l'espace de noms (namespace) spécifique. - + namespace espace de noms - + Translation file - absolute or relative path and QM file name. Fichier de traduction - chemin absolu ou relatif et nom du fichier QM. - + path/QM-filename chemin/nom-du-fichier-QM - + Set language. <code> example: 'en' or 'en_US'. Ignore environment setting. Définir la langue. <code> exemple : 'en' ou 'en_US'. Ignore les paramètres d'environnement. - + code code - + Writes debug messages to the debug file Écrit les messages de débogage dans le fichier de debug - + Process pending database import (internal use) Traiter l'importation de base de données en attente (usage interne) - + Force update of all value lists (DXCC, SATs, etc.) Forcer la mise à jour de toutes les listes (DXCC, SATs, etc.) diff --git a/i18n/qlog_it.qm b/i18n/qlog_it.qm index d83985df..9a9758b1 100644 Binary files a/i18n/qlog_it.qm and b/i18n/qlog_it.qm differ diff --git a/i18n/qlog_it.ts b/i18n/qlog_it.ts index e11a2568..193ff82c 100644 --- a/i18n/qlog_it.ts +++ b/i18n/qlog_it.ts @@ -199,20 +199,117 @@ + Bandmap Guide + Aiuto Bandmap + + + + Guide + + + + Fields Campi - + Must not be empty Non deve essere vuoto - + + Leave unchanged + Lascia invariato + + + + Off + Disattivato + + + Unsaved Non Salvato + + AdifRecoveryManager + + + Startup ADI found more than %1 new QSOs in %2. Use the standard Import. Load point was moved to the end of the file. + L'ADI di avvio contiene più di %1 nuovi QSO in %2. Usare l'importazione standard. Il punto di caricamento è stato spostato alla fine del file. + + + + Startup ADI Station Profile does not exist: %1 + Il profilo stazione per Startup ADI non esiste: %1 + + + + Cannot open Startup ADI records from %1 + Impossibile aprire i record Startup ADI da %1 + + + + Startup ADI from %1 finished with %n error(s); load point was not advanced. + + Startup ADI da %1 terminato con %n errore/i; il punto di caricamento non è stato avanzato. + + + + + + Startup ADI was disabled for %n file(s) because the assigned Station Profile no longer exists. + + Startup ADI è stato disattivato per %n file perché il profilo stazione assegnato non esiste più. + + + + + + AdifRecoveryReaderWorker + + + Startup ADI filename is empty + Il nome file di Startup ADI è vuoto + + + + Startup ADI file does not exist: %1 + Il file Startup ADI non esiste: %1 + + + + Startup ADI initialized at the end of file + Startup ADI inizializzato alla fine del file + + + + Startup ADI file was reset; load point moved to the end + Il file Startup ADI è stato reimpostato; punto di caricamento spostato alla fine + + + + Cannot open Startup ADI file: %1 + Impossibile aprire il file Startup ADI: %1 + + + + Cannot seek Startup ADI file: %1 + Impossibile posizionarsi nel file Startup ADI: %1 + + + + Cannot read Startup ADI file: %1 + Impossibile leggere il file Startup ADI: %1 + + + + Too many ADIF records for automatic recovery + Troppi record ADIF per il ripristino automatico + + AlertRuleDetail @@ -495,42 +592,42 @@ AlertTableModel - + Rule Name Nome della Regola - + Callsign Indicativo - + Frequency Frequenza - + Mode Modo - + Updated Aggiornato - + Last Update Último Aggiornamento - + Last Comment Último Commento - + Member Membro @@ -580,78 +677,83 @@ Awards Diplomi - - - Options - Opzioni - Award Diploma - + + 🌐 Rules + 🌐 Regole + + + My DXCC Entity Mia Entità DXCC - + User Filter Filtro Utente - + Confirmed by Confermato da - + LoTW LoTW - + eQSL eQSL - + Paper Cartaceo - + Mode Modo - + CW CW - + Phone Fonia - + Digi Digi - + Not-Worked Only Solo non lavorato - + Not-Confirmed Only Non confermato - + + Double-click a row/cell to show QSOs + Doppio clic su riga/cella per mostrare i QSO + + + Show Mostra @@ -666,7 +768,7 @@ ITU - + WAC WAC @@ -731,79 +833,84 @@ - + US Counties Contee degli Stati Uniti - + Russian Districts Distretti della Russia - + Japanese Cities/Ku/Guns Città giapponesi / Ku / Gun - + NZ Counties Contee della Nuova Zelanda - + Spanish DMEs DME spagnoli - + Ukrainian Districts Distretti dell’Ucraina - + No User Filter Nessun filtro utente - + DELETED Eliminato - + North America Nord America - + South America South America - + Europe Europa - + Africa Africa - + Oceania Oceanía - + Asia Asia - - Antarctica - Antartide + + WAAC + + + + + WAIP + @@ -829,6 +936,198 @@ In attesa + + BandmapGuideDialog + + + Bandmap Guide + Aiuto Bandmap + + + + Import guide + Importa guida + + + + Import + Importa + + + + Export guide + Exportar guía + + + + Export + Esporta + + + + New guide + Nuova guida + + + + New + Nuovo + + + + Copy guide + Copia guida + + + + Copy + Copia + + + + Delete guide + Elimina guida + + + + Delete + + + + + Guide Name: + Nome guida: + + + + Ranges: + Intervalli: + + + + From + Da + + + + To + A + + + + Color + Colore + + + + Label + Etichetta + + + + Add range + Aggiungi intervallo + + + + Add + Aggiungi + + + + Remove selected range + Rimuovi intervallo selezionato + + + + Remove + Rimuovi + + + + + MHz + MHz + + + + + New Guide + Nuova guida + + + + Copy - %1 + Copia – %1 + + + + Delete Guide + Elimina guida + + + + Delete guide '%1'? + Eliminare la guida “%1”? + + + + Import Guide + Importa guida + + + + QLog Bandmap Guide (*.qbg);;JSON (*.json) + Guida Bandmap QLog (*.qbg);;JSON (*.json) + + + + Import Failed + Importazione fallita + + + + Export Guide + Esporta guida + + + + QLog Bandmap Guide (*.qbg) + Guida Bandmap QLog (*.qbg) + + + + Export Failed + Esportazione fallita + + + + Guide Color + Colore guida + + + + + + QLog Warning + Avviso QLog + + + + Guide name cannot be empty. + Il nome guida non può essere vuoto. + + + + Guide name '%1' is already used. + Il nome guida “%1” è già in uso. + + + + Guide '%1' contains an invalid range. + La guida “%1” contiene un intervallo non valido. + + BandmapWidget @@ -867,30 +1166,60 @@ min(s) - + Bandmap Bandmap - + Show Band Mostra Banda - + Center RX Centra RX - + Show Emergency Frequencies Mostra frequenze di emergenza - + + Show IBP Frequencies + Mostra frequenze IBP + + + + Show Guide + Mostra guida + + + + Off + Disattivato + + + + No Guide + Nessuna guida + + + + Edit Guide... + Modifica guida... + + + SOS + + + IBP + IBP + CWCatKey @@ -1167,27 +1496,27 @@ CWKeyer - + No CW Keyer Profile selected Nessun Profilo Keyer CW selezionato - + Initialization Error Errore di inizializzazione - + Internal Error Errore Interno - + Connection Error Errore di connessione - + Cannot open the Keyer connection Impossibile aprire la connessione Keyer @@ -1817,198 +2146,198 @@ Importa - + Export template Esporta modello - + Export Esporta - + New template Nuovo modello - + New Nuovo - + Copy existing template Copia modello esistente - + Copy Copia - + Delete template Elimina modello - + Delete - + Template Name: Nome del modello: - + Contest Name: Nome del contest: - + Default Mode: Modalità predefinita: - + QSO Line Columns: Colonne riga QSO: - + Contest name as required by the rules. It is possible to enter a custom string if it is not included in the list. Nome del contest secondo le regole. È possibile inserire una stringa personalizzata se non è inclusa nell’elenco. - + Seq. N. - + QSO Field Campo QSO - + Formatter Formattatore - + Width Larghezza - + Label Etichetta - + Add line Aggiungi riga - + Add Aggiungi - + Remove selected line Rimuovi riga selezionata - + Remove Rimuovi - + New Template Nuovo modello - + Copy - %1 Copia – %1 - + Delete Template Elimina modello - + Delete template '%1'? Eliminare il modello «%1»? - + Import Template Importa modello - - + + QLog Cabrillo Template (*.qct) Modello Cabrillo QLog (*.qct) - + Import Failed Importazione fallita - + Export Template Esporta modello - + Export Failed Esportazione fallita - + Failed to write file: %1 Impossibile scrivere il file: %1 - + File not found: %1 File non trovato: %1 - + Cannot open file: %1 Impossibile aprire il file: %1 - + Invalid template file: missing name File modello non valido: nome mancante - + QLog Error Errore di QLog - + Cannot start database transaction. Impossibile avviare la transazione del database. - + QLog Warning Avviso QLog - + Cannot save template '%1': %2 Impossibile salvare il modello «%1»: %2 @@ -2075,20 +2404,24 @@ - - + + Sunrise Alba - - + + Sunset Tramonto - - + + + + + + N/A N/A @@ -2152,7 +2485,7 @@ Altri - + Done Fatto @@ -2160,12 +2493,12 @@ ColumnSettingGenericDialog - + Unselect All Deseleziona tutto - + Select All Seleziona tutto @@ -2178,7 +2511,7 @@ Impostazione visibilità colonna - + Done Fatto @@ -4228,70 +4561,60 @@ Data - + New Entity Nuova Entità - + New Band Nuova Banda - + New Mode Nuovo Modo - + New Band&Mode Nuovi Banda&Modo - + New Slot Nuovo Slot - + Confirmed Confermato - + Worked Lavorato - + Hz Hz - + kHz kHz - + GHz GHz - + MHz MHz - - - - - - - - Yes - - @@ -4299,136 +4622,146 @@ - No - No + Yes + + + + + + No + No + + + + Requested Richiesto - + Queued In coda - - - + + + Invalid non valido - + Bureau Bureau - + Direct Diretto - + Electronic Elettronica - - - - - - - - + + + + + + + + Blank Vuoto - + Modified Modificato - + Grayline Grayline - + Other Altro - + Short Path Short Path - + Long Path Long Path - + Not Heard Non ascoltato - + Uncertain Incerto - + Straight Key Tasto Verticale - + Sideswiper Tasto a coltello - + Mechanical semi-automatic keyer or Bug Tasto semiautomatico o Bug - + Mechanical fully-automatic keyer or Bug Tasto meccanico automatico o Bug - + Single Paddle Tasto a singola paletta - + Dual Paddle Tasto a doppia paletta - + Computer Driven Comandato dal Computer - + Confirmed (AG) Confermato (AG) - + Confirmed (no AG) Confermato (non-AG) - + Unknown Sconosciuto @@ -5073,57 +5406,57 @@ Example: DxTableModel - + Time Ora - + Callsign Indicativo - + Frequency Frequenza - + Mode Modo - + Spotter Spotter - + Comment Commento - + Continent Continente - + Spotter Continent Continente Spotter - + Band Banda - + Member Membro - + Country Nazione @@ -5137,7 +5470,7 @@ Example: - + Connect Connette @@ -5303,67 +5636,67 @@ Example: DXC - Ricerca - + My Continent Mio continente - + Auto Automatico - + Connecting... Sta Connettendo... - + DX Cluster is temporarily unavailable DX Cluster è temporaneamente non disponibile - + DXC Server Error DXC Errore del server - + An invalid callsign Indicativo non valido - + DX Cluster Password DX Cluster Password - + Security Notice Avviso di sicurezza - + The password can be sent via an unsecured channel La password può essere inviata tramite un canale non protetto - + Server Server - + Username Nome utente - + Disconnect Disconnette - + DX Cluster Command Comando del DX Cluster @@ -5371,22 +5704,22 @@ Example: DxccTableModel - + Worked Lavorato - + eQSL eQSL - + LoTW LoTW - + Paper Cartaceo @@ -5502,7 +5835,7 @@ Example: - + POTA POTA @@ -5682,42 +6015,42 @@ Example: Impossibile Segnare QSO esportati come Inviati - + Generic Generico - + QSLs QSLs - + All Tutte - + Minimal Mínimo - + QSL-specific Specifico per QSL - + Custom 1 Personalizzato 1 - + Custom 2 Personalizzato 2 - + Custom 3 Personalizzato 3 @@ -5846,132 +6179,132 @@ Questa password sarà necessaria in seguito per ripristinarle. Impossibile impostare auto_power_on - + Cannot set no_xchg to 1 Impossibile impostare no_xchg su 1 - + Rig Open Error Connessione Radio fallita - + Set TX Frequency Error Errore nell’impostazione della frequenza TX - + Set Frequency Error Errore impostazione fequenza - + Set Split Error Errore nell’impostazione dello split - + Set Mode Error Errore impostazione Modo - + Set Split Mode Error Errore nell’impostazione della modalità split - + Set PTT Error Errore impostazione PTT - + Cannot sent Morse This cannot be displayed impossibile inviare il codice Morse - + Cannot stop Morse This cannot be displayed impossibile fermare codice Morse - + Get PTT Error This cannot be displayed Errore PTT - + Get Frequency Error Errore della Frequenza - + Get Mode Error Errore del Modo - + Get VFO Error Errore VFO - + Get PWR Error This cannot be displayed Errore PWR - + Get PWR (power2mw) Error This cannot be displayed Errore PWR (power2mw) - + Get RIT Function Error This cannot be displayed Errore funzione RIT - + Get RIT Error This cannot be displayed Errore RIT - + Get XIT Function Error This cannot be displayed Errore funzione XIT - + Get XIT Error This cannot be displayed Errore XIT - + Get Split Error - + Get TX Frequency Error - + Get KeySpeed Error This cannot be displayed Errore di KeySpeed - + Set KeySpeed Error This cannot be displayed Errore impostazione KeySpeed @@ -6009,140 +6342,260 @@ Questa password sarà necessaria in seguito per ripristinarle. ImportDialog - + Import Importa - + Date Range Intervallo di date - + Import all or only QSOs from the given period Importa tutti o solo i QSO del periodo specificato - + All Tutti - + File File - + ADX ADX - + Browse Cerca - + Options Opzioni - - + + The value is used when an input record does not contain the ADIF value Il valore viene utilizzato quando una registrazione inserita non contiene il valore ADIF - + Defaults Valori di Defaults - + + Values are used only for fields that are missing in the import file. Existing values are preserved. + I valori vengono usati solo per i campi mancanti nel file di importazione. I valori esistenti vengono mantenuti. + + + + <p>⚠ Missing QSL Sent fields are set to <b>"N"</b> (do not send) by default in ADIF. + <p>⚠ I campi QSL Sent mancanti sono impostati su <b>"N"</b> (non inviare) per impostazione predefinita in ADIF. + + + My Profile Mio Profilo - + My Rig Mia Radio - - + + Comment Commento - + + Used only for missing QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT, and DCL_QSL_SENT fields where default is "N"; otherwise, the value from the input is used. + Usato solo per i campi QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT e DCL_QSL_SENT mancanti, dove il valore predefinito è “N”; altrimenti viene usato il valore dell’input. + + + + QSL Sent status + + + + + Used only for missing QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT, and DCL_QSL_SENT fields where default is "N"; otherwise, the value from the input is used.<p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Usato solo per i campi QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT e DCL_QSL_SENT mancanti, dove il valore predefinito è “N”; altrimenti viene usato il valore dell’input.<p><b>In coda</b> (pronto), <b>No</b> (non inviare), <b>Ignora</b> (non tracciare), <b>Richiesto</b> (richiesto), <b>Sì</b> (già inviato). + + + + Used only when the imported ADIF record does not contain the selected field. Explicit ADIF values are kept. + Usato solo quando il record ADIF importato non contiene il campo selezionato. I valori ADIF espliciti vengono mantenuti. + + + + Default value for missing DCL_QSL_SENT. <p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Valore predefinito per DCL_QSL_SENT mancante. <p><b>In coda</b> (pronto), <b>No</b> (non inviare), <b>Ignora</b> (non tracciare), <b>Richiesto</b> (richiesto), <b>Sì</b> (già inviato). + + + + Default value for missing EQSL_QSL_SENT. <p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Valore predefinito per EQSL_QSL_SENT mancante. <p><b>In coda</b> (pronto), <b>No</b> (non inviare), <b>Ignora</b> (non tracciare), <b>Richiesto</b> (richiesto), <b>Sì</b> (già inviato). + + + + Default value for missing LOTW_QSL_SENT. <p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Valore predefinito per LOTW_QSL_SENT mancante. <p><b>In coda</b> (pronto), <b>No</b> (non inviare), <b>Ignora</b> (non tracciare), <b>Richiesto</b> (richiesto), <b>Sì</b> (già inviato). + + + + LoTW + LoTW + + + + DCL + DCL + + + + Paper QSL + QSL cartacea + + + + eQSL + eQSL + + + + Default value for missing QSL_SENT.<p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + Valore predefinito per QSL_SENT mancante.<p><b>In coda</b> (pronto), <b>No</b> (non inviare), <b>Ignora</b> (non tracciare), <b>Richiesto</b> (richiesto), <b>Sì</b> (già inviato). + + + If DXCC is missing in the imported record, it will be resolved from the callsign. Se il DXCC manca nel record importato, verrà determinato dal nominativo. - + Fill missing DXCC Entity Information Compilare le informazioni mancanti dell’entità DXCC + + + Queued (ready to send) + In coda (pronto per l’invio) + + + + Ignored (do not track) + Ignorato (non tracciare) + + + + Requested (requested again) + Richiesto (richiesto di nuovo) + + + + Yes (already sent) + Sì (già inviato) + + + + Custom... + Personalizzato... + + + + Queued + In coda + + + + Requested + Richiesto + + Ignored + Ignorato + + + + No + No + + + + Yes + + + + &Import &Importa - + Select File Seleziona File - - + + The values below will be used when an input record does not contain the ADIF values I valori seguenti verranno utilizzati quando una registrazione inserita non contiene i valori ADIF - + <p><b>In-Log QSO:</b></p><p> <p><b>In-Log QSO:</b></p><p> - + <p><b>Importing:</b></p><p> <p><b>Importazione:</b></p><p> - + Duplicate QSO QSO duplicato - + <p>Do you want to import duplicate QSO?</p>%1 %2 <p>Vuoi importare QSO duplicati?</p>%1 %2 - + Save to File Salva sul file - + QLog Import Summary Riepilogo dell'importazione QLog - + Import date Data di importazione - + Imported file File importato - + Imported: %n contact(s) Importato: %n contatto @@ -6150,7 +6603,7 @@ Questa password sarà necessaria in seguito per ripristinarle. - + Warning(s): %n Allarme: %n @@ -6158,7 +6611,7 @@ Questa password sarà necessaria in seguito per ripristinarle. - + Error(s): %n Errore: %n @@ -6166,17 +6619,17 @@ Questa password sarà necessaria in seguito per ripristinarle. - + Details Dettagli - + Import Result Risultato importazione - + Save Details... Salva dettagli... @@ -6603,18 +7056,53 @@ Questa password sarà necessaria in seguito per ripristinarle. Importato - - + + missing QSO_DATE + QSO_DATE mancante + + + + missing CREDIT_GRANTED + CREDIT_GRANTED mancante + + + + missing CALL/DXCC + CALL/DXCC mancante + + + + no matching QSO + nessun QSO corrispondente + + + + cannot update QSO %1: %2 + impossibile aggiornare il QSO %1: %2 + + + + matched QSO: + QSO corrispondente: + + + + credit_granted: + + + + + DXCC State: Stato DXCC: - + Error Errore - + Warning Attenzione @@ -6622,945 +7110,956 @@ Questa password sarà necessaria in seguito per ripristinarle. LogbookModel - + QSO ID QSO N° - + Time on Inizio - + Time off Fine - + Call Indicativo - + RSTs RSTs - + RSTr RSTr - + Frequency Frequenza - - + + Band Banda - - + + Mode Modo - + Submode Submodo - + Name (ASCII) Nome (ASCII) - + QTH (ASCII) QTH (ASCII) - - + + Gridsquare Griglia - + DXCC DXCC - + Country (ASCII) Nazione (ASCII) - + Continent Continente - + CQZ CQZone - + ITU ITU - + Prefix Prefisso - + State Stato - + County Contea - + IOTA IOTA - + QSLr QSLr - + QSLr Date Data QSLr - + QSLs QSLs - + QSLs Date Data QSLs - + LoTWr LoTWr - + LoTWr Date Data LoTWr - + LoTWs LoTWs - + LoTWs Date Data LoTWs - + TX PWR TX PWR - + Additional Fields Campi aggiuntivi - + Address (ASCII) Indirizzo (ASCII) - + Address Indirizzo - + Age Età - + Altitude Altitudine - + A-Index A-Index - + Antenna Az Antenna Azim - + Antenna El Antenna Elev - + Signal Path Percorso del segnale - + ARRL Section Sezione ARRL - + Award Submitted Award inviato - + Award Granted Award concesso - + Band RX Banda RX - + Gridsquare Extended Griglia Estesa - + Contest Check Controllo Contest - + Class Categoria - + ClubLog Upload Date Data di caricamento su Clublog - + ClubLog Upload State Stato caricamento ClubLog - + Comment (ASCII) Commento (ASCII) - - + + Comment Commento - + Contacted Operator Operatore Contattato - + Contest ID Identificativo Contest - - + + Country Nazione - + + Mode/Submode + + + + + Mode: %1 +Submode: %2 + + + + County Alt Contea Alt - + Credit Submitted Credito inviato - + Credit Granted Credito concesso - + DOK DOK - + DCLr Date Data DCLr - + DCLs Date Data DCLs - + DCLr DCLr - + DCLs DCLs - + Distance Distanza - + Email Email - - + + Owner Callsign Callsign del Titolare - + eQSL AG eQSL AG - + eQSLr Date Data eQSLr - + eQSLs Date Data eQSLs - + eQSLr eQSLr - + eQSLs eQSLs - + FISTS Number Numero FISTS - + FISTS CC FISTS CC - + EME Init Inizio EME - + Frequency RX Frequenza RX - + Guest Operator Operatore ospite - + HamlogEU Upload Date Data di caricamento HamlogEU - + HamlogEU Upload Status Stato di caricamento HamlogEU - + HamQTH Upload Date Data di caricamento HamQTH - + HamQTH Upload Status Stato di caricamento HamQTH - + HRDLog Upload Date Data di caricamento di HRDLog - + HRDLog Upload Status Stato di caricamento di HRDLog - + IOTA Island ID IOTA Island ID - + K-Index K-Index - + Latitude Latitudine - + Longitude Longitudine - + Max Bursts Max Bursts - + MS Shower Name Nome della doccia MeteorScatter - + My Altitude Mia Altitudine - + My Antenna (ASCII) Mia Antenna (ASCII) - + My Antenna Mia Antenna - + My City (ASCII) Mia Città (ASCII) - + My City Mia Città - + My County Mia contea - + My County Alt Mia contea Alt - + My Country (ASCII) Mia nazione (ASCII) - + My Country Mia nazione - + My CQZ Mia CQ Zone - + My DARC DOK Mio DARC DOK - + My DXCC Mio DXCC - + My FISTS Mio FISTS - + My Gridsquare Mia Griglia - + My Gridsquare Extended Mia Griglia Estesa - + My IOTA Mio IOTA - + My IOTA Island ID Mio IOTA Island ID - + My ITU Mio ITU - + My Latitude Mia Latitudine - + My Longitude Mia Longitudine - + My Name (ASCII) Mio Nome (ASCII) - + My Name Mio nome - + My Postal Code (ASCII) Mio Codice Postale (ASCII) - + My Postal Code Mio Codice Postale - + My POTA Ref Mia Ref POTA - + My Rig (ASCII) Mia Radio (ASCII) - + My Rig Mia Radio - + My Special Interest Activity (ASCII) Mie attività di interesse specialel (ASCII) - + My Special Interest Activity Mie attività di interesse speciale - + My Spec. Interes Activity Info (ASCII) informazioni sulle mie attività di interesse speciale (ASCII) - + My Spec. Interest Activity Info Informazioni sulle mie attività di interesse speciale - + My SOTA Mio SOTA - + My State Mio Stato - - + + My Street Mia strada - + My USA-CA Counties Mio USA-CA Counties - + My VUCC Grids Mia Griglia VUCC - + Name Nome - + Notes (ASCII) Note (ASCII) - + QRZ Download Date Data di scaricamento QRZ - + QRZ Download Status Stato del download di QRZ - + QSLs Message (ASCII) QSLs Messaggio (ASCII) - + QSLs Message QSLs Messaggio - + QSLr Message QSLr Messaggio - + RcvPWR Potenza ricevuta - + RcvNr Numero Ricevuto - + RcvExch Scambio Ricevuto - + SentNr Numero Inviato - + SentExch Scambio Inviato - - + + Notes Note - + #MS Bursts #MS Bursts - + #MS Pings #MS Pings - + POTA POTA - + Contest Precedence Precedenza del Contest - + Propagation Mode Modo di Propagazione - + Public Encryption Key Chiave di crittografia pubblica - + QRZ Upload Date Data di caricamento QRZ - + QRZ Upload Status Stato caricamento QRZ - + QSL Message Messaggio della QSL - + CW Key Info Info sul Tasto CW - + CW Key Type Tipo di Tasto CW - + My CW Key Info Mie Info sul Tasto CW - + My CW Key Type Mio Tipo di Tasto CW - + Operator Callsign Call dell'operatore - + QSLr Via QSLr Vía - + QSLs Via QSLs Vía - + QSL Via QSL vía - + QSO Completed QSO Completato - + QSO Random QSO occasionale - + QTH QTH - + Region Regione - + Rig (ASCII) Radio (ASCII) - + Rig Radio - + SAT Mode SAT Mode - + SAT Name Nome del satellite - + Solar Flux Flusso solare - + SIG (ASCII) SIG (ASCII) - + SIG SIG - + SIG Info (ASCII) SIG Info (ASCII) - + SIG Info SIG Info - + Silent Key Silent Key - + SKCC Member Miembro SKCC - + SOTA SOTA - + Logging Station Callsign Indicativo della stazione - + SWL SWL - + Ten-Ten Number Ten-Ten Number - + UKSMG Member Miembro UKSMG - + USA-CA Counties USA-CA Counties - + VE Prov VE Prov - + VUCC VUCC - + Web Web - + My ARRL Section Mia Sezione ARRL - + My WWFF Mio WWFF - + WWFF WWFF - + RST Sent RST Inviato - + RST Rcvd RST Ricevuto - + Paper Cartaceo - + LoTW LoTW - + eQSL eQSL - + QSL Received QSL Ricevuta - + QSL Sent QSL inviata @@ -7569,8 +8068,8 @@ Questa password sarà necessaria in seguito per ripristinarle. LogbookWidget - - + + Delete Elimina @@ -7657,73 +8156,73 @@ Questa password sarà necessaria in seguito per ripristinarle. - + Callsign Indicativo - + Gridsquare Griglia - + POTA POTA - + SOTA SOTA - + WWFF WWFF - + SIG SIG - + IOTA IOTA - + Delete the selected contacts? Eliminare i contatti selezionati? - + Clublog's <b>Immediately Send</b> supports only one-by-one deletion<br><br>Do you want to continue despite the fact<br>that the DELETE operation will not be sent to Clublog? L'<b>Invio immediato</b> di Clublog supporta solo l'eliminazione uno per uno<br><br>Vuoi continuare nonostante il fatto<br>che l'operazione DELETE non verrà inviata a Clublog? - + Deleting QSOs Elimina QSOs - + Update Aggiornare - + By updating, all selected rows will be affected.<br>The value currently edited in the column will be applied to all selected rows.<br><br>Do you want to edit them? L'aggiornamento avrà effetto su tutte le righe selezionate.<br>Il valore attualmente modificato nella colonna verrà applicato a tutte le righe selezionate.<br><br>Vuoi modificarle? - + Count: %n Conteggio: %n @@ -7731,89 +8230,124 @@ Questa password sarà necessaria in seguito per ripristinarle. - + Downloading eQSL Image Download immagine da eQSL - - - + + + Cancel Elimina - + All Bands Tutte le Bande - + All Modes Tutte le modalità - + All Countries Tutti i paesi - + No User Filter Nessun filtro utente - + QLog Warning Avviso QLog - + Each batch supports up to 100 QSOs. Ogni lotto supporta fino a 100 QSO. - + QSOs Update Progress Aggiornamento QSO Progressi - - - + + + QLog Error Errore di QLog - + Callbook login failed Accesso al Logbook non riuscito - + Callbook error: Errore del Callbook: - + All Clubs Tutti i club - + eQSL Download Image failed: Download immagine da eQSL fallito: + + LotwDXCCCreditDownloader + + + Cannot open test LoTW DXCC credit file + Impossibile aprire il file di test dei crediti DXCC LoTW + + + + + Incomplete LoTW DXCC credit response + Risposta incompleta dei crediti DXCC LoTW + + + + + Cannot open temporary file + Impossibile aprire il file temporaneo + + + + LoTW is not configured properly + LoTW non è configurato correttamente + + + + LoTW returned a non-ADIF response + LoTW ha restituito una risposta non ADIF + + + + Incorrect login or password + Nome utente o password errati + + LotwQSLDownloader - + Cannot open temporary file Impossibile aprire il file temporaneo - + Incorrect login or password Nome utente o password errati @@ -7821,73 +8355,73 @@ Questa password sarà necessaria in seguito per ripristinarle. LotwUploader - + Upload cancelled by user Upload cancellato dall'utente - + Upload rejected by LoTW Upload rifiutato da LoTW - + Unexpected response from TQSL server Risposta inaspettata dal server TQSL - + TQSL utility error Errore di TQSL - + TQSLlib error Errore di TQSLlib - + Unable to open input file Impossibile aprire il file di input - + Unable to open output file Impossibile aprire il file di output - + All QSOs were duplicates or out of date range Tutti i QSO erano duplicati o non rientravano nell'intervallo di date - + Some QSOs were duplicates or out of date range Alcuni QSO erano duplicati o non rientravano nell'intervallo di date - + Command syntax error Errore di Sintassi - + LoTW Connection error (no network or LoTW is unreachable) Errore di connessione LoTW (nessuna rete o LoTW non è raggiungibile) - - + + Unexpected Error from TQSL Errore inaspettato da TQSL - + TQSL not found TQSL non trovato - + TQSL crashed TQSL si è bloccato @@ -7925,620 +8459,684 @@ Questa password sarà necessaria in seguito per ripristinarle. &Servizi - + Toolbar Barra degli strumenti - - + + Clock Orologio - - + + Map Mappa - - + + DX Cluster DX Cluster - + WSJTX WSJTX - - + + Rotator Rotore - - + + Bandmap Bandmap - - + + Rig Radio - - + + Online Map Mappa Online - - + + CW Console Console CW - - + + Chat Chat - - + + Profile Image Foto su QRZ.com/HamQTH - - + + Alerts Avvisi - + &Settings &Impostazioni - + &Import &Importa - + &Export &Esporta - + Connect R&ig Connetti &Radio - + &About &A proposito di - + + Print QS&L + Stampa QS&L + + + Upload Caricamento - + Service - Upload QSOs Servizio – Carica QSOs - + Download QSLs Scarica le QSL - + Service - Download QSLs Servizion - Scarica le QSL - + Quit Esci - + Application - Quit Chiudi applicazione - - + + New QSO - Clear Nuovo QSO - Pulisci - - + + New QSO - Save Nuovo QSO - Salva - + S&tatistics S&tatistiche - + Wsjtx Wsjtx - + Connect R&otator Connetti R&otore - + QSO &Filters &Filtri QSO - + &Awards &Diplomi - + Edit Rules Modifica Regole - + Clear Cancella - + Show Alerts Mostra Avvisi - + Beep Beep - - + + Contest Contest - + Dupe Check Controllo Duplicati - + Sequence Sequenza - + Linking Exchange With Collega scambio con - - - - - - - + + + + + + + Pack Data && Settings Impacchettare dati e impostazioni - - + + Unpack Data && Settings Estrarre dati e impostazioni - + QSL &Gallery &Galleria QSL - + Developer Tools Strumenti dev - + Run custom read-only SQL queries against the logbook database Eseguire query SQL personalizzate in sola lettura sul database del logbook - - Print QSL &Labels - &Stampa etichette QSL - - - + DXCC &Submission List &Elenco di invio DXCC - + Generate a list of contacts to submit for ARRL DXCC award credit Generare un elenco di contatti da presentare per il credito del premio ARRL DXCC - + Connect &CW Keyer Connetti &CW Keyer - + &Wiki &Wiki - + Report &Bug... &segnala un errore... - + &Manual Entry &Inserimento Manuale - + Switch New Contact dialog to the manually entry mode<br/>(time, freq, profiles etc. are not taken from their common sources) Passa alla modalità di inserimento manuale nella finestra di dialogo Nuovo contatto<br/>(ora, frequenza, profili ecc. non vengono presi dalle loro fonti comuni) - + Mailing List... Mailing List... - + Edit Modifica - - + + Save Arrangement Salva disposizione - + Keep Options Mantieni opzioni - + Restore connection options after application restart Ripristina le opzioni di connessione dopo il riavvio dell'applicazione - + Logbook - Search Callsign Logbook - Cerca Callsign - - + + New QSO - Add text from Callsign field to Bandmap Nuovo QSO: aggiungi testo dal campo del nominativo alla Bandmap - + Rig - Band Down Radio - Banda Giù - + Rig - Band Up Radio - Banda Su - + New QSO - Use Callsign from the Whisperer Nuovo QSO: usa il nominativo del Whisperer - + CW Console - Key Speed Up Console CW: aumenta velocità - + CW Console - Key Speed Down Console CW: diminuisci velocità - + CW Console - Profile Up Console CW: carica profilo - + CW Console - Profile Down Console CW: scarica profilo - + Rig - PTT On/Off Radio - PTT On/Off - + All Bands Tutte le Bande - + Each Band Ogni Banda - + Each Band && Mode Ogni Banda && Modo - + No Check Nessun Controllo - + Single Singolo - + Per Band Per Banda - + Stop Stop - + Reset Reset - + None Nessuno - + + Download LoTW DXCC Credits + Scarica crediti DXCC LoTW + + + + Service - Download LoTW DXCC Credits + Servizio - Scarica crediti DXCC LoTW + + + Theme: Native Tema: Native - + Theme: QLog Light Tema: QLog Light - + Theme: QLog Dark Tema: QLog Dark - + What's New Novità - + Export Cabrillo Esporta Cabrillo - + Color Theme Tema colore - + Not enabled for non-Fusion style Non abilitato per stili diversi da Fusion - + Press to tune the alert Premi per sintonizzare l'allarme - + + Startup ADI + + + + Clublog Immediately Upload Error Errore di caricamento immediato di Clublog - - - + + + <b>Error Detail:</b> <b>Dettagli errore:</b> - + op: op: - + A New Version Una nuova versione - + A new version %1 is available. Una nuova versione %1 è disponibile. - + Remind Me Later Ricordamelo più tardi - + Download Download - + + + QLog Warning + Avviso QLog + + + + LoTW is not configured properly.<p>Please, use <b>Settings</b> dialog to configure it.</p> + LoTW non è configurato correttamente.<p>Usare la finestra <b>Impostazioni</b> per configurarlo.</p> + + + + + QLog Error + Errore di QLog + + + + Cannot load local DXCC entities from the logbook: + Impossibile caricare le entità DXCC locali dal log: + + + + Unknown DXCC Entity + Entità DXCC sconosciuta + + + + Cannot determine a local DXCC entity from logbook contacts. + Impossibile determinare un’entità DXCC locale dai contatti del log. + + + + LoTW DXCC Credits + Crediti DXCC LoTW + + + + Select the local DXCC entity for which LoTW DXCC credits will be downloaded: + Selezionare l’entità DXCC locale per cui scaricare i crediti DXCC LoTW: + + + + Cancel + + + + + Downloading LoTW DXCC credits + Download crediti DXCC LoTW + + + + Processing LoTW DXCC credits + Elaborazione crediti DXCC LoTW + + + + LoTW DXCC Credit Import Summary + Riepilogo importazione crediti DXCC LoTW + + + + LoTW DXCC credit import failed: + Importazione crediti DXCC LoTW non riuscita: + + + Failed to encrypt credentials. Impossibile cifrare le credenziali. - + Database files (*.dbe);;All files (*) File di database (*.dbe);;Tutti i file (*) - + Failed to create temporary file. Impossibile creare il file temporaneo. - + Failed to dump the database. Impossibile esportare il database. - + Compressing database... Compressione del database… - + Database successfully dumped to %1 Database esportato con successo in %1 - + Failed to compress the database. Impossibile comprimere il database. - + Failed to prepare database for import. Impossibile preparare il database per l’importazione. - + Classic Classico - + Do you want to remove the Contest filter %1? Vuoi rimuovere il Contest filter %1? - + Contest: Contest: - + <h1>QLog %1</h1><p>&copy; 2019 Thomas Gatzweiler DL2IC<br/>&copy; 2021-2026 Ladislav Foldyna OK1MLG<br/>&copy; 2025-2026 Michael Morgan AA5SH<br/>&copy; 2025-2026 Kyle Boyle VE9KZ</p><p>Based on Qt %2<br/>%3<br/>%4<br/>%5</p><p>Icon by <a href='http://www.iconshock.com'>Icon Shock</a><br />Satellite images by <a href='http://www.nasa.gov'>NASA</a><br />ZoneDetect by <a href='https://github.com/BertoldVdb/ZoneDetect'>Bertold Van den Bergh</a><br />TimeZone Database by <a href='https://github.com/evansiroky/timezone-boundary-builder'>Evan Siroky</a> <h1>QLog %1</h1><p>&copy; 2019 Thomas Gatzweiler DL2IC<br/>&copy; 2021-2026 Ladislav Foldyna OK1MLG<br/>&copy; 2025-2026 Michael Morgan AA5SH<br/>&copy; 2025-2026 Kyle Boyle VE9KZ</p><p>Basato su Qt %2<br/>%3<br/>%4<br/>%5</p><p>Icona di <a href=' http://www.iconshock.com'>Icon Shock</a><br />Immagini satellitari di <a href='http://www.nasa.gov'>NASA</a><br />ZoneDetect di <a href='https://github.com/BertoldVdb/ZoneDetect'>Bertold Van den Bergh</a><br />Database TimeZone di <a href='https://github.com/evansiroky/timezone -boundary-builder'>Evan Siroky</a> - + About A proposito di - + N/A N/A - MapWebChannelHandler + MapPageController - - - - Grid - Griglia - - - - - - Gray-Line - Gray-line + + Aurora + Aurora - - - + Beam Direzione TX - - - - Aurora - Aurora + + Chat + Chat - - - - MUF - MUF + + Grid + Griglia - - - + + Gray-Line + Gray-line + + + IBP IBP - - - - Chat - Chat + + MUF + MUF - - - + WSJTX - CQ WSJTX - CQ - - - + Path Direzioni @@ -8724,12 +9322,12 @@ Questa password sarà necessaria in seguito per ripristinarle. Antenna - + Blank Vuoto - + W W @@ -8809,112 +9407,112 @@ Questa password sarà necessaria in seguito per ripristinarle. Accesso al Logbook non riuscito - + LP LP - + New Entity! Nuova Entità! - + New Band! Nuova Banda! - + New Mode! Nuovo Modo! - + New Band & Mode! Nuova Banda e Modo! - + New Slot! Nuovo Slot! - + Worked Lavorato - + Confirmed Confermato - + GE GE - + GM GM - + GA GA - + m - + Callbook search is inactive Ricerca sul Callbook inattiva - + Callbook search is active Ricerca sul Callbook attiva - + Contest ID must be filled in to activate L'ID del contest deve essere compilato per attivare - + two or four adjacent Maidenhead grid locators, each four characters long, (ex. EN98,FM08,EM97,FM07) due o quattro localizzatori di griglia Maidenhead adiacenti, ciascuno lungo quattro caratteri, (es. EN98,FM08,EM97,FM07) - + the contacted station's DARC DOK (District Location Code) (ex. A01) il DARC DOK (District Location Code) della stazione contattata (ex. A01) - + World Wide Flora & Fauna World Wide Flora & Fauna - + Special Activity Group Gruppo di Attività Speciali - + Special Activity Group Information Informazioni sul gruppo di attività speciali - + It is not the name of the contest but it is an assigned<br>Contest ID (ex. CQ-WW-CW for CQ WW DX Contest (CW)) Non è il nome del Contest ma è un ID del Contest assegnato (ad esempio CQ-WW-CW per CQ WW DX Contest (CW)) - + Description of the contacted station's equipment Descrizione dell'attrezzatura della stazione contattata @@ -9128,7 +9726,7 @@ Verificate o aggiornate le seguenti impostazioni. QCoreApplication - + QLog Help QLog Help @@ -9158,31 +9756,31 @@ Verificate o aggiornate le seguenti impostazioni. - + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + QLog Warning Avviso QLog @@ -9213,63 +9811,63 @@ Aggiornamento elenco club non riuscito. Impossibile rimuovere i vecchi recordErrore di rete. Impossibile scaricare l'elenco dei club per - - - + + + - - - - + + + + QLog Error Errore di QLog - + QLog is already running QLog è già in esecuzione - + Failed to process pending database import. Impossibile elaborare l’importazione in sospeso del database. - + The database was imported successfully, but the stored passwords could not be restored (decryption failed or the data is corrupted). All service passwords have been cleared and must be re-entered in Settings. Il database è stato importato con successo, ma le password memorizzate non sono state ripristinate (decrittazione fallita o dati corrotti). Tutte le password dei servizi sono state cancellate e devono essere reinserite nelle Impostazioni. - + Could not connect to database. Impossibile collegare il Database. - + Could not export a QLog database to ADIF as a backup.<p>Try to export your log to ADIF manually Impossibile esportare un database QLog in ADIF come backup.<p>Prova a esportare manualmente il tuo Log in ADIF - + Database migration failed. Errore nella Migrazione del database. - + - + QLog Info QLog Info - + Activity name is already exists. Il Nome dell'Attività esiste già. @@ -9294,33 +9892,33 @@ Aggiornamento elenco club non riuscito. Impossibile rimuovere i vecchi recordImpossibile aggiornare le Regole di allarme - + DXC Server Name Error Errore nel nome del server DXC - + DXC Server address must be in format<p><b>[username@]hostname:port</b> (ex. hamqth.com:7300)</p> L'indirizzo del server DXC deve essere nel formato<p><b>[nomeutente@]nomehost:porta</b> (es. hamqth.com:7300)</p> - + DX Cluster Password DX Cluster Password - + Invalid Password Password non corretta - + DXC Server Connection Error Errore di connessione al server DXC - + Filename is empty Filename vuoto @@ -9355,128 +9953,128 @@ Aggiornamento elenco club non riuscito. Impossibile rimuovere i vecchi record - + Filter name is already exists. Il nome del filtro esiste già. - + <b>Rig Error:</b> <b>Errore della Radio:</b> - + <b>Rotator Error:</b> <b>Errore del rotore:</b> - + <b>CW Keyer Error:</b> <b>Errore Keyer CW:</b> - + The fields <b>%0</b> will not be saved because the <b>%1</b> is not filled. I campi <b>%0</b> non verranno salvati perché <b>%1</b> non è compilato. - + Your callsign is empty. Please, set your Station Profile Il tuo nominativo è vuoto. Per favore, imposta il Profilo della tua Stazione - - + + Please, define at least one Station Locations Profile Per favore, definisci almeno un profilo di posizioni della stazione - + WSJTX Multicast is enabled but the Address is not a multicast address. WSJTX Multicast è abilitato ma l'indirizzo non è un indirizzo multicast. - + Loop detected. Raw UDP forward uses the same port as the WSJT-X receiving port. Rilevato loop. L’inoltro UDP grezzo utilizza la stessa porta del porto di ricezione WSJT-X. - + Rig port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device La porta della Radio deve essere una porta COM valida.<br>Per Windows utilizzare COMxx, per sistemi operativi di tipo Unix utilizzare un percorso per il dispositivo - + Rig PTT port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device La porta PTT della Radio deve essere una porta COM valida.<br>Per Windows utilizzare COMxx, per sistemi operativi di tipo Unix utilizzare un percorso al dispositivo - + <b>TX Range</b>: Max Frequency must not be 0. <b>Intervallo TX</b>: la frequenza massima non deve essere 0. - + <b>TX Range</b>: Max Frequency must not be under Min Frequency. <b>Intervallo TX</b>: la frequenza massima non deve essere inferiore alla frequenza minima. - + Rotator port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device La porta del rotore deve essere una porta COM valida.<br>Per Windows utilizzare COMxx, per sistemi operativi di tipo Unix utilizzare un percorso per il dispositivo - + CW Keyer port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device La porta del keyer CW deve essere una porta COM valida.<br>Per Windows utilizzare COMxx, per sistemi operativi di tipo Unix utilizzare un percorso per il dispositivo - + Cannot change the CW Keyer Model to <b>Morse over CAT</b><br>No Morse over CAT support for Rig(s) <b>%1</b> Impossibile modificare il modello CW Keyer in <b>Morse su CAT</b><br>Nessun supporto Morse su CAT per a Radio <b>%1</b> - + Cannot delete the CW Keyer Profile<br>The CW Key Profile is used by Rig(s): <b>%1</b> Impossibile eliminare il profilo CW Keyer<br>Il profilo CW Keyer è utilizzato da Radio: <b>%1</b> - + Callsign has an invalid format L'indicativo ha un formato non valido - + Operator Callsign has an invalid format Il Call dell'Operatore ha un formato non valido - + Gridsquare has an invalid format La Griglia ha un formato non valido - + VUCC Grids have an invalid format (must be 2 or 4 Gridsquares separated by ',') Le griglie VUCC hanno un formato non valido (devono essere 2 o 4 Gridsquare separati da ',') - + Country must not be empty Nazione non deve essere vuoto - + CQZ must not be empty La CQ Zone non deve essere vuota - + ITU must not be empty ITU non deve essere vuoto - + Cannot update QSO Filter Conditions Impossibile aggiornare le Condizioni del Filtro QSO @@ -9484,79 +10082,79 @@ Aggiornamento elenco club non riuscito. Impossibile rimuovere i vecchi record QObject - + km km - + miles miglia - + Connection Refused Connessione rifiutata - + Host closed the connection Host ha chiuso la connessione - + Host not found Host non trovato - + Timeout Timeout - + Network Error Errore di rete - + Internal Error Errore Interno - + Importing Database Importazione database - + Opening Database Apertura Database - + Backuping Database Backup del database - + Migrating Database Migrazione del database - + Starting Application Avvio applicazione @@ -9656,12 +10254,12 @@ Aggiornamento elenco club non riuscito. Impossibile rimuovere i vecchi recordMio DXCC - + Cannot connect to DXC Server <p>Reason <b>: Impossibile connettersi al server DXC <p>Motivo <b>: - + <b>Imported</b>: %n contact(s) <b>Importato</b>: %n contatto @@ -9669,7 +10267,7 @@ Aggiornamento elenco club non riuscito. Impossibile rimuovere i vecchi record - + <b>Warning(s)</b>: %n <b>Allarme</b>: %n @@ -9677,7 +10275,7 @@ Aggiornamento elenco club non riuscito. Impossibile rimuovere i vecchi record - + <b>Error(s)</b>: %n <b>Errore</b>: %n @@ -9755,6 +10353,32 @@ Aggiornamento elenco club non riuscito. Impossibile rimuovere i vecchi recordWorked Lavorato + + + IARU Region 1 + + + + + + Failed to write file: %1 + Impossibile scrivere il file: %1 + + + + Cannot open file: %1 + Impossibile aprire il file: %1 + + + + Invalid guide file: %1 + File guida non valido: %1 + + + + Invalid guide file: missing title + File guida non valido: titolo mancante + QRZCallbook @@ -9767,7 +10391,7 @@ Aggiornamento elenco club non riuscito. Impossibile rimuovere i vecchi record QRZUploader - + General Error Errore generico @@ -9790,33 +10414,33 @@ Aggiornamento elenco club non riuscito. Impossibile rimuovere i vecchi recordOrdina per: - + Export Filtered Esporta filtrati - + Date (Newest) Data (più recente) - + Date (Oldest) Data (più vecchia) - + Callsign (A-Z) Nominativo (A-Z) - + Callsign (Z-A) Nominativo (Z-A) - - + + %n QSL card(s) %n scheda(e) QSL @@ -9824,72 +10448,72 @@ Aggiornamento elenco club non riuscito. Impossibile rimuovere i vecchi record - + All QSL Cards Tutte le schede QSL - + Favorites Preferiti - + By Country Per paese - + By Date Per data - + By Band Per banda - + By Mode Per modalità - + By Continent Per continente - + Remove from Favorites Rimuovi dai preferiti - + Add to Favorites Aggiungi ai preferiti - + Open Apri - + Save... Salva… - + Save QSL Card Salva scheda QSL - + Export QSL Cards Esporta schede QSL - + Exported %1 of %2 cards Esportate %1 di %2 schede @@ -9932,28 +10556,23 @@ Aggiornamento elenco club non riuscito. Impossibile rimuovere i vecchi recordDettagli - - New QSLs: - Nuove QSLs: + + New QSLs: + Nuove QSL: - Updated QSOs: - QSO aggiornati: + Updated QSOs: + QSO aggiornati: - - Unmatched QSLs: - QSL senza corrispondenza: + + Unmatched QSLs: + QSL non abbinate: QSLPrintLabelDialog - - - Print QSL Labels - Stampa etichette QSL - Filter @@ -9985,235 +10604,397 @@ Aggiornamento elenco club non riuscito. Impossibile rimuovere i vecchi recordFiltro Utente - + Label Template Modello etichetta - + + Page Size: Dimensione pagina: - + Columns: Colonna: - + Rows: Riga: - + + Label Width: Larghezza etichetta: - + + Print QSL Labels / Cards + Stampa etichette / cartoline QSL + + + + Print Mode + Modalità stampa + + + + Mode: + Modo: + + + + + QSL Card + Cartolina QSL + + + + Card Width: + Larghezza cartolina: + + + + Card Height: + Altezza cartolina: + + + + Card Gap: + Spazio tra cartoline: + + + + Label Height: Altezza etichetta: - + + Label X Offset: + Offset X etichetta: + + + + Label Y Offset: + Offset Y etichetta: + + + + Label Background: + Sfondo etichetta: + + + + Fill under label + Riempi sotto etichetta + + + + + Color + Colore + + + + Background Image: + Immagine di sfondo: + + + + Browse + Cerca + + + + Clear + Cancella + + + Left Margin: Margine sinistro: - + Top Margin: Margine superiore: - + H Spacing: Spaziatura orizzontale: - + V Spacing: Spaziatura verticale: - + Label Appearance Aspetto etichetta - + Print Label Borders Stampa bordi etichette - + QSOs per Label: QSO per etichetta: - + Footer Left Text: Testo piè di pagina sinistro: - + Footer Right Text: Testo piè di pagina destro: - + Skip Label: Salta etichetta: - + Sans Font: Carattere sans-serif: - + Mono Font: Carattere monospaziato: - + + Text Color: + Colore testo: + + + Callsign Size: Dimensione del nominativo: - + "To Radio" Size: Dimensione «To Radio»: - + "To Radio" Text: Testo «To Radio»: - + Header Size: Dimensione intestazione: - + Data Size: Dimensione dati: - + Date Header Text: Testo intestazione data: - + Date Format: Formato data: - + Time Header Text: Testo intestazione ora: - + Band Header Text: Testo intestazione banda: - + Mode Header Text: Testo intestazione modalità: - + QSL Header Text: Testo intestazione QSL: - + Extra Column: Colonna aggiuntiva: - + Extra Column Text Testo della colonna aggiuntiva - + (DB column name) (nome colonna DB) - - + + No matching QSOs found Nessun QSO corrispondente trovato - - + + Page 0 of 0 Pagina 0 di 0 - + Labels: 0 (0 pages) Etichette: 0 (0 pagine) - + Print Stampa - + Export as PDF Esporta come PDF - - + + Export as Images + Esporta immagini + + + + Label Sheet + Foglio etichette + + + + Custom Personalizzato - + Empty Vuoto - + QSOs matching this station profile QSO che corrispondono a questo profilo di stazione - + + Select Label Text Color + Seleziona colore testo etichetta + + + + Select Label Background Color + Seleziona colore sfondo etichetta + + + + + + Select QSL Card Background + Seleziona sfondo cartolina QSL + + + + Images (*.png *.jpg *.jpeg *.bmp) + Immagini (*.png *.jpg *.jpeg *.bmp) + + + + Cannot read selected image file. + Impossibile leggere il file immagine selezionato. + + + + Selected file is not a valid image. + Il file selezionato non è un’immagine valida. + + + + Cards: %1 (%2 pages) + Cartoline: %1 (%2 pagine) + + + Labels: %1 (%2 pages) Etichette: %1 (%2 pagine) - + Page %1 of %2 Pagina %1 di %2 - + Export PDF Esporta PDF - + PDF Files (*.pdf) File PDF (*.pdf) - + + + + Export QSL Card Images + Esporta immagini cartoline QSL + + + + Some image files already exist. Overwrite them? + Alcuni file immagine esistono già. Sovrascriverli? + + + + Exported %n QSL card image(s). + + Esportate %n immagine/i di cartoline QSL. + + + + + + Exported %1 of %2 QSL card images. + Esportate %1 di %2 immagini di cartoline QSL. + + + + QSOs were not marked as sent. + I QSO non sono stati contrassegnati come inviati. + + + Mark as Sent Contrassegna come inviato - + Mark printed/exported QSOs as sent? Contrassegnare i QSO stampati/esportati come inviati? @@ -10270,7 +11051,7 @@ Aggiornamento elenco club non riuscito. Impossibile rimuovere i vecchi record - + Blank Vuoto @@ -10683,318 +11464,318 @@ Aggiornamento elenco club non riuscito. Impossibile rimuovere i vecchi recordMembro: - + &Reset &Reset - + &Lookup &Cerca - - - + + + No No - - - + + + Yes - - - + + + Requested Richiesto - - - + + + Queued In coda - - - + + + Ignored Ignorato - + Bureau Bureau - + Direct Diretto - + Electronic Elettronica - + Submit changes Inviare le modifiche - + Really submit all changes? Inviare davvero tutte le modifiche? - - - - + + + + QLog Error Errore di QLog - + Cannot save all changes - internal error Impossibile salvare tutte le modifiche: errore interno - + Cannot save all changes - try to reset all changes Impossibile salvare tutte le modifiche: provare a ripristinare tutte le modifiche - + QSO Detail Dettagli del QSO - + Edit QSO Modifica QSO - + Downloading eQSL Image Download immagine da eQSL - + Cancel Cancella - + eQSL Download Image failed: Download immagine da eQSL fallito: - + DX Callsign must not be empty Il nominativo DX non deve essere vuoto - + DX callsign has an incorrect format l'indicativo DX ha un formato non corretto - - + + TX Frequency or Band must be filled La frequenza o la banda TX devono essere compilate - + TX Band should be La Banda TX dovrebbe essere - + RX Band should be La Banda RX dovrebbe essere - - + + DX Grid has an incorrect format La Griglia DX ha un formato non corretto - + Based on callsign, DXCC Country is different from the entered value - expecting In base al nominativo, il Paese DXCC è diverso dal valore inserito come previsto - + Based on callsign, DXCC Continent is different from the entered value - expecting In base al nominativo, il continente DXCC è diverso dal valore inserito come previsto - + Based on callsign, DXCC ITU is different from the entered value - expecting In base al nominativo, DXCC ITU è diverso dal valore inserito come previsto - + Based on callsign, DXCC CQZ is different from the entered value - expecting In base al nominativo, DXCC CQZ è diverso dal valore inserito come previsto - + VUCC has an incorrect format VUCC ha un formato non corretto - + Based on Frequencies, Sat Mode should be In base alle frequenze, la modalità satellitare dovrebbe essere - + blank vuoto - + Sat name must not be empty Il nome del Satellite non deve essere vuoto - + Own Callsign must not be empty Il proprio nominativo non deve essere vuoto - + Own callsign has an incorrect format Il proprio indicativo ha un formato non corretto - + Own VUCC Grids have an incorrect format La propria Griglia VUCC ha un formato non corretto - + Based on own callsign, own DXCC ITU is different from the entered value - expecting In base al proprio nominativo, il proprio DXCC ITU è diverso dal valore inserito previsto - + Based on own callsign, own DXCC CQZ is different from the entered value - expecting In base al proprio nominativo, il proprio DXCC CQZ è diverso dal valore inserito previsto - + Based on own callsign, own DXCC Country is different from the entered value - expecting In base al proprio nominativo, il proprio Paese DXCC è diverso dal valore inserito previsto - + Based on SOTA Summit, QTH does not match SOTA Summit Name - expecting Secondo il SOTA Summit, QTH non corrisponde al nome del SOTA Summit come previsto - + Based on SOTA Summit, Grid does not match SOTA Grid - expecting Secondo il SOTA Summit, la Griglia non corrisponde alla Griglia SOTA, come previsto - + Based on POTA record, QTH does not match POTA Name - expecting Secondo i record POTA, il QTH non corrisponde al nome POTA come previsto - + Based on POTA record, Grid does not match POTA Grid - expecting Secondo i record POTA, la griglia non corrisponde alla griglia POTA come previsto - + Based on SOTA Summit, my QTH does not match SOTA Summit Name - expecting Secondo il SOTA Summit, il mio QTH non corrisponde al nome SOTA Summit come previsto - + Based on SOTA Summit, my Grid does not match SOTA Grid - expecting Secondo il SOTA Summit, la mia griglia non corrisponde alla griglia SOTA, come previsto - + Based on POTA record, my QTH does not match POTA Name - expecting Secondo i record POTA, il mio QTH non corrisponde al nome POTA come previsto - + Based on POTA record, my Grid does not match POTA Grid - expecting Secondo i record POTA, la mia griglia non corrisponde alla griglia POTA, come previsto - + LoTW Sent Status to <b>No</b> does not make any sense if QSL Sent Date is set. Set Date to 1.1.1900 to leave the date field blank Lo stato di invio LoTW di <b>No</b> non ha alcun senso se è impostata la data di invio della QSL. Imposta la data su 1.1.1900 per lasciare vuoto il campo della data - + Date should be present for LoTW Sent Status <b>Yes</b> La data deve essere presente per lo stato inviato LoTW <b>Sì</b> - + eQSL Sent Status to <b>No</b> does not make any sense if QSL Sent Date is set. Set Date to 1.1.1900 to leave the date field blank Lo stato di invio eQSL <b>No</b> non ha alcun senso se è impostata la data di invio QSL. Imposta la data su 1.1.1900 per lasciare vuoto il campo della data - + Date should be present for eQSL Sent Status <b>Yes</b> La data deve essere presente per lo stato inviato eQSL <b>Sì</b> - + Paper Sent Status to <b>No</b> does not make any sense if QSL Sent Date is set. Set Date to 1.1.1900 to leave the date field blank Lo stato di invio QSL Cartacea pari a <b>No</b> non ha alcun senso se è impostata la data di invio QSL. Imposta la data su 1.1.1900 per lasciare vuoto il campo della data - + Date should be present for Paper Sent Status <b>Yes</b> La data deve essere presente per lo stato di invio cartaceo <b>Sì</b> - + Callbook error: Errore del Callbook: - - + + <b>Warning: </b> <b>Allarme: </b> - + Validation Validazione - + Yellow marked fields are invalid.<p>Nevertheless, save the changes?</p> I campi contrassegnati in giallo non sono validi.<p>Salvare comunque le modifiche?</p> - + &Save &Salva - + &Edit &Modifica @@ -11032,52 +11813,52 @@ Aggiornamento elenco club non riuscito. Impossibile rimuovere i vecchi recordAggiungi condizione - + Equal Uguale - + Not Equal Non uguale - + Contains Contiene - + Not Contains Non Contiene - + Greater Than Maggiore di - + Less Than Minore di - + Starts with Inizia con - + RegExp - + Remove Rimuovi - + Must not be empty Non deve essere vuoto @@ -11113,27 +11894,27 @@ Aggiornamento elenco club non riuscito. Impossibile rimuovere i vecchi record Rig - + No Rig Profile selected Nessun Profilo Radio selezionato - + Rigctld Error Errore Rigctld - + Initialization Error Errore di inizializzazione - + Internal Error Errore Interno - + Cannot open Rig Impossibile aprire Radio @@ -11151,36 +11932,66 @@ Aggiornamento elenco club non riuscito. Impossibile rimuovere i vecchi record - + Disconnected Disconnesso - - + + MHz MHz - + Disable Split Disattiva split - + RIT: 0.00000 MHz - + XIT: 0.00000 MHz - + PWR: %1W + + + OUT + + + + + Outside Bandmap Guide range + Fuori dall’intervallo della guida Bandmap + + + + SOS + + + + + Emergency frequency: %1 MHz + Frequenza di emergenza: %1 MHz + + + + IBP + IBP + + + + International Beacon Project: %1 MHz + + RigctldAdvancedDialog @@ -11270,57 +12081,57 @@ Installare Hamlib o specificare manualmente il percorso. RigctldManager - + rigctld executable not found in /app/bin/. This should not happen in Flatpak build. Eseguibile rigctld non trovato in /app/bin/. Questo non dovrebbe accadere nella build Flatpak. - + rigctld executable not found. Please install Hamlib or specify the path in Advanced settings. Eseguibile rigctld non trovato. Installare Hamlib o specificare il percorso nelle impostazioni avanzate. - + Hamlib major version mismatch: QLog was compiled with Hamlib %1 but rigctld reports version %2.%3.%4. Rig model IDs are incompatible between major versions. Incompatibilità della versione principale di Hamlib: QLog è stato compilato con Hamlib %1, ma rigctld riporta la versione %2.%3.%4. Gli ID dei modelli dei trasmettitori non sono compatibili tra le versioni principali. - + Port %1 is already in use. Another rigctld or application may be running on this port. La porta %1 è già in uso. Un altro rigctld o applicazione potrebbe essere in esecuzione su questa porta. - + rigctld started but not responding on port %1. rigctld è stato avviato ma non risponde sulla porta %1. - + Failed to start rigctld: %1 %2 Impossibile avviare rigctld: %1 %2 - + rigctld crashed. rigctld è crashato. - + rigctld timed out. rigctld ha superato il tempo limite. - + Write error with rigctld. Errore di scrittura con rigctld. - + Read error with rigctld. Errore di lettura con rigctld. - + Unknown rigctld error. Errore rigctld sconosciuto. @@ -11328,22 +12139,22 @@ Installare Hamlib o specificare manualmente il percorso. Rotator - + No Rotator Profile selected Nessun Profilo Rotore selezionato - + Initialization Error Errore di inizializzazione - + Internal Error Errore Interno - + Cannot open Rotator Impossibile aprire Rotore @@ -11470,7 +12281,7 @@ Installare Hamlib o specificare manualmente il percorso. - + Callsign Indicativo @@ -11539,20 +12350,21 @@ Installare Hamlib o specificare manualmente il percorso. - - - - - - - - - - + + + + + + - - - + + + + + + + + Add Aggiungi @@ -11647,6 +12459,7 @@ Installare Hamlib o specificare manualmente il percorso. + Description Descrizione @@ -12038,7 +12851,7 @@ Installare Hamlib o specificare manualmente il percorso. - + Start rigctld daemon to share rig with other applications (e.g. WSJT-X) Avviare il demone rigctld per condividere il trasmettitore con altre applicazioni (es. WSJT-X) @@ -12175,14 +12988,14 @@ Installare Hamlib o specificare manualmente il percorso. - + Serial Seriale - - + + Network Rete @@ -12233,8 +13046,8 @@ Installare Hamlib o specificare manualmente il percorso. - - + + HamQTH @@ -12243,7 +13056,7 @@ Installare Hamlib o specificare manualmente il percorso. - + Username Nome utente @@ -12253,15 +13066,15 @@ Installare Hamlib o specificare manualmente il percorso. - + Password Password - - + + QRZ.com @@ -12321,22 +13134,97 @@ Installare Hamlib o specificare manualmente il percorso. I QSO vengono caricati immediatamente - + + Startup ADI + + + + + Configured ADI/ADIF files are checked only at startup. A newly added file starts at its current end, so only later appended QSOs are loaded. This is not a live watcher; if many new QSOs are found, loading stops and the standard Import should be used. + I file ADI/ADIF configurati vengono controllati solo all’avvio. Un file appena aggiunto parte dalla sua fine attuale, quindi vengono caricati solo i QSO aggiunti successivamente. Non è un monitoraggio in tempo reale; se vengono trovati molti nuovi QSO, il caricamento si arresta ed è necessario usare l’importazione standard. + + + + Removing a file also forgets its recovery position. + Rimuovendo un file viene dimenticata anche la sua posizione di ripristino. + + + + Remove + Rimuovi + + + + Used when a file row has Missing QSL Sent set to Custom. Explicit ADIF values are kept. + Usato quando una riga file ha QSL Sent mancante impostato su Personalizzato. I valori ADIF espliciti vengono mantenuti. + + + + Custom QSL Sent Defaults + Valori predefiniti QSL Sent personalizzati + + + + Paper QSL + QSL cartacea + + + + DCL + DCL + + + + Select the <b>Bandmap Guide</b> profile shown as visual frequency hints. It does not affect mode identification. + Selezionare il profilo <b>Guida Bandmap</b> mostrato come suggerimento visivo di frequenza. Non influisce sull’identificazione del modo. + + + + Manage + Gestisci + + + + Double-click cells to edit start/end frequency, enabled state, or SAT mode. Band names are fixed; new bands cannot be added here. + Doppio clic sulle celle per modificare frequenza iniziale/finale, stato abilitato o modo SAT. I nomi delle bande sono fissi; qui non è possibile aggiungere nuove bande. + + + + QSO DXCC Status Colors + Colori stato DXCC QSO + + + + Used for DX spots, Bandmap, WSJT-X and QSO status hints. Confirmed has no highlight by default. Click a color cell to choose a color or set No color. + Usato per DX spot, Bandmap, WSJT-X e suggerimenti di stato QSO. Confermato non ha evidenziazione per impostazione predefinita. Fare clic su una cella colore per scegliere un colore o impostare Nessun colore. + + + + Restore Defaults + Ripristina predefiniti + + + + Shortcuts + Scorciatoie + + + Danger Zone Zona pericolosa - + <b>⚠ This is a danger zone. Proceed with caution, as actions performed here cannot be undone and may have a significant impact on your log.</b> <b>⚠ Questa è una zona pericolosa. Procedere con cautela, poiché le azioni eseguite non possono essere annullate e possono avere un impatto significativo sul tuo log.</b> - + Delete All QSOs Elimina tutti i QSO - + Delete All Passwords from the Secure Store Elimina tutte le password dall’archivio sicuro @@ -12347,7 +13235,8 @@ Installare Hamlib o specificare manualmente il percorso. - + + eQSL @@ -12380,7 +13269,8 @@ Installare Hamlib o specificare manualmente il percorso. - + + LoTW @@ -12415,118 +13305,121 @@ Installare Hamlib o specificare manualmente il percorso. In uso un'istanza TQSL interna - + Others Altri - + Status Confirmed By Confermato da - + Paper Cartaceo - + Chat Chat - + <b>Security Notice:</b> QLog stores all passwords in the Secure Storage. Unfortunately, ON4KST uses a protocol where this password is sent over an unsecured channel as plaintext.</p><p>Please exercise caution when choosing your password for this service, as your password is sent over an unsecured channel in plaintext form.</p> <b>Avviso di sicurezza:</b> QLog memorizza tutte le password nell'archivio sicuro. Sfortunatamente, ON4KST utilizza un protocollo in cui questa password viene inviata su un canale non protetto come testo normale.</p><p>Si prega di prestare attenzione quando si sceglie la password per questo servizio, poiché la password viene inviata su un canale non protetto in formato testo normale.< /p> - + Bands Bande - + Modes Modi - + The '>' character is interpreted as a marker for the initial cursor position in the Report column. <br/>Ex.: '5>9' means the cursor will be positioned on the second character Il carattere ">" è interpretato come un indicatore della posizione iniziale del cursore nella colonna Report.</br>Es.: "5>9" significa che il cursore sarà posizionato sul secondo carattere - + Color CQ Spots Colorare gli spot CQ - + Enable/Disable sending color-coded status indicators back to WSJT-X for each callsign calling CQ Abilitare/disabilitare l’invio di indicatori di stato codificati a colori verso WSJT-X per ogni nominativo che chiama CQ - + Rig Status Radio - + GUI GUI - + Time Format Formato orario - + 24-hour 24 ore - + AM/PM AM/PM - + Unit System Sistema di unità - + Metric Metrico - + Imperial Imperiale - + Date Format Formato data - + System Sistema - + + + + Custom Personalizzato - + <a href="https://doc.qt.io/qt-6/qdate.html#fromString-1">Time Format Documentation</a> <a href="https://doc.qt.io/qt-6/qdate.html#fromString-1">Documentazione del formato orario</a> - - + + DXCC @@ -12552,7 +13445,7 @@ Installare Hamlib o specificare manualmente il percorso. - + API Key Chiave API @@ -12562,52 +13455,52 @@ Installare Hamlib o specificare manualmente il percorso. Endpoint - + Wsjtx - + Raw UDP Forward Inoltro UDP non elaborato - + <p>List of IP addresses to which QLog forwards raw UDP WSJT-X packets.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Elenco di indirizzi IP a cui QLog inoltra i pacchetti UDP WSJT-X grezzi.</p>Gli indirizzi IP sono separati da uno spazio e hanno la forma IP:PORT - - - - - - + + + + + + ex. 192.168.1.1:1234 192.168.2.1:1234 - + Port Porta - + Port where QLog listens an incoming traffic from WSJT-X Porta dove QLog ascolta il traffico in entrata da WSJT-X - + Join Multicast Unisciti a Multicast - + Enable/Disable Multicast option for WSJTX Abilita/Disabilita opzione Multicast per WSJTX - + Multicast Address Indirizzo Multicast @@ -12661,328 +13554,528 @@ Installare Hamlib o specificare manualmente il percorso. Lasciare vuoto per il rilevamento automatico - + Specify Multicast Address. <br>On some Linux systems it may be necessary to enable multicast on the loop-back network interface. Specificare l'indirizzo multicast. <br>Su alcuni sistemi Linux potrebbe essere necessario abilitare il multicast sull'interfaccia di rete loopback. - + TTL TTL - + Time-To-Live determines the range<br> over which a multicast packet is propagated in your intranet. Time-To-Live determina l'intervallo<br> entro il quale un pacchetto multicast viene propagato nella tua Intranet. - + Notifications Notifiche - + LogID - + <p>Assigned LogID to the current log.</p>The LogID is sent in the Network Nofitication messages as a unique instance identified.<p> The ID is generated automatically and cannot be changed</> <p>LogID assegnato al registro corrente.</p>Il LogID viene inviato nei messaggi di notifica di rete come un'istanza univoca identificata.<p> L'ID viene generato automaticamente e non può essere modificato</> - + DX Spots DX Spots - + <p> List of IP addresses to which QLog sends UDP notification packets with DX Cluster Spots.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Elenco degli indirizzi IP a cui QLog invia pacchetti di notifiche UDP con DX Cluster Spots.</p>Gli indirizzi IP sono separati da uno spazio e hanno la forma IP:PORT - + Spot Alerts Allarmi Spot - + <p> List of IP addresses to which QLog sends UDP notification packets about user Spot Alerts.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Elenco degli indirizzi IP a cui QLog invia pacchetti di notifiche UDP sugli Spot Alerts dell'utente.</p>Gli indirizzi IP sono separati da uno spazio e hanno la forma IP:PORT - + QSO Changes Modifiche al QSO - + <p> List of IP addresses to which QLog sends UDP notification packets about a new/updated/deleted QSO in the log.</p>The IP addresses are separated by a space and have the form IP:PORT <p> Elenco degli indirizzi IP a cui QLog invia pacchetti di notifica UDP relativi a un QSO nuovo/aggiornato/eliminato nel log.</p>Gli indirizzi IP sono separati da uno spazio e hanno la forma IP:PORT - + Wsjtx CQ Spots Wsjtx CQ Spots - + <p> List of IP addresses to which QLog sends UDP notification packets with WSJTX CQ Spots.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Elenco degli indirizzi IP a cui QLog invia pacchetti di notifiche UDP con WSJTX CQ Spots.</p>Gli indirizzi IP sono separati da uno spazio e hanno la forma IP:PORT - + <p> List of IP addresses to which QLog sends UDP notification packets when Rig State changes.</p>The IP addresses are separated by a space and have the form IP:PORT <p>Elenco di indirizzi IP a cui QLog invia pacchetti di notifica UDP quando cambia lo stato della Radio.</p>Gli indirizzi IP sono separati da uno spazio e hanno il formato IP:PORT - - + + Special - Omnirig Speciale - Omnirig - + Cannot be changed Non può essere modificato - - + + Name Nome - + Report Report - - + + State Stato - + Start (MHz) Inizio (MHz) - + End (MHz) Fine (MHz) - + SAT Mode SAT Mode - - - + + + Disabled Disabilitato - - + + None Nessuno - + Hardware - + Software Software - + + + + + No No - + Even Pari - + Odd Dispari - + Mark Segna - + Space Spazio - + Dummy Fittizio - + Morse Over CAT Morse Over CAT - + WinKey WinKey - + CWDaemon - + FLDigi - + Single Paddle Single Paddle - + IAMBIC A - + IAMBIC B - + Ultimate Definitivo - + High High - + Low Low - + + Duplicate + Duplicato + + + + Already worked QSO + QSO già collegato + + + + New Entity + Nuova Entità + + + + DXCC entity not worked yet + Entità DXCC non ancora collegata + + + + New Band / Mode + Nuova banda / modo + + + + New band, mode, or band and mode + Nuova banda, modo o entrambi + + + + New Slot + Nuovo Slot + + + + New band and mode combination + Nuova combinazione banda/modo + + + + Worked + Lavorato + + + + Worked but not confirmed + Collegato, ma non confermato + + + + Confirmed + Confermato + + + + Confirmed QSO; no highlight by default + QSO confermato; nessuna evidenziazione predefinita + + + + Status + Stato + + + + Color + Colore + + + + Choose Color... + Scegli colore... + + + + Default + Predefinito + + + + No Color + Nessun colore + + + + Status Color + Colore stato + + + + No color + Nessun colore + + + + No highlight. Click to choose a color or set no color. + Nessuna evidenziazione. Fare clic per scegliere un colore o impostare nessun colore. + + + + Click to change color or set no color. + Fare clic per cambiare colore o impostare nessun colore. + + + Press <b>Modify</b> to confirm the profile changes or <b>Cancel</b>. Premi <b>Modifica</b> per confermare le modifiche al profilo o <b>Annulla</b>. - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + Modify Modifica - - - - - - - - - - + + + + + + + + + + Must not be empty Non deve essere vuoto - + Select File Seleziona File - + Auto Detect Rilevamento automatico - + TQSL was not found on this system. Please install TQSL or specify the path manually. TQSL non è stato trovato su questo sistema. Installare TQSL o specificare manualmente il percorso. - + Not found Non trovato - + Rig sharing is only available for Hamlib driver La condivisione del trasmettitore è disponibile solo per il driver Hamlib - + Rig sharing is not available for network connection La condivisione del trasmettitore non è disponibile per connessioni di rete - + + Off + Disattivato + + + Delete Passwords Elimina password - + All passwords have been deleted Tutte le password sono state eliminate - + Deleting all QSOs... Eliminazione di tutti i QSO... - + Error Errore - + Failed to delete all QSOs. Impossibile eliminare tutti i QSO. - + + Enabled + Abilitato + + + + Path + Direzioni + + + + Station Profile + Profilo della stazione + + + + Missing QSL Sent + QSL Sent mancante + + + + Last Recovery + Ultimo ripristino + + + + + + Queued + In coda + + + + + + + Ignored + Ignorato + + + + + + + Requested + Richiesto + + + + + + + Yes + + + + + Station Profile does not exist. Select another profile and enable this row again. + Il profilo stazione non esiste. Selezionare un altro profilo e riabilitare questa riga. + + + + File exists + Il file esiste + + + + File does not exist + Il file non esiste + + + + Startup ADI initialized + Startup ADI inizializzato + + + + Select ADIF File + Seleziona file ADIF + + + + ADIF Files (*.adi *.adif);;All Files (*) + File ADIF (*.adi *.adif);;Tutti i file (*) + + + members Membri - + Required internet connection during application start Connessione Internet richiesta durante l'avvio dell'applicazione @@ -13040,7 +14133,7 @@ Installare TQSL o specificare manualmente il percorso. StatisticsWidget - + Statistics Statistiche @@ -13111,7 +14204,7 @@ Installare TQSL o specificare manualmente il percorso. - + Band Banda @@ -13136,146 +14229,145 @@ Installare TQSL o specificare manualmente il percorso. Cartaceo - + Year Anno - + Month Mese - + Day in Week Giorno della settimana - + Hour Ora - + Mode Modo - + Continent Continente - + Propagation Mode Modo di Propagazione - + Confirmed / Not Confirmed Confermato / Non Confermato - + Countries Nazioni - + Big Gridsquares Grandi quadrati della griglia - + Distance Distanza - + QSOs QSOs - + Confirmed/Worked Grids Griglia Confermata/Lavorata - + ODX ODX - + Sun Dom - + Mon Lun - + Tue Mar - + Wed Mer - + Thu Gio - + Fri Ven - + Sat Sab - - - + + + Not specified Non specificato - + Confirmed Confermato - + Not Confirmed Non confermato - + No User Filter Nessun filtro utente - + Over 50000 QSOs. Display them? Oltre 50000 QSO. Visualizzarli? - - + Rendering QSOs... Rendering dei QSO… - + All Tutti @@ -13329,17 +14421,17 @@ Installare TQSL o specificare manualmente il percorso. ToAllTableModel - + Time Ora - + Spotter Spotter - + Message Messaggio @@ -13612,27 +14704,27 @@ Installare TQSL o specificare manualmente il percorso. UserListModel - + Callsign Indicativo - + Gridsquare Griglia - + Distance Distanza - + Azimuth Azimuth - + Comment Commento @@ -13640,47 +14732,47 @@ Installare TQSL o specificare manualmente il percorso. WCYTableModel - + Time Ora - + K - + expK - + A - + R - + SFI - + SA - + GMF - + Au @@ -13688,27 +14780,27 @@ Installare TQSL o specificare manualmente il percorso. WWVTableModel - + Time Ora - + SFI - + A - + K - + Info Info @@ -13834,37 +14926,37 @@ Installare TQSL o specificare manualmente il percorso. WsjtxTableModel - + Callsign Indicativo - + Gridsquare Griglia - + Distance Distanza - + SNR - + Last Activity Ultima Attività - + Last Message Ultimo messaggio - + Member Membro @@ -13905,47 +14997,47 @@ Installare TQSL o specificare manualmente il percorso. main - + Run with the specific namespace. Esegui con lo spazio dei nomi specifico. - + namespace spazio dei nomi - + Translation file - absolute or relative path and QM file name. File di traduzione: percorso assoluto o relativo e nome file QM. - + path/QM-filename percorso/nome file QM - + Set language. <code> example: 'en' or 'en_US'. Ignore environment setting. Imposta la lingua. <codice> esempio: 'en' o 'en_US'. Ignora l'impostazione dell'ambiente. - + code codice - + Writes debug messages to the debug file Scrive messaggi di debug nel file di debug - + Process pending database import (internal use) Elaborare l’importazione in sospeso del database (uso interno) - + Force update of all value lists (DXCC, SATs, etc.) Forza l’aggiornamento di tutte le liste di valori (DXCC, SAT, ecc.) diff --git a/i18n/qlog_zh_CN.qm b/i18n/qlog_zh_CN.qm index c991af2c..ea477d1f 100644 Binary files a/i18n/qlog_zh_CN.qm and b/i18n/qlog_zh_CN.qm differ diff --git a/i18n/qlog_zh_CN.ts b/i18n/qlog_zh_CN.ts index 4ee57328..76d3883a 100644 --- a/i18n/qlog_zh_CN.ts +++ b/i18n/qlog_zh_CN.ts @@ -199,20 +199,115 @@ + Bandmap Guide + + + + + Guide + + + + Fields 字段 - + Must not be empty 不能为空 - + + Leave unchanged + + + + + Off + + + + Unsaved 未保存 + + AdifRecoveryManager + + + Startup ADI found more than %1 new QSOs in %2. Use the standard Import. Load point was moved to the end of the file. + + + + + Startup ADI Station Profile does not exist: %1 + + + + + Cannot open Startup ADI records from %1 + + + + + Startup ADI from %1 finished with %n error(s); load point was not advanced. + + + + + + + Startup ADI was disabled for %n file(s) because the assigned Station Profile no longer exists. + + + + + + + AdifRecoveryReaderWorker + + + Startup ADI filename is empty + + + + + Startup ADI file does not exist: %1 + + + + + Startup ADI initialized at the end of file + + + + + Startup ADI file was reset; load point moved to the end + + + + + Cannot open Startup ADI file: %1 + + + + + Cannot seek Startup ADI file: %1 + + + + + Cannot read Startup ADI file: %1 + + + + + Too many ADIF records for automatic recovery + + + AlertRuleDetail @@ -495,42 +590,42 @@ AlertTableModel - + Rule Name 规则名 - + Callsign 呼号 - + Frequency 频率 - + Mode 模式 - + Updated 已更新 - + Last Update 最后更新 - + Last Comment 最后备注 - + Member 成员 @@ -580,78 +675,83 @@ Awards 奖项 - - - Options - 选项 - Award 奖项 - + + 🌐 Rules + + + + My DXCC Entity 我的 DXCC 实体 - + User Filter 用户过滤器 - + Confirmed by 确认自 - + LoTW LoTW - + eQSL eQSL - + Paper 纸质卡片 - + Mode 模式 - + CW CW - + Phone 语音通话 - + Digi 数字模式 - + Not-Worked Only 未通联 - + Not-Confirmed Only + 未确认 + + + + Double-click a row/cell to show QSOs - + Show 显示 @@ -666,7 +766,7 @@ ITU - + WAC WAC @@ -731,79 +831,84 @@ - + US Counties - + Russian Districts - + Japanese Cities/Ku/Guns - + NZ Counties - + Spanish DMEs - + Ukrainian Districts - + No User Filter 无用户筛选器 - + DELETED - + North America 北美 - + South America 南美 - + Europe 欧洲 - + Africa 非洲 - + Oceania 大洋洲 - + Asia 亚洲 - - Antarctica - 南极洲 + + WAAC + + + + + WAIP + @@ -829,6 +934,198 @@ 等待中 + + BandmapGuideDialog + + + Bandmap Guide + + + + + Import guide + + + + + Import + 导入 + + + + Export guide + + + + + Export + + + + + New guide + + + + + New + 新建 + + + + Copy guide + + + + + Copy + + + + + Delete guide + + + + + Delete + 删除 + + + + Guide Name: + + + + + Ranges: + + + + + From + + + + + To + + + + + Color + + + + + Label + + + + + Add range + + + + + Add + 添加 + + + + Remove selected range + + + + + Remove + + + + + + MHz + MHz + + + + + New Guide + + + + + Copy - %1 + + + + + Delete Guide + + + + + Delete guide '%1'? + + + + + Import Guide + + + + + QLog Bandmap Guide (*.qbg);;JSON (*.json) + + + + + Import Failed + + + + + Export Guide + + + + + QLog Bandmap Guide (*.qbg) + + + + + Export Failed + + + + + Guide Color + + + + + + + QLog Warning + + + + + Guide name cannot be empty. + + + + + Guide name '%1' is already used. + + + + + Guide '%1' contains an invalid range. + + + BandmapWidget @@ -867,30 +1164,60 @@ 分钟 - + Bandmap 波段地图 - + Show Band 显示波段 - + Center RX 居中 RX - + Show Emergency Frequencies - + + Show IBP Frequencies + + + + + Show Guide + + + + + Off + + + + + No Guide + + + + + Edit Guide... + + + + SOS + + + IBP + IBP + CWCatKey @@ -1167,27 +1494,27 @@ CWKeyer - + No CW Keyer Profile selected 未选择 CW 电键配置文件 - + Initialization Error 初始化出错 - + Internal Error 内部错误 - + Connection Error 连接出错 - + Cannot open the Keyer connection 无法打开电键连接 @@ -1287,7 +1614,7 @@ File: - + 文件: @@ -1816,198 +2143,198 @@ 导入 - + Export template - + Export - + New template - + New 新建 - + Copy existing template - + Copy - + Delete template - + Delete 删除 - + Template Name: - + Contest Name: - + Default Mode: - + QSO Line Columns: - + Contest name as required by the rules. It is possible to enter a custom string if it is not included in the list. - + Seq. - + QSO Field - + Formatter - + Width - + Label - + Add line - + Add 添加 - + Remove selected line - + Remove - + New Template - + Copy - %1 - + Delete Template - + Delete template '%1'? - + Import Template - - + + QLog Cabrillo Template (*.qct) - + Import Failed - + Export Template - + Export Failed - + Failed to write file: %1 - + File not found: %1 - + Cannot open file: %1 - + Invalid template file: missing name - + QLog Error - + Cannot start database transaction. - + QLog Warning - + Cannot save template '%1': %2 @@ -2074,20 +2401,24 @@ Form - - + + Sunrise 日出 - - + + Sunset 日落 - - + + + + + + N/A N/A @@ -2151,7 +2482,7 @@ 其他 - + Done 完成 @@ -2159,12 +2490,12 @@ ColumnSettingGenericDialog - + Unselect All 反选全部 - + Select All 选择全部 @@ -2177,7 +2508,7 @@ 列可见性设置 - + Done 完成 @@ -4227,70 +4558,60 @@ Data - + New Entity 新实体 - + New Band 新波段 - + New Mode 新模式 - + New Band&Mode 新波段模式(&M) - + New Slot 新组合 - + Confirmed 已确认 - + Worked 已通联 - + Hz Hz - + kHz kHz - + GHz GHz - + MHz MHz - - - - - - - - Yes - - @@ -4298,136 +4619,146 @@ - No - + Yes + + + + + + No + + + + + Requested 已请求 - + Queued 排队中 - - - + + + Invalid 无效 - + Bureau 卡片局 - + Direct 直邮 - + Electronic 电子卡片 - - - - - - - - + + + + + + + + Blank 空白 - + Modified 已修改 - + Grayline 灰线 - + Other 其他 - + Short Path 短路径 - + Long Path 长路径 - + Not Heard 没听到过 - + Uncertain 不确定 - + Straight Key 直键 - + Sideswiper 扫拨键 - + Mechanical semi-automatic keyer or Bug 机械半自动或臭虫键 - + Mechanical fully-automatic keyer or Bug 机械全自动或臭虫键 - + Single Paddle 单桨键 - + Dual Paddle 双桨键 - + Computer Driven 电脑驱动 - + Confirmed (AG) - + Confirmed (no AG) - + Unknown 未知 @@ -4546,7 +4877,7 @@ Developer Tools - + 开发者工具 @@ -5069,57 +5400,57 @@ Example: DxTableModel - + Time 时间 - + Callsign 呼号 - + Frequency 频率 - + Mode 模式 - + Spotter 报点者 - + Comment 备注 - + Continent 大洲 - + Spotter Continent 报点者大洲 - + Band 波段 - + Member 成员 - + Country 国家/地区 @@ -5133,7 +5464,7 @@ Example: - + Connect 连接 @@ -5298,67 +5629,67 @@ Example: 应该显示哪些列 - + My Continent 我的大洲 - + Auto 自动 - + Connecting... 正在连接... - + DX Cluster is temporarily unavailable DX 集群暂时不可用 - + DXC Server Error DXC 服务器出错 - + An invalid callsign 无效的呼号 - + DX Cluster Password DX 集群密码 - + Security Notice 安全注意 - + The password can be sent via an unsecured channel 密码可能通过不安全的通道发送 - + Server 服务器 - + Username 用户名 - + Disconnect 断开连接 - + DX Cluster Command DX集群命令 @@ -5366,22 +5697,22 @@ Example: DxccTableModel - + Worked 已通联 - + eQSL eQSL - + LoTW LoTW - + Paper 纸质卡片 @@ -5497,7 +5828,7 @@ Example: - + POTA POTA @@ -5677,42 +6008,42 @@ Example: 无法标记已导出的 QSOs 为已发送 - + Generic 常规 - + QSLs QSL - + All 全部 - + Minimal 最小化 - + QSL-specific QSL-特定 - + Custom 1 自定义1 - + Custom 2 自定义2 - + Custom 3 自定义3 @@ -5840,122 +6171,122 @@ The password will be needed to restore them later. 无法设置自动开机 - + Cannot set no_xchg to 1 - + Rig Open Error 打开电台出错 - + Set TX Frequency Error - + Set Frequency Error 设置频率出错 - + Set Split Error - + Set Mode Error 设置模式出错 - + Set Split Mode Error - + Set PTT Error 设置 PTT 出错 - + Cannot sent Morse 无法发送摩尔斯 - + Cannot stop Morse 无法停止摩尔斯 - + Get PTT Error 获取PTT出错 - + Get Frequency Error 获取频率出错 - + Get Mode Error 获取模式出错 - + Get VFO Error 获取VFO出错 - + Get PWR Error 获取功率出错 - + Get PWR (power2mw) Error 获取功率(power2mw)出错 - + Get RIT Function Error 获取RIT功能出错 - + Get RIT Error 获取RIT出错 - + Get XIT Function Error 获取XIT功能出错 - + Get XIT Error 获取XIT出错 - + Get Split Error - + Get TX Frequency Error - + Get KeySpeed Error 获取电键速度出错 - + Set KeySpeed Error 设置电键速度出错 @@ -5992,171 +6323,291 @@ The password will be needed to restore them later. ImportDialog - + Import 导入 - + Date Range 日期范围 - + Import all or only QSOs from the given period 导入所有或仅导入指定时间段的 QSO - + All 全部 - + File 文件 - + ADX ADX - + Browse 浏览 - + Options 选项 - - + + The value is used when an input record does not contain the ADIF value 当输入记录不包含ADIF值时使用该值 - + Defaults 默认 - + + Values are used only for fields that are missing in the import file. Existing values are preserved. + + + + + <p>⚠ Missing QSL Sent fields are set to <b>"N"</b> (do not send) by default in ADIF. + + + + My Profile 我的配置文件 - + My Rig 我的设备 - - + + Comment 备注 - + + Used only for missing QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT, and DCL_QSL_SENT fields where default is "N"; otherwise, the value from the input is used. + + + + + QSL Sent status + + + + + Used only for missing QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT, and DCL_QSL_SENT fields where default is "N"; otherwise, the value from the input is used.<p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + + + + + Used only when the imported ADIF record does not contain the selected field. Explicit ADIF values are kept. + + + + + Default value for missing DCL_QSL_SENT. <p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + + + + + Default value for missing EQSL_QSL_SENT. <p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + + + + + Default value for missing LOTW_QSL_SENT. <p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + + + + + LoTW + LoTW + + + + DCL + + + + + Paper QSL + + + + + eQSL + eQSL + + + + Default value for missing QSL_SENT.<p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + + + + If DXCC is missing in the imported record, it will be resolved from the callsign. - + Fill missing DXCC Entity Information + + + Queued (ready to send) + + + + + Ignored (do not track) + + + + + Requested (requested again) + + + + + Yes (already sent) + + + + + Custom... + + + + + Queued + 排队中 + + + + Requested + 已请求 + + Ignored + 已忽视 + + + + No + + + + + Yes + + + + &Import 导入(&I) - + Select File 选择文件 - - + + The values below will be used when an input record does not contain the ADIF values 当输入记录不包含ADIF值时,将使用下面的值 - + <p><b>In-Log QSO:</b></p><p> <p><b>在日志 QSO:</b></p><p> - + <p><b>Importing:</b></p><p> <p><b>导入中:</b></p><p> - + Duplicate QSO 重复的 QSO - + <p>Do you want to import duplicate QSO?</p>%1 %2 <p>是否要导入重复的QSO?</p> %1 %2 - + Save to File 保存至文件 - + QLog Import Summary QLog 导入摘要 - + Import date 导入日期 - + Imported file 导入的文件 - + Imported: %n contact(s) 已导入: %n 通联记录 - + Warning(s): %n 告警: %n - + Error(s): %n 错误: %n - + Details 详情 - + Import Result 导入结果 - + Save Details... 保存详情... @@ -6424,7 +6875,7 @@ The password will be needed to restore them later. Unpack Data & Settings - + 解压数据 & 设置 @@ -6439,42 +6890,42 @@ The password will be needed to restore them later. Select Database - + 选择数据库 File: - + 文件: Browse - 浏览 + 浏览 Status: No file selected - + 状态: 未选择文件 Decrypt Credentials - + 解密凭据 Enter password to decrypt credentials - + 输入密码以解密凭据 Password: - + 密码: Load && Restart - + 加载 && 重启 @@ -6583,18 +7034,53 @@ The password will be needed to restore them later. 已导入 - - + + missing QSO_DATE + + + + + missing CREDIT_GRANTED + + + + + missing CALL/DXCC + + + + + no matching QSO + + + + + cannot update QSO %1: %2 + + + + + matched QSO: + + + + + credit_granted: + + + + + DXCC State: DXCC状态: - + Error 错误 - + Warning 告警 @@ -6602,945 +7088,956 @@ The password will be needed to restore them later. LogbookModel - + QSO ID QSO ID - + Time on 开始时间 - + Time off 结束时间 - + Call 呼号 - + RSTs RST发 - + RSTr RST收 - + Frequency 频率 - - + + Band 波段 - - + + Mode 模式 - + Submode 子模式 - + Name (ASCII) 姓名(ASCII) - + QTH (ASCII) QTH (ASCII) - - + + Gridsquare 网格坐标 - + DXCC DXCC - + Country (ASCII) 国家(ASCII) - + Continent 大洲 - + CQZ CQ分区 - + ITU ITU - + Prefix 前缀 - + State - + County 县/郡 - + IOTA IOTA - + QSLr QSL收 - + QSLr Date QSL收日期 - + QSLs QSL发 - + QSLs Date QSL发日期 - + LoTWr LoTW收 - + LoTWr Date LoTW收日期 - + LoTWs LoTW发 - + LoTWs Date LoTW发日期 - + TX PWR 发送功率 - + Additional Fields 附加字段 - + Address (ASCII) 地址 (ASCII) - + Address 地址 - + Age 年龄 - + Altitude 海拔 - + A-Index A-指数 - + Antenna Az 天线方位 - + Antenna El 天线单元数 - + Signal Path 信号路径 - + ARRL Section ARRL 划分 - + Award Submitted 已提交奖项 - + Award Granted 已授予奖项 - + Band RX 波段 RX - + Gridsquare Extended 网格坐标扩展 - + Contest Check 竞赛检查 - + Class 等级 - + ClubLog Upload Date ClubLog 上传日期 - + ClubLog Upload State ClubLog 上传状态 - + Comment (ASCII) 备注 (ASCII) - - + + Comment 备注 - + Contacted Operator 联系过的操作员 - + Contest ID 竞赛标识 - - + + Country 国家/地区 - + + Mode/Submode + + + + + Mode: %1 +Submode: %2 + + + + County Alt 县/郡备用标识 - + Credit Submitted 积分已提交 - + Credit Granted 已授予的积分 - + DOK DOK - + DCLr Date DCLr 日期 - + DCLs Date DCLs 日期 - + DCLr - + DCLs - + Distance 距离 - + Email 电子邮件 - - + + Owner Callsign 主人呼号 - + eQSL AG - + eQSLr Date eQSL 接收日期 - + eQSLs Date eQSL 发送日期 - + eQSLr eQSL 接收 - + eQSLs eQSL 发送 - + FISTS Number FISTS 序号 - + FISTS CC FISTS CC - + EME Init EME Init - + Frequency RX 频率 RX - + Guest Operator 客座操作员 - + HamlogEU Upload Date HamlogEU 上传日期 - + HamlogEU Upload Status HamlogEU 上传状态 - + HamQTH Upload Date HamQTH 上传日期 - + HamQTH Upload Status HamQTH 上传状态 - + HRDLog Upload Date HRDLog 上传日期 - + HRDLog Upload Status HRDLog 上传状态 - + IOTA Island ID IOTA 岛屿 ID - + K-Index K-指数 - + Latitude 纬度 - + Longitude 经度 - + Max Bursts 最大流星散射爆发 - + MS Shower Name 流星雨名称 - + My Altitude 我的海拔 - + My Antenna (ASCII) 我的天线 (ASCII) - + My Antenna 我的天线 - + My City (ASCII) 我的城市 (ASCII) - + My City 我的城市 - + My County 我的县/郡 - + My County Alt 我的县/郡备用标识 - + My Country (ASCII) 我的国家/地区 (ASCII) - + My Country 我的国家/地区 - + My CQZ 我的 CQ 分区 - + My DARC DOK 我的DARC DOK - + My DXCC 我的 DXCC - + My FISTS 我的 FISTS - + My Gridsquare 我的网格坐标 - + My Gridsquare Extended 我的网格扩展 - + My IOTA 我的 IOTA - + My IOTA Island ID 我的 IOTA 海岛 ID - + My ITU 我的 ITU - + My Latitude 我的纬度 - + My Longitude 我的经度 - + My Name (ASCII) 我的姓名 (ASCII) - + My Name 我的姓名 - + My Postal Code (ASCII) 我的邮编 (ASCII) - + My Postal Code 我的邮编 - + My POTA Ref 我的 POTA 编号 - + My Rig (ASCII) 我的设备 (ASCII) - + My Rig 我的设备 - + My Special Interest Activity (ASCII) 我的特殊兴趣活动 (ASCII) - + My Special Interest Activity 我的特殊兴趣活动 - + My Spec. Interes Activity Info (ASCII) 我的特殊兴趣活动信息 (ASCII) - + My Spec. Interest Activity Info 我的特殊兴趣活动信息 - + My SOTA 我的 SOTA - + My State 我的省/州 - - + + My Street 我的街道 - + My USA-CA Counties 我的 USA-CA 县 - + My VUCC Grids 我的 VUCC 网格 - + Name - + Notes (ASCII) 笔记(ASCII) - + QRZ Download Date QRZ下载数据 - + QRZ Download Status QRZ下载状态 - + QSLs Message (ASCII) QSLs 信息 (ASCII) - + QSLs Message QSLs 信息 - + QSLr Message QSLr 信息 - + RcvPWR 接收功率 - + RcvNr 接收序号 - + RcvExch 接收交换信息 - + SentNr 发送序号 - + SentExch 发送交换信息 - - + + Notes 笔记 - + #MS Bursts 流星散射爆发数 - + #MS Pings 流星散射 ping 数 - + POTA POTA - + Contest Precedence 比赛优先 - + Propagation Mode 传播模式 - + Public Encryption Key 公钥加密算法 - + QRZ Upload Date QRZ 上传日期 - + QRZ Upload Status QRZ 上传状态 - + QSL Message QSL 信息 - + CW Key Info CW电键信息 - + CW Key Type CW电键类型 - + My CW Key Info 我的CW电键信息 - + My CW Key Type 我的CW电键类型 - + Operator Callsign 操作员呼号 - + QSLr Via QSL 接收经由 - + QSLs Via QSL 发送经由 - + QSL Via QSL 经由 - + QSO Completed 通联完成 - + QSO Random 随机通联 - + QTH QTH - + Region 地区 - + Rig (ASCII) 设备 (ASCII) - + Rig 设备 - + SAT Mode 卫星模式 - + SAT Name 卫星名称 - + Solar Flux 太阳通量 - + SIG (ASCII) 特别活动或兴趣团体名 (ASCII) - + SIG SIG - + SIG Info (ASCII) 特别活动或兴趣团体信息 (ASCII) - + SIG Info SIG 信息 - + Silent Key - 静默电键 + Silent Key - + SKCC Member SKCC 成员 - + SOTA SOTA - + Logging Station Callsign 日志电台呼号 - + SWL SWL - + Ten-Ten Number Ten-Ten 序号 - + UKSMG Member UKSMG 成员 - + USA-CA Counties USA-CA 县 - + VE Prov VE 省 - + VUCC VUCC - + Web 网站 - + My ARRL Section 我的 ARRL 划分 - + My WWFF 我的 WWFF - + WWFF WWFF - + RST Sent RST 发 - + RST Rcvd RST 收 - + Paper 纸质卡片 - + LoTW LoTW - + eQSL eQSL - + QSL Received QSL 已接收 - + QSL Sent QSL 已发送 @@ -7549,8 +8046,8 @@ The password will be needed to restore them later. LogbookWidget - - + + Delete 删除 @@ -7583,12 +8080,12 @@ The password will be needed to restore them later. Mark QSL RCVD - + 标记QSL已接收 Mark QSL Requested - + 标记QSL已请求 @@ -7637,236 +8134,271 @@ The password will be needed to restore them later. - + Callsign 呼号 - + Gridsquare 网格坐标 - + POTA POTA - + SOTA SOTA - + WWFF WWFF - + SIG SIG - + IOTA IOTA - + Delete the selected contacts? 删除选中的联系人? - + Clublog's <b>Immediately Send</b> supports only one-by-one deletion<br><br>Do you want to continue despite the fact<br>that the DELETE operation will not be sent to Clublog? Clublog's <b>Immediately Send</b> supports only one-by-one deletion<br><br>Do you want to continue despite the fact<br>that the DELETE operation will not be sent to Clublog? - + Deleting QSOs 删除 QSO - + Update 更新 - + By updating, all selected rows will be affected.<br>The value currently edited in the column will be applied to all selected rows.<br><br>Do you want to edit them? 通过更新,所有选定的行都将受到影响。<br>当前在列中编辑的值将应用于所有选定的行。<br><br>您想要编辑他们吗? - + Count: %n 计数:%n - + Downloading eQSL Image 正在下载 eQSL 图片 - - - + + + Cancel 取消 - + All Bands 所有波段 - + All Modes 所有模式 - + All Countries 所有国家和地区 - + No User Filter 无用户筛选器 - + QLog Warning QLog警告 - + Each batch supports up to 100 QSOs. 每个批次最多支持 100 个 QSO。 - + QSOs Update Progress QSO更新进度 - - - + + + QLog Error QLog 错误 - + Callbook login failed 电台黄页登陆失败 - + Callbook error: 电台黄页错误: - + All Clubs 所有俱乐部 - + eQSL Download Image failed: eQSL 图片下载失败: + + LotwDXCCCreditDownloader + + + Cannot open test LoTW DXCC credit file + + + + + + Incomplete LoTW DXCC credit response + + + + + + Cannot open temporary file + 无法打开临时文件 + + + + LoTW is not configured properly + + + + + LoTW returned a non-ADIF response + + + + + Incorrect login or password + 登录名或密码不正确 + + LotwQSLDownloader - + Cannot open temporary file 无法打开临时文件 - + Incorrect login or password - + 登录名或密码不正确 LotwUploader - + Upload cancelled by user 用户取消上传 - + Upload rejected by LoTW 上传被 LoTW 拒绝 - + Unexpected response from TQSL server 来自 TQSL 服务器的意外响应 - + TQSL utility error TQS L实用程序错误 - + TQSLlib error TQSLlib 错误 - + Unable to open input file 无法打开输入文件 - + Unable to open output file 无法打开输出文件 - + All QSOs were duplicates or out of date range 所有 QSO 都是重复的或超出了日期范围 - + Some QSOs were duplicates or out of date range 部分 QSO 是重复的或超出了日期范围 - + Command syntax error 命令语法错误 - + LoTW Connection error (no network or LoTW is unreachable) LoTW 连接错误 (没有网络或 LoTW 不可达) - - + + Unexpected Error from TQSL 来自 TQSL 的意外错误 - + TQSL not found 未找到 TQSL - + TQSL crashed TQSL 已崩溃 @@ -7904,621 +8436,686 @@ The password will be needed to restore them later. 服务(&R) - + Toolbar 工具栏 - - + + Clock 时钟 - - + + Map 地图 - - + + DX Cluster DX动态 - + WSJTX WSJTX - - + + Rotator 云台 - - + + Bandmap 波段地图 - - + + Rig 设备 - - + + Online Map 在线地图 - - + + CW Console CW 控制台 - - + + Chat 聊天 - - + + Profile Image 资料图片 - - + + Alerts 提醒 - + &Settings 设置(&S) - + &Import 导入(&I) - + &Export 导出(&E) - + Connect R&ig 连接设备(&I) - + &About 关于(&A) - + + Print QS&L + + + + Upload 上传 - + Service - Upload QSOs 服务-上传 QSOs - + Download QSLs 下载 QSLs - + Service - Download QSLs 服务-下载 QSLs - + Quit 退出 - + Application - Quit 程序 - 退出 - - + + New QSO - Clear 新 QSO - 清除 - - + + New QSO - Save 新 QSO - 保存 - + S&tatistics 统计(&T) - + Wsjtx Wsjtx - + Connect R&otator 连接云台(&O) - + QSO &Filters QSO过滤(&F) - + &Awards 奖项(&A) - + Edit Rules 编辑规则 - + Clear 清除 - + Show Alerts 显示提醒 - + Beep 鸣响 - - + + Contest 比赛 - + Dupe Check 查重 - + Sequence 序列 - + Linking Exchange With 链接交换信息至 - - - - - - - + + + + + + + Pack Data && Settings - + 打包数据 && 设置 - - + + Unpack Data && Settings - + 解压数据 && 设置 - + QSL &Gallery - + QSL展廊 - + Developer Tools - + 开发者工具 - + Run custom read-only SQL queries against the logbook database - + 对日志数据库运行自定义只读SQL查询 - - Print QSL &Labels - - - - + DXCC &Submission List - + DXCC &提交列表 - + Generate a list of contacts to submit for ARRL DXCC award credit - + 生成要提交以获得ARRL DXCC奖项积分的联系人列表 - + Connect &CW Keyer 连接CW键(&C) - + &Wiki &Wiki - + Report &Bug... 报告&Bug... - + &Manual Entry 手动输入(&M) - + Switch New Contact dialog to the manually entry mode<br/>(time, freq, profiles etc. are not taken from their common sources) 将 “新建联系人” 对话框切换到手动输入模式<br/>(时间、频率、配置文件等不是从它们的公共来源中获取) - + Mailing List... 邮件列表 ... - + Edit 编辑 - - + + Save Arrangement 保存布局 - + Keep Options 保持选项 - + Restore connection options after application restart 应用程序重新启动后恢复连接选项 - + Logbook - Search Callsign 日志本 - 查找呼号 - - + + New QSO - Add text from Callsign field to Bandmap 新 QSO - 将呼号字段文本添加到波段图上 - + Rig - Band Down 设备 - 波段向下 - + Rig - Band Up 设备 - 波段向上 - + New QSO - Use Callsign from the Whisperer 新 QSO - 使用来自 Whisperer 的呼号 - + CW Console - Key Speed Up CW 控制台 - 增加键速 - + CW Console - Key Speed Down CW 控制台 - 减慢键速 - + CW Console - Profile Up CW 控制台 - 上一个配置文件 - + CW Console - Profile Down CW 控制台 - 下一个配置文件 - + Rig - PTT On/Off 设备 - PTT ON/OFF - + All Bands 全部波段 - + Each Band 每个波段 - + Each Band && Mode 每个波段 && 模式 - + No Check 不做检查 - + Single 统一值 - + Per Band 各波段独立 - + Stop 停止 - + Reset 重置 - + None - + + Download LoTW DXCC Credits + + + + + Service - Download LoTW DXCC Credits + + + + Theme: Native - + Theme: QLog Light - + 主题: QLog Light - + Theme: QLog Dark - + 主题: QLog Dark - + What's New - + 新功能 - + Export Cabrillo - + 导出 Cabrillo - + Color Theme - + 颜色主题 - + Not enabled for non-Fusion style 非融合样式不允许使用 - + Press to tune the alert 按下调节提醒 - + + Startup ADI + + + + Clublog Immediately Upload Error Clublog 立即上传出错 - - - + + + <b>Error Detail:</b> <b>错误详情:</b> - + op: 操作员: - + A New Version 新版本 - + A new version %1 is available. 新版本 %1 可用。 - + Remind Me Later 稍后提醒我 - + Download 下载 - - Failed to encrypt credentials. + + + QLog Warning - - Database files (*.dbe);;All files (*) + + LoTW is not configured properly.<p>Please, use <b>Settings</b> dialog to configure it.</p> - - Failed to create temporary file. + + + QLog Error - - Failed to dump the database. + + Cannot load local DXCC entities from the logbook: - - Compressing database... + + Unknown DXCC Entity - + + Cannot determine a local DXCC entity from logbook contacts. + + + + + LoTW DXCC Credits + + + + + Select the local DXCC entity for which LoTW DXCC credits will be downloaded: + + + + + Cancel + 取消 + + + + Downloading LoTW DXCC credits + + + + + Processing LoTW DXCC credits + + + + + LoTW DXCC Credit Import Summary + + + + + LoTW DXCC credit import failed: + + + + + Failed to encrypt credentials. + 加密凭据失败。 + + + + Database files (*.dbe);;All files (*) + 数据库文件 (*.dbe);;所有文件 (*) + + + + Failed to create temporary file. + 创建临时文件失败。 + + + + Failed to dump the database. + 转储数据库失败。 + + + + Compressing database... + 正在压缩数据库... + + + Database successfully dumped to %1 - + 数据库成功转储到 +%1 - + Failed to compress the database. - + 压缩数据库失败。 - + Failed to prepare database for import. - + 准备数据库导入失败。 - + Classic 经典 - + Do you want to remove the Contest filter %1? 你要删除比赛过滤器 %1 吗? - + Contest: 比赛: - + <h1>QLog %1</h1><p>&copy; 2019 Thomas Gatzweiler DL2IC<br/>&copy; 2021-2026 Ladislav Foldyna OK1MLG<br/>&copy; 2025-2026 Michael Morgan AA5SH<br/>&copy; 2025-2026 Kyle Boyle VE9KZ</p><p>Based on Qt %2<br/>%3<br/>%4<br/>%5</p><p>Icon by <a href='http://www.iconshock.com'>Icon Shock</a><br />Satellite images by <a href='http://www.nasa.gov'>NASA</a><br />ZoneDetect by <a href='https://github.com/BertoldVdb/ZoneDetect'>Bertold Van den Bergh</a><br />TimeZone Database by <a href='https://github.com/evansiroky/timezone-boundary-builder'>Evan Siroky</a> - + About 关于 - + N/A N/A - MapWebChannelHandler + MapPageController - - - - Grid - 网格 + + Aurora + 极光 - - - - Gray-Line - 灰线 + + Beam + 波束 - - - - Beam - 波束 + + Chat + 聊天 - - - - Aurora - 极光 + + Grid + 网格 - - - - MUF - MUF + + Gray-Line + 灰线 - - - + IBP - IBP + IBP - - - - Chat - 聊天 + + MUF + MUF - - - + WSJTX - CQ - WSJTX - CQ + WSJTX - CQ - - - + Path - 路径 + 路径 @@ -8702,12 +9299,12 @@ The password will be needed to restore them later. 天线 - + Blank 空白 - + W W @@ -8782,87 +9379,87 @@ The password will be needed to restore them later. 电台黄页登陆失败 - + LP 长路径 - + New Entity! 新实体! - + New Band! 新波段! - + New Mode! 新模式! - + New Band & Mode! 新波段与模式! - + New Slot! 新组合! - + Worked 已通联 - + Confirmed 已确认 - + GE GE - + GM GM - + GA GA - + m m - + Callbook search is active 电台黄页搜索可用 - + Contest ID must be filled in to activate 必须填写 竞赛标识 才能激活 - + It is not the name of the contest but it is an assigned<br>Contest ID (ex. CQ-WW-CW for CQ WW DX Contest (CW)) 这里要填的不是比赛名称,而是分配的竞赛标识<br>(例如CQ-WW-CW 代表CQ WW DX比赛(CW)) - + Description of the contacted station's equipment 已通联台站的设备描述 - + Callbook search is inactive 电台黄页搜索不可用 @@ -8872,27 +9469,27 @@ The password will be needed to restore them later. 展开/折叠 - + two or four adjacent Maidenhead grid locators, each four characters long, (ex. EN98,FM08,EM97,FM07) 两个或四个相邻的梅登黑德网格定位器,每个四个字符长,(例如 EN98, FM08, EM97, FM07) - + the contacted station's DARC DOK (District Location Code) (ex. A01) 联络电台的DARC DOK (地区位置代码) (例如 A01) - + World Wide Flora & Fauna 世界动植物 (可选参数) - + Special Activity Group 特别活动或兴趣团体名称 - + Special Activity Group Information 特别活动或兴趣团体信息 @@ -8992,7 +9589,7 @@ The password will be needed to restore them later. Error Occurred - + 发生错误 @@ -9050,7 +9647,7 @@ The password will be needed to restore them later. Toggle Favorite - + 切换收藏 @@ -9058,39 +9655,41 @@ The password will be needed to restore them later. Platform-specific Settings - + 平台特定设置 The database was exported from a different platform. Please verify or update the following settings. You can leave fields empty and configure them later in Settings. - + 数据库是从不同平台导出的。 +请验证或更新以下设置。 +您可以将字段留空,并在“设置”中稍后配置它们。 Setting - + 设置 Value - + Continue - + 继续 Select File - 选择文件 + 选择文件 All Files (*) - + 所有文件 (*) @@ -9104,7 +9703,7 @@ You can leave fields empty and configure them later in Settings. QCoreApplication - + QLog Help QLog 帮助 @@ -9134,31 +9733,31 @@ You can leave fields empty and configure them later in Settings. - + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + QLog Warning QLog 告警 @@ -9188,63 +9787,63 @@ You can leave fields empty and configure them later in Settings. 网络错误。无法下载俱乐部列表 - - - + + + - - - - + + + + QLog Error QLog 出错 - + QLog is already running 已经有 QLog 实例正在运行 - + Failed to process pending database import. - + 处理待处理的数据库导入失败。 - + The database was imported successfully, but the stored passwords could not be restored (decryption failed or the data is corrupted). All service passwords have been cleared and must be re-entered in Settings. - + 数据库导入成功,但存储的密码无法恢复(解密失败或数据已损坏)。所有服务密码已被清除,必须在“设置”中重新输入。 - + Could not connect to database. 无法连接至数据库。 - + Could not export a QLog database to ADIF as a backup.<p>Try to export your log to ADIF manually 无法导出 QLog 数据库至 ADIF 备份。<p>请尝试手动导出您的日志至 ADIF - + Database migration failed. 数据库迁移失败。 - + - + QLog Info QLog 信息 - + Activity name is already exists. 活动名称已存在。 @@ -9269,33 +9868,33 @@ You can leave fields empty and configure them later in Settings. 无法更新提醒规则 - + DXC Server Name Error DXC 服务器名称错误 - + DXC Server address must be in format<p><b>[username@]hostname:port</b> (ex. hamqth.com:7300)</p> DXC 服务器地址格式必须为<p><b>[用户名@]主机名:端口</b> (例. hamqth.com:7300)</p> - + DX Cluster Password DX 集群密码 - + Invalid Password 无效的密码 - + DXC Server Connection Error DXC 服务器连接出错 - + Filename is empty 文件名为空 @@ -9329,128 +9928,128 @@ You can leave fields empty and configure them later in Settings. - + Filter name is already exists. 过滤器名称已存在。 - + <b>Rig Error:</b> <b>设备出错:</b> - + <b>Rotator Error:</b> <b>旋转云台出错:</b> - + <b>CW Keyer Error:</b> <b>CW 键控器出错:</b> - + The fields <b>%0</b> will not be saved because the <b>%1</b> is not filled. 不会保存字段<b>%0</b>,因为<b>%1</b>未填充。 - + Your callsign is empty. Please, set your Station Profile 你的呼号为空。请先设置台站配置文件 - - + + Please, define at least one Station Locations Profile 请至少定义一个站点配置文件 - + WSJTX Multicast is enabled but the Address is not a multicast address. WSJTX 多播已启用,但地址不是多播地址。 - + Loop detected. Raw UDP forward uses the same port as the WSJT-X receiving port. - + 检测到循环。原始 UDP 转发使用与 WSJT-X 接收端口相同的端口。 - + Rig port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device 设备端口必须是一个有效的 COM 端口。<br>Windows 使用 COMxxx,对于unix-like OS 使用设备路径 - + Rig PTT port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device 电台PTT端口必须为可用的串口。<br>Windows是类似COMxx,类UNIX操作系统则为设备路径名 - + <b>TX Range</b>: Max Frequency must not be 0. <b>TX 范围</b>: 最大频率不能为 0。 - + <b>TX Range</b>: Max Frequency must not be under Min Frequency. <b>TX 范围</b>: 最大频率不能低于最小频率。 - + Rotator port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device 旋转云台端口必须是一个有效的 COM 端口。<br>对于Windows 使用 COMxxx,对于unix-like OS 使用设备路径 - + CW Keyer port must be a valid COM port.<br>For Windows use COMxx, for unix-like OS use a path to device CW 键控器端口必须是一个有效的 COM 端口。<br>对于Windows 使用 COMxxx,对于unix-like OS 使用设备路径 - + Cannot change the CW Keyer Model to <b>Morse over CAT</b><br>No Morse over CAT support for Rig(s) <b>%1</b> 无法将 CW 键控器模型改为 <b>Morse over CAT</b><br>设备 <b>%1</b>不支持Morse over CAT - + Cannot delete the CW Keyer Profile<br>The CW Key Profile is used by Rig(s): <b>%1</b> 无法删除 CW 键控器配置文件<br>本 CW 键控器配置文件正在被设备<b>%1</b>使用 - + Callsign has an invalid format 呼号格式无效 - + Operator Callsign has an invalid format 操作员呼号格式无效 - + Gridsquare has an invalid format 网格坐标格式无效 - + VUCC Grids have an invalid format (must be 2 or 4 Gridsquares separated by ',') VUCC 网格格式无效 (必须是 2 或 4 个被 ',' 分隔的网格坐标) - + Country must not be empty 国家/地区 不能为空 - + CQZ must not be empty CQ 分区不能为空 - + ITU must not be empty ITU 不能为空 - + Cannot update QSO Filter Conditions 无法更新QSO过滤器条件 @@ -9458,79 +10057,79 @@ You can leave fields empty and configure them later in Settings. QObject - + km 公里 - + miles 英里 - + Connection Refused 连接被拒绝 - + Host closed the connection 主机关闭连接 - + Host not found 找不到主机 - + Timeout 超时 - + Network Error 网络错误 - + Internal Error 内部错误 - + Importing Database - + 导入数据库 - + Opening Database 打开数据库 - + Backuping Database 备份数据库 - + Migrating Database 迁移数据库 - + Starting Application 启动应用程序 @@ -9630,26 +10229,26 @@ You can leave fields empty and configure them later in Settings. 我的 DXCC - + Cannot connect to DXC Server <p>Reason <b>: 无法连接至 DXC 服务器 <p>原因 <b>: - + <b>Imported</b>: %n contact(s) <b>已导入</b>: %n 通联 - + <b>Warning(s)</b>: %n <b>告警</b>: %n - + <b>Error(s)</b>: %n <b>错误</b>: %n @@ -9658,33 +10257,33 @@ You can leave fields empty and configure them later in Settings. Not a valid QLog database - + 不是有效的 QLog 数据库 Database version too new (requires newer QLog version) - + 数据库版本过新(需要更新的 QLog 版本) Database is not QLog Export file - + 数据库不是 QLog 导出文件 TQSL Path - TQSL路径 + TQSL路径 (Flatpak internal path) - + (Flatpak 内部路径) Rig - 设备 + 设备 @@ -9704,27 +10303,53 @@ You can leave fields empty and configure them later in Settings. CW Keyer - + CW 电键 TOTAL Worked - 全部已通联 + 全部已通联 TOTAL Confirmed - 全部已确认 + 全部已确认 Confirmed - 已确认 + 已确认 Worked - 已通联 + 已通联 + + + + IARU Region 1 + + + + + + Failed to write file: %1 + + + + + Cannot open file: %1 + + + + + Invalid guide file: %1 + + + + + Invalid guide file: missing title + @@ -9738,7 +10363,7 @@ You can leave fields empty and configure them later in Settings. QRZUploader - + General Error 常见错误 @@ -9748,120 +10373,120 @@ You can leave fields empty and configure them later in Settings. QSL Card Gallery - + QSL 卡片画廊 Search by callsign... - + 按呼号搜索... Sort by: - + 排序方式: - + Export Filtered - + 导出筛选结果 - + Date (Newest) - + 日期(最新) - + Date (Oldest) - + 日期(最旧) - + Callsign (A-Z) - + 呼号(A-Z) - + Callsign (Z-A) - + 呼号(Z-A) - - + + %n QSL card(s) - + All QSL Cards - + 所有 QSL 卡片 - + Favorites - + 收藏 - + By Country - + By Date - + By Band - + By Mode - + By Continent - + Remove from Favorites - + 从收藏中移除 - + Add to Favorites - + 添加到收藏 - + Open - 打开 + 打开 - + Save... - + 保存... - + Save QSL Card - + 保存 QSL 卡片 - + Export QSL Cards - + 导出 QSL 卡片 - + Exported %1 of %2 cards - + 已导出 %1 / %2 张卡片 @@ -9902,292 +10527,447 @@ You can leave fields empty and configure them later in Settings. 详情 - - New QSLs: - 新 QSLs: + + New QSLs: + - Updated QSOs: - 更新 QSOs: + Updated QSOs: + - - Unmatched QSLs: - 未匹配的 QSLs: - 不匹配的 QSL: + + Unmatched QSLs: + QSLPrintLabelDialog - - - Print QSL Labels - - Filter - + 过滤器 Date Range - + 日期范围 My Callsign - 我的呼号 + 我的呼号 Station Profile - 台站配置 + 台站配置 QSL Sent - QSL 已发送 + QSL 已发送 User Filter - 用户过滤器 + 用户过滤器 - + Label Template - + 标签模板 - + + Page Size: - + 页面大小: - + Columns: - + 列数: - + Rows: - + 行数: - + + Label Width: + 标签宽度: + + + + Print QSL Labels / Cards - - Label Height: + + Print Mode - - Left Margin: + + Mode: - - Top Margin: + + + QSL Card - - H Spacing: + + Card Width: - - V Spacing: + + Card Height: - - Label Appearance + + Card Gap: - - Print Label Borders + + + Label Height: + 标签高度: + + + + Label X Offset: + + + Label Y Offset: + + + + + Label Background: + + + + + Fill under label + + + + + + Color + + + + + Background Image: + + + + + Browse + 浏览 + + Clear + 清除 + + + + Left Margin: + 左边界: + + + + Top Margin: + 上边界: + + + + H Spacing: + 水平间距: + + + + V Spacing: + 垂直间距: + + + + Label Appearance + 标签外观: + + + + Print Label Borders + 打印标签边框 + + + QSOs per Label: - + 每个标签的QSO数量: - + Footer Left Text: - + 页脚左文本: - + Footer Right Text: - + 页脚右文本: - + Skip Label: - + 跳过标签: - + Sans Font: - + 无衬线字体: - + Mono Font: + 等宽字体: + + + + Text Color: - + Callsign Size: - + 呼号大小: - + "To Radio" Size: - + "To Radio" 大小: - + "To Radio" Text: - + "To Radio" 文本: - + Header Size: - + 页眉大小: - + Data Size: - + 数据大小: - + Date Header Text: - + 日期页眉文本: - + Date Format: - + 日期格式: - + Time Header Text: - + 时间页眉文本: - + Band Header Text: - + 波段页眉文本: - + Mode Header Text: - + 模式页眉文本: - + QSL Header Text: - + QSL页眉文本: - + Extra Column: - + 额外列: - + Extra Column Text - + 额外列文本: - + (DB column name) - + (数据库列名) - - + + No matching QSOs found - + 未找到匹配的 QSO - - + + Page 0 of 0 - + 第 0 页,共 0 页 - + Labels: 0 (0 pages) - + 标签: 0 (0 页) - + Print - + 打印 - + Export as PDF + 导出为 PDF + + + + Export as Images - - + + Label Sheet + + + + + Custom - 自定义 + 自定义 - + Empty - + - + QSOs matching this station profile + 匹配此台站配置的 QSO + + + + Select Label Text Color - - Labels: %1 (%2 pages) + + Select Label Background Color - - Page %1 of %2 + + + + Select QSL Card Background - - Export PDF + + Images (*.png *.jpg *.jpeg *.bmp) + + + + + Cannot read selected image file. + + + + + Selected file is not a valid image. + + + + + Cards: %1 (%2 pages) - + + Labels: %1 (%2 pages) + 标签: %1 (%2 页) + + + + Page %1 of %2 + 第 %1 页,共 %2 页 + + + + Export PDF + 导出 PDF + + + PDF Files (*.pdf) + PDF 文件 (*.pdf) + + + + + + Export QSL Card Images - - Mark as Sent + + Some image files already exist. Overwrite them? + + + Exported %n QSL card image(s). + + + + - - Mark printed/exported QSOs as sent? + + Exported %1 of %2 QSL card images. + + + + + QSOs were not marked as sent. + + + Mark as Sent + 标记为已发送 + + + + Mark printed/exported QSOs as sent? + 标记打印/导出的 QSO 为已发送? + QSODetailDialog @@ -10241,7 +11021,7 @@ You can leave fields empty and configure them later in Settings. - + Blank 空白 @@ -10654,318 +11434,318 @@ You can leave fields empty and configure them later in Settings. 成员: - + &Reset 重置(&R) - + &Lookup 查找(&L) - - - + + + No - - - + + + Yes - - - + + + Requested 已请求 - - - + + + Queued 排队中 - - - + + + Ignored 已忽视 - + Bureau 卡片局 - + Direct 直邮 - + Electronic 电子卡片 - + Submit changes 提交更改 - + Really submit all changes? 确认提交所有更改吗? - - - - + + + + QLog Error QLog 错误 - + Cannot save all changes - internal error 无法保存所有更改 - 内部错误 - + Cannot save all changes - try to reset all changes 无法保存所有更改 - 尝试重置所有更改 - + QSO Detail QSO 详情 - + Edit QSO 编辑 QSO - + Downloading eQSL Image 下载 eQSL 图片 - + Cancel 取消 - + eQSL Download Image failed: 下载 eQSL 图片失败: - + DX Callsign must not be empty DX 呼号不能为空 - + DX callsign has an incorrect format DX 呼号格式不正确 - - + + TX Frequency or Band must be filled 必须填入发射频率或波段 - + TX Band should be 发射波段应该为 - + RX Band should be 接收波段应该为 - - + + DX Grid has an incorrect format DX 网格格式错误 - + Based on callsign, DXCC Country is different from the entered value - expecting 根据呼号,DXCC 国家与输入值不同 - 期望 - + Based on callsign, DXCC Continent is different from the entered value - expecting 根据呼号,DXCC 大洲与输入值期望不同 - 期望 - + Based on callsign, DXCC ITU is different from the entered value - expecting 根据呼号,DXCC ITU 与输入值不同 - 期望 - + Based on callsign, DXCC CQZ is different from the entered value - expecting 根据呼号,DXCC CQZ 与输入值不同 - 期望 - + VUCC has an incorrect format VUCC 格式错误 - + Based on Frequencies, Sat Mode should be 基于频率,卫星模式应该为 - + blank 空白 - + Sat name must not be empty 卫星名不能为空 - + Own Callsign must not be empty 自己的呼号不能为空 - + Own callsign has an incorrect format 自己的呼号格式错误 - + Own VUCC Grids have an incorrect format 自己的 VUCC 网格格式错误 - + Based on own callsign, own DXCC ITU is different from the entered value - expecting 基于自己的呼号,自己的 DXCC ITU 与输入的值不同 - 期望 - + Based on own callsign, own DXCC CQZ is different from the entered value - expecting 基于自己的呼号,自己的 DXCC CQ分区 与输入的值不同 - 期望 - + Based on own callsign, own DXCC Country is different from the entered value - expecting 基于自己的呼号,自己的 DXCC 国家/地区与输入的值不同 - 期望 - + Based on SOTA Summit, QTH does not match SOTA Summit Name - expecting 基于 SOTA 山峰,QTH 不匹配 SOTA 山峰名称 - 期待 - + Based on SOTA Summit, Grid does not match SOTA Grid - expecting 基于 SOTA 山峰,网格坐标不匹配 SOTA 山峰名称 - 期待 - + Based on POTA record, QTH does not match POTA Name - expecting 基于 POTA 记录,QTH 不匹配 POTA 名称 - 期待 - + Based on POTA record, Grid does not match POTA Grid - expecting 基于 POTA 记录,网格坐标不匹配 POTA 名称 - 期待 - + Based on SOTA Summit, my QTH does not match SOTA Summit Name - expecting 基于 SOTA 山峰,我的 QTH 不匹配 SOTA 山峰名称 - 期待 - + Based on SOTA Summit, my Grid does not match SOTA Grid - expecting 基于 SOTA 山峰,我的网格坐标不匹配 SOTA 山峰名称 - 期待 - + Based on POTA record, my QTH does not match POTA Name - expecting 基于 POTA 记录,我的 QTH 不匹配 POTA 名称 - 期待 - + Based on POTA record, my Grid does not match POTA Grid - expecting 基于 POTA 记录,我的网格坐标不匹配 POTA 名称 - 期待 - + LoTW Sent Status to <b>No</b> does not make any sense if QSL Sent Date is set. Set Date to 1.1.1900 to leave the date field blank 如果已经设置了 QSL 发送日期, LoTW 发送状态设为<b>否</b> 没有任何意义。设置日期为 1.1.1900 以让日期字段为空 - + Date should be present for LoTW Sent Status <b>Yes</b> 需要为 LoTW 发送状态 提供日期。<b>是</b> - + eQSL Sent Status to <b>No</b> does not make any sense if QSL Sent Date is set. Set Date to 1.1.1900 to leave the date field blank 如果已经设置了 QSL 发送日期, eQSL 发送状态设为<b>否</b> 没有任何意义。设置日期为 1.1.1900 以让日期字段为空 - + Date should be present for eQSL Sent Status <b>Yes</b> 需要为 eQSL 发送状态提供日期 <b>是</b> - + Paper Sent Status to <b>No</b> does not make any sense if QSL Sent Date is set. Set Date to 1.1.1900 to leave the date field blank 如果已经设置了 QSL 发送日期, 纸质卡片发送状态设为<b>否</b> 没有任何意义。设置日期为 1.1.1900 以让日期字段为空 - + Date should be present for Paper Sent Status <b>Yes</b> 需要为纸质卡片发送状态提供日期 <b>是</b> - + Callbook error: 电台黄页错误: - - + + <b>Warning: </b> <b>告警</b>: %n - + Validation 验证 - + Yellow marked fields are invalid.<p>Nevertheless, save the changes?</p> 黄色标记的字段无效。<p>依然保存更改?</p> - + &Save 保存(&S) - + &Edit 编辑(&E) @@ -11003,52 +11783,52 @@ You can leave fields empty and configure them later in Settings. 添加条件 - + Equal 等于 - + Not Equal 不等于 - + Contains 包含 - + Not Contains 不包含 - + Greater Than 大于 - + Less Than 小于 - + Starts with 起始自 - + RegExp - + 正则表达式 - + Remove 删除 - + Must not be empty 不能为空 @@ -11084,27 +11864,27 @@ You can leave fields empty and configure them later in Settings. Rig - + No Rig Profile selected 未选择设备配置文件 - + Rigctld Error - + Rigctld 错误 - + Initialization Error 初始化出错 - + Internal Error 内部错误 - + Cannot open Rig 无法打开设备 @@ -11122,36 +11902,66 @@ You can leave fields empty and configure them later in Settings. 接收 - + Disconnected - - + + MHz MHz - + Disable Split - + RIT: 0.00000 MHz RIT: 0.00000 MHz - + XIT: 0.00000 MHz XIT: 0.00000 MHz - + PWR: %1W 功率: %1W + + + OUT + + + + + Outside Bandmap Guide range + + + + + SOS + + + + + Emergency frequency: %1 MHz + + + + + IBP + IBP + + + + International Beacon Project: %1 MHz + + RigctldAdvancedDialog @@ -11168,47 +11978,47 @@ You can leave fields empty and configure them later in Settings. Leave empty for auto-detection - + 留空以进行自动检测 Browse - 浏览 + 浏览 Auto-detect Rigctld path - + 自动检测 Rigctld 路径 Auto-Detect - + 自动检测 Rigctld Version: - + Rigctld 版本: Additional Arguments: - + 额外参数: e.g. -v -v for verbose logging - + 例如 -v -v 用于详细日志记录 Cannot be changed - + 无法被更改 Auto Detect - + 自动检测 @@ -11240,57 +12050,57 @@ Please install Hamlib or specify the path manually. RigctldManager - + rigctld executable not found in /app/bin/. This should not happen in Flatpak build. - + rigctld executable not found. Please install Hamlib or specify the path in Advanced settings. - + Hamlib major version mismatch: QLog was compiled with Hamlib %1 but rigctld reports version %2.%3.%4. Rig model IDs are incompatible between major versions. - + Port %1 is already in use. Another rigctld or application may be running on this port. - + rigctld started but not responding on port %1. - + Failed to start rigctld: %1 %2 - + rigctld crashed. - + rigctld timed out. - + Write error with rigctld. - + Read error with rigctld. - + Unknown rigctld error. @@ -11298,22 +12108,22 @@ Please install Hamlib or specify the path manually. Rotator - + No Rotator Profile selected 未选择旋转云台配置文件 - + Initialization Error 初始化出错 - + Internal Error 内部错误 - + Cannot open Rotator 无法打开云台 @@ -11440,7 +12250,7 @@ Please install Hamlib or specify the path manually. - + Callsign 呼号 @@ -11509,20 +12319,21 @@ Please install Hamlib or specify the path manually. - - - - - - - - - - + + + + + + - - - + + + + + + + + Add 添加 @@ -11592,6 +12403,7 @@ Please install Hamlib or specify the path manually. + Description 描述 @@ -11869,7 +12681,7 @@ Please install Hamlib or specify the path manually. Leave empty for auto-detection - + 留空以进行自动检测 @@ -11879,7 +12691,7 @@ Please install Hamlib or specify the path manually. Auto-Detect - + 自动检测 @@ -11887,27 +12699,27 @@ Please install Hamlib or specify the path manually. - + Color CQ Spots - + Enable/Disable sending color-coded status indicators back to WSJT-X for each callsign calling CQ - + Unit System - + Metric - + Imperial @@ -12082,7 +12894,7 @@ Please install Hamlib or specify the path manually. - + Start rigctld daemon to share rig with other applications (e.g. WSJT-X) @@ -12208,7 +13020,7 @@ Please install Hamlib or specify the path manually. - + API Key API密钥 @@ -12218,72 +13030,75 @@ Please install Hamlib or specify the path manually. 站点地址 - + Others 其他 - + Status Confirmed By 确认自 - + Paper 纸质卡片 - + The '>' character is interpreted as a marker for the initial cursor position in the Report column. <br/>Ex.: '5>9' means the cursor will be positioned on the second character “>” 字符被解读为 “报告” 列中初始光标位置的标记。 <br/>示例:“5>9” 表示光标将定位在第二个字符上 - + Rig Status 设备状态 - + <p> List of IP addresses to which QLog sends UDP notification packets when Rig State changes.</p>The IP addresses are separated by a space and have the form IP:PORT <p>当设备状态发生变化时,QLog向其发送 UDP 通知数据包的 IP 地址列表。</p>IP地址用空格分隔,形式为IP:PORT - + GUI 用户界面 - + Time Format 时间格式 - + 24-hour 24小时 - + AM/PM - + Date Format 日期格式 - + System 系统 - + + + + Custom 自定义 - + <a href="https://doc.qt.io/qt-6/qdate.html#fromString-1">Time Format Documentation</a> <a href="https://doc.qt.io/qt-6/qdate.html#fromString-1">时间格式文档</a> @@ -12295,14 +13110,14 @@ Please install Hamlib or specify the path manually. - + Serial 串口 - - + + Network 网络 @@ -12393,8 +13208,8 @@ Please install Hamlib or specify the path manually. - - + + HamQTH HamQTH @@ -12403,7 +13218,7 @@ Please install Hamlib or specify the path manually. - + Username 用户名 @@ -12413,15 +13228,15 @@ Please install Hamlib or specify the path manually. - + Password 密码 - - + + QRZ.com QRZ.com @@ -12487,7 +13302,8 @@ Please install Hamlib or specify the path manually. - + + eQSL eQSL @@ -12520,7 +13336,8 @@ Please install Hamlib or specify the path manually. - + + LoTW LoTW @@ -12540,418 +13357,693 @@ Please install Hamlib or specify the path manually. 使用内部 TQSL 实例 - + + Startup ADI + + + + + Configured ADI/ADIF files are checked only at startup. A newly added file starts at its current end, so only later appended QSOs are loaded. This is not a live watcher; if many new QSOs are found, loading stops and the standard Import should be used. + + + + + Removing a file also forgets its recovery position. + + + + + Remove + + + + + Used when a file row has Missing QSL Sent set to Custom. Explicit ADIF values are kept. + + + + + Custom QSL Sent Defaults + + + + + Paper QSL + + + + + DCL + + + + Chat 聊天 - + <b>Security Notice:</b> QLog stores all passwords in the Secure Storage. Unfortunately, ON4KST uses a protocol where this password is sent over an unsecured channel as plaintext.</p><p>Please exercise caution when choosing your password for this service, as your password is sent over an unsecured channel in plaintext form.</p> <b>安全通知:</b> QLog将所有密码存储在安全存储中。不幸的是,ON4KST使用一种协议,该协议将密码以明文形式通过不安全的通道发送。</p><p>请谨慎选择此服务的密码,因为您的密码以明文形式通过不安全的通道发送。</p> - + Bands 波段 - + + Select the <b>Bandmap Guide</b> profile shown as visual frequency hints. It does not affect mode identification. + + + + + Manage + + + + + Double-click cells to edit start/end frequency, enabled state, or SAT mode. Band names are fixed; new bands cannot be added here. + + + + Modes 模式 - + + QSO DXCC Status Colors + + + + + Used for DX spots, Bandmap, WSJT-X and QSO status hints. Confirmed has no highlight by default. Click a color cell to choose a color or set No color. + + + + + Restore Defaults + + + + + Shortcuts + + + + Danger Zone - + <b>⚠ This is a danger zone. Proceed with caution, as actions performed here cannot be undone and may have a significant impact on your log.</b> - + Delete All QSOs - + Delete All Passwords from the Secure Store - - + + DXCC DXCC - + Wsjtx Wsjtx - + Raw UDP Forward 原始UDP转发 - + <p>List of IP addresses to which QLog forwards raw UDP WSJT-X packets.</p>The IP addresses are separated by a space and have the form IP:PORT <p> QLog转发UDP WSJT-X原始报文的IP地址列表。</p> IP地址之间用空格分隔,格式为IP:PORT - - - - - - + + + + + + ex. 192.168.1.1:1234 192.168.2.1:1234 例. 192.168.1.1:1234 192.168.2.1:1234 - + Port 端口 - + Port where QLog listens an incoming traffic from WSJT-X QLog 监听来自 WSJT-X 的传入数据流的端口 - + Join Multicast 加入多播 - + Enable/Disable Multicast option for WSJTX 启用/禁用 WSJTX 的多播选项 - + Multicast Address 多播地址 - + Specify Multicast Address. <br>On some Linux systems it may be necessary to enable multicast on the loop-back network interface. 指定组播地址。<br>在某些Linux系统上,可能需要在环路网络接口上启用多播。 - + TTL TTL - + Time-To-Live determines the range<br> over which a multicast packet is propagated in your intranet. 生存时间 (Time-To-Live) 决定了组播数据包在内部网中传播的范围。 - + Notifications 通知 - + LogID 日志ID - + <p>Assigned LogID to the current log.</p>The LogID is sent in the Network Nofitication messages as a unique instance identified.<p> The ID is generated automatically and cannot be changed</> <p>为当前日志分配 LogID。</p> LogID作为唯一实例标识在网络通知消息中发送。<p>该ID为自动生成,不可手动修改</> - + DX Spots DX 报点 - + <p> List of IP addresses to which QLog sends UDP notification packets with DX Cluster Spots.</p>The IP addresses are separated by a space and have the form IP:PORT <p> QLog 发送 DX 集群报点 UDP 通知报文的 IP 地址列表。</p> IP 地址之间用空格分隔,格式为 IP:PORT - + Spot Alerts 实时报点提醒 - + <p> List of IP addresses to which QLog sends UDP notification packets about user Spot Alerts.</p>The IP addresses are separated by a space and have the form IP:PORT <p> QLog 向用户发送 实时报点提醒 UDP 报文的 IP 地址列表。</p> IP 地址之间用空格分隔,格式为 IP:PORT - + QSO Changes QSO 变更 - + <p> List of IP addresses to which QLog sends UDP notification packets about a new/updated/deleted QSO in the log.</p>The IP addresses are separated by a space and have the form IP:PORT <p>日志中 新增/更新/删 除QSO时,QLog 向其发送 UDP 通知报文的 IP 地址列表。</p> IP地址之间用空格分隔,格式为 IP:PORT - + Wsjtx CQ Spots WSJTX CQ 报点 - + <p> List of IP addresses to which QLog sends UDP notification packets with WSJTX CQ Spots.</p>The IP addresses are separated by a space and have the form IP:PORT <p> QLog 通过 WSJTX CQ 报点 发送 UDP 通知报文的 IP 地址列表。</p> IP地址之间用空格分隔,格式为 IP:PORT - - + + Special - Omnirig 特别的 - Ominirig - + Cannot be changed - + 无法被更改 - - + + Name 姓名 - + Report 报告 - - + + State 状态 - + Start (MHz) 起始(MHz) - + End (MHz) 结束(MHz) - + SAT Mode 卫星模式 - - - + + + Disabled 禁用 - - + + None - + Hardware 硬件 - + Software 软件 - + + + + + No - + Even 偶校验 - + Odd 奇校验 - + Mark 1校验 - + Space 0校验 - + Dummy 虚拟 - + Morse Over CAT Morse Over CAT - + WinKey - + CWDaemon CWDaemon - + FLDigi FLDigi - + Single Paddle 单桨电键 - + IAMBIC A IAMBIC A - + IAMBIC B IAMBIC B - + Ultimate Ultimate - + High - + Low - + + Duplicate + 重复 + + + + Already worked QSO + + + + + New Entity + 新实体 + + + + DXCC entity not worked yet + + + + + New Band / Mode + + + + + New band, mode, or band and mode + + + + + New Slot + 新组合 + + + + New band and mode combination + + + + + Worked + 已通联 + + + + Worked but not confirmed + + + + + Confirmed + 已确认 + + + + Confirmed QSO; no highlight by default + + + + + Status + + + + + Color + + + + + Choose Color... + + + + + Default + + + + + No Color + + + + + Status Color + + + + + No color + + + + + No highlight. Click to choose a color or set no color. + + + + + Click to change color or set no color. + + + + Press <b>Modify</b> to confirm the profile changes or <b>Cancel</b>. 按<b>修改</b>确认更改配置文件或按<b>取消</b>。 - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + Modify 修改 - - - - - - - - - - + + + + + + + + + + Must not be empty 不能为空 - + Select File 选择文件 - + Auto Detect - + 自动检测 - + TQSL was not found on this system. Please install TQSL or specify the path manually. - + Not found - + Rig sharing is only available for Hamlib driver - + Rig sharing is not available for network connection - + + Off + + + + Delete Passwords - + All passwords have been deleted - + Deleting all QSOs... - + Error 错误 - + Failed to delete all QSOs. - + + Enabled + 启用 + + + + Path + 路径 + + + + Station Profile + 台站配置 + + + + Missing QSL Sent + + + + + Last Recovery + + + + + + + Queued + 排队中 + + + + + + + Ignored + 已忽视 + + + + + + + Requested + 已请求 + + + + + + + Yes + + + + + Station Profile does not exist. Select another profile and enable this row again. + + + + + File exists + + + + + File does not exist + + + + + Startup ADI initialized + + + + + Select ADIF File + + + + + ADIF Files (*.adi *.adif);;All Files (*) + + + + members 成员 - + Required internet connection during application start 在应用程序启动时需要互联网连接 @@ -13009,7 +14101,7 @@ Please install TQSL or specify the path manually. StatisticsWidget - + Statistics 统计分析 @@ -13080,7 +14172,7 @@ Please install TQSL or specify the path manually. - + Band 波段 @@ -13105,146 +14197,145 @@ Please install TQSL or specify the path manually. 纸质卡片 - + Year - + Month - + Day in Week 星期几 - + Hour 小时 - + Mode 模式 - + Continent 大洲 - + Propagation Mode 传播模式 - + Confirmed / Not Confirmed 已确认/未确认 - + Countries 国家/地区 - + Big Gridsquares 大网格坐标 - + Distance 距离 - + QSOs QSO - + Confirmed/Worked Grids 确认/已通联 网格 - + ODX ODX - + Sun 星期日 - + Mon 星期一 - + Tue 星期二 - + Wed 星期三 - + Thu 星期四 - + Fri 星期五 - + Sat 星期六 - - - + + + Not specified 未指定 - + Confirmed 已确认 - + Not Confirmed 未确认 - + No User Filter 无用户筛选器 - + Over 50000 QSOs. Display them? 超过50000个QSO。显示全部吗? - - + Rendering QSOs... - + All 全部 @@ -13274,7 +14365,7 @@ Please install TQSL or specify the path manually. Error Occurred - + 发生错误 @@ -13298,17 +14389,17 @@ Please install TQSL or specify the path manually. ToAllTableModel - + Time 时间 - + Spotter 报点者 - + Message 消息 @@ -13474,7 +14565,7 @@ Please install TQSL or specify the path manually. Unspecified - 未指定 + 未指定 @@ -13582,27 +14673,27 @@ Please install TQSL or specify the path manually. UserListModel - + Callsign 呼号 - + Gridsquare 网格坐标 - + Distance 距离 - + Azimuth 方位 - + Comment 备注 @@ -13610,47 +14701,47 @@ Please install TQSL or specify the path manually. WCYTableModel - + Time 时间 - + K K - + expK expK - + A A - + R R - + SFI SFI - + SA SA - + GMF GMF - + Au Au @@ -13658,27 +14749,27 @@ Please install TQSL or specify the path manually. WWVTableModel - + Time 时间 - + SFI SFI - + A A - + K K - + Info 信息 @@ -13804,37 +14895,37 @@ Please install TQSL or specify the path manually. WsjtxTableModel - + Callsign 呼号 - + Gridsquare 网格坐标 - + Distance 距离 - + SNR SNR - + Last Activity 前次活跃 - + Last Message 前次消息 - + Member 成员 @@ -13875,47 +14966,47 @@ Please install TQSL or specify the path manually. main - + Run with the specific namespace. 使用特定的命名空间运行。 - + namespace 命名空间 - + Translation file - absolute or relative path and QM file name. 翻译文件 - 绝对或相对路径及 QM 文件名。 - + path/QM-filename 路径/QM-文件名 - + Set language. <code> example: 'en' or 'en_US'. Ignore environment setting. 设置语言。<code> 示例: 'en' 或 'en_US'。忽略环境设置。 - + code 代码 - + Writes debug messages to the debug file 将调试信息写入debug文件 - + Process pending database import (internal use) - + Force update of all value lists (DXCC, SATs, etc.) diff --git a/logformat/AdiFormat.cpp b/logformat/AdiFormat.cpp index 8d0ca30d..50cd1145 100644 --- a/logformat/AdiFormat.cpp +++ b/logformat/AdiFormat.cpp @@ -36,11 +36,30 @@ void AdiFormat::exportContact(const QSqlRecord& record, qCDebug(function_parameters)<\n\n"; } +void AdiFormat::normalizeGridFields(QSqlRecord &record) +{ + FCT_IDENTIFICATION; + + QString gridsquare = record.value("gridsquare").toString().trimmed().toUpper(); + if ( gridsquare.isEmpty() ) + return; + + if ( gridsquare.length() > GRID_BASE_LENGTH ) + { + record.setValue("gridsquare_ext", gridsquare.mid(GRID_BASE_LENGTH)); + gridsquare = gridsquare.left(GRID_BASE_LENGTH); + } + record.setValue("gridsquare", gridsquare); +} + void AdiFormat::writeField(const QString &name, bool presenceCondition, const QString &value, const QString &type) { @@ -54,7 +73,9 @@ void AdiFormat::writeField(const QString &name, bool presenceCondition, if (!presenceCondition) return; /* ADIF does not support UTF-8 characterset therefore the Accents are remove */ - QString accentless(Data::removeAccents(value)); + const QString accentless(normalizeLineBreaks(Data::removeAccents(value), + preserveFieldLineBreaks(name, type), + QStringLiteral("\r\n"))); qCDebug(runtime) << "Accentless: " << accentless; @@ -111,7 +132,25 @@ void AdiFormat::writeSQLRecord(const QSqlRecord &record, const QStringList &keys = fields.keys(); for (const QString &key : keys) { - writeField(key, ALWAYS_PRESENT, fields.value(key).toString()); + if ( !isExportableFieldName(key) ) + { + qCDebug(runtime) << "Skipping invalid ADIF field from fields JSON:" << key; + continue; + } + + const QJsonValue fieldValue = fields.value(key); + if ( fieldValue.isObject() ) + { + const QJsonObject fieldObject = fieldValue.toObject(); + writeField(key, + ALWAYS_PRESENT, + fieldObject.value(QStringLiteral("value")).toString(), + fieldObject.value(QStringLiteral("type")).toString()); + } + else + { + writeField(key, ALWAYS_PRESENT, fieldValue.toString()); + } } /* Add application-specific tags */ @@ -125,6 +164,37 @@ void AdiFormat::writeSQLRecord(const QSqlRecord &record, } } +bool AdiFormat::isExportableFieldName(const QString &name) +{ + const int length = name.length(); + if ( length == 0 ) + return false; + + const QChar * const data = name.constData(); + const ushort first = data[0].unicode(); + if ( !((first >= 'A' && first <= 'Z') || (first >= 'a' && first <= 'z')) ) + return false; + + if ( name.startsWith(QStringLiteral("xml"), Qt::CaseInsensitive) ) + return false; + + for ( int i = 0; i < length; ++i ) + { + const ushort c = data[i].unicode(); + const bool isLetter = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); + const bool isDigit = c >= '0' && c <= '9'; + + if ( !isLetter && !isDigit && c != '_' ) + return false; + } + + if ( !name.startsWith(QStringLiteral("APP_"), Qt::CaseInsensitive) ) + return true; + + const int fieldStart = name.indexOf(QLatin1Char('_'), 4); + return fieldStart > 4 && fieldStart < length - 1; +} + void AdiFormat::readField(QString& field, QString& value) { FCT_IDENTIFICATION; @@ -239,6 +309,7 @@ void AdiFormat::readField(QString& field, QString& value) if (!inHeader) { return; } + headerFields.insert(field.toLower(), value); break; } } @@ -502,6 +573,8 @@ void AdiFormat::contactFields2SQLRecord(QMap &contact, QSqlRe record.setValue("start_time", start_time); record.setValue("end_time", end_time); + + normalizeGridFields(record); } const QString AdiFormat::formatOuput(OutputFieldFormatter formatter, const QVariant &in) @@ -645,6 +718,48 @@ void AdiFormat::preprocessINTLField(const QString &fieldName, } } +bool AdiFormat::isMultilineField(const QString &name) +{ + static const QSet multilineFields({ + QStringLiteral("address"), + QStringLiteral("address_intl"), + QStringLiteral("notes"), + QStringLiteral("notes_intl"), + QStringLiteral("qslmsg"), + QStringLiteral("qslmsg_intl"), + QStringLiteral("qslmsg_rcvd"), + QStringLiteral("rig"), + QStringLiteral("rig_intl") + }); + + const QString lowerName = name.toLower(); + + return multilineFields.contains(lowerName) + || lowerName.startsWith(QStringLiteral("app_")); +} + +bool AdiFormat::preserveFieldLineBreaks(const QString &name, const QString &type) +{ + return type.compare(QStringLiteral("M"), Qt::CaseInsensitive) == 0 + || (type.isEmpty() && isMultilineField(name)); +} + +QString AdiFormat::normalizeLineBreaks(const QString &value, + bool preserveLineBreaks, + const QString &lineBreak) +{ + QString normalized(value); + normalized.replace(QStringLiteral("\r\n"), QStringLiteral("\n")); + normalized.replace(QLatin1Char('\r'), QLatin1Char('\n')); + + if ( preserveLineBreaks ) + normalized.replace(QLatin1Char('\n'), lineBreak); + else + normalized.remove(QLatin1Char('\n')); + + return normalized; +} + bool AdiFormat::readContact(QMap& contact) { FCT_IDENTIFICATION; @@ -662,7 +777,7 @@ bool AdiFormat::readContact(QMap& contact) return true; } - if (!value.isEmpty()) + if (!field.isEmpty()) { contact[field] = QVariant(value); } @@ -671,11 +786,14 @@ bool AdiFormat::readContact(QMap& contact) return false; } -AdiFormat::AdiFormat(QTextStream &stream) : +AdiFormat::AdiFormat(QTextStream &stream, bool preserveFieldLengths) : LogFormat(stream) { FCT_IDENTIFICATION; + if ( preserveFieldLengths && stream.device() ) + stream.device()->setTextModeEnabled(false); + #if QT_VERSION >= QT_VERSION_CHECK(6,0,0) stream.setEncoding(QStringConverter::Latin1); #else @@ -701,6 +819,41 @@ bool AdiFormat::importNext(QSqlRecord& record) return true; } +void AdiFormat::importStart() +{ + FCT_IDENTIFICATION; + + headerFields.clear(); + state = START; + inHeader = false; +} + +bool AdiFormat::importNextDXCCCredit(DXCCCreditRecord &credit) +{ + FCT_IDENTIFICATION; + + QVariantMap adifRecord; + + if ( !readContact(adifRecord) ) + return false; + + credit.call = adifRecord.value("call").toString().trimmed().toUpper(); + credit.band = adifRecord.value("band").toString().trimmed().toLower(); + credit.dxcc = adifRecord.value("dxcc").toString().trimmed(); + credit.mode = adifRecord.value("mode").toString().trimmed().toUpper(); + credit.propMode = adifRecord.value("prop_mode").toString().trimmed().toUpper(); + credit.dxccModeGroup = adifRecord.value("app_lotw_modegroup").toString().trimmed().toUpper(); + + if ( credit.dxccModeGroup.isEmpty() ) + credit.dxccModeGroup = adifRecord.value("app_lotw_mode").toString().trimmed().toUpper(); + + credit.creditGranted = adifRecord.value("credit_granted").toString().trimmed().toUpper(); + credit.awardEntity = headerFields.value("app_lotw_dxccrecord_entity").toString().trimmed(); + credit.qsoDate = parseDate(adifRecord.value("qso_date").toString()); + + return true; +} + QDate AdiFormat::parseDate(const QString &date) { FCT_IDENTIFICATION; diff --git a/logformat/AdiFormat.h b/logformat/AdiFormat.h index 97b5cdd9..50fbe52f 100644 --- a/logformat/AdiFormat.h +++ b/logformat/AdiFormat.h @@ -6,7 +6,8 @@ class AdiFormat : public LogFormat { public: - explicit AdiFormat(QTextStream& stream); + explicit AdiFormat(QTextStream& stream, + bool preserveFieldLengths = true); virtual bool importNext(QSqlRecord& ) override; @@ -16,6 +17,10 @@ class AdiFormat : public LogFormat static QMap fieldname2INTLNameMapping; + const static int GRID_BASE_LENGTH = 8; + + static void normalizeGridFields(QSqlRecord &record); + template static void preprocessINTLFields(T &contact) { @@ -27,6 +32,8 @@ class AdiFormat : public LogFormat } protected: + virtual bool importNextDXCCCredit(DXCCCreditRecord&) override; + virtual void importStart() override; virtual void writeField(const QString &name, bool presenceCondition, const QString &value, @@ -38,6 +45,10 @@ class AdiFormat : public LogFormat QSqlRecord &record); void contactFields2SQLRecord(QMap &contact, QSqlRecord &record); + static bool preserveFieldLineBreaks(const QString &name, const QString &type); + static QString normalizeLineBreaks(const QString &value, + bool preserveLineBreaks, + const QString &lineBreak); enum OutputFieldFormatter { @@ -112,7 +123,10 @@ class AdiFormat : public LogFormat static void preprocessINTLField(const QString &fieldName, const QString &fieldIntlName, QSqlRecord &contact); + static bool isExportableFieldName(const QString &name); + static bool isMultilineField(const QString &name); + QVariantMap headerFields; ParserState state = START; bool inHeader = false; }; diff --git a/logformat/AdxFormat.cpp b/logformat/AdxFormat.cpp index 2c0eb0ed..eeb148f0 100644 --- a/logformat/AdxFormat.cpp +++ b/logformat/AdxFormat.cpp @@ -7,7 +7,7 @@ MODULE_IDENTIFICATION("qlog.logformat.adxformat"); AdxFormat::AdxFormat(QTextStream &stream) : - AdiFormat(stream), + AdiFormat(stream, false), writer(nullptr), reader(nullptr) { @@ -113,9 +113,79 @@ void AdxFormat::exportContact(const QSqlRecord& record, QMap * writer->writeStartElement("RECORD"); - writeSQLRecord(record, applTags); + QSqlRecord exportRecord(record); + normalizeGridFields(exportRecord); + writeSQLRecord(exportRecord, applTags); + + writer->writeEndElement(); +} + +QString AdxFormat::applicationFieldKey(const QString &programId, + const QString &fieldName) +{ + return QStringLiteral("app_%1_%2").arg(programId, fieldName).toLower(); +} + +QString AdxFormat::attributeValue(const QXmlStreamAttributes &attributes, + const QString &name) +{ + for (const QXmlStreamAttribute &attribute : attributes) + { + if ( attribute.name().compare(name, Qt::CaseInsensitive) == 0 ) + return attribute.value().toString(); + } + + return QString(); +} + +bool AdxFormat::splitApplicationFieldName(const QString &name, + QString &programId, + QString &fieldName) +{ + const QString prefix(QStringLiteral("app_")); + if ( !name.startsWith(prefix, Qt::CaseInsensitive) ) + return false; + + const int fieldStart = name.indexOf(QLatin1Char('_'), prefix.length()); + if ( fieldStart <= prefix.length() ) + return false; + + programId = name.mid(prefix.length(), fieldStart - prefix.length()); + fieldName = name.mid(fieldStart + 1); + + return !programId.isEmpty() && !fieldName.isEmpty(); +} + +bool AdxFormat::writeApplicationField(const QString &name, + bool presenceCondition, + const QString &value, + const QString &type) +{ + QString programId; + QString fieldName; + if ( !splitApplicationFieldName(name, programId, fieldName) ) + return false; + + if ( !presenceCondition ) + return true; + + const QString outputType = type.isEmpty() ? QStringLiteral("M") : type.toUpper(); + const QString outputValue(normalizeLineBreaks(value, + preserveFieldLineBreaks(name, outputType), + QStringLiteral("\n"))); + + if ( outputValue.isEmpty() ) + return true; + + writer->writeStartElement("APP"); + writer->writeAttribute("PROGRAMID", programId); + writer->writeAttribute("FIELDNAME", fieldName); + writer->writeAttribute("TYPE", outputType); + writer->writeCharacters(outputValue); writer->writeEndElement(); + + return true; } void AdxFormat::writeField(const QString &name, @@ -130,9 +200,16 @@ void AdxFormat::writeField(const QString &name, << value << type; - if (value.isEmpty() || !presenceCondition ) return; + if ( writeApplicationField(name, presenceCondition, value, type) ) + return; + + const QString outputValue(normalizeLineBreaks(value, + preserveFieldLineBreaks(name, type), + QStringLiteral("\n"))); - writer->writeTextElement(name.toUpper(), value); + if (outputValue.isEmpty() || !presenceCondition ) return; + + writer->writeTextElement(name.toUpper(), outputValue); } void AdxFormat::writeSQLRecord(const QSqlRecord &record, QMap *applTags) @@ -171,7 +248,31 @@ bool AdxFormat::readContact(QVariantMap & contact) while (reader->readNextStartElement() ) { qCDebug(runtime)<<"adding element " << reader->name(); - contact[reader->name().toLatin1().toLower()] = QVariant(reader->readElementText()); + if ( reader->name().compare(QStringLiteral("APP"), Qt::CaseInsensitive) == 0 ) + { + const QXmlStreamAttributes attributes = reader->attributes(); + const QString programId = attributeValue(attributes, QStringLiteral("PROGRAMID")); + const QString fieldName = attributeValue(attributes, QStringLiteral("FIELDNAME")); + const QString type = attributeValue(attributes, QStringLiteral("TYPE")); + const QString value = reader->readElementText(); + + if ( !programId.isEmpty() && !fieldName.isEmpty() ) + { + QVariantMap appField; + appField.insert(QStringLiteral("value"), value); + if ( !type.isEmpty() ) + appField.insert(QStringLiteral("type"), type.toUpper()); + contact[applicationFieldKey(programId, fieldName)] = appField; + } + else + { + contact[reader->name().toLatin1().toLower()] = QVariant(value); + } + } + else + { + contact[reader->name().toLatin1().toLower()] = QVariant(reader->readElementText()); + } } return true; } diff --git a/logformat/AdxFormat.h b/logformat/AdxFormat.h index 122fc7e4..f70206c0 100644 --- a/logformat/AdxFormat.h +++ b/logformat/AdxFormat.h @@ -1,6 +1,7 @@ #ifndef QLOG_LOGFORMAT_ADXFORMAT_H #define QLOG_LOGFORMAT_ADXFORMAT_H +#include #include #include "AdiFormat.h" @@ -28,6 +29,18 @@ class AdxFormat : public AdiFormat virtual bool readContact(QVariantMap& ) override; private: + static QString applicationFieldKey(const QString &programId, + const QString &fieldName); + static QString attributeValue(const QXmlStreamAttributes &attributes, + const QString &name); + static bool splitApplicationFieldName(const QString &name, + QString &programId, + QString &fieldName); + bool writeApplicationField(const QString &name, + bool presenceCondition, + const QString &value, + const QString &type); + QXmlStreamWriter *writer; QXmlStreamReader *reader; }; diff --git a/logformat/CSVFormat.cpp b/logformat/CSVFormat.cpp index 46972d76..e02b0ee7 100644 --- a/logformat/CSVFormat.cpp +++ b/logformat/CSVFormat.cpp @@ -94,5 +94,12 @@ QString CSVFormat::csvStringValue(const QString &value) { FCT_IDENTIFICATION; - return ((value.contains(delimiter))? "\"" + value + "\"" : value); + QString csvValue(value); + csvValue.replace("\"", "\"\""); + + return ( csvValue.contains(delimiter) + || csvValue.contains('\n') + || csvValue.contains('\r') + || csvValue.contains('"') ) ? "\"" + csvValue + "\"" + : csvValue; } diff --git a/logformat/JsonFormat.cpp b/logformat/JsonFormat.cpp index 44c14cb3..f398272b 100644 --- a/logformat/JsonFormat.cpp +++ b/logformat/JsonFormat.cpp @@ -51,13 +51,16 @@ void JsonFormat::writeField(const QString &name, << value << type; - if (value.isEmpty() || !presenceCondition) return; + const QString outputValue(normalizeLineBreaks(value, + preserveFieldLineBreaks(name, type), + QStringLiteral("\n"))); - contact[name] = value; + if (outputValue.isEmpty() || !presenceCondition) return; + + contact[name] = outputValue; } bool JsonFormat::importNext(QSqlRecord&) { return false; } - diff --git a/logformat/LogFormat.cpp b/logformat/LogFormat.cpp index 3e846827..14b3c927 100644 --- a/logformat/LogFormat.cpp +++ b/logformat/LogFormat.cpp @@ -784,6 +784,376 @@ unsigned long LogFormat::runImport(QTextStream& importLogStream, #undef RECORDIDX +QStringList LogFormat::splitCreditValues(const QString &value) +{ + QStringList ret; + const QStringList values = value.split(',', +#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) + Qt::SkipEmptyParts); +#else + QString::SkipEmptyParts); +#endif + + for ( const QString &rawValue : values ) + { + const QString trimmed = rawValue.trimmed(); + if ( !trimmed.isEmpty() ) + ret << trimmed; + } + + return ret; +} + +QString LogFormat::mergeCreditValues(const QString ¤tValue, const QString &newValue) +{ + QStringList merged; + QSet seen; + + auto appendIfMissing = [&merged, &seen](const QString &value) + { + const QString key = value.toUpper(); + if ( !seen.contains(key) ) + { + seen.insert(key); + merged << value; + } + }; + + const QStringList currentValues = splitCreditValues(currentValue); + for ( const QString &value : currentValues ) + appendIfMissing(value); + + const QStringList newValues = splitCreditValues(newValue); + for ( const QString &value : newValues ) + appendIfMissing(value); + + return merged.join(','); +} + +bool LogFormat::isSatelliteDXCCCredit(const DXCCCreditRecord &credit) +{ + return splitCreditValues(credit.creditGranted).contains("DXCC_SATELLITE", + Qt::CaseInsensitive); +} + +bool LogFormat::isDXCCEntityCode(const QString &call) +{ + bool ok = false; + call.trimmed().toUInt(&ok); + return ok; +} + +QString LogFormat::escapeSqlLikePattern(const QString &value) +{ + QString ret = value; + ret.replace("\\", "\\\\"); + ret.replace("%", "\\%"); + ret.replace("_", "\\_"); + return ret; +} + +QString LogFormat::formatDXCCCreditReport(const DXCCCreditRecord &credit, + const QStringList &addInfo) +{ + QString bandOrPropMode = credit.band; + if ( !credit.propMode.isEmpty() ) + bandOrPropMode = bandOrPropMode.isEmpty() ? credit.propMode : bandOrPropMode + "/" + credit.propMode; + + return QString("%1; %2; %3%4 %5") + .arg(credit.qsoDate.isValid() ? credit.qsoDate.toString(Qt::ISODate) : "-", + credit.call.isEmpty() ? "-" : credit.call, + bandOrPropMode.isEmpty() ? "-" : bandOrPropMode, + addInfo.isEmpty() ? "" : ";", + addInfo.join(", ")); +} + +bool LogFormat::selectDXCCCreditMatches(const DXCCCreditRecord &credit, + const DXCCCreditCallMatch callMatch, + const bool matchMode, + QList &matches, + QString &error) +{ + matches.clear(); + error.clear(); + + QStringList where = + { + "ABS(JULIANDAY(date(start_time)) - JULIANDAY(date(:qso_date))) <= 1" + }; + + // DXCC credits can come from LoTW or paper cards, and users may import + // credits before importing QSL state. Do not require local QSL received flags. + + if ( !credit.band.isEmpty() ) + where << "upper(band) = upper(:band)"; + + if ( !credit.propMode.isEmpty() ) + { + where << "upper(prop_mode) = upper(:prop_mode)"; + } + else + { + // LoTW reports satellite credits with PROP_MODE=SAT/DXCC_SATELLITE. + // Keep normal DXCC credits from matching satellite QSOs by date/call/band. + where << "(prop_mode IS NULL OR upper(prop_mode) <> 'SAT')"; + } + + if ( !credit.dxcc.isEmpty() ) + where << "dxcc = :dxcc"; + + if ( !credit.awardEntity.isEmpty() ) + where << "my_dxcc = :award_entity"; + + switch ( callMatch ) + { + case EXACT_CALL_MATCH: + where << "callsign = upper(:call)"; + break; + + case PREFIX_CALL_MATCH: + where << "upper(callsign) LIKE upper(:call_prefix) ESCAPE '\\'"; + break; + + case NO_CALL_MATCH: + break; + } + + if ( matchMode ) + { + if ( !credit.dxccModeGroup.isEmpty() ) + { + QString modeCondition = "(SELECT dxcc FROM modes WHERE upper(name) = upper(contacts.mode) LIMIT 1) = :dxcc_mode_group"; + if ( !credit.mode.isEmpty() ) + modeCondition = "(" + modeCondition + " OR upper(mode) = upper(:mode))"; + where << modeCondition; + } + else if ( !credit.mode.isEmpty() ) + { + where << "upper(mode) = upper(:mode)"; + } + } + + QSqlQuery query; + const QString sql = QString("SELECT id, callsign, band, mode, start_time, prop_mode, credit_granted " + "FROM contacts WHERE %1 ORDER BY start_time, id").arg(where.join(" AND ")); + + if ( !query.prepare(sql) ) + { + error = query.lastError().text(); + return false; + } + + query.bindValue(":qso_date", credit.qsoDate.toString(Qt::ISODate)); + + if ( !credit.band.isEmpty() ) + query.bindValue(":band", credit.band); + + if ( !credit.propMode.isEmpty() ) + query.bindValue(":prop_mode", credit.propMode); + + if ( !credit.dxcc.isEmpty() ) + query.bindValue(":dxcc", credit.dxcc); + + if ( !credit.awardEntity.isEmpty() ) + query.bindValue(":award_entity", credit.awardEntity); + + switch ( callMatch ) + { + case EXACT_CALL_MATCH: + query.bindValue(":call", credit.call); + break; + + case PREFIX_CALL_MATCH: + query.bindValue(":call_prefix", escapeSqlLikePattern(credit.call) + "%"); + break; + + case NO_CALL_MATCH: + break; + } + + if ( matchMode ) + { + if ( !credit.dxccModeGroup.isEmpty() ) + query.bindValue(":dxcc_mode_group", credit.dxccModeGroup); + if ( !credit.mode.isEmpty() ) + query.bindValue(":mode", credit.mode); + } + + if ( !query.exec() ) + { + error = query.lastError().text(); + return false; + } + + while ( query.next() ) + { + DXCCCreditMatch match; + match.id = query.value(0).toULongLong(); + match.callsign = query.value(1).toString(); + match.band = query.value(2).toString(); + match.mode = query.value(3).toString(); + match.startTime = query.value(4).toDateTime(); + match.propMode = query.value(5).toString(); + match.creditGranted = query.value(6).toString(); + matches << match; + } + + return true; +} + +void LogFormat::runDXCCCreditImport() +{ + FCT_IDENTIFICATION; + + QSLMergeStat stats = {QStringList(), QStringList(), QStringList(), QStringList(), 0}; + + this->importStart(); + + while ( true ) + { + DXCCCreditRecord credit; + + if ( !this->importNextDXCCCredit(credit) ) break; + + stats.qsosDownloaded++; + + if ( stats.qsosDownloaded % 100 == 0 ) + emit importPosition(stream.pos()); + + credit.dxccModeGroup = LotwDXCCCreditDownloader::dxccModeGroupFromLotw(credit.dxccModeGroup); + + if ( credit.propMode.isEmpty() && isSatelliteDXCCCredit(credit) ) + credit.propMode = "SAT"; + + const bool callIsDXCCEntityCode = isDXCCEntityCode(credit.call); + + if ( credit.dxcc.isEmpty() && callIsDXCCEntityCode ) + credit.dxcc = credit.call; + + QStringList validationErrors; + + if ( !credit.qsoDate.isValid() ) validationErrors << tr("missing QSO_DATE"); + if ( credit.creditGranted.isEmpty() ) validationErrors << tr("missing CREDIT_GRANTED"); + if ( credit.call.isEmpty() && credit.dxcc.isEmpty() ) validationErrors << tr("missing CALL/DXCC"); + + if ( !validationErrors.isEmpty() ) + { + stats.errorQSLs.append(formatDXCCCreditReport(credit, validationErrors)); + continue; + } + + const bool hasExactCall = !credit.call.isEmpty() && !callIsDXCCEntityCode; + const bool canUseDXCCOnlyMatch = !hasExactCall; + const bool hasMode = !credit.dxccModeGroup.isEmpty() || !credit.mode.isEmpty(); + QList matches; + QString sqlError; + + if ( hasExactCall ) + { + if ( !selectDXCCCreditMatches(credit, EXACT_CALL_MATCH, hasMode, matches, sqlError) ) + { + stats.errorQSLs.append(formatDXCCCreditReport(credit, {sqlError})); + continue; + } + + if ( matches.isEmpty() && hasMode ) + { + if ( !selectDXCCCreditMatches(credit, EXACT_CALL_MATCH, false, matches, sqlError) ) + { + stats.errorQSLs.append(formatDXCCCreditReport(credit, {sqlError})); + continue; + } + } + + if ( matches.isEmpty() ) + { + if ( !selectDXCCCreditMatches(credit, PREFIX_CALL_MATCH, hasMode, matches, sqlError) ) + { + stats.errorQSLs.append(formatDXCCCreditReport(credit, {sqlError})); + continue; + } + + if ( matches.isEmpty() && hasMode ) + { + if ( !selectDXCCCreditMatches(credit, PREFIX_CALL_MATCH, false, matches, sqlError) ) + { + stats.errorQSLs.append(formatDXCCCreditReport(credit, {sqlError})); + continue; + } + } + } + } + + if ( matches.isEmpty() && !credit.dxcc.isEmpty() && canUseDXCCOnlyMatch ) + { + if ( !selectDXCCCreditMatches(credit, NO_CALL_MATCH, hasMode, matches, sqlError) ) + { + stats.errorQSLs.append(formatDXCCCreditReport(credit, {sqlError})); + continue; + } + + if ( matches.isEmpty() && hasMode ) + { + if ( !selectDXCCCreditMatches(credit, NO_CALL_MATCH, false, matches, sqlError) ) + { + stats.errorQSLs.append(formatDXCCCreditReport(credit, {sqlError})); + continue; + } + } + } + + if ( matches.isEmpty() ) + { + stats.unmatchedQSLs.append(formatDXCCCreditReport(credit, {tr("no matching QSO")})); + continue; + } + + QSqlQuery updateQuery; + if ( !updateQuery.prepare("UPDATE contacts SET credit_granted = :credit_granted WHERE id = :id") ) + { + stats.errorQSLs.append(formatDXCCCreditReport(credit, {updateQuery.lastError().text()})); + continue; + } + + for ( const DXCCCreditMatch &match : static_cast&>(matches) ) + { + const QString mergedCredits = mergeCreditValues(match.creditGranted, credit.creditGranted); + const QString normalizedCurrentCredits = splitCreditValues(match.creditGranted).join(','); + + if ( mergedCredits == normalizedCurrentCredits ) + continue; + + updateQuery.bindValue(":credit_granted", mergedCredits); + updateQuery.bindValue(":id", match.id); + + if ( !updateQuery.exec() ) + { + stats.errorQSLs.append(formatDXCCCreditReport(credit, + {tr("cannot update QSO %1: %2") + .arg(match.id) + .arg(updateQuery.lastError().text())})); + continue; + } + + QStringList details; + if ( matches.size() > 1 && match.startTime.isValid() ) + { + details << tr("matched QSO:") + " " + + match.startTime.toTimeZone(QTimeZone::utc()).toString("yyyy-MM-dd hh:mm:ss"); + } + details << tr("credit_granted:") + " " + credit.creditGranted; + + stats.updatedQSOs.append(formatDXCCCreditReport(credit, details)); + } + } + + emit importPosition(stream.pos()); + + this->importEnd(); + + emit QSLMergeFinished(stats); +} + void LogFormat::runQSLImport(QSLFrom fromService) { FCT_IDENTIFICATION; diff --git a/logformat/LogFormat.h b/logformat/LogFormat.h index bc3f19ce..8e98c735 100644 --- a/logformat/LogFormat.h +++ b/logformat/LogFormat.h @@ -57,6 +57,7 @@ class LogFormat : public QObject { unsigned long *warnings, unsigned long *errors); void runQSLImport(QSLFrom fromService); + void runDXCCCreditImport(); long runExport(); long runExport(const QList&); void setDefaults(QMap& defaults); @@ -89,10 +90,35 @@ class LogFormat : public QObject { void QSLMergeFinished(QSLMergeStat stats); protected: + struct DXCCCreditRecord + { + QString call; + QString band; + QString dxcc; + QString mode; + QString propMode; + QString dxccModeGroup; + QString creditGranted; + QString awardEntity; + QDate qsoDate; + }; + QTextStream& stream; QMap* defaults; + virtual bool importNextDXCCCredit(DXCCCreditRecord&) { return false; } private: + struct DXCCCreditMatch + { + qulonglong id = 0; + QString callsign; + QString band; + QString mode; + QDateTime startTime; + QString propMode; + QString creditGranted; + }; + enum ImportLogSeverity { INFO_SEVERITY, @@ -100,11 +126,32 @@ class LogFormat : public QObject { ERROR_SEVERITY }; + enum DXCCCreditCallMatch + { + NO_CALL_MATCH, + EXACT_CALL_MATCH, + PREFIX_CALL_MATCH + }; + bool isDateRange(); bool inDateRange(QDate date); QString importLogSeverityToString(ImportLogSeverity); + static QStringList splitCreditValues(const QString &value); + static QString mergeCreditValues(const QString ¤tValue, + const QString &newValue); + static bool isSatelliteDXCCCredit(const DXCCCreditRecord &credit); + static bool isDXCCEntityCode(const QString &call); + static QString escapeSqlLikePattern(const QString &value); + static QString formatDXCCCreditReport(const DXCCCreditRecord &credit, + const QStringList &addInfo = QStringList()); + static bool selectDXCCCreditMatches(const DXCCCreditRecord &credit, + const DXCCCreditCallMatch callMatch, + const bool matchMode, + QList &matches, + QString &error); + void writeImportLog(QTextStream& errorLogStream, ImportLogSeverity severity, const QString &msg); diff --git a/models/AlertTableModel.cpp b/models/AlertTableModel.cpp index 80d65fdd..030548af 100644 --- a/models/AlertTableModel.cpp +++ b/models/AlertTableModel.cpp @@ -41,6 +41,12 @@ QVariant AlertTableModel::data(const QModelIndex& index, int role) const { return Data::statusToColor(selectedRecord.alert.spot.status, selectedRecord.alert.spot.dupeCount, QColor(Qt::transparent)); } + else if ( index.column() == COLUMN_CALLSIGN && role == Qt::ForegroundRole ) + { + return Data::textColorForBackground(Data::statusToColor(selectedRecord.alert.spot.status, + selectedRecord.alert.spot.dupeCount, + QColor(Qt::transparent))); + } else if ( role == Qt::UserRole ) { switch ( index.column() ) diff --git a/models/DxccTableModel.cpp b/models/DxccTableModel.cpp index f481159b..5b060d1c 100644 --- a/models/DxccTableModel.cpp +++ b/models/DxccTableModel.cpp @@ -30,13 +30,16 @@ QVariant DxccTableModel::data(const QModelIndex &index, int role) const if ( (LogParam::getDxccConfirmedByLotwState() && containsL) || (LogParam::getDxccConfirmedByPaperState() && containsP) || (LogParam::getDxccConfirmedByEqslState() && containsE) ) - return Data::statusToColor(DxccStatus::NewMode, false, Qt::green); + return Data::statusToColor(DxccStatus::NewMode, false, Qt::transparent); if ( containsL || containsP ||containsE || currData.contains("W") ) return Data::statusToColor(DxccStatus::Worked, false, Qt::transparent); } break; + case Qt::ForegroundRole: + return Data::textColorForBackground(data(index, Qt::BackgroundRole).value()); + case Qt::DisplayRole: { const QString &currData = QSqlQueryModel::data(index, Qt::DisplayRole).toString(); diff --git a/models/LogbookModel.cpp b/models/LogbookModel.cpp index 6ecf01aa..b15abd90 100644 --- a/models/LogbookModel.cpp +++ b/models/LogbookModel.cpp @@ -6,6 +6,7 @@ #include "data/BandPlan.h" #include +#include LogbookModel::LogbookModel(QObject* parent, QSqlDatabase db) : QSqlTableModel(parent, db) @@ -18,11 +19,133 @@ LogbookModel::LogbookModel(QObject* parent, QSqlDatabase db) setHeaderData(it.key(), Qt::Horizontal, getFieldNameTranslation(it.key())); } +int LogbookModel::columnCount(const QModelIndex &parent) const +{ + const int realColumnCount = QSqlTableModel::columnCount(parent); + + if ( parent.isValid() ) + return realColumnCount; + + // QSqlTableModel can report zero columns before the first select. The + // logbook view still needs the full logical column set so column visibility + // UI can include the virtual Mode/Submode column consistently. + return qMax(realColumnCount, static_cast(COLUMN_LAST_ELEMENT)) + 1; +} + +QVariant LogbookModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if ( orientation == Qt::Horizontal + && section == COLUMN_MODE_SUBMODE + && role == Qt::DisplayRole ) + { + return tr("Mode/Submode"); + } + + return QSqlTableModel::headerData(section, orientation, role); +} + +Qt::ItemFlags LogbookModel::flags(const QModelIndex &index) const +{ + if ( !index.isValid() ) + return QSqlTableModel::flags(index); + + if ( index.column() == COLUMN_MODE_SUBMODE ) + return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable; + + return QSqlTableModel::flags(index); +} + +void LogbookModel::sort(int column, Qt::SortOrder order) +{ + // The merged Mode/Submode column is UI-only and has no matching DB field. + // Keep header sorting usable without generating an invalid SQL ORDER BY. + QSqlTableModel::sort(column == COLUMN_MODE_SUBMODE ? COLUMN_MODE : column, order); +} + +QVariant LogbookModel::modeSubmodeData(int row, int role) const +{ + const QString mode = QSqlTableModel::data(this->index(row, COLUMN_MODE), Qt::DisplayRole).toString(); + const QString submode = QSqlTableModel::data(this->index(row, COLUMN_SUBMODE), Qt::DisplayRole).toString(); + + if ( role == Qt::DisplayRole ) + return submode.isEmpty() ? mode : submode; + + if ( role == Qt::EditRole ) + { + // QTableQSOView reads EditRole after committing an editor and reuses it + // for group editing, so the virtual column must expose both real fields. + QVariantMap value; + value.insert("mode", mode); + value.insert("submode", submode); + return value; + } + + if ( role == Qt::ToolTipRole ) + return tr("Mode: %1\nSubmode: %2").arg(mode, submode); + + return QVariant(); +} + +bool LogbookModel::setModeSubmodeData(int row, const QString &newMode, const QString &newSubmode, int role) +{ + if ( !Data::instance()->isSubmodeForMode(newMode, newSubmode) ) + return false; + + const EditStrategy originalEditStrategy = editStrategy(); + const bool submitCombinedChange = originalEditStrategy == QSqlTableModel::OnFieldChange; + + // In the normal logbook table mode each QSqlTableModel::setData() is + // submitted immediately. The virtual Mode/Submode column maps to two real + // DB fields, so we temporarily collect both edits and submit them together. + if ( submitCombinedChange ) + setEditStrategy(QSqlTableModel::OnManualSubmit); + + // Always write both real fields. This clears a stale submode when the user + // changes from a submode-based mode, for example MFSK/FT4, to a plain mode. + bool updateResult = QSqlTableModel::setData(this->index(row, COLUMN_MODE), + newMode.isEmpty() ? QVariant() : QVariant(newMode), + role); + + if ( updateResult ) + updateResult = QSqlTableModel::setData(this->index(row, COLUMN_SUBMODE), + newSubmode.isEmpty() ? QVariant() : QVariant(newSubmode), + role); + + if ( submitCombinedChange ) + { + // submitAll() keeps beforeUpdate/contactUpdated semantics, but emits + // them for one row update containing both Mode and Submode values. + if ( updateResult ) + updateResult = submitAll(); + + // If either staged edit or the submit fails, discard the partial in-memory + // change before restoring OnFieldChange. + if ( !updateResult ) + revertRow(row); + + setEditStrategy(originalEditStrategy); + } + + if ( updateResult ) + emitModeSubmodeChanged(row); + + return updateResult; +} + +void LogbookModel::emitModeSubmodeChanged(int row) +{ + const QModelIndex modeSubmodeIndex = this->index(row, COLUMN_MODE_SUBMODE); + emit dataChanged(modeSubmodeIndex, modeSubmodeIndex, {Qt::DisplayRole, Qt::EditRole, Qt::ToolTipRole}); +} + QVariant LogbookModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); + if ( index.column() == COLUMN_MODE_SUBMODE ) + return modeSubmodeData(index.row(), role); + if (role == Qt::DecorationRole && index.column() == COLUMN_CALL) { const QString &flag = Data::instance()->dxccFlag(QSqlTableModel::data(this->index(index.row(), COLUMN_DXCC), Qt::DisplayRole).toInt()); @@ -139,6 +262,15 @@ bool LogbookModel::setData(const QModelIndex &index, const QVariant &value, int bool main_update_result = true; bool depend_update_result = true; + if ( role == Qt::EditRole && index.column() == COLUMN_MODE_SUBMODE ) + { + const QVariantMap modeSubmode = value.toMap(); + const QString newMode = modeSubmode.value("mode").toString(); + const QString newSubmode = modeSubmode.value("submode").toString(); + + return setModeSubmodeData(index.row(), newMode, newSubmode, role); + } + if ( role == Qt::EditRole ) { switch ( index.column() ) @@ -221,6 +353,25 @@ bool LogbookModel::setData(const QModelIndex &index, const QVariant &value, int break; } + case COLUMN_MODE: + { + const QString currentSubmode = QSqlTableModel::data(this->index(index.row(), COLUMN_SUBMODE), Qt::DisplayRole).toString(); + + // Mode defines the valid submode list. Direct mode edits must not + // leave a stale submode that belongs to the previous mode. + if ( !Data::instance()->isSubmodeForMode(value.toString(), currentSubmode) ) + depend_update_result = QSqlTableModel::setData(this->index(index.row(), COLUMN_SUBMODE), QVariant(), role); + + break; + } + + case COLUMN_SUBMODE: + { + const QString currentMode = QSqlTableModel::data(this->index(index.row(), COLUMN_MODE), Qt::DisplayRole).toString(); + depend_update_result = Data::instance()->isSubmodeForMode(currentMode, value.toString()); + break; + } + case COLUMN_GRID: { if ( ! value.toString().isEmpty() ) @@ -581,6 +732,12 @@ bool LogbookModel::setData(const QModelIndex &index, const QVariant &value, int } } + if ( main_update_result && depend_update_result + && (index.column() == COLUMN_MODE || index.column() == COLUMN_SUBMODE) ) + { + emitModeSubmodeChanged(index.row()); + } + return main_update_result && depend_update_result; } diff --git a/models/LogbookModel.h b/models/LogbookModel.h index 422f6edb..2e5ba273 100644 --- a/models/LogbookModel.h +++ b/models/LogbookModel.h @@ -11,7 +11,11 @@ class LogbookModel : public QSqlTableModel public: explicit LogbookModel(QObject* parent = nullptr, QSqlDatabase db = QSqlDatabase()); + int columnCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + void sort(int column, Qt::SortOrder order) override; bool setData(const QModelIndex &index, const QVariant &value, int role) override; void updateExternalServicesUploadStatus( const QModelIndex &index, int role, bool &updateResult ); void updateUploadToModified( const QModelIndex &index, int role, int column, bool &updateResult ); @@ -199,11 +203,19 @@ class LogbookModel : public QSqlTableModel COLUMN_QRZCOM_QSO_DOWNLOAD_STATUS = 178, COLUMN_QSLMSG_RCVD = 179, COLUMN_EQSL_AG = 180, - COLUMN_LAST_ELEMENT = 181 + COLUMN_LAST_ELEMENT = 181, + + // Virtual UI-only column. Keep it equal to COLUMN_LAST_ELEMENT so existing + // loops over real contacts fields (`i < COLUMN_LAST_ELEMENT`) do not treat + // it as an ADIF/DB field. + COLUMN_MODE_SUBMODE = COLUMN_LAST_ELEMENT }; private: static QMap fieldNameTranslationMap; + QVariant modeSubmodeData(int row, int role) const; + bool setModeSubmodeData(int row, const QString &newMode, const QString &newSubmode, int role); + void emitModeSubmodeChanged(int row); public: static const QString getFieldNameTranslation(const LogbookModel::ColumnID key) diff --git a/models/WsjtxTableModel.cpp b/models/WsjtxTableModel.cpp index cb50275a..8d23b122 100644 --- a/models/WsjtxTableModel.cpp +++ b/models/WsjtxTableModel.cpp @@ -39,6 +39,12 @@ QVariant WsjtxTableModel::data(const QModelIndex& index, int role) const { return Data::statusToColor(entry.status, entry.dupeCount, QColor(Qt::transparent)); } + else if (index.column() == COLUMN_CALLSIGN && role == Qt::ForegroundRole) + { + return Data::textColorForBackground(Data::statusToColor(entry.status, + entry.dupeCount, + QColor(Qt::transparent))); + } else if (index.column() > COLUMN_CALLSIGN && role == Qt::BackgroundRole) { if ( entry.receivedTime.secsTo(QDateTime::currentDateTimeUtc()) >= spotPeriod * 0.8) @@ -47,10 +53,6 @@ QVariant WsjtxTableModel::data(const QModelIndex& index, int role) const return QColor(Qt::darkGray); } } - else if (index.column() == COLUMN_CALLSIGN && role == Qt::ForegroundRole) - { - //return Data::statusToInverseColor(entry.status, QColor(Qt::black)); - } else if (index.column() == COLUMN_CALLSIGN && role == Qt::ToolTipRole) { return QCoreApplication::translate("DBStrings", entry.dxcc.country.toUtf8().constData()) + " [" + Data::statusToText(entry.status) + "]"; @@ -184,3 +186,17 @@ void WsjtxTableModel::removeSpot(const QString &callsign) endResetModel(); } +void WsjtxTableModel::refreshStatusColors() +{ + if ( wsjtxData.isEmpty() ) + return; + + emit dataChanged(createIndex(0, COLUMN_CALLSIGN), + createIndex(wsjtxData.size() - 1, COLUMN_CALLSIGN), + {Qt::BackgroundRole, Qt::ForegroundRole}); +} + +QList WsjtxTableModel::entries() const +{ + return wsjtxData; +} diff --git a/models/WsjtxTableModel.h b/models/WsjtxTableModel.h index e25d7c9e..6ab59528 100644 --- a/models/WsjtxTableModel.h +++ b/models/WsjtxTableModel.h @@ -32,6 +32,8 @@ class WsjtxTableModel : public QAbstractTableModel { void setCurrentSpotPeriod(float); void clear(); void removeSpot(const QString &callsign); + void refreshStatusColors(); + QList entries() const; private: QList wsjtxData; diff --git a/res/macos/Info.plist b/res/macos/Info.plist new file mode 100644 index 00000000..31b854a6 --- /dev/null +++ b/res/macos/Info.plist @@ -0,0 +1,50 @@ + + + + + CFBundleAllowMixedLocalizations + + CFBundleDevelopmentRegion + en + CFBundleExecutable + @EXECUTABLE@ + CFBundleIconFile + @ICON@ + CFBundleIdentifier + io.github.foldynl.qlog + CFBundleName + QLog + CFBundleDisplayName + QLog + CFBundlePackageType + APPL + CFBundleSignature + ???? + CFBundleShortVersionString + @SHORT_VERSION@ + CFBundleVersion + @FULL_VERSION@ + LSMinimumSystemVersion + 14.0 + LSApplicationCategoryType + public.app-category.utilities + NSHumanReadableCopyright + Copyright © OK1MLG. Licensed under the GNU GPL v3. + NSPrincipalClass + NSApplication + NSSupportsAutomaticGraphicsSwitching + + NSHighResolutionCapable + + + + NSLocalNetworkUsageDescription + QLog needs local network access to communicate with WSJT-X, network-attached rigs, rotators, and other amateur-radio software running on devices on your network. + + diff --git a/res/map/onlinemap.html b/res/map/onlinemap.html index a69bf98f..f40dc49d 100644 --- a/res/map/onlinemap.html +++ b/res/map/onlinemap.html @@ -8,7 +8,7 @@ - + @@ -16,10 +16,37 @@ body { padding: 0; margin: 0; + background: #f3f4f6; } html, body, #map { height: 100%; } + body.qlog-map-dark { + background: #454d55; + } + body.qlog-map-dark .leaflet-control-layers, + body.qlog-map-dark .leaflet-bar a, + body.qlog-map-dark .leaflet-control-attribution { + background: rgba(70, 78, 87, 0.92); + color: #f3f5f7; + border-color: rgba(148, 156, 166, 0.58); + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.18); + } + body.qlog-map-dark .leaflet-bar a { + border-bottom-color: rgba(148, 156, 166, 0.58); + } + body.qlog-map-dark .leaflet-bar a:hover, + body.qlog-map-dark .leaflet-control-layers-expanded { + background: rgba(82, 91, 101, 0.96); + } + body.qlog-map-dark .leaflet-control-attribution a { + color: #d6dbe0; + } + body.qlog-map-dark .leaflet-popup-content-wrapper, + body.qlog-map-dark .leaflet-popup-tip { + background: #4c5560; + color: #eceff2; + } .leaflet-tooltip { border: 0px; color: #232D45; @@ -71,6 +98,9 @@ z-index: 9999; } .busy.hidden{ display:none; } + body.qlog-map-dark .busy { + background: rgba(62, 70, 79, 0.46); + } .spinner{ width: 42px; @@ -80,9 +110,14 @@ border-radius: 50%; animation: spin 0.9s linear infinite; } + body.qlog-map-dark .spinner { + border-color: rgba(255,255,255,0.18); + border-top-color: rgba(255,255,255,0.72); + } @keyframes spin { to { transform: rotate(360deg); } } .label{ margin-top: 10px; font: 14px sans-serif; color:#333; } + body.qlog-map-dark .label{ color:#d7dce2; } @@ -105,6 +140,80 @@ var staticMapTime = null; var map = L.map('map', {center: [0,0], zoom: 2, minZoom: 1}); + var layerControl = null; + var mapBridge = null; + const qlogLayerStateKeys = new Map(); + let qlogLayerStateBridgeConnected = false; + + function mapLayerByKey(key) { + return window[key] || null; + } + + function sendToQt(method) { + if (!window.mapBridge || typeof window.mapBridge[method] !== 'function') + return; + + window.mapBridge[method].apply(window.mapBridge, Array.prototype.slice.call(arguments, 1)); + } + + function setQLogLayerVisibility(key, visible) { + const layer = mapLayerByKey(key); + if (!layer) + return; + + if (visible) + map.addLayer(layer); + else + map.removeLayer(layer); + } + + function restoreQLogLayerStates(states) { + states.forEach(function(state) { + setQLogLayerVisibility(state.key, state.visible); + }); + } + + function configureLayerControl(layers) { + const overlays = {}; + qlogLayerStateKeys.clear(); + + layers.forEach(function(layerSpec) { + const layer = mapLayerByKey(layerSpec.key); + if (!layer) + return; + + overlays[layerSpec.label] = layer; + qlogLayerStateKeys.set(layer, layerSpec.key); + }); + + if (layerControl) + map.removeControl(layerControl); + + layerControl = new L.Control.Layers(null, overlays, {}).addTo(map); + } + + function handleQLogLayerStateChanged(layer, state) { + const key = qlogLayerStateKeys.get(layer); + if (!key) + return; + + sendToQt('handleLayerSelectionChanged', key, state); + } + + function connectQtMapBridge(handler) { + window.mapBridge = handler; + + if (qlogLayerStateBridgeConnected) + return; + + map.on('overlayadd', function(e) { + handleQLogLayerStateChanged(e.layer, 'on'); + }); + map.on('overlayremove', function(e) { + handleQLogLayerStateChanged(e.layer, 'off'); + }); + qlogLayerStateBridgeConnected = true; + } map.createPane('lowestPane'); map.getPane('lowestPane').style.zIndex = 200; @@ -156,6 +265,254 @@ popupAnchor: [1, -14] }); + const mapIcons = { + greenIcon: greenIcon, + greenIconSmall: greenIconSmall, + yellowIcon: yellowIcon, + yellowIconSmall: yellowIconSmall, + homeIcon: homeIcon + }; + + const fastMarkerThreshold = 2000; + var fastMarkersLayer = null; + const fastMarkerImages = {}; + + function resolveIcon(icon) { + return mapIcons[icon] || null; + } + + function normalizePoint(point) { + if (Array.isArray(point)) { + return { + label: point[0], + lat: point[1], + lng: point[2], + icon: point[3] + }; + } + + return point; + } + + function markerOptions(point) { + const icon = resolveIcon(point.icon); + return icon ? {icon: icon} : {}; + } + + function fastMarkerImage(url, onLoad) { + if (!url) + return null; + + if (fastMarkerImages[url]) { + if (onLoad && !fastMarkerImages[url].loaded) + fastMarkerImages[url].callbacks.push(onLoad); + return fastMarkerImages[url]; + } + + const image = new Image(); + fastMarkerImages[url] = { image: image, loaded: false, callbacks: onLoad ? [onLoad] : [] }; + image.onload = function() { + const entry = fastMarkerImages[url]; + entry.loaded = true; + const callbacks = entry.callbacks; + entry.callbacks = []; + callbacks.forEach(callback => callback()); + }; + image.src = url; + + return fastMarkerImages[url]; + } + + function fastMarkerPoint(point) { + const mapPoint = normalizePoint(point); + const icon = resolveIcon(mapPoint.icon); + if (!icon) + return mapPoint; + + const iconOptions = icon.options || {}; + const iconSize = iconOptions.iconSize || [25, 41]; + mapPoint.iconOptions = iconOptions; + mapPoint.iconSize = iconSize; + mapPoint.iconAnchor = iconOptions.iconAnchor || [Math.floor(iconSize[0] / 2), iconSize[1]]; + + return mapPoint; + } + + const FastMarkerLayer = L.Layer.extend({ + initialize: function(points) { + this.points = points.map(fastMarkerPoint); + this.pendingImageUrls = {}; + }, + + onAdd: function(map) { + this.map = map; + this.shadowCanvas = L.DomUtil.create('canvas', 'qlog-fast-marker-shadows'); + this.iconCanvas = L.DomUtil.create('canvas', 'qlog-fast-markers'); + this.shadowCanvas.style.pointerEvents = 'none'; + this.iconCanvas.style.pointerEvents = 'none'; + map.getPane('shadowPane').appendChild(this.shadowCanvas); + map.getPane('markerPane').appendChild(this.iconCanvas); + map.on('moveend zoomend resize', this.reset, this); + map.on('click', this.handleClick, this); + this.reset(); + }, + + onRemove: function(map) { + map.off('moveend zoomend resize', this.reset, this); + map.off('click', this.handleClick, this); + L.DomUtil.remove(this.shadowCanvas); + L.DomUtil.remove(this.iconCanvas); + this.shadowCanvas = null; + this.iconCanvas = null; + this.map = null; + }, + + reset: function() { + if (!this.map || !this.shadowCanvas || !this.iconCanvas) + return; + + const size = this.map.getSize(); + const topLeft = this.map.containerPointToLayerPoint([0, 0]); + [this.shadowCanvas, this.iconCanvas].forEach(canvas => { + L.DomUtil.setPosition(canvas, topLeft); + canvas.width = size.x; + canvas.height = size.y; + }); + this.draw(); + }, + + drawMarkerImage: function(ctx, iconOptions, markerPoint, imageKey, sizeKey, anchorKey, drawnKeys) { + const url = iconOptions[imageKey]; + const size = iconOptions[sizeKey]; + const anchor = iconOptions[anchorKey] || iconOptions.iconAnchor; + if (!url || !size || !anchor) + return; + + const imageEntry = fastMarkerImage(url, this.pendingImageUrls[url] ? null : () => { + delete this.pendingImageUrls[url]; + this.draw(); + }); + if (!imageEntry || !imageEntry.loaded) { + this.pendingImageUrls[url] = true; + return; + } + + const x = markerPoint.x - anchor[0]; + const y = markerPoint.y - anchor[1]; + + if (drawnKeys) { + const key = [url, size[0], size[1], Math.round(x), Math.round(y)].join('|'); + if (drawnKeys[key]) + return; + drawnKeys[key] = true; + } + + ctx.drawImage(imageEntry.image, x, y, size[0], size[1]); + }, + + visiblePoints: function() { + const size = this.map.getSize(); + const visible = []; + + this.points.forEach(point => { + if (!point.iconOptions) + return; + + const markerPoint = this.map.latLngToContainerPoint([point.lat, point.lng]); + if (markerPoint.x < -point.iconSize[0] + || markerPoint.y < -point.iconSize[1] + || markerPoint.x > size.x + point.iconSize[0] + || markerPoint.y > size.y + point.iconSize[1]) + return; + + visible.push({ + point: point, + markerPoint: markerPoint + }); + }); + + return visible; + }, + + draw: function() { + if (!this.map || !this.shadowCanvas || !this.iconCanvas) + return; + + const shadowCtx = this.shadowCanvas.getContext('2d'); + const iconCtx = this.iconCanvas.getContext('2d'); + const size = this.map.getSize(); + shadowCtx.clearRect(0, 0, size.x, size.y); + iconCtx.clearRect(0, 0, size.x, size.y); + const visible = this.visiblePoints(); + const drawnShadows = {}; + + visible.forEach(item => { + this.drawMarkerImage(shadowCtx, item.point.iconOptions, item.markerPoint, + 'shadowUrl', 'shadowSize', 'shadowAnchor', drawnShadows); + }); + + visible.forEach(item => { + this.drawMarkerImage(iconCtx, item.point.iconOptions, item.markerPoint, + 'iconUrl', 'iconSize', 'iconAnchor'); + }); + }, + + handleClick: function(event) { + const clickPoint = this.map.latLngToContainerPoint(event.latlng); + let bestPoint = null; + let bestDistance = Number.MAX_VALUE; + + this.points.forEach(point => { + if (!point.iconOptions) + return; + + const markerPoint = this.map.latLngToContainerPoint([point.lat, point.lng]); + const left = markerPoint.x - point.iconAnchor[0]; + const top = markerPoint.y - point.iconAnchor[1]; + const right = left + point.iconSize[0]; + const bottom = top + point.iconSize[1]; + + if (clickPoint.x < left || clickPoint.x > right + || clickPoint.y < top || clickPoint.y > bottom) + return; + + const dx = markerPoint.x - clickPoint.x; + const dy = markerPoint.y - clickPoint.y; + const distance = dx * dx + dy * dy; + if (distance < bestDistance) { + bestDistance = distance; + bestPoint = point; + } + }); + + if (bestPoint) { + L.popup() + .setLatLng([bestPoint.lat, bestPoint.lng]) + .setContent(bestPoint.label) + .openOn(this.map); + } + } + }); + + function clearFastMarkerLayer() { + if (!fastMarkersLayer) + return; + + map.removeLayer(fastMarkersLayer); + fastMarkersLayer = null; + } + + function normalizePath(path) { + if (Array.isArray(path)) { + return { + from: {lat: path[0], lng: path[1]}, + to: {lat: path[2], lng: path[3]} + }; + } + + return path; + } + var language = navigator.language || navigator.userLanguage; var mapUrl; @@ -174,7 +531,41 @@ }).addTo(map); var maidenheadConfWorked = L.maidenheadConfWorked({color : 'rgba(255, 0, 0, 0.5)'}).addTo(map); - var grayline = L.terminator({fillOpacity: 0.2}) + var grayline = L.terminator() + + function setTerminatorTheme(isDark) { + const styles = isDark + ? { + civil: { fillColor: '#c8c8c8', fillOpacity: 0.13, stroke: true, color: '#d6d6d6', opacity: 0.26, weight: 1 }, + nautical: { fillColor: '#aeaeae', fillOpacity: 0.16, stroke: true, color: '#c2c2c2', opacity: 0.24, weight: 1 }, + astronomical: { fillColor: '#949494', fillOpacity: 0.19, stroke: true, color: '#adadad', opacity: 0.22, weight: 1 } + } + : { + civil: { fillColor: '#767676', fillOpacity: 0.14, stroke: true, color: '#5f5f5f', opacity: 0.18, weight: 1 }, + nautical: { fillColor: '#626262', fillOpacity: 0.17, stroke: true, color: '#555', opacity: 0.16, weight: 1 }, + astronomical: { fillColor: '#4e4e4e', fillOpacity: 0.20, stroke: true, color: '#484848', opacity: 0.14, weight: 1 } + }; + + Object.keys(styles).forEach(function(name) { + const terminator = grayline.getTerminator(name); + if (terminator) + terminator.setStyle(styles[name]); + }); + } + + function setMapDarkMode(isDark) { + document.body.classList.toggle('qlog-map-dark', isDark); + + const tileFilter = isDark + ? 'invert(1) hue-rotate(180deg) brightness(1.42) contrast(0.70) saturate(0.62)' + : ''; + map.getPanes().tilePane.style.filter = tileFilter; + map.getPanes().tilePane.style.webkitFilter = tileFilter; + + setTerminatorTheme(isDark); + } + + setMapDarkMode(false); setInterval(function(){updateTerminator(grayline)}, 60000); @@ -199,16 +590,20 @@ } async function runBusy(msg, fn) { + if (!msg) { + fn(); + return; + } + const token = ++busyToken; busyShow(msg); - await new Promise(requestAnimationFrame); await new Promise(requestAnimationFrame); fn(); - await new Promise(requestAnimationFrame); - await new Promise(requestAnimationFrame); - if (token === busyToken) busyHide(); + setTimeout(function() { + if (token === busyToken) busyHide(); + }, 0); } // Configuration for Aurora Heatmap @@ -235,7 +630,9 @@ // Path 1 - usually used for rendering Path between my station and QSO station var pathLayer = L.layerGroup().addTo(map); const geodesic = new L.Geodesic([],{ wrap: false, color: "Fuchsia", steps: 5}); + const shortPathsGeodesic = new L.Geodesic([],{ wrap: false, color: "#cc2e41", steps: 5, opacity: 0.7, weight: 1}); pathLayer.addLayer(geodesic); + pathLayer.addLayer(shortPathsGeodesic); var antPathLayer = L.layerGroup().addTo(map); var antGeodesic1 = new L.Geodesic([],{ wrap: false, color: "orange", steps: 5, @@ -247,6 +644,7 @@ function drawPath(points) { geodesic.setLatLngs([]); + shortPathsGeodesic.setLatLngs([]); if (points.length != 2) return; @@ -255,18 +653,26 @@ } function drawShortPaths(coords) { - pathLayer.clearLayers(); - coords.forEach(function(coord) { - let geodesic = new L.Geodesic([],{ wrap: false, color: "#cc2e41", steps: 5, opacity: 0.7, weight: 1}); - pathLayer.addLayer(geodesic); - geodesic.setLatLngs([{lat: coord[0], lng: coord[1]}, {lat: coord[2], lng: coord[3]}]); + const paths = []; + geodesic.setLatLngs([]); + coords.forEach(function(rawPath) { + const path = normalizePath(rawPath); + paths.push([path.from, path.to]); }); + shortPathsGeodesic.setLatLngs(paths); } function drawShortPathsBusy(coords, text) { return runBusy(text, () => drawShortPaths(coords)); } + function drawPointsAndShortPathsBusy(points, coords, text) { + return runBusy(text, function() { + drawPoints(points); + drawShortPaths(coords); + }); + } + // function computes a destination point based on distance and azimuth function beamEndPosition(fromPoint, distance, azimuth) { const M_PI = Math.PI; @@ -308,15 +714,27 @@ var markersLayer = L.layerGroup().addTo(map); + function drawPointMarkers(layer, points) { + layer.clearLayers(); + points.forEach(function(point) { + const mapPoint = normalizePoint(point); + let marker = L.marker([mapPoint.lat, mapPoint.lng], markerOptions(mapPoint)) + .bindPopup(mapPoint.label); + layer.addLayer(marker); + }); + } + // generic function to render point on the map function drawPoints(points) { markersLayer.clearLayers(); - points.forEach(function(point) { - let marker = L.marker([point[1], point[2]], { - icon: point[3]}) - .bindPopup(point[0]); - markersLayer.addLayer(marker); - }); + clearFastMarkerLayer(); + + if (points.length > fastMarkerThreshold) { + fastMarkersLayer = new FastMarkerLayer(points).addTo(map); + return; + } + + drawPointMarkers(markersLayer, points); } function drawPointsBusy(points, text) { @@ -325,7 +743,8 @@ // generic function for flying to the point function flyToPoint(point, zoom) { - map.flyTo([point[1], point[2]],zoom); + const mapPoint = normalizePoint(point); + map.flyTo([mapPoint.lat, mapPoint.lng], zoom); } var markersLayer2 = L.layerGroup().addTo(map); @@ -333,13 +752,7 @@ // generic function to render the sencond layer of the points // It is usually used to render My QTHs function drawPointsGroup2(points) { - markersLayer2.clearLayers(); - points.forEach(function(point) { - let marker = L.marker([point[1], point[2]], { - icon: point[3]}) - .bindPopup(point[0]); - markersLayer2.addLayer(marker); - }); + drawPointMarkers(markersLayer2, points); } // compute 3-color gradient @@ -400,13 +813,14 @@ mufLayer.clearLayers(); points.forEach(function(point) { - let marker = new L.marker([point[1], point[2]], {opacity: 0.001 }); - marker.bindTooltip(point[0], {permanent: true, direction : 'bottom', offset: [-16, 8], className: 'muf-tooltip' }); + const mapPoint = normalizePoint(point); + let marker = new L.marker([mapPoint.lat, mapPoint.lng], {opacity: 0.001 }); + marker.bindTooltip(mapPoint.label, {permanent: true, direction : 'bottom', offset: [-16, 8], className: 'muf-tooltip' }); // color gradient is calculated for number interval 0 - 1. // MUF value is a fgrequency. Threfore is it needed to recalculate Freq to interval 0 - 1 // QLog currently recalculate the interval 0 - 50 MHz. This is due to the fact that // most of the values move in this interval and it is easy to see the individual frequency ranges - const oldValue = parseFloat(point[0]); + const oldValue = parseFloat(mapPoint.label); const oldMin = 0; const oldMax = 50; //frequency input range (0 - 50MHz) const newMin = 0; const newMax = 1; //mapped to range 0 - 1 const newValue = (oldValue - oldMin) / (oldMax - oldMin) * (newMax - newMin) + newMin; @@ -417,44 +831,36 @@ }); } - // Definition for IBP - const band = [{band_name :"20m", freq : 14.1}, - {band_name :"17m", freq : 18.11}, - {band_name :"15m", freq : 21.15}, - {band_name :"12m", freq : 24.93}, - {band_name :"10m", freq : 28.2}, - ]; - - const beacons = [{name: '4U1UN', lat: 40.7501, lon: -73.9682, active: true}, - {name: 'VE8AT', lat: 79.9949, lon: -85.8451, active: true}, - {name: 'W6WX', lat: 37.1599, lon: -121.9083, active: true}, - {name: 'KH6RS', lat: 20.7652, lon: -156.3502, active: true}, - {name: 'ZL6B', lat: -41.04350, lon: 175.5952, active: false}, - {name: 'VK6RBP',lat: -32.1093, lon: 116.0712, active: true}, - {name: 'JA2IGY',lat: 34.4613, lon: 136.7818, active: true}, - {name: 'RR9O', lat: 55.0484, lon: 82.9227, active: true}, - {name: 'VR2B', lat: 22.2705, lon: 114.1507, active: true}, - {name: '4S7B', lat: 6.8915, lon: 79.8559, active: true}, - {name: 'ZS6DN', lat: -26.6531, lon: 27.9474, active: true}, - {name: '5Z4B', lat: -1.2687, lon: 36.8094, active: false}, - {name: '4X6TU', lat: 32.0622, lon: 34.8069, active: true}, - {name: 'OH2B', lat: 60.2920, lon: 24.3942, active: true}, - {name: 'CS3B', lat: 32.8217, lon: -17.2325, active: true}, - {name: 'LU4AA', lat: -34.6439, lon: -58.4138, active: true}, - {name: 'OA4B', lat: -12.0940, lon: -77.0165, active: true}, - {name: 'YV5B', lat: 9.0964, lon: -67.8239, active: true}] - var currentBand=""; // to control which band is displayed - QLog core sets it. + var ibpBands = []; + var ibpBeacons = []; + var ibpCurrentBand = ""; var IBPLayer = L.layerGroup().addTo(map); var prevStationIndex = -1; + function configureIbpData(bands, beacons) { + ibpBands = Array.isArray(bands) ? bands : []; + ibpBeacons = Array.isArray(beacons) ? beacons : []; + prevStationIndex = -1; + updateBeacon(); + } + + function setIbpCurrentBand(bandName) { + ibpCurrentBand = bandName || ""; + prevStationIndex = -1; + updateBeacon(); + } + function IBPCallsignPressed(e) { - let currentIndex = band.findIndex(object => {return object.band_name === currentBand;}); - foo.IBPCallsignClicked(this.callsign, band[currentIndex].freq); + const currentBand = ibpBands.find(object => {return object.name === ibpCurrentBand;}); + if (!currentBand) + return; + + sendToQt('IBPCallsignClicked', this.callsign, currentBand.frequency); } - // render IPB Beacon Point + // render IBP Beacon Point function beaconPoint(beacon, color) { - let pointColor = color + let pointColor = color; if ( !beacon.active ) { pointColor = 'red'; } @@ -465,32 +871,32 @@ mirrorLon = beacon.lon + 360; } let markerMirror = new L.marker([beacon.lat, mirrorLon], {opacity: 0.001 }); - marker.bindTooltip(beacon.name, {permanent: true, direction : 'bottom', offset: [-16, 8], className: 'muf-tooltip' }); + marker.bindTooltip(beacon.callsign, {permanent: true, direction : 'bottom', offset: [-16, 8], className: 'muf-tooltip' }); marker.on('dblclick', IBPCallsignPressed); - marker.callsign = beacon.name; - markerMirror.bindTooltip(beacon.name, {permanent: true, direction : 'bottom', offset: [-16, 8], className: 'muf-tooltip' }); + marker.callsign = beacon.callsign; + markerMirror.bindTooltip(beacon.callsign, {permanent: true, direction : 'bottom', offset: [-16, 8], className: 'muf-tooltip' }); markerMirror.on('dblclick', IBPCallsignPressed); - markerMirror.callsign = beacon.name; + markerMirror.callsign = beacon.callsign; marker.getTooltip().setContent(`
${marker.getTooltip().getContent()}
`); markerMirror.getTooltip().setContent(`
${markerMirror.getTooltip().getContent()}
`); IBPLayer.addLayer(marker); IBPLayer.addLayer(markerMirror); } - // Update IPB Beacon Points + // Update IBP Beacon Points function updateBeacon() { - currentBandIndex = band.findIndex(object => {return object.band_name === currentBand;}); + const currentBandIndex = ibpBands.findIndex(object => {return object.name === ibpCurrentBand;}); - if ( currentBandIndex == -1 ) { + if ( currentBandIndex == -1 || ibpBeacons.length == 0 ) { IBPLayer.clearLayers(); return; } let today = new Date(); - let n = Math.floor(today.getTime()/10000) % 18; - let currentStationIndex = (18 + n - currentBandIndex) % 18; - let nextStationIndex = (19 + n - currentBandIndex) % 18; + let n = Math.floor(today.getTime()/10000) % ibpBeacons.length; + let currentStationIndex = (ibpBeacons.length + n - currentBandIndex) % ibpBeacons.length; + let nextStationIndex = (ibpBeacons.length + 1 + n - currentBandIndex) % ibpBeacons.length; if ( currentStationIndex == prevStationIndex ) { return; @@ -501,18 +907,18 @@ prevStationIndex = currentStationIndex; // show current - let b = beacons[currentStationIndex] + let b = ibpBeacons[currentStationIndex]; beaconPoint(b, '#FFFF33'); // show next - b = beacons[nextStationIndex] + b = ibpBeacons[nextStationIndex]; beaconPoint(b, '#1AA260'); } var chatStationsLayer = L.layerGroup().addTo(map); function chatCallsignPressed(e) { - foo.chatCallsignClicked(this.callsign); + sendToQt('chatCallsignClicked', this.callsign); } // generic function to render the third layer of the points @@ -520,10 +926,11 @@ function drawPointsGroup3(points) { chatStationsLayer.clearLayers(); points.forEach(function(point) { - let marker = new L.marker([point[1], point[2]], {opacity: 0.001 }); - marker.bindTooltip(point[0], {pane: 'lowestPane', permanent: true, direction : 'bottom', offset: [-16, 8], className: 'chat-tooltip' }); + const mapPoint = normalizePoint(point); + let marker = new L.marker([mapPoint.lat, mapPoint.lng], {opacity: 0.001 }); + marker.bindTooltip(mapPoint.label, {pane: 'lowestPane', permanent: true, direction : 'bottom', offset: [-16, 8], className: 'chat-tooltip' }); marker.on('dblclick', chatCallsignPressed); - marker.callsign = point[0]; + marker.callsign = mapPoint.label; marker.getTooltip().setContent(`
${marker.getTooltip().getContent()}
`); chatStationsLayer.addLayer(marker); }); @@ -532,26 +939,27 @@ setInterval(function(){updateBeacon()}, 250); function wsjtxCallsignPressed(e) { - foo.wsjtxCallsignClicked(this.callsign); + sendToQt('wsjtxCallsignClicked', this.callsign); } var wsjtxStationsLayer = L.layerGroup(); // this function show WSJTX spots - function addWSJTXSpot(lat, lng, callsign, color) { + function addWSJTXSpot(point, color, textColor) { + const spot = normalizePoint(point); const fadeSchedule = [ { timeout: 30000, className: 'fade-out75' }, { timeout: 45000, className: 'fade-out50' }, ]; const tooltipHTML = content => - `
${content}
`; + `
${content}
`; function createSpotMarker(latitude, longitude) { const marker = new L.marker([latitude, longitude], { opacity: 0.001 }); - marker.callsign = callsign; + marker.callsign = spot.label; - marker.bindTooltip(callsign, { + marker.bindTooltip(spot.label, { pane: 'lowestPane', permanent: true, direction: 'bottom', @@ -568,11 +976,11 @@ } // the main marker - const marker = createSpotMarker(lat, lng); + const marker = createSpotMarker(spot.lat, spot.lng); // mirror marker - const mirrorLon = lng >= 0 ? lng - 360 : lng + 360; - const markerMirror = createSpotMarker(lat, mirrorLon); + const mirrorLon = spot.lng >= 0 ? spot.lng - 360 : spot.lng + 360; + const markerMirror = createSpotMarker(spot.lat, mirrorLon); wsjtxStationsLayer.addLayer(marker); wsjtxStationsLayer.addLayer(markerMirror); diff --git a/res/res.qrc b/res/res.qrc index eb5f8db0..c4b53a44 100644 --- a/res/res.qrc +++ b/res/res.qrc @@ -51,5 +51,7 @@ sql/migration_036.sql sql/migration_037.sql sql/migration_038.sql + sql/migration_039.sql + sql/migration_040.sql diff --git a/res/sql/migration_039.sql b/res/sql/migration_039.sql new file mode 100644 index 00000000..db8e9d06 --- /dev/null +++ b/res/sql/migration_039.sql @@ -0,0 +1,1948 @@ +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AB', 'Alberta', 1); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BC', 'British Columbia', 1); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MB', 'Manitoba', 1); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NB', 'New Brunswick', 1); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NL', 'Newfoundland and Labrador', 1); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NS', 'Nova Scotia', 1); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NT', 'Northwest Territories', 1); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NU', 'Nunavut', 1); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ON', 'Ontario', 1); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PE', 'Prince Edward Island', 1); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('QC', 'Québec', 1); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SK', 'Saskatchewan', 1); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('YT', 'Yukon', 1); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('001', 'Brändö', 5); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('002', 'Eckerö', 5); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('003', 'Finström', 5); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('004', 'Föglö', 5); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('005', 'Geta', 5); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('006', 'Hammarland', 5); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('007', 'Jomala', 5); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('008', 'Kumlinge', 5); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('009', 'Kökar', 5); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('010', 'Lemland', 5); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('011', 'Lumparland', 5); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('012', 'Maarianhamina', 5); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('013', 'Saltvik', 5); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('014', 'Sottunga', 5); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('015', 'Sund', 5); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('016', 'Vårdö', 5); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AK', 'Alaska', 6); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AN', 'Andaman and Nicobar Islands', 11); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AL', 'Altaysky Kraj', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AM', 'Amurskaya oblast', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BA', 'Republic of Bashkortostan', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BU', 'Republic of Buryatia', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CB', 'Chelyabinsk', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CK', 'Chukotka Autonomous Okrug', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CT', 'Zabaykalsky Kraj', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('EA', 'Yevreyskaya Autonomous Oblast', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GA', 'Republic Gorny Altay', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HA', 'Republic of Khakassia', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HK', 'Khabarovsk', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HM', 'Khanty-Mansyisky Autonomous Okrug', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('IR', 'Irkutsk', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KE', 'Kemerovo', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KK', 'Krasnoyarsk', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KN', 'Kurgan', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KO', 'Republic of Komi', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KT', 'Kamchatka', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MG', 'Magadan', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NS', 'Novosibirsk', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('OB', 'Orenburg', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('OM', 'Omsk', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PK', 'Primorsky Kraj', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PM', 'Perm`', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SL', 'Sakhalin', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SV', 'Sverdlovskaya oblast', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TN', 'Tyumen''', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TO', 'Tomsk', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TU', 'Republic of Tuva', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('YA', 'Republic of Sakha', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('YN', 'Yamalo-Nenetsky Autonomous Okrug', 15); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('IB', 'Balears', 21); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BR', 'Brest', 27); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HM', 'Horad Minsk', 27); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HO', 'Gomel', 27); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HR', 'Grodno', 27); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MA', 'Mogilev', 27); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MI', 'Minsk', 27); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VI', 'Vitebsk', 27); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GC', 'Las Palmas', 29); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TF', 'Santa Cruz de Tenerife', 29); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CE', 'Ceuta', 32); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ML', 'Melilla', 32); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AGS', 'Aguascalientes', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AGU', 'Aguascalientes', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BC', 'Baja California', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BCN', 'Baja California', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BCS', 'Baja California Sur', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CAM', 'Campeche', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CHH', 'Chihuahua', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CHP', 'Chiapas', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CHS', 'Chiapas', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CMX', 'Ciudad de México', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('COA', 'Coahuila de Zaragoza', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('COL', 'Colima', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DF', 'Distrito Federal', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DGO', 'Durango', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DUR', 'Durango', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('EMX', 'Estado de México', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GRO', 'Guerrero', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GTO', 'Guanajuato', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GUA', 'Guanajuato', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HGO', 'Hidalgo', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HID', 'Hidalgo', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('JAL', 'Jalisco', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MEX', 'México', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MIC', 'Michoacán de Ocampo', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MOR', 'Morelos', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NAY', 'Nayarit', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NL', 'Nuevo Leon', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NLE', 'Nuevo León', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('OAX', 'Oaxaca', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PUE', 'Puebla', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('QRO', 'Querétaro de Arteaga', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('QTR', 'Quintana Roo', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('QUE', 'Querétaro', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ROO', 'Quintana Roo', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SIN', 'Sinaloa', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SLP', 'San Luis Potosí', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SON', 'Sonora', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TAB', 'Tabasco', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TAM', 'Tamaulipas', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TLA', 'Tlaxcala', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TLX', 'Tlaxcala', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TMS', 'Tamaulipas', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VER', 'Veracruz de Ignacio de la Llave', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('YUC', 'Yucatán', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZAC', 'Zacatecas', 50); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AD', 'Republic of Adygeya', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AO', 'Astrakhan''', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AR', 'Arkhangelsk', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BO', 'Belgorod', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BR', 'Bryansk', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CN', 'Republic Chechnya', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CU', 'Republic of Chuvashia', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DA', 'Republic of Daghestan', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('IN', 'Republic of Ingushetia', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('IV', 'Ivanovo', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KB', 'Republic of Kabardino-Balkaria', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KC', 'Republic of Karachaevo-Cherkessia', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KG', 'Kaluga', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KI', 'Kirov', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KL', 'Republic of Karelia', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KM', 'Republic of Kalmykia', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KR', 'Krasnodar', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KS', 'Kostroma', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KU', 'Kursk', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LO', 'Leningradskaya oblast', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LP', 'Lipetsk', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MA', 'City of Moscow', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MD', 'Republic of Mordovia', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MO', 'Moscowskaya oblast', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MR', 'Republic of Marij-El', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MU', 'Murmansk', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NN', 'Nizhni Novgorod', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NO', 'Nenetsky Autonomous Okrug', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NV', 'Novgorodskaya oblast', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('OR', 'Oryel', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PE', 'Penza', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PS', 'Pskov', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RA', 'Ryazan''', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RO', 'Rostov-on-Don', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SA', 'Saratov', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SM', 'Smolensk', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SO', 'Republic of Northern Ossetia', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SP', 'City of St. Petersburg', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SR', 'Samara', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ST', 'Stavropol''', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TA', 'Republic of Tataria', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TB', 'Tambov', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TL', 'Tula', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TV', 'Tver''', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('UD', 'Republic of Udmurtia', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('UL', 'Ulyanovsk', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VG', 'Volgograd', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VL', 'Vladimir', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VO', 'Vologda', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VR', 'Voronezh', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('YR', 'Yaroslavl', 54); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AR', 'Arkhangelsk', 61); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FJL', 'Franz Josef Land', 61); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('01', 'Pinar del Río', 70); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('03', 'La Habana', 70); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('04', 'Matanzas', 70); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('05', 'Villa Clara', 70); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('06', 'Cienfuegos', 70); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('07', 'Sancti Spíritus', 70); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('08', 'Ciego de Ávila', 70); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('09', 'Camagüey', 70); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('10', 'Las Tunas', 70); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('11', 'Holguín', 70); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('12', 'Granma', 70); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('13', 'Santiago de Cuba', 70); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('14', 'Guantánamo', 70); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('15', 'Artemisa', 70); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('16', 'Mayabeque', 70); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('99', 'Isla de la Juventud', 70); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('A', 'Salta', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('B', 'Buenos Aires Province', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('C', 'Capital federal', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('D', 'San Luis', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('E', 'Entre Ríos', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('F', 'La Rioja', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('G', 'Santiago del Estero', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('H', 'Chaco', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('J', 'San Juan', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('K', 'Catamarca', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('L', 'La Pampa', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('M', 'Mendoza', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('N', 'Misiones', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('P', 'Formosa', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('Q', 'Neuquén', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('R', 'Río Negro', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('S', 'Santa Fe', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('T', 'Tucumán', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('U', 'Chubut', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('V', 'Tierra del Fuego', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('W', 'Corrientes', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('X', 'Córdoba', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('Y', 'Jujuy', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('Z', 'Santa Cruz', 100); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AC', 'Acre', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AL', 'Alagoas', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AM', 'Amazonas', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AP', 'Amapá', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BA', 'Bahia', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CE', 'Ceará', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DF', 'Distrito Federal', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ES', 'Espírito Santo', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GO', 'Goiás', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MA', 'Maranhão', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MG', 'Minas Gerais', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MS', 'Mato Grosso do Sul', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MT', 'Mato Grosso', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PA', 'Pará', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PB', 'Paraíba', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PE', 'Pernambuco', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PI', 'Piauí', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PR', 'Paraná', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RJ', 'Rio de Janeiro', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RN', 'Rio Grande do Norte', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RO', 'Rondônia', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RR', 'Roraima', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RS', 'Rio Grande do Sul', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SC', 'Santa Catarina', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SE', 'Sergipe', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SP', 'São Paulo', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TO', 'Tocantins', 108); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HI', 'Hawaii', 110); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AI', 'Aisén del General Carlos Ibañez del Campo', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AN', 'Antofagasta', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AP', 'Arica y Parinacota', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AR', 'La Araucanía', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AT', 'Atacama', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BI', 'Biobío', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CO', 'Coquimbo', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('I', 'Tarapacá', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('II', 'Antofagasta', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('III', 'Atacama', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('IV', 'Coquimbo', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('IX', 'La Araucanía', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LI', 'Libertador General Bernardo O''Higgins', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LL', 'Los Lagos', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LR', 'Los Ríos', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MA', 'Magallanes', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ML', 'Maule', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NB', 'Ñuble', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RM', 'Region Metropolitana de Santiago', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TA', 'Tarapacá', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('V', 'Valparaíso', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VI', 'Libertador General Bernardo O''Higgins', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VII', 'Maule', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VIII', 'Bío-Bío', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VS', 'Valparaíso', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('X', 'Los Lagos', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('XI', 'Aisén del General Carlos Ibáñez del Campo', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('XII', 'Magallanes', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('XIV', 'Los Ríos', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('XV', 'Arica y Parinacota', 112); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('22', 'Jan Mayen', 118); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KA', 'Kalingrad', 126); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('01', 'Concepción', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('02', 'San Pedro', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('03', 'Cordillera', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('04', 'Guairá', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('05', 'Caeguazú', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('06', 'Caazapl', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('07', 'Itapua', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('08', 'Miaiones', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('09', 'Paraguarí', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('1', 'Concepción', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('10', 'Alto Paraná', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('11', 'Central', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('12', 'Ñeembucú', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('13', 'Amambay', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('14', 'Canindeyú', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('15', 'Presidente Hayes', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('16', 'Alto Paraguay', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('19', 'Boquerón', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('2', 'San Pedro', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('3', 'Cordillera', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('4', 'Guairá', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('5', 'Caeguazú', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('6', 'Caazapá', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('7', 'Itapúa', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('8', 'Misiones', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('9', 'Paraguarí', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ASU', 'Asunción', 132); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('A', 'Seoul', 137); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('B', 'Pusan', 137); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('C', 'Kyunggi-do', 137); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('D', 'Kangwon-do', 137); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('E', 'Choongchungbuk-do', 137); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('F', 'Choongchungnam-do', 137); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('G', 'Chollabuk-do', 137); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('H', 'Chollanam-do', 137); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('IS', 'Special Island', 137); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('K', 'Kyungsangbuk-do', 137); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('L', 'Kyungsangnam-do', 137); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('M', 'Cheju-do', 137); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('N', 'Inchon', 137); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('P', 'Taegu', 137); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('Q', 'Kwangju', 137); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('R', 'Taejon', 137); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('S', 'Ulsan', 137); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('T', 'Sejong', 137); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KI', 'Kure Island', 138); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LD', 'Lakshadweep', 142); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AR', 'Artigas', 144); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CA', 'Canelones', 144); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CL', 'Cerro Largo', 144); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CO', 'Colonia', 144); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DU', 'Durazno', 144); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FD', 'Florida', 144); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FS', 'Flores', 144); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LA', 'Lavalleja', 144); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MA', 'Maldonado', 144); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MO', 'Montevideo', 144); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PA', 'Paysandú', 144); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RN', 'Río Negro', 144); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RO', 'Rocha', 144); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RV', 'Rivera', 144); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SA', 'Salto', 144); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SJ', 'San José', 144); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SO', 'Soriano', 144); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TA', 'Tacuarembó', 144); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TT', 'Treinta y Tres', 144); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LH', 'Lord Howe Is', 147); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AM', 'Amazonas', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AN', 'Anzoátegui', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AP', 'Apure', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AR', 'Aragua', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BA', 'Barinas', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BO', 'Bolívar', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CA', 'Carabobo', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CO', 'Cojedes', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DA', 'Delta Amacuro', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DC', 'Distrito Capital', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FA', 'Falcón', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GU', 'Guárico', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LA', 'Lara', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ME', 'Mérida', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MI', 'Miranda', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MO', 'Monagas', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NE', 'Nueva Esparta', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PO', 'Portuguesa', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SU', 'Sucre', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TA', 'Táchira', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TR', 'Trujillo', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VA', 'Vargas', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('YA', 'Yaracuy', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZU', 'Zulia', 148); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AC', 'Açores', 149); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ACT', 'Australian Capital Territory', 150); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NSW', 'New South Wales', 150); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NT', 'Northern Territory', 150); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('QLD', 'Queensland', 150); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SA', 'South Australia', 150); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TAS', 'Tasmania', 150); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VIC', 'Victoria', 150); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WA', 'Western Australia', 150); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LO', 'Leningradskaya Oblast', 151); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MV', 'Malyj Vysotskij', 151); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MA', 'Macquarie Is', 153); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CPK', 'Chimbu', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CPM', 'Central', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('EBR', 'East New Britain', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('EHG', 'Eastern Highlands', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('EPW', 'Enga', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ESW', 'East Sepik', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GPK', 'Gulf', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HLA', 'Hela', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('JWK', 'Jiwaka', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MBA', 'Milne Bay', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MPL', 'Morobe', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MPM', 'Madang', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MRL', 'Manus', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NCD', 'National Capital District', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NIK', 'New Ireland', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NPP', 'Northern', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NSA', 'North Solomons', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NSB', 'Bougainville', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SAN', 'West Sepik', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SHM', 'Southern Highlands', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WBK', 'West New Britain', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WBR', 'West New Britain', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WHM', 'Western Highlands', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WPD', 'Western', 163); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AUK', 'Auckland', 170); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BOP', 'Bay of Plenty', 170); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CAN', 'Canterbury', 170); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GIS', 'Gisborne', 170); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HKB', 'Hawke''s Bay', 170); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MBH', 'Marlborough', 170); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MWT', 'Manawatū-Whanganui', 170); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NSN', 'Nelson', 170); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NTL', 'Northland', 170); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('OTA', 'Otago', 170); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('STL', 'Southland', 170); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TAS', 'Tasman', 170); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TKI', 'Taranaki', 170); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WGN', 'Greater Wellington', 170); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WKO', 'Waikato', 170); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WTC', 'West Coast', 170); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MT', 'Minami Torishima', 177); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('O', 'Ogasawara', 192); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AM', 'Amstetten', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BC', 'Bregenz', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BL', 'Bruck/Leitha', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BM', 'Bruck-Mürzzuschlag', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BN', 'Baden', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BR', 'Braunau/Inn', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BZ', 'Bludenz', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DL', 'Deutschlandsberg', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DO', 'Dornbirn', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('EC', 'Eisenstadt', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('EF', 'Eferding', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('EU', 'Eisenstadt-Umgebung', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FE', 'Feldkirchen', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FK', 'Feldkirch', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FR', 'Freistadt', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GB', 'Gröbming', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GC', 'Graz', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GD', 'Gmünd', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GF', 'Gänserndorf', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GM', 'Gmunden', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GR', 'Grieskirchen', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GS', 'Güssing', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GU', 'Graz-Umgebung', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HA', 'Hallein', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HE', 'Hermagor', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HF', 'Hartberg-Fürstenfeld', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HL', 'Hollabrunn', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HO', 'Horn', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('IC', 'Innsbruck', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('IL', 'Innsbruck-Land', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('IM', 'Imst', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('JE', 'Jennersdorf', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('JO', 'St. Johann', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KB', 'Kitzbühel', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KC', 'Klagenfurt', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KI', 'Kirchdorf', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KL', 'Klagenfurt-Land', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KO', 'Korneuburg', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KR', 'Krems-Region', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KS', 'Krems', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KU', 'Kufstein', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LA', 'Landeck', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LB', 'Leibnitz', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LC', 'Linz', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LE', 'Leoben', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LF', 'Lilienfeld', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LI', 'Liezen', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LL', 'Linz-Land', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LN', 'Leoben-Land', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LZ', 'Lienz', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MA', 'Mattersburg', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MD', 'Mödling', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ME', 'Melk', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MI', 'Mistelbach', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MT', 'Murtal', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MU', 'Murau', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ND', 'Neusiedl/See', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NK', 'Neunkirchen', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('OP', 'Oberpullendorf', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('OW', 'Oberwart', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PC', 'St. Pölten', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PE', 'Perg', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PL', 'St. Pölten-Land', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RE', 'Reutte', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RI', 'Ried/Innkreis', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RO', 'Rohrbach', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SB', 'Scheibbs', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SC', 'Salzburg', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SD', 'Schärding', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SE', 'Steyr-Land', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SL', 'Salzburg-Land', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SO', 'Südoststeiermark', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SP', 'Spittal/Drau', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SR', 'Steyr', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SV', 'St.Veit/Glan', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SW', 'Schwechat', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SZ', 'Schwaz', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TA', 'Tamsweg', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TU', 'Tulln', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('UU', 'Urfahr', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VB', 'Vöcklabruck', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VI', 'Villach', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VK', 'Völkermarkt', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VL', 'Villach-Land', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VO', 'Voitsberg', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WB', 'Wr.Neustadt-Bezirk', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WC', 'Wien', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WE', 'Wels', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WL', 'Wels-Land', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WN', 'Wr.Neustadt', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WO', 'Wolfsberg', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WT', 'Waidhofen/Thaya', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WU', 'Wien-Umgebung', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WY', 'Waidhofen/Ybbs', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WZ', 'Weiz', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZE', 'Zell Am See', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZT', 'Zwettl', 206); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AN', 'Antwerpen', 209); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BR', 'Brussels', 209); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BW', 'Brabant Wallon', 209); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HT', 'Hainaut', 209); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LB', 'Limburg', 209); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LG', 'Liêge', 209); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LU', 'Luxembourg', 209); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NM', 'Namur', 209); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('OV', 'Oost-Vlaanderen', 209); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VB', 'Vlaams Brabant', 209); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WV', 'West-Vlaanderen', 209); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BL', 'Blagoevgrad', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BU', 'Burgas', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DO', 'Dobrič', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GA', 'Gabrovo', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HA', 'Haskovo', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KA', 'Kărdžali', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KD', 'Kjustendil', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LV', 'Loveč', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MN', 'Montana', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PA', 'Pazardžik', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PD', 'Plovdiv', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PK', 'Pernik', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PL', 'Pleven', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RS', 'Ruse', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RZ', 'Razgrad', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SF', 'Sofija', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SL', 'Sliven', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SM', 'Smoljan', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SN', 'Šumen', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SO', 'Sofija Grad', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SS', 'Silistra', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SZ', 'Stara Zagora', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TA', 'Tărgovište', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VD', 'Vidin', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VN', 'Varna', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VR', 'Vraca', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VT', 'Veliko Tărnovo', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('YA', 'Yambol', 212); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('2A', 'Corse-du-Sud', 214); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('2B', 'Haute-Corse', 214); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('015', 'Koebenhavns amt', 221); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('020', 'Frederiksborg amt', 221); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('025', 'Roskilde amt', 221); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('030', 'Vestsjaellands amt', 221); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('035', 'Storstrøm amt', 221); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('040', 'Bornholms amt', 221); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('042', 'Fyns amt', 221); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('050', 'Sínderjylland amt', 221); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('055', 'Ribe amt', 221); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('060', 'Vejle amt', 221); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('065', 'Ringkøbing amt', 221); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('070', 'Århus amt', 221); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('076', 'Viborg amt', 221); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('080', 'Nordjyllands amt', 221); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('101', 'Copenhagen City', 221); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('147', 'Frederiksberg', 221); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('100', 'Somero', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('102', 'Alastaro', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('103', 'Askainen', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('104', 'Aura', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('105', 'Dragsfjärd', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('106', 'Eura', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('107', 'Eurajoki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('108', 'Halikko', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('109', 'Harjavalta', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('110', 'Honkajoki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('111', 'Houtskari', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('112', 'Huittinen', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('115', 'Iniö', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('116', 'Jämijärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('117', 'Kaarina', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('119', 'Kankaanpää', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('120', 'Karinainen', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('122', 'Karvia', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('123', 'Äetsä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('124', 'Kemiö', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('126', 'Kiikala', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('128', 'Kiikoinen', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('129', 'Kisko', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('130', 'Kiukainen', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('131', 'Kodisjoki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('132', 'Kokemäki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('133', 'Korppoo', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('134', 'Koski tl', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('135', 'Kullaa', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('136', 'Kustavi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('137', 'Kuusjoki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('138', 'Köyliö', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('139', 'Laitila', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('140', 'Lappi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('141', 'Lavia', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('142', 'Lemu', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('143', 'Lieto', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('144', 'Loimaa', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('145', 'Loimaan kunta', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('147', 'Luvia', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('148', 'Marttila', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('149', 'Masku', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('150', 'Mellilä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('151', 'Merikarvia', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('152', 'Merimasku', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('154', 'Mietoinen', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('156', 'Muurla', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('157', 'Mynämäki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('158', 'Naantali', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('159', 'Nakkila', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('160', 'Nauvo', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('161', 'Noormarkku', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('162', 'Nousiainen', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('163', 'Oripää', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('164', 'Paimio', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('165', 'Parainen', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('167', 'Perniö', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('168', 'Pertteli', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('169', 'Piikkiö', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('170', 'Pomarkku', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('171', 'Pori', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('172', 'Punkalaidun', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('173', 'Pyhäranta', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('174', 'Pöytyä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('175', 'Raisio', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('176', 'Rauma', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('178', 'Rusko', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('179', 'Rymättylä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('180', 'Salo', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('181', 'Sauvo', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('182', 'Siikainen', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('183', 'Suodenniemi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('184', 'Suomusjärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('185', 'Säkylä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('186', 'Särkisalo', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('187', 'Taivassalo', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('188', 'Tarvasjoki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('189', 'Turku', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('190', 'Ulvila', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('191', 'Uusikaupunki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('192', 'Vahto', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('193', 'Vammala', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('194', 'Vampula', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('195', 'Vehmaa', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('196', 'Velkua', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('198', 'Västanfjärd', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('199', 'Yläne', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('201', 'Artjärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('202', 'Askola', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('204', 'Espoo', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('205', 'Hanko', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('206', 'Helsinki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('207', 'Hyvinkää', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('208', 'Inkoo', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('209', 'Järvenpää', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('210', 'Karjaa', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('211', 'Karjalohja', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('212', 'Karkkila', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('213', 'Kauniainen', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('214', 'Kerava', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('215', 'Kirkkonummi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('216', 'Lapinjärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('217', 'Liljendal', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('218', 'Lohjan kaupunki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('220', 'Loviisa', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('221', 'Myrskylä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('222', 'Mäntsälä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('223', 'Nummi-Pusula', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('224', 'Nurmijärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('225', 'Orimattila', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('226', 'Pernaja', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('227', 'Pohja', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('228', 'Pornainen', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('229', 'Porvoo', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('231', 'Pukkila', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('233', 'Ruotsinpyhtää', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('234', 'Sammatti', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('235', 'Sipoo', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('236', 'Siuntio', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('238', 'Tammisaari', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('241', 'Tuusula', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('242', 'Vantaa', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('243', 'Vihti', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('301', 'Asikkala', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('303', 'Forssa', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('304', 'Hattula', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('305', 'Hauho', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('306', 'Hausjärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('307', 'Hollola', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('308', 'Humppila', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('309', 'Hämeenlinna', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('310', 'Janakkala', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('311', 'Jokioinen', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('312', 'Juupajoki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('313', 'Kalvola', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('314', 'Kangasala', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('315', 'Hämeenkoski', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('316', 'Kuhmalahti', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('318', 'Kuru', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('319', 'Kylmäkoski', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('320', 'Kärkölä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('321', 'Lahti', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('322', 'Lammi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('323', 'Lempäälä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('324', 'Loppi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('325', 'Luopioinen', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('326', 'Längelmäki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('327', 'Mänttä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('328', 'Nastola', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('329', 'Nokia', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('330', 'Orivesi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('331', 'Padasjoki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('332', 'Pirkkala', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('333', 'Pälkäne', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('334', 'Renko', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('335', 'Riihimäki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('336', 'Ruovesi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('337', 'Sahalahti', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('340', 'Tammela', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('341', 'Tampere', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('342', 'Toijala', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('344', 'Tuulos', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('345', 'Urjala', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('346', 'Valkeakoski', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('347', 'Vesilahti', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('348', 'Viiala', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('349', 'Vilppula', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('350', 'Virrat', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('351', 'Ylöjärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('352', 'Ypäjä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('353', 'Hämeenkyrö', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('354', 'Ikaalinen', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('355', 'Kihniö', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('356', 'Mouhijärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('357', 'Parkano', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('358', 'Viljakkala', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('402', 'Enonkoski', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('403', 'Hartola', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('404', 'Haukivuori', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('405', 'Heinola', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('407', 'Heinävesi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('408', 'Hirvensalmi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('409', 'Joroinen', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('410', 'Juva', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('411', 'Jäppilä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('412', 'Kangaslampi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('413', 'Kangasniemi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('414', 'Kerimäki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('415', 'Mikkeli', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('417', 'Mäntyharju', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('418', 'Pertunmaa', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('419', 'Pieksämäki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('420', 'Pieksänmaa', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('421', 'Punkaharju', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('422', 'Puumala', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('423', 'Rantasalmi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('424', 'Ristiina', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('425', 'Savonlinna', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('426', 'Savonranta', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('427', 'Sulkava', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('428', 'Sysmä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('502', 'Elimäki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('503', 'Hamina', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('504', 'Iitti', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('505', 'Imatra', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('506', 'Jaala', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('507', 'Joutseno', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('509', 'Kotka', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('510', 'Kouvola', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('511', 'Kuusankoski', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('513', 'Lappeenranta', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('514', 'Lemi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('515', 'Luumäki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('516', 'Miehikkälä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('518', 'Parikkala', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('519', 'Pyhtää', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('520', 'Rautjärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('521', 'Ruokolahti', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('522', 'Saari', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('523', 'Savitaipale', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('525', 'Suomenniemi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('526', 'Taipalsaari', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('527', 'Uukuniemi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('528', 'Valkeala', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('530', 'Virolahti', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('531', 'Ylämaa', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('532', 'Anjalankoski', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('601', 'Alahärmä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('602', 'Alajärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('603', 'Alavus', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('604', 'Evijärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('605', 'Halsua', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('606', 'Hankasalmi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('607', 'Himanka', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('608', 'Ilmajoki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('609', 'Isojoki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('610', 'Isokyrö', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('611', 'Jalasjärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('612', 'Joutsa', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('613', 'Jurva', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('614', 'Jyväskylä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('615', 'Jyväskylän mlk', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('616', 'Jämsä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('617', 'Jämsänkoski', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('619', 'Kannonkoski', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('620', 'Kannus', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('621', 'Karijoki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('622', 'Karstula', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('623', 'Kaskinen', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('624', 'Kauhajoki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('625', 'Kauhava', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('626', 'Kaustinen', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('627', 'Keuruu', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('628', 'Kinnula', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('629', 'Kivijärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('630', 'Kokkola', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('632', 'Konnevesi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('633', 'Korpilahti', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('634', 'Korsnäs', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('635', 'Kortesjärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('636', 'Kristiinankaupunki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('637', 'Kruunupyy', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('638', 'Kuhmoinen', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('639', 'Kuortane', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('640', 'Kurikka', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('641', 'Kyyjärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('642', 'Kälviä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('643', 'Laihia', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('644', 'Lappajärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('645', 'Lapua', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('646', 'Laukaa', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('647', 'Lehtimäki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('648', 'Leivonmäki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('649', 'Lestijärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('650', 'Lohtaja', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('651', 'Luhanka', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('652', 'Luoto', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('653', 'Maalahti', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('654', 'Maksamaa', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('655', 'Multia', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('656', 'Mustasaari', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('657', 'Muurame', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('658', 'Nurmo', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('659', 'Närpiö', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('660', 'Oravainen', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('661', 'Perho', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('662', 'Peräseinäjoki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('663', 'Petäjävesi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('664', 'Pietarsaari', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('665', 'Pedersöre', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('666', 'Pihtipudas', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('668', 'Pylkönmäki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('669', 'Saarijärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('670', 'Seinäjoki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('671', 'Soini', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('672', 'Sumiainen', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('673', 'Suolahti', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('675', 'Teuva', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('676', 'Toholampi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('677', 'Toivakka', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('678', 'Töysä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('679', 'Ullava', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('680', 'Uurainen', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('681', 'Uusikaarlepyy', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('682', 'Vaasa', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('683', 'Veteli', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('684', 'Viitasaari', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('685', 'Vimpeli', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('686', 'Vähäkyrö', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('687', 'Vöyri', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('688', 'Ylihärmä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('689', 'Ylistaro', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('690', 'Ähtäri', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('692', 'Äänekoski', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('701', 'Eno', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('702', 'Iisalmi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('703', 'Ilomantsi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('704', 'Joensuu', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('705', 'Juankoski', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('706', 'Juuka', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('707', 'Kaavi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('708', 'Karttula', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('709', 'Keitele', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('710', 'Kesälahti', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('711', 'Kiihtelysvaara', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('712', 'Kitee', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('713', 'Kiuruvesi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('714', 'Kontiolahti', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('715', 'Kuopio', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('716', 'Lapinlahti', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('717', 'Leppävirta', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('718', 'Lieksa', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('719', 'Liperi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('720', 'Maaninka', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('721', 'Nilsiä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('722', 'Nurmes', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('723', 'Outokumpu', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('724', 'Pielavesi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('725', 'Polvijärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('726', 'Pyhäselkä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('727', 'Rautalampi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('728', 'Rautavaara', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('729', 'Rääkkylä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('730', 'Siilinjärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('731', 'Sonkajärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('732', 'Suonenjoki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('733', 'Tervo', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('734', 'Tohmajärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('735', 'Tuupovaara', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('736', 'Tuusniemi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('737', 'Valtimo', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('738', 'Varkaus', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('739', 'Varpaisjärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('740', 'Vehmersalmi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('741', 'Vesanto', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('742', 'Vieremä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('743', 'Värtsilä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('801', 'Alavieska', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('802', 'Haapajärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('803', 'Haapavesi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('804', 'Hailuoto', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('805', 'Haukipudas', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('806', 'Hyrynsalmi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('807', 'Ii', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('808', 'Kajaani', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('810', 'Kalajoki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('811', 'Kempele', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('812', 'Kestilä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('813', 'Kiiminki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('814', 'Kuhmo', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('815', 'Kuivaniemi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('816', 'Kuusamo', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('817', 'Kärsämäki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('818', 'Liminka', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('819', 'Lumijoki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('820', 'Merijärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('821', 'Muhos', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('822', 'Nivala', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('823', 'Oulainen', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('824', 'Oulu', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('825', 'Oulunsalo', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('826', 'Paltamo', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('827', 'Pattijoki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('828', 'Piippola', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('829', 'Pudasjärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('830', 'Pulkkila', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('831', 'Puolanka', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('832', 'Pyhäjoki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('833', 'Pyhäjärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('834', 'Pyhäntä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('835', 'Raahe', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('836', 'Rantsila', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('837', 'Reisjärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('838', 'Ristijärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('839', 'Ruukki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('840', 'Sievi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('841', 'Siikajoki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('842', 'Sotkamo', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('843', 'Suomussalmi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('844', 'Taivalkoski', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('846', 'Tyrnävä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('847', 'Utajärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('848', 'Vaala', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('849', 'Vihanti', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('850', 'Vuolijoki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('851', 'Yli-Ii', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('852', 'Ylikiiminki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('853', 'Ylivieska', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('901', 'Enontekiö', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('902', 'Inari', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('903', 'Kemi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('904', 'Keminmaa', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('905', 'Kemijärvi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('907', 'Kittilä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('908', 'Kolari', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('909', 'Muonio', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('910', 'Pelkosenniemi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('911', 'Pello', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('912', 'Posio', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('913', 'Ranua', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('914', 'Rovaniemi', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('915', 'Rovaniemen mlk', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('916', 'Salla', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('917', 'Savukoski', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('918', 'Simo', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('919', 'Sodankylä', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('920', 'Tervola', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('921', 'Tornio', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('922', 'Utsjoki', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('923', 'Ylitornio', 224); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CA', 'Cagliari', 225); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CI', 'Carbonia-Iglesias', 225); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MD', 'Medio Campidano', 225); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NU', 'Nuoro', 225); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('OG', 'Ogliastra', 225); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('OR', 'Oristano', 225); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('OT', 'Olbia-Tempio', 225); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SS', 'Sassari', 225); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SU', 'Sud Sardegna', 225); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VS', 'MedioCampidano', 225); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('01', 'Ain', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('02', 'Aisne', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('03', 'Allier', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('04', 'Alpes-de-Haute-Provence', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('05', 'Hautes-Alpes', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('06', 'Alpes-Maritimes', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('07', 'Ardèche', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('08', 'Ardennes', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('09', 'Ariège', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('10', 'Aube', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('11', 'Aude', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('12', 'Aveyron', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('13', 'Bouches-du-Rhône', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('14', 'Calvados', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('15', 'Cantal', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('16', 'Charente', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('17', 'Charente-Maritime', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('18', 'Cher', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('19', 'Corrèze', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('21', 'Côte-d''Or', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('22', 'Côtes-d''Armor', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('23', 'Creuse', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('24', 'Dordogne', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('25', 'Doubs', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('26', 'Drôme', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('27', 'Eure', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('28', 'Eure-et-Loir', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('29', 'Finistère', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('30', 'Gard', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('31', 'Haute-Garonne', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('32', 'Gers', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('33', 'Gironde', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('34', 'Hérault', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('35', 'Ille-et-Vilaine', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('36', 'Indre', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('37', 'Indre-et-Loire', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('38', 'Isère', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('39', 'Jura', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('40', 'Landes', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('41', 'Loir-et-Cher', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('42', 'Loire', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('43', 'Haute-Loire', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('44', 'Loire-Atlantique', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('45', 'Loiret', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('46', 'Lot', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('47', 'Lot-et-Garonne', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('48', 'Lozère', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('49', 'Maine-et-Loire', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('50', 'Manche', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('51', 'Marne', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('52', 'Haute-Marne', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('53', 'Mayenne', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('54', 'Meurthe-et-Moselle', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('55', 'Meuse', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('56', 'Morbihan', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('57', 'Moselle', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('58', 'Nièvre', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('59', 'Nord', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('60', 'Oise', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('61', 'Orne', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('62', 'Pas-de-Calais', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('63', 'Puy-de-Dôme', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('64', 'Pyrénées-Atlantiques', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('65', 'Hautes-Pyrénées', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('66', 'Pyrénées-Orientales', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('67', 'Bas-Rhin', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('68', 'Haut-Rhin', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('69', 'Rhône', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('70', 'Haute-Saône', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('71', 'Saône-et-Loire', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('72', 'Sarthe', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('73', 'Savoie', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('74', 'Haute-Savoie', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('75', 'Paris', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('76', 'Seine-Maritime', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('77', 'Seine-et-Marne', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('78', 'Yvelines', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('79', 'Deux-Sèvres', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('80', 'Somme', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('81', 'Tarn', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('82', 'Tarn-et-Garonne', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('83', 'Var', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('84', 'Vaucluse', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('85', 'Vendée', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('86', 'Vienne', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('87', 'Haute-Vienne', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('88', 'Vosges', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('89', 'Yonne', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('90', 'Territoire de Belfort', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('91', 'Essonne', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('92', 'Hauts-de-Seine', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('93', 'Seine-Saint-Denis', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('94', 'Val-de-Marne', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('95', 'Val-d''Oise', 227); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BB', 'Brandenburg', 230); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BE', 'Berlin', 230); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BW', 'Baden-Württemberg', 230); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BY', 'Bayern', 230); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HB', 'Bremen', 230); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HE', 'Hessen', 230); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HH', 'Hamburg', 230); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MV', 'Mecklenburg-Vorpommern', 230); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NI', 'Niedersachsen', 230); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NW', 'Nordrhein-Westfalen', 230); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RP', 'Rheinland-Pfalz', 230); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SH', 'Schleswig-Holstein', 230); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SL', 'Saarland', 230); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SN', 'Sachsen', 230); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ST', 'Sachsen-Anhalt', 230); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TH', 'Thüringen', 230); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BA', 'Baranya', 239); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BE', 'Békés', 239); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BN', 'Bács-Kiskun', 239); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BO', 'Borsod', 239); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BP', 'Budapest', 239); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CS', 'Csongrád', 239); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FE', 'Fejér', 239); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GY', 'Gyõr', 239); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HB', 'Hajdú-Bihar', 239); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HE', 'Heves', 239); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KO', 'Komárom', 239); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NG', 'Nógrád', 239); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PE', 'Pest', 239); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SA', 'Szabolcs', 239); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SO', 'Somogy', 239); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SZ', 'Szolnok', 239); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TO', 'Tolna', 239); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VA', 'Vas', 239); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VE', 'Veszprém', 239); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZA', 'Zala', 239); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('C', 'Cork', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CE', 'Clare', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CN', 'Cavan', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CO', 'Cork', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CW', 'Carlow', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('D', 'Dublin', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DL', 'Donegal', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('G', 'Galway', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KE', 'Kildare', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KK', 'Kilkenny', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KY', 'Kerry', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LD', 'Longford', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LH', 'Louth', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LK', 'Limerick', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LM', 'Leitrim', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LS', 'Laois', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MH', 'Meath', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MN', 'Monaghan', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MO', 'Mayo', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('OY', 'Offaly', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RN', 'Roscommon', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SO', 'Sligo', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TA', 'Tipperary', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WD', 'Waterford', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WH', 'Westmeath', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WW', 'Wicklow', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WX', 'Wexford', 245); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AG', 'Agrigento', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AL', 'Alessandria', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AN', 'Ancona', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AO', 'Aosta', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AP', 'Ascoli Piceno', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AQ', 'L''Aquila', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AR', 'Arezzo', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AT', 'Asti', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AV', 'Avellino', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BA', 'Bari', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BG', 'Bergamo', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BI', 'Biella', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BL', 'Belluno', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BN', 'Benevento', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BO', 'Bologna', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BR', 'Brindisi', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BS', 'Brescia', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BT', 'Barletta-Andria-Trani', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BZ', 'Bolzano', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CB', 'Campobasso', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CE', 'Caserta', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CH', 'Chieti', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CL', 'Caltanissetta', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CN', 'Cuneo', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CO', 'Como', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CR', 'Cremona', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CS', 'Cosenza', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CT', 'Catania', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CZ', 'Catanzaro', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('EN', 'Enna', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FC', 'Forlì-Cesena', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FE', 'Ferrara', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FG', 'Foggia', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FI', 'Firenze', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FM', 'Fermo', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FO', 'Forlì', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FR', 'Frosinone', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GE', 'Genova', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GO', 'Gorizia', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GR', 'Grosseto', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('IM', 'Imperia', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('IS', 'Isernia', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KR', 'Crotone', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LC', 'Lecco', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LE', 'Lecce', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LI', 'Livorno', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LO', 'Lodi', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LT', 'Latina', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LU', 'Lucca', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MB', 'Monza e Brianza', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MC', 'Macerata', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ME', 'Messina', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MI', 'Milano', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MN', 'Mantova', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MO', 'Modena', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MS', 'Massa Carrara', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MT', 'Matera', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NA', 'Napoli', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NO', 'Novara', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PA', 'Palermo', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PC', 'Piacenza', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PD', 'Padova', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PE', 'Pescara', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PG', 'Perugia', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PI', 'Pisa', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PN', 'Pordenone', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PO', 'Prato', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PR', 'Parma', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PS', 'Pesaro e Urbino', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PT', 'Pistoia', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PU', 'Pesaro e Urbino', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PV', 'Pavia', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PZ', 'Potenza', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RA', 'Ravenna', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RC', 'Reggio Calabria', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RE', 'Reggio Emilia', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RG', 'Ragusa', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RI', 'Rieti', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RM', 'Roma', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RN', 'Rimini', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RO', 'Rovigo', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SA', 'Salerno', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SI', 'Siena', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SO', 'Sondrio', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SP', 'La Spezia', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SR', 'Siracusa', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SV', 'Savona', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TA', 'Taranto', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TE', 'Teramo', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TN', 'Trento', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TO', 'Torino', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TP', 'Trapani', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TR', 'Terni', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TS', 'Trieste', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TV', 'Treviso', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('UD', 'Udine', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VA', 'Varese', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VB', 'Verbano Cusio Ossola', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VC', 'Vercelli', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VE', 'Venezia', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VI', 'Vicenza', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VR', 'Verona', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VT', 'Viterbo', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VV', 'Vibo Valentia', 248); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MD', 'Madeira', 256); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('21', 'Svalbard', 259); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DR', 'Drenthe', 263); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FL', 'Flevoland', 263); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FR', 'Fryslân', 263); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GD', 'Gelderland', 263); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GE', 'Gelderland', 263); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GR', 'Groningen', 263); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LB', 'Limburg', 263); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LI', 'Limburg', 263); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NB', 'Noord-Brabant', 263); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NH', 'Noord-Holland', 263); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('OV', 'Overijssel', 263); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('UT', 'Utrecht', 263); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZE', 'Zeeland', 263); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZH', 'Zuid-Holland', 263); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZL', 'Zeeland', 263); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('03', 'Oslo', 266); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('11', 'Rogaland', 266); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('15', 'Møre og Romsdal', 266); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('18', 'Nordland', 266); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('30', 'Viken', 266); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('34', 'Innlandet', 266); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('38', 'Vestfold og Telemark', 266); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('42', 'Agder', 266); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('46', 'Vestland', 266); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('50', 'Trøndelag', 266); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('54', 'Troms og Finnmark', 266); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('B', 'Lubuskie', 269); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('C', 'Lodzkie', 269); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('D', 'Dolnoslaskie', 269); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('F', 'Pomorskie', 269); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('G', 'Slaskie', 269); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('J', 'Warminsko-Mazurskie', 269); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('K', 'Podkarpackie', 269); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('L', 'Lubelskie', 269); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('M', 'Malopolskie', 269); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('O', 'Podlaskie', 269); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('P', 'Kujawsko-Pomorskie', 269); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('R', 'Mazowieckie', 269); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('S', 'Swietokrzyskie', 269); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('U', 'Opolskie', 269); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('W', 'Wielkopolskie', 269); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('Z', 'Zachodnio-Pomorskie', 269); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AV', 'Aveiro', 272); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BG', 'Bragança', 272); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BJ', 'Beja', 272); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BR', 'Braga', 272); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CB', 'Castelo Branco', 272); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CO', 'Coimbra', 272); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('EV', 'Evora', 272); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FR', 'Faro', 272); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GD', 'Guarda', 272); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LR', 'Leiria', 272); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LX', 'Lisboa', 272); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PG', 'Portalegre', 272); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PT', 'Porto', 272); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SR', 'Santarem', 272); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ST', 'Setubal', 272); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VC', 'Viana do Castelo', 272); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VR', 'Vila Real', 272); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VS', 'Viseu', 272); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AB', 'Alba', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AG', 'Argeș', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AR', 'Arad', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('B', 'București', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BC', 'Bacău', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BH', 'Bihor', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BN', 'Bistrița-Năsăud', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BR', 'Brăila', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BT', 'Botoșani', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BU', 'Bucureşti', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BV', 'Brașov', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BZ', 'Buzău', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CJ', 'Cluj', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CL', 'Călărași', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CS', 'Caraș-Severin', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CT', 'Constanța', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CV', 'Covasna', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DB', 'Dâmbovița', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DJ', 'Dolj', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GJ', 'Gorj', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GL', 'Galați', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GR', 'Giurgiu', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HD', 'Hunedoara', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HR', 'Harghita', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('IF', 'Ilfov', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('IL', 'Ialomița', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('IS', 'Iași', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MH', 'Mehedinți', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MM', 'Maramureș', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MS', 'Mureș', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NT', 'Neamț', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('OT', 'Olt', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PH', 'Prahova', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SB', 'Sibiu', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SJ', 'Sălaj', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SM', 'Satu Mare', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SV', 'Suceava', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TL', 'Tulcea', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TM', 'Timiș', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TR', 'Teleorman', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VL', 'Vâlcea', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VN', 'Vrancea', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VS', 'Vaslui', 275); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('A', 'Alicante', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AB', 'Albacete', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AL', 'Almeria', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AV', 'Ávila', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('B', 'Barcelona', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BA', 'Badajoz', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BI', 'Bizkaia', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BU', 'Burgos', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('C', 'A Coruña', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CA', 'Cádiz', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CC', 'Cáceres', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CO', 'Córdoba', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CR', 'Ciudad Real', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CS', 'Castellón', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CU', 'Cuenca', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GI', 'Girona', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GR', 'Granada', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GU', 'Guadalajara', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('H', 'Huelva', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HU', 'Huesca', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('J', 'Jaén', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('L', 'Lleida', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LE', 'León', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LO', 'La Rioja', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LU', 'Lugo', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('M', 'Madrid', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MA', 'Málaga', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MU', 'Murcia', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NA', 'Navarra', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('O', 'Asturias', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('OR', 'Ourense', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('OU', 'Ourense', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('P', 'Palencia', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PO', 'Pontevedra', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('S', 'Cantabria', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SA', 'Salamanca', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SE', 'Sevilla', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SG', 'Segovia', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SO', 'Soria', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SS', 'Gipuzkoa', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('T', 'Tarragona', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TE', 'Teruel', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TO', 'Toledo', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('V', 'València', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VA', 'Valladolid', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VI', 'Álava', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('Z', 'Zaragoza', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZA', 'Zamora', 281); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AB', 'Stockholms län', 284); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AC', 'Västerbottens län', 284); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BD', 'Norrbottens län', 284); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('C', 'Uppsala län', 284); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('D', 'Södermanlands län', 284); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('E', 'Östergötlands län', 284); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('F', 'Jönköpings län', 284); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('G', 'Kronobergs län', 284); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('H', 'Kalmar län', 284); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('I', 'Gotlands län', 284); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('K', 'Blekinge län', 284); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('M', 'Skåne län', 284); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('N', 'Hallands län', 284); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('O', 'Västra Götalands län', 284); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('S', 'Värmlands län', 284); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('T', 'Örebro län', 284); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('U', 'Västmanlands län', 284); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('W', 'Dalarnas län', 284); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('X', 'Gävleborgs län', 284); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('Y', 'Västernorrlands län', 284); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('Z', 'Jämtlands län', 284); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AG', 'Aargau', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AI', 'Appenzell Innerrhoden', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AR', 'Appenzell Ausserrhoden', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BE', 'Bern', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BL', 'Basel Landschaft', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BS', 'Basel Stadt', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FR', 'Freiburg / Fribourg', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GE', 'Genf / Genève', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GL', 'Glarus', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GR', 'Graubuenden / Grisons', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('JU', 'Jura', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LU', 'Luzern', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NE', 'Neuenburg / Neuchâtel', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NW', 'Nidwalden', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('OW', 'Obwalden', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SG', 'St. Gallen', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SH', 'Schaffhausen', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SO', 'Solothurn', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SZ', 'Schwyz', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TG', 'Thurgau', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TI', 'Tessin / Ticino', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('UR', 'Uri', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VD', 'Waadt / Vaud', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VS', 'Wallis / Valais', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZG', 'Zug', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZH', 'Zuerich', 287); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CH', 'Cherkas''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CN', 'Chernivets''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CR', 'Chernihivs''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DN', 'Dnipropetrovs''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DO', 'Donets''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HA', 'Kharkivs''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HE', 'Khersons''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HM', 'Khmel''nyts''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('IF', 'Ivano-Frankivs''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KI', 'Kirovohrads''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KO', 'Kyivs''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KR', 'Respublika Krym', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KV', 'Kyïv', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LU', 'Luhans''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LV', 'L''vivs''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NI', 'Mykolaivs''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('OD', 'Odes''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PO', 'Poltavs''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RI', 'Rivnens''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SL', 'Sevastopol''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SU', 'Sums''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TE', 'Ternopil''s''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VI', 'Vinnyts''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VO', 'Volyos''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZA', 'Zakarpats''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZH', 'Zhytomyrs''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZP', 'Zaporiz''ka Oblast''', 288); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AL', 'Alabama', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AR', 'Arkansas', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AZ', 'Arizona', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CA', 'California', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CO', 'Colorado', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CT', 'Connecticut', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DC', 'District of Columbia', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DE', 'Delaware', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FL', 'Florida', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GA', 'Georgia', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('IA', 'Iowa', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ID', 'Idaho', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('IL', 'Illinois', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('IN', 'Indiana', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KS', 'Kansas', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KY', 'Kentucky', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LA', 'Louisiana', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MA', 'Massachusetts', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MD', 'Maryland', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ME', 'Maine', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MI', 'Michigan', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MN', 'Minnesota', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MO', 'Missouri', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MS', 'Mississippi', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MT', 'Montana', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NC', 'North Carolina', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ND', 'North Dakota', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NE', 'Nebraska', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NH', 'New Hampshire', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NJ', 'New Jersey', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NM', 'New Mexico', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NV', 'Nevada', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NY', 'New York', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('OH', 'Ohio', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('OK', 'Oklahoma', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('OR', 'Oregon', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PA', 'Pennsylvania', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RI', 'Rhode Island', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SC', 'South Carolina', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SD', 'South Dakota', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TN', 'Tennessee', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TX', 'Texas', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('UT', 'Utah', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VA', 'Virginia', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VT', 'Vermont', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WA', 'Washington', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WI', 'Wisconsin', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WV', 'West Virginia', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WY', 'Wyoming', 291); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AH', 'Anhui', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BJ', 'Beijing', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CQ', 'Chongqing', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FJ', 'Fujian', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GD', 'Guangdong', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GS', 'Gansu', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GX', 'Guangxi Zhuangzu', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GZ', 'Guizhou', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HA', 'Henan', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HB', 'Hubei', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HE', 'Hebei', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HI', 'Hainan', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HL', 'Heilongjiang', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HN', 'Hunan', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('JL', 'Jilin', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('JS', 'Jiangsu', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('JX', 'Jiangxi', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LN', 'Liaoning', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NM', 'Nei Mongol', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NX', 'Ningxia Huizu', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('QH', 'Qinghai', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SC', 'Sichuan', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SD', 'Shandong', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SH', 'Shanghai', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SN', 'Shaanxi', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SX', 'Shanxi', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TJ', 'Tianjin', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('XJ', 'Xinjiang Uygur', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('XZ', 'Xizang', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('YN', 'Yunnan', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZJ', 'Zhejiang', 318); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AP', 'Andhra Pradesh', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AR', 'Arunāchal Pradesh', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AS', 'Assam', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BR', 'Bihār', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CG', 'Chhattīsgarh', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CH', 'Chandīgarh', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DD', 'Damān and Diu', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DL', 'Delhi', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DN', 'Dādra and Nagar Haveli', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GA', 'Goa', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GJ', 'Gujarāt', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HP', 'Himāchal Pradesh', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HR', 'Haryāna', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('JH', 'Jhārkhand', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('JK', 'Jammu and Kashmīr', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KA', 'Karnātaka', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KL', 'Kerala', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LA', 'Ladākh', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MH', 'Mahārāshtra', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ML', 'Meghālaya', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MN', 'Manipur', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MP', 'Madhya Pradesh', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MZ', 'Mizoram', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NL', 'Nāgāland', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('OD', 'Odisha', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PB', 'Punjab', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PY', 'Puducherry', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RJ', 'Rājasthān', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SK', 'Sikkim', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TG', 'Telangāna', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TN', 'Tamil Nādu', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TR', 'Tripura', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('UK', 'Uttarākhand', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('UP', 'Uttar Pradesh', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WB', 'West Bengal', 324); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('01', 'Hokkaido', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('02', 'Aomori', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('03', 'Iwate', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('04', 'Akita', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('05', 'Yamagata', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('06', 'Miyagi', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('07', 'Fukushima', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('08', 'Niigata', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('09', 'Nagano', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('10', 'Tokyo', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('11', 'Kanagawa', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('12', 'Chiba', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('13', 'Saitama', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('14', 'Ibaraki', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('15', 'Tochigi', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('16', 'Gunma', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('17', 'Yamanashi', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('18', 'Shizuoka', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('19', 'Gifu', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('20', 'Aichi', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('21', 'Mie', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('22', 'Kyoto', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('23', 'Shiga', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('24', 'Nara', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('25', 'Osaka', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('26', 'Wakayama', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('27', 'Hyogo', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('28', 'Toyama', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('29', 'Fukui', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('30', 'Ishikawa', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('31', 'Okayama', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('32', 'Shimane', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('33', 'Yamaguchi', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('34', 'Tottori', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('35', 'Hiroshima', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('36', 'Kagawa', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('37', 'Tokushima', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('38', 'Ehime', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('39', 'Kochi', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('40', 'Fukuoka', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('41', 'Saga', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('42', 'Nagasaki', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('43', 'Kumamoto', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('44', 'Oita', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('45', 'Miyazaki', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('46', 'Kagoshima', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('47', 'Okinawa', 339); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ABR', 'Abra', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AGN', 'Agusan del Norte', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AGS', 'Agusan del Sur', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AKL', 'Aklan', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ALB', 'Albay', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ANT', 'Antique', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('APA', 'Apayao', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('AUR', 'Aurora', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BAN', 'Bataan', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BAS', 'Basilan', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BEN', 'Benguet', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BIL', 'Biliran', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BOH', 'Bohol', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BTG', 'Batangas', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BTN', 'Batanes', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BUK', 'Bukidnon', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BUL', 'Bulacan', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CAG', 'Cagayan', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CAM', 'Camiguin', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CAN', 'Camarines Norte', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CAP', 'Capiz', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CAS', 'Camarines Sur', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CAT', 'Catanduanes', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CAV', 'Cavite', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CEB', 'Cebu', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('COM', 'Davao de Oro', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DAO', 'Davao Oriental', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DAS', 'Davao del Sur', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DAV', 'Davao del Norte', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('EAS', 'Eastern Samar', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GUI', 'Guimaras', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('IFU', 'Ifugao', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ILI', 'Iloilo', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ILN', 'Ilocos Norte', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ILS', 'Ilocos Sur', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ISA', 'Isabela', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KAL', 'Kalinga', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LAG', 'Laguna', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LAN', 'Lanao del Norte', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LAS', 'Lanao del Sur', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LEY', 'Leyte', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LUN', 'La Union', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MAD', 'Marinduque', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MAG', 'Maguindanao', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MAS', 'Masbate', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MDC', 'Mindoro Occidental', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MDR', 'Mindoro Oriental', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MOU', 'Mountain Province', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MSC', 'Misamis Occidental', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MSR', 'Misamis Oriental', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NCO', 'Cotabato', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NEC', 'Negros Occidental', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NER', 'Negros Oriental', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NSA', 'Northern Samar', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NUE', 'Nueva Ecija', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NUV', 'Nueva Vizcaya', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PAM', 'Pampanga', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PAN', 'Pangasinan', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PLW', 'Palawan', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('QUE', 'Quezon', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('QUI', 'Quirino', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RIZ', 'Rizal', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ROM', 'Romblon', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SAR', 'Sarangani', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SCO', 'South Cotabato', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SIG', 'Siquijor', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SLE', 'Southern Leyte', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SLU', 'Sulu', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SOR', 'Sorsogon', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SUK', 'Sultan Kudarat', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SUN', 'Surigao del Norte', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SUR', 'Surigao del Sur', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TAR', 'Tarlac', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TAW', 'Tawi-Tawi', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('WSA', 'Western Samar', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZAN', 'Zamboanga del Norte', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZAS', 'Zamboanga del Sur', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZMB', 'Zambales', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZSI', 'Zamboanga Sibugay', 375); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CHA', 'Changhua', 386); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CYI', 'Chiayi', 386); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CYQ', 'Chiayi', 386); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HSQ', 'Hsinchu', 386); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HSZ', 'Hsinchu', 386); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HUA', 'Hualien', 386); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ILA', 'Yilan', 386); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KEE', 'Keelung', 386); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KHH', 'Kaohsiung', 386); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KIN', 'Kinmen', 386); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LIE', 'Lienchiang', 386); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MIA', 'Miaoli', 386); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NAN', 'Nantou', 386); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NWT', 'New Taipei', 386); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PEN', 'Penghu', 386); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PIF', 'Pingtung', 386); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TAO', 'Taoyuan', 386); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TNN', 'Tainan', 386); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TPE', 'Taipei', 386); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TTT', 'Taitung', 386); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TXG', 'Taichung', 386); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('YUN', 'Yunlin', 386); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('01', 'Zagrebačka županija', 497); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('02', 'Krapinsko-Zagorska županija', 497); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('03', 'Sisačko-Moslavačka županija', 497); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('04', 'Karlovačka županija', 497); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('05', 'Varaždinska županija', 497); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('06', 'Koprivničko-Križevačka županija', 497); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('07', 'Bjelovarsko-Bilogorska županija', 497); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('08', 'Primorsko-Goranska županija', 497); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('09', 'Ličko-Senjska županija', 497); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('10', 'Virovitičko-Podravska županija', 497); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('11', 'Požeško-Slavonska županija', 497); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('12', 'Brodsko-Posavska županija', 497); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('13', 'Zadarska županija', 497); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('14', 'Osječko-Baranjska županija', 497); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('15', 'Šibensko-Kninska županija', 497); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('16', 'Vukovarsko-Srijemska županija', 497); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('17', 'Splitsko-Dalmatinska županija', 497); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('18', 'Istarska županija', 497); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('19', 'Dubrovačko-Neretvanska županija', 497); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('20', 'Međimurska županija', 497); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('21', 'Grad Zagreb', 497); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('APA', 'Praha 1', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('APB', 'Praha 2', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('APC', 'Praha 3', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('APD', 'Praha 4', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('APE', 'Praha 5', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('APF', 'Praha 6', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('APG', 'Praha 7', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('APH', 'Praha 8', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('API', 'Praha 9', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('APJ', 'Praha 10', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BBE', 'Beroun', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BBN', 'Benesov', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BKD', 'Kladno', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BKH', 'Kutna Hora', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BKO', 'Kolin', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BMB', 'Mlada Boleslav', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BME', 'Melnik', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BNY', 'Nymburk', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BPB', 'Pribram', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BPV', 'Praha vychod', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BPZ', 'Praha zapad', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BRA', 'Rakovnik', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CBU', 'Ceske Budejovice', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CCK', 'Cesky Krumlov', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CJH', 'Jindrichuv Hradec', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CPE', 'Pelhrimov', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CPI', 'Pisek', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CPR', 'Prachatice', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CST', 'Strakonice', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CTA', 'Tabor', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DCH', 'Cheb', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DDO', 'Domazlice', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DKL', 'Klatovy', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DKV', 'Karlovy Vary', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DPJ', 'Plzen jih', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DPM', 'Plzen mesto', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DPS', 'Plzen sever', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DRO', 'Rokycany', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DSO', 'Sokolov', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DTA', 'Tachov', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ECH', 'Chomutov', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ECL', 'Ceska Lipa', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('EDE', 'Decin', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('EJA', 'Jablonec n. Nisou', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ELI', 'Liberec', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ELO', 'Louny', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ELT', 'Litomerice', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('EMO', 'Most', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ETE', 'Teplice', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('EUL', 'Usti nad Labem', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FCR', 'Chrudim', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FHB', 'Havlickuv Brod', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FHK', 'Hradec Kralove', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FJI', 'Jicin', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FNA', 'Nachod', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FPA', 'Pardubice', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FRK', 'Rychn n. Kneznou', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FSE', 'Semily', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FSV', 'Svitavy', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FTR', 'Trutnov', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('FUO', 'Usti nad Orlici', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GBL', 'Blansko', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GBM', 'Brno mesto', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GBR', 'Breclav', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GBV', 'Brno venkov', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GHO', 'Hodonin', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GJI', 'Jihlava', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GKR', 'Kromeriz', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GPR', 'Prostejov', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GTR', 'Trebic', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GUH', 'Uherske Hradiste', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GVY', 'Vyskov', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GZL', 'Zlin', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GZN', 'Znojmo', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GZS', 'Zdar nad Sazavou', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HBR', 'Bruntal', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HFM', 'Frydek-Mistek', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HJE', 'Jesenik', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HKA', 'Karvina', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HNJ', 'Novy Jicin', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HOL', 'Olomouc', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HOP', 'Opava', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HOS', 'Ostrava', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HPR', 'Prerov', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HSU', 'Sumperk', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HVS', 'Vsetin', 503); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BAA', 'Bratislava 1', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BAB', 'Bratislava 2', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BAC', 'Bratislava 3', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BAD', 'Bratislava 4', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BAE', 'Bratislava 5', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BAN', 'Banovce n. Bebr.', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BAR', 'Bardejov', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BBY', 'Banska Bystrica', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BRE', 'Brezno', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BST', 'Banska Stiavnica', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('BYT', 'Bytca', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('CAD', 'Cadca', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DET', 'Detva', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DKU', 'Dolny Kubin', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('DST', 'Dunajska Streda', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GAL', 'Galanta', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('GEL', 'Gelnica', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HLO', 'Hlohovec', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('HUM', 'Humenne', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ILA', 'Ilava', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KEA', 'Kosice 1', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KEB', 'Kosice 2', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KEC', 'Kosice 3', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KED', 'Kosice 4', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KEO', 'Kosice-okolie', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KEZ', 'Kezmarok', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KNM', 'Kysucke N. Mesto', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KOM', 'Komarno', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('KRU', 'Krupina', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LEV', 'Levoca', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LMI', 'Liptovsky Mikulas', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LUC', 'Lucenec', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('LVC', 'Levice', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MAL', 'Malacky', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MAR', 'Martin', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MED', 'Medzilaborce', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MIC', 'Michalovce', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('MYJ', 'Myjava', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NAM', 'Namestovo', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NIT', 'Nitra', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NMV', 'Nove Mesto n. Vah', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('NZA', 'Nove Zamky', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PAR', 'Partizanske', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PBY', 'Povazska Bystrica', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PEZ', 'Pezinok', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PIE', 'Piestany', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('POL', 'Poltar', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('POP', 'Poprad', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PRE', 'Presov', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PRI', 'Prievidza', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('PUC', 'Puchov', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('REV', 'Revuca', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ROZ', 'Roznava', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RSO', 'Rimavska Sobota', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('RUZ', 'Ruzomberok', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SAB', 'Sabinov', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SAL', 'Sala', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SEA', 'Senica', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SEN', 'Senec', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SKA', 'Skalica', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SLU', 'Stara Lubovna', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SNI', 'Snina', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SNV', 'Spisska Nova Ves', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SOB', 'Sobrance', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('STR', 'Stropkov', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('SVI', 'Svidnik', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TNC', 'Trencin', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TOP', 'Topolcany', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TRE', 'Trebisov', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TRN', 'Trnava', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TTE', 'Turcianske Teplice', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('TVR', 'Tvrdosin', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VKR', 'Velky Krtis', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('VRT', 'Vranov nad Toplou', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZAR', 'Zarnovica', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZIH', 'Ziar nad Hronom', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZIL', 'Zilina', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZMO', 'Zlate Moravce', 504); +INSERT OR IGNORE INTO adif_enum_primary_subdivision (code, subdivision_name, dxcc) VALUES ('ZVO', 'Zvolen', 504); diff --git a/res/sql/migration_040.sql b/res/sql/migration_040.sql new file mode 100644 index 00000000..e69de29b diff --git a/rig/Rig.cpp b/rig/Rig.cpp index 9d2c7bfc..7ad96cd8 100644 --- a/rig/Rig.cpp +++ b/rig/Rig.cpp @@ -1,6 +1,11 @@ #include "Rig.h" #include "RigctldManager.h" #include "core/debug.h" + +#include +#include +#include + #include "rig/drivers/HamlibRigDrv.h" #ifdef Q_OS_WIN #include "rig/drivers/OmnirigRigDrv.h" @@ -149,6 +154,42 @@ void Rig::stopTimer() Q_ASSERT( check ); } +void Rig::shutdown() +{ + FCT_IDENTIFICATION; + + if ( QThread::currentThread() == thread() ) + { + shutdownImpl(); + return; + } + + if ( !thread() || !thread()->isRunning() ) + { + qCWarning(runtime) << "Cannot shut down Rig because owner thread is not running"; + return; + } + + QSharedPointer shutdownDone = QSharedPointer::create(); + bool check = QMetaObject::invokeMethod(this, [this, shutdownDone]() + { + shutdownImpl(); + shutdownDone->release(); + }, Qt::QueuedConnection); + + if ( !check ) + { + qCWarning(runtime) << "Cannot queue Rig shutdown"; + return; + } + + if ( !shutdownDone->tryAcquire(1, SHUTDOWN_TIMEOUT_MS) ) + { + qCWarning(runtime) << "Rig shutdown did not finish within" + << SHUTDOWN_TIMEOUT_MS << "ms"; + } +} + void Rig::stopTimerImplt() { FCT_IDENTIFICATION; @@ -365,6 +406,14 @@ void Rig::closeImpl() __closeRig(); } +void Rig::shutdownImpl() +{ + FCT_IDENTIFICATION; + + closeImpl(); + stopTimerImplt(); +} + void Rig::__closeRig() { FCT_IDENTIFICATION; @@ -759,7 +808,17 @@ Rig::~Rig() { FCT_IDENTIFICATION; - __closeRig(); + if ( !rigDriver && !rigctldManager ) + return; + + if ( QThread::currentThread() == thread() ) + { + __closeRig(); + } + else + { + qCWarning(runtime) << "Skipping Rig shutdown from non-owner thread"; + } } void Rig::sendHeartBeat() diff --git a/rig/Rig.h b/rig/Rig.h index 9ce563dd..6e290ad9 100644 --- a/rig/Rig.h +++ b/rig/Rig.h @@ -91,6 +91,7 @@ public slots: void start(); void open(); void close(); + void shutdown(); void stopTimer(); void setFrequency(double); @@ -128,6 +129,7 @@ private slots: void stopTimerImplt(); void openImpl(); void closeImpl(); + void shutdownImpl(); void setVFOFrequencyImpl(VFOID, double); void setSplitImpl(bool); @@ -143,6 +145,8 @@ private slots: void sendHeartBeat(); private: + static const int SHUTDOWN_TIMEOUT_MS = 5000; + class DrvParams { public: diff --git a/rig/RigctldManager.cpp b/rig/RigctldManager.cpp index 5cbeb53c..94afd111 100644 --- a/rig/RigctldManager.cpp +++ b/rig/RigctldManager.cpp @@ -20,7 +20,15 @@ RigctldManager::RigctldManager(QObject *parent) RigctldManager::~RigctldManager() { FCT_IDENTIFICATION; - stop(); + + if ( thread() == QThread::currentThread() ) + { + stop(); + } + else if ( rigctldProcess ) + { + qCWarning(runtime) << "Skipping rigctld shutdown from non-owner thread"; + } } bool RigctldManager::start(const RigProfile &profile) @@ -134,28 +142,30 @@ void RigctldManager::stop() { FCT_IDENTIFICATION; - if ( !rigctldProcess ) return; + if ( !rigctldProcess || stoppingInProgress ) return; stoppingInProgress = true; - if ( rigctldProcess->state() != QProcess::NotRunning ) - { - qCDebug(runtime) << "Stopping rigctld"; + QProcess *process = rigctldProcess; + rigctldProcess = nullptr; - // Disconnect signals to prevent error notifications during controlled shutdown - rigctldProcess->disconnect(); + // During a normal stop, stop receiving this process's signals here. Leave + // any other QProcess connections alone. + disconnect(process, nullptr, this, nullptr); - rigctldProcess->terminate(); - if ( !rigctldProcess->waitForFinished(3000) ) + if ( process->state() != QProcess::NotRunning ) + { + qCDebug(runtime) << "Stopping rigctld"; + process->terminate(); + if ( !process->waitForFinished(3000) ) { qCWarning(runtime) << "rigctld did not terminate gracefully, killing"; - rigctldProcess->kill(); - rigctldProcess->waitForFinished(1000); + process->kill(); + process->waitForFinished(1000); } } - delete rigctldProcess; - rigctldProcess = nullptr; + delete process; stoppingInProgress = false; emit stopped(); diff --git a/rig/drivers/HamlibRigDrv.cpp b/rig/drivers/HamlibRigDrv.cpp index 4869fc82..52170905 100644 --- a/rig/drivers/HamlibRigDrv.cpp +++ b/rig/drivers/HamlibRigDrv.cpp @@ -277,6 +277,23 @@ bool HamlibRigDrv::open() qCDebug(runtime) << "Rig Open Error" << lastErrorText; // return false; ignore the error - no critical } + +#ifdef RIG_MODEL_FT950 + // Tentative workaround for #1017: FT-950 was reported to jump to VFO-B and + // back on band changes. This is untested on a real FT-950, but Hamlib + // sources suggest that disabling Yaesu band-select restore should avoid + // extra VFO queries after set_freq(). + if ( rigProfile.model == RIG_MODEL_FT950 ) + { + const auto token = rig_token_lookup(rig, "disable_yaesu_bandselect"); + + if ( token != RIG_CONF_END && rig_set_conf(rig, token, "1") != RIG_OK ) + { + qCDebug(runtime) << "Cannot set disable_yaesu_bandselect to 1"; + } + } +#endif + #if 0 if ( rig_set_conf(rig, rig_token_lookup(rig, "no_xchg"), "1") != RIG_OK ) { diff --git a/rotator/Rotator.cpp b/rotator/Rotator.cpp index 6b5e0cfa..def0d90e 100644 --- a/rotator/Rotator.cpp +++ b/rotator/Rotator.cpp @@ -2,6 +2,9 @@ #include "core/debug.h" #include "rotator/drivers/HamlibRotDrv.h" #include "rotator/drivers/PSTRotDrv.h" +#include +#include +#include MODULE_IDENTIFICATION("qlog.rotator.rotator"); @@ -35,7 +38,17 @@ Rotator::~Rotator() { FCT_IDENTIFICATION; + if ( !rotDriver ) + return; + + if ( QThread::currentThread() != thread() ) + { + qCWarning(runtime) << "Skipping Rotator shutdown from non-owner thread"; + return; + } + __closeRot(); + stopTimerImplt(); } double Rotator::getAzimuth() @@ -114,6 +127,42 @@ void Rotator::stopTimer() Q_ASSERT( check ); } +void Rotator::shutdown() +{ + FCT_IDENTIFICATION; + + if ( QThread::currentThread() == thread() ) + { + shutdownImpl(); + return; + } + + if ( !thread() || !thread()->isRunning() ) + { + qCWarning(runtime) << "Cannot shut down Rotator because owner thread is not running"; + return; + } + + QSharedPointer shutdownDone = QSharedPointer::create(); + bool check = QMetaObject::invokeMethod(this, [this, shutdownDone]() + { + shutdownImpl(); + shutdownDone->release(); + }, Qt::QueuedConnection); + + if ( !check ) + { + qCWarning(runtime) << "Cannot queue Rotator shutdown"; + return; + } + + if ( !shutdownDone->tryAcquire(1, SHUTDOWN_TIMEOUT_MS) ) + { + qCWarning(runtime) << "Rotator shutdown did not finish within" + << SHUTDOWN_TIMEOUT_MS << "ms"; + } +} + void Rotator::stopTimerImplt() { FCT_IDENTIFICATION; @@ -259,6 +308,14 @@ void Rotator::closeImpl() __closeRot(); } +void Rotator::shutdownImpl() +{ + FCT_IDENTIFICATION; + + closeImpl(); + stopTimerImplt(); +} + void Rotator::__closeRot() { FCT_IDENTIFICATION; diff --git a/rotator/Rotator.h b/rotator/Rotator.h index 8a9c5956..dc614f96 100644 --- a/rotator/Rotator.h +++ b/rotator/Rotator.h @@ -42,6 +42,7 @@ public slots: void start(); void open(); void close(); + void shutdown(); void stopTimer(); void sendState(); @@ -52,9 +53,12 @@ private slots: void stopTimerImplt(); void openImpl(); void closeImpl(); + void shutdownImpl(); void sendStateImpl(); private: + static const int SHUTDOWN_TIMEOUT_MS = 5000; + Rotator(QObject *parent = nullptr); ~Rotator(); diff --git a/service/emailqsl/EmailQSLService.cpp b/service/emailqsl/EmailQSLService.cpp new file mode 100644 index 00000000..7a80effe --- /dev/null +++ b/service/emailqsl/EmailQSLService.cpp @@ -0,0 +1,824 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "EmailQSLService.h" +#include "core/debug.h" +#include "core/LogParam.h" + +MODULE_IDENTIFICATION("qlog.service.emailqsl"); + +// --------------------------------------------------------------------------- +// EmailQSLFieldOverlay +// --------------------------------------------------------------------------- + +QJsonObject EmailQSLFieldOverlay::toJson() const +{ + QJsonObject o; + o["type"] = type; + o["fieldName"] = fieldName; + o["x"] = x; + o["y"] = y; + o["fontFamily"] = fontFamily; + o["fontSize"] = fontSize; + o["color"] = color; + o["bold"] = bold; + o["italic"] = italic; + o["width"] = width; + o["height"] = height; + o["fillColor"] = fillColor; + o["opacity"] = opacity; + o["cornerRadius"] = cornerRadius; + return o; +} + +EmailQSLFieldOverlay EmailQSLFieldOverlay::fromJson(const QJsonObject &obj) +{ + EmailQSLFieldOverlay f; + f.type = obj.value("type").toString(QStringLiteral("TEXT")); + f.fieldName = obj.value("fieldName").toString(); + f.x = obj.value("x").toInt(); + f.y = obj.value("y").toInt(); + f.fontFamily = obj.value("fontFamily").toString(QStringLiteral("Arial")); + f.fontSize = obj.value("fontSize").toInt(14); + f.color = obj.value("color").toString(QStringLiteral("#000000")); + f.bold = obj.value("bold").toBool(); + f.italic = obj.value("italic").toBool(); + f.width = obj.value("width").toInt(120); + f.height = obj.value("height").toInt(60); + f.fillColor = obj.value("fillColor").toString(QStringLiteral("#FFFF99")); + f.opacity = obj.value("opacity").toInt(80); + f.cornerRadius = obj.value("cornerRadius").toInt(8); + return f; +} + +// --------------------------------------------------------------------------- +// EmailQSLBase — credential registration +// --------------------------------------------------------------------------- + +const QString EmailQSLBase::SECURE_STORAGE_KEY = QStringLiteral("EmailQSL"); +REGISTRATION_SECURE_SERVICE(EmailQSLBase); + +void EmailQSLBase::registerCredentials() +{ + CredentialRegistry::instance().add(SECURE_STORAGE_KEY, []() + { + return QList + { + { SECURE_STORAGE_KEY, []() { return getSmtpUsername(); } } + }; + }); +} + +// --------------------------------------------------------------------------- +// EmailQSLBase — settings persisted via LogParam (log_param DB table) +// --------------------------------------------------------------------------- + +QString EmailQSLBase::getSmtpHost() +{ + return LogParam::getEmailQSLSmtpHost(); +} + +void EmailQSLBase::setSmtpHost(const QString &host) +{ + LogParam::setEmailQSLSmtpHost(host); +} + +int EmailQSLBase::getSmtpPort() +{ + return LogParam::getEmailQSLSmtpPort(); +} + +void EmailQSLBase::setSmtpPort(int port) +{ + LogParam::setEmailQSLSmtpPort(port); +} + +int EmailQSLBase::getSmtpEncryption() +{ + return LogParam::getEmailQSLSmtpEncryption(); +} + +void EmailQSLBase::setSmtpEncryption(int enc) +{ + LogParam::setEmailQSLSmtpEncryption(enc); +} + +QString EmailQSLBase::getSmtpUsername() +{ + return LogParam::getEmailQSLSmtpUsername(); +} + +void EmailQSLBase::setSmtpUsername(const QString &username) +{ + LogParam::setEmailQSLSmtpUsername(username); +} + +QString EmailQSLBase::getSmtpPassword() +{ + return getPassword(SECURE_STORAGE_KEY, getSmtpUsername()); +} + +void EmailQSLBase::saveSmtpCredentials(const QString &username, const QString &password) +{ + const QString oldUsername = getSmtpUsername(); + if (oldUsername != username && !oldUsername.isEmpty()) + deletePassword(SECURE_STORAGE_KEY, oldUsername); + setSmtpUsername(username); + savePassword(SECURE_STORAGE_KEY, username, password); +} + +QString EmailQSLBase::getFromAddress() +{ + return LogParam::getEmailQSLFromAddress(); +} + +void EmailQSLBase::setFromAddress(const QString &addr) +{ + LogParam::setEmailQSLFromAddress(addr); +} + +QString EmailQSLBase::getFromName() +{ + return LogParam::getEmailQSLFromName(); +} + +void EmailQSLBase::setFromName(const QString &name) +{ + LogParam::setEmailQSLFromName(name); +} + +QString EmailQSLBase::getSubjectTemplate() +{ + return LogParam::getEmailQSLSubjectTemplate( + QStringLiteral("QSL Card from {MY_CALLSIGN} for our QSO on {QSO_DATE}")); +} + +void EmailQSLBase::setSubjectTemplate(const QString &tmpl) +{ + LogParam::setEmailQSLSubjectTemplate(tmpl); +} + +QString EmailQSLBase::getBodyTemplate() +{ + return LogParam::getEmailQSLBodyTemplate( + QStringLiteral("Dear {NAME},\n\nPlease find my QSL card attached confirming our contact.\n\n" + "Callsign: {MY_CALLSIGN}\n" + "Date: {QSO_DATE}\nTime: {TIME_ON} UTC\n" + "Band: {BAND}\nMode: {MODE}\n" + "RST Sent: {RST_SENT}\nRST Rcvd: {RST_RCVD}\n\n" + "73,\n{MY_CALLSIGN}")); +} + +void EmailQSLBase::setBodyTemplate(const QString &tmpl) +{ + LogParam::setEmailQSLBodyTemplate(tmpl); +} + +QString EmailQSLBase::getCardImagePath() +{ + return LogParam::getEmailQSLCardImagePath(); +} + +void EmailQSLBase::setCardImagePath(const QString &path) +{ + LogParam::setEmailQSLCardImagePath(path); +} + +QList EmailQSLBase::getCardFieldOverlays() +{ + QList result; + const QJsonArray arr = QJsonDocument::fromJson(LogParam::getEmailQSLCardOverlays()).array(); + for (const QJsonValue &v : arr) + result.append(EmailQSLFieldOverlay::fromJson(v.toObject())); + return result; +} + +void EmailQSLBase::setCardFieldOverlays(const QList &overlays) +{ + QJsonArray arr; + for (const EmailQSLFieldOverlay &o : overlays) + arr.append(o.toJson()); + LogParam::setEmailQSLCardOverlays(QJsonDocument(arr).toJson(QJsonDocument::Compact)); +} + +// --------------------------------------------------------------------------- +// EmailQSLBase — sent-tracking via contacts.fields JSON +// --------------------------------------------------------------------------- + +static const QString EMAIL_QSL_SENT_KEY = QStringLiteral("email_qsl_sent_dt"); + +QDateTime EmailQSLBase::getEmailSentDateTime(const QSqlRecord &record) +{ + const QByteArray raw = record.value(QStringLiteral("fields")).toByteArray(); + const QJsonObject fields = QJsonDocument::fromJson(raw).object(); + const QString dtStr = fields.value(EMAIL_QSL_SENT_KEY).toString(); + return dtStr.isEmpty() ? QDateTime() : QDateTime::fromString(dtStr, Qt::ISODate); +} + +bool EmailQSLBase::hasEmailBeenSentToCallsign(const QString &callsign, int excludeId) +{ + QSqlQuery q; + q.prepare(QStringLiteral("SELECT fields FROM contacts WHERE callsign = :cs AND id != :ex")); + q.bindValue(":cs", callsign.toUpper()); + q.bindValue(":ex", excludeId); + if (!q.exec()) + return false; + + while (q.next()) + { + const QByteArray raw = q.value(0).toByteArray(); + const QJsonObject fields = QJsonDocument::fromJson(raw).object(); + if (fields.contains(EMAIL_QSL_SENT_KEY)) + return true; + } + return false; +} + +void EmailQSLBase::recordEmailSent(int contactId, const QSqlRecord ¤tRecord) +{ + FCT_IDENTIFICATION; + + const QByteArray raw = currentRecord.value(QStringLiteral("fields")).toByteArray(); + QJsonObject fields = QJsonDocument::fromJson(raw).object(); + fields[EMAIL_QSL_SENT_KEY] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + QSqlQuery q; + q.prepare(QStringLiteral("UPDATE contacts SET fields = :f WHERE id = :id")); + q.bindValue(":f", QJsonDocument(fields).toJson(QJsonDocument::Compact)); + q.bindValue(":id", contactId); + if (!q.exec()) + qCWarning(runtime) << "recordEmailSent update failed:" << q.lastError().text(); +} + +// --------------------------------------------------------------------------- +// EmailQSLBase — merge fields +// --------------------------------------------------------------------------- + +QString EmailQSLBase::fieldValue(const QString &key, const QSqlRecord &record) +{ + if (key == QLatin1String("CALLSIGN")) + return record.value(QStringLiteral("callsign")).toString().toUpper(); + + if (key == QLatin1String("QSO_DATE")) + { + const QDateTime dt = record.value(QStringLiteral("start_time")).toDateTime(); + return dt.isValid() ? dt.toUTC().toString(QStringLiteral("dd-MMM-yyyy")).toUpper() : QString(); + } + if (key == QLatin1String("QSO_DATE_ISO")) + { + const QDateTime dt = record.value(QStringLiteral("start_time")).toDateTime(); + return dt.isValid() ? dt.toUTC().toString(QStringLiteral("yyyyMMdd")) : QString(); + } + if (key == QLatin1String("TIME_ON")) + { + const QDateTime dt = record.value(QStringLiteral("start_time")).toDateTime(); + return dt.isValid() ? dt.toUTC().toString(QStringLiteral("HHmm")) : QString(); + } + if (key == QLatin1String("FREQ")) + { + bool ok; + double mhz = record.value(QStringLiteral("freq")).toDouble(&ok); + return ok ? QString::number(mhz, 'f', 3) : QString(); + } + if (key == QLatin1String("BAND")) return record.value(QStringLiteral("band")).toString().toUpper(); + if (key == QLatin1String("MODE")) return record.value(QStringLiteral("mode")).toString().toUpper(); + if (key == QLatin1String("SUBMODE")) return record.value(QStringLiteral("submode")).toString().toUpper(); + if (key == QLatin1String("RST_SENT")) return record.value(QStringLiteral("rst_sent")).toString(); + if (key == QLatin1String("RST_RCVD")) return record.value(QStringLiteral("rst_rcvd")).toString(); + if (key == QLatin1String("NAME")) + { + const QString n = record.value(QStringLiteral("name_intl")).toString(); + return n.isEmpty() ? record.value(QStringLiteral("name")).toString() : n; + } + if (key == QLatin1String("QTH")) + { + const QString q = record.value(QStringLiteral("qth_intl")).toString(); + return q.isEmpty() ? record.value(QStringLiteral("qth")).toString() : q; + } + if (key == QLatin1String("COUNTRY")) + { + const QString c = record.value(QStringLiteral("country_intl")).toString(); + return c.isEmpty() ? record.value(QStringLiteral("country")).toString() : c; + } + if (key == QLatin1String("GRIDSQUARE")) return record.value(QStringLiteral("gridsquare")).toString().toUpper(); + if (key == QLatin1String("DXCC")) return record.value(QStringLiteral("dxcc")).toString(); + if (key == QLatin1String("CQZ")) return record.value(QStringLiteral("cqz")).toString(); + if (key == QLatin1String("ITUZ")) return record.value(QStringLiteral("ituz")).toString(); + if (key == QLatin1String("TX_PWR")) return record.value(QStringLiteral("tx_pwr")).toString(); + if (key == QLatin1String("EMAIL")) return record.value(QStringLiteral("email")).toString(); + if (key == QLatin1String("MY_CALLSIGN")) return record.value(QStringLiteral("station_callsign")).toString().toUpper(); + if (key == QLatin1String("MY_GRIDSQUARE")) return record.value(QStringLiteral("my_gridsquare")).toString().toUpper(); + if (key == QLatin1String("OPERATOR")) return record.value(QStringLiteral("operator")).toString().toUpper(); + if (key == QLatin1String("COMMENT")) + { + const QString c = record.value(QStringLiteral("comment_intl")).toString(); + return c.isEmpty() ? record.value(QStringLiteral("comment")).toString() : c; + } + if (key == QLatin1String("SOTA_REF")) return record.value(QStringLiteral("sota_ref")).toString(); + if (key == QLatin1String("POTA_REF")) return record.value(QStringLiteral("pota_ref")).toString(); + if (key == QLatin1String("WWFF_REF")) return record.value(QStringLiteral("wwff_ref")).toString(); + if (key == QLatin1String("IOTA")) return record.value(QStringLiteral("iota")).toString(); + if (key == QLatin1String("SIG")) return record.value(QStringLiteral("sig_intl")).toString(); + if (key == QLatin1String("CONTEST_ID")) return record.value(QStringLiteral("contest_id")).toString(); + + // Fallback: try direct lowercase column name + const QString col = key.toLower(); + if (record.indexOf(col) >= 0) + return record.value(col).toString(); + + return QString(); +} + +QString EmailQSLBase::applyMergeFields(const QString &tmpl, const QSqlRecord &record) +{ + QString result = tmpl; + static const QRegularExpression rx(QStringLiteral("\\{([A-Z0-9_]+)\\}")); + QRegularExpressionMatchIterator it = rx.globalMatch(tmpl); + while (it.hasNext()) + { + const QRegularExpressionMatch m = it.next(); + const QString key = m.captured(1); + const QString value = fieldValue(key, record); + result.replace(QLatin1Char('{') + key + QLatin1Char('}'), value); + } + return result; +} + +QList EmailQSLBase::availableMergeFields() +{ + return { + { "CALLSIGN", QObject::tr("Contact callsign") }, + { "QSO_DATE", QObject::tr("QSO date (dd-MMM-yyyy)") }, + { "QSO_DATE_ISO", QObject::tr("QSO date (YYYYMMDD)") }, + { "TIME_ON", QObject::tr("QSO start time UTC (HHmm)") }, + { "FREQ", QObject::tr("Frequency (MHz)") }, + { "BAND", QObject::tr("Band") }, + { "MODE", QObject::tr("Mode") }, + { "SUBMODE", QObject::tr("Sub-mode") }, + { "RST_SENT", QObject::tr("RST sent") }, + { "RST_RCVD", QObject::tr("RST received") }, + { "NAME", QObject::tr("Contact name") }, + { "QTH", QObject::tr("Contact QTH") }, + { "COUNTRY", QObject::tr("Country") }, + { "GRIDSQUARE", QObject::tr("Grid square") }, + { "DXCC", QObject::tr("DXCC entity number") }, + { "CQZ", QObject::tr("CQ zone") }, + { "ITUZ", QObject::tr("ITU zone") }, + { "TX_PWR", QObject::tr("TX power") }, + { "EMAIL", QObject::tr("Contact email address") }, + { "MY_CALLSIGN", QObject::tr("My callsign") }, + { "MY_GRIDSQUARE", QObject::tr("My grid square") }, + { "OPERATOR", QObject::tr("Operator callsign") }, + { "COMMENT", QObject::tr("Comment") }, + { "SOTA_REF", QObject::tr("SOTA reference") }, + { "POTA_REF", QObject::tr("POTA reference") }, + { "WWFF_REF", QObject::tr("WWFF reference") }, + { "IOTA", QObject::tr("IOTA reference") }, + { "SIG", QObject::tr("Special interest group") }, + { "CONTEST_ID", QObject::tr("Contest ID") }, + }; +} + +// --------------------------------------------------------------------------- +// EmailQSLBase — card rendering +// --------------------------------------------------------------------------- + +QPixmap EmailQSLBase::renderCard(const QSqlRecord &record) +{ + FCT_IDENTIFICATION; + return renderCard(getCardImagePath(), record, getCardFieldOverlays()); +} + +QPixmap EmailQSLBase::renderCard(const QString &imagePath, + const QSqlRecord &record, + const QList &overlays) +{ + FCT_IDENTIFICATION; + + QPixmap pixmap(imagePath); + if (pixmap.isNull()) + { + qCWarning(runtime) << "renderCard: could not load image:" << imagePath; + return QPixmap(); + } + + // Overlay x, y, fontSize and BOX width/height are all stored as absolute + // pixel values scaled to the source image dimensions. No extra scaling is + // applied here — the values are used as-is. addDefaultOverlays() and + // onCardImageChanged() are responsible for keeping them proportional whenever + // the background image changes. + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing); + painter.setRenderHint(QPainter::TextAntialiasing); + + for (const EmailQSLFieldOverlay &ov : overlays) + { + if (ov.type == QLatin1String("BOX")) + { + QColor fill(ov.fillColor); + fill.setAlphaF(ov.opacity / 100.0); + painter.setPen(QPen(QColor(ov.color), 1.5)); + painter.setBrush(fill); + painter.drawRoundedRect(ov.x, ov.y, ov.width, ov.height, + ov.cornerRadius, ov.cornerRadius); + + if (!ov.fieldName.isEmpty()) + { + QFont font(ov.fontFamily, ov.fontSize > 0 ? ov.fontSize : 11); + font.setBold(ov.bold); + painter.setFont(font); + painter.setPen(QColor(ov.color)); + painter.setBrush(Qt::NoBrush); + QFontMetrics fm(font); + painter.drawText(ov.x, ov.y - fm.descent() - 2, ov.fieldName); + } + } + else if (ov.type == QLatin1String("LABEL")) + { + if (ov.fieldName.isEmpty()) + continue; + + QFont font(ov.fontFamily, ov.fontSize); + font.setBold(ov.bold); + font.setItalic(ov.italic); + painter.setFont(font); + painter.setPen(QColor(ov.color)); + painter.setBrush(Qt::NoBrush); + painter.drawText(ov.x, ov.y, ov.fieldName); + } + else // TEXT — merge-field substitution + { + const QString value = fieldValue(ov.fieldName, record); + if (value.isEmpty()) + continue; + + QFont font(ov.fontFamily, ov.fontSize); + font.setBold(ov.bold); + font.setItalic(ov.italic); + painter.setFont(font); + painter.setPen(QColor(ov.color)); + painter.setBrush(Qt::NoBrush); + painter.drawText(ov.x, ov.y, value); + } + } + painter.end(); + return pixmap; +} + +// --------------------------------------------------------------------------- +// SmtpWorker +// --------------------------------------------------------------------------- + +SmtpWorker::SmtpWorker(const QString &host, int port, int encryption, + const QString &username, const QString &password, + const QString &fromAddress, const QString &fromName, + const QString &toAddress, + const QString &subject, const QString &body, + const QByteArray &imageData, const QString &imageName, + QObject *parent) + : QObject(parent), + m_host(host), m_port(port), m_encryption(encryption), + m_username(username), m_password(password), + m_fromAddress(fromAddress), m_fromName(fromName), + m_toAddress(toAddress), + m_subject(subject), m_body(body), + m_imageData(imageData), m_imageName(imageName) +{ +} + +bool SmtpWorker::waitForResponse(QSslSocket *socket, QByteArray &out, int timeoutMs) +{ + out.clear(); + // Multi-line responses end with "NNN " (space after code); single with "NNN " + // Keep reading until we get a line without a dash after the code. + do { + if (!socket->waitForReadyRead(timeoutMs)) + return false; + out += socket->readAll(); + } while (out.size() > 3 && out[out.size()-2] != '\r' && + (out.size() < 4 || out[3] == '-')); + // More robust: check if last complete line starts with "NNN " (no dash) + // Parse the last CRLF-terminated line + const QList lines = out.split('\n'); + for (const QByteArray &line : lines) + { + if (line.length() >= 4 && line[3] == ' ') + return true; // found a final response line + if (line.length() >= 4 && line[3] == '-') + continue; // continuation line + } + return !out.isEmpty(); +} + +int SmtpWorker::responseCode(const QByteArray &response) +{ + // Find the last "NNN " line + const QList lines = response.split('\n'); + for (int i = lines.size() - 1; i >= 0; --i) + { + const QByteArray &line = lines.at(i).trimmed(); + if (line.length() >= 3) + { + bool ok; + int code = line.left(3).toInt(&ok); + if (ok) + return code; + } + } + return -1; +} + +bool SmtpWorker::sendCommand(QSslSocket *socket, const QByteArray &cmd, + int expectedCode, QByteArray &response) +{ + socket->write(cmd); + if (!socket->waitForBytesWritten(10000)) + return false; + if (!waitForResponse(socket, response)) + return false; + return responseCode(response) == expectedCode; +} + +QByteArray SmtpWorker::buildMimeMessage() +{ + const QString boundary = QStringLiteral("----QLogEmailQSL_%1") + .arg(QDateTime::currentMSecsSinceEpoch()); + + QByteArray msg; + + // Encode From display name safely + const QByteArray fromDisplay = ("\"" + m_fromName + "\" <" + m_fromAddress + ">").toUtf8(); + msg += "MIME-Version: 1.0\r\n"; + msg += "From: " + fromDisplay + "\r\n"; + msg += "To: <" + m_toAddress.toUtf8() + ">\r\n"; + // RFC 2047 encoded subject + msg += "Subject: =?UTF-8?B?" + m_subject.toUtf8().toBase64() + "?=\r\n"; + msg += "Content-Type: multipart/mixed; boundary=\"" + boundary.toUtf8() + "\"\r\n"; + msg += "\r\n"; + + // --- Text part --- + msg += "--" + boundary.toUtf8() + "\r\n"; + msg += "Content-Type: text/plain; charset=UTF-8\r\n"; + msg += "Content-Transfer-Encoding: base64\r\n"; + msg += "\r\n"; + const QByteArray bodyB64 = m_body.toUtf8().toBase64(); + for (int i = 0; i < bodyB64.size(); i += 76) + msg += bodyB64.mid(i, 76) + "\r\n"; + + // --- Image attachment (if present) --- + if (!m_imageData.isEmpty()) + { + msg += "\r\n--" + boundary.toUtf8() + "\r\n"; + const QString mimeType = m_imageName.endsWith(QLatin1String(".png"), Qt::CaseInsensitive) + ? QStringLiteral("image/png") + : QStringLiteral("image/jpeg"); + msg += "Content-Type: " + mimeType.toUtf8() + + "; name=\"" + m_imageName.toUtf8() + "\"\r\n"; + msg += "Content-Transfer-Encoding: base64\r\n"; + msg += "Content-Disposition: attachment; filename=\"" + + m_imageName.toUtf8() + "\"\r\n"; + msg += "\r\n"; + const QByteArray imgB64 = m_imageData.toBase64(); + for (int i = 0; i < imgB64.size(); i += 76) + msg += imgB64.mid(i, 76) + "\r\n"; + } + + msg += "\r\n--" + boundary.toUtf8() + "--\r\n"; + return msg; +} + +void SmtpWorker::run() +{ + FCT_IDENTIFICATION; + + QSslSocket socket; + socket.setProtocol(QSsl::AnyProtocol); + socket.setPeerVerifyMode(QSslSocket::VerifyNone); // tolerate self-signed certs + + // ---- Connect ---- + if (m_encryption == EmailQSLBase::ENCRYPTION_SSL_TLS) + { + socket.connectToHostEncrypted(m_host, static_cast(m_port)); + if (!socket.waitForEncrypted(15000)) + { + emit finished(false, tr("SSL/TLS connection failed: %1").arg(socket.errorString())); + return; + } + } + else + { + socket.connectToHost(m_host, static_cast(m_port)); + if (!socket.waitForConnected(15000)) + { + emit finished(false, tr("Could not connect to %1:%2 — %3") + .arg(m_host).arg(m_port).arg(socket.errorString())); + return; + } + } + + // ---- Read greeting (220) ---- + QByteArray resp; + if (!waitForResponse(&socket, resp) || responseCode(resp) != 220) + { + emit finished(false, tr("Server did not send a valid greeting (expected 220).")); + return; + } + + // ---- EHLO ---- + const QByteArray localHost = QHostInfo::localHostName().toUtf8(); + if (!sendCommand(&socket, "EHLO " + localHost + "\r\n", 250, resp)) + { + // Try HELO as fallback + if (!sendCommand(&socket, "HELO " + localHost + "\r\n", 250, resp)) + { + emit finished(false, tr("EHLO/HELO rejected by server.")); + return; + } + } + + // ---- STARTTLS upgrade ---- + if (m_encryption == EmailQSLBase::ENCRYPTION_STARTTLS) + { + if (!sendCommand(&socket, "STARTTLS\r\n", 220, resp)) + { + emit finished(false, tr("STARTTLS not accepted by server.")); + return; + } + socket.startClientEncryption(); + if (!socket.waitForEncrypted(15000)) + { + emit finished(false, tr("TLS handshake failed: %1").arg(socket.errorString())); + return; + } + // Re-EHLO after TLS negotiation + if (!sendCommand(&socket, "EHLO " + localHost + "\r\n", 250, resp)) + { + emit finished(false, tr("EHLO after STARTTLS rejected.")); + return; + } + } + + // ---- AUTH LOGIN ---- + if (!m_username.isEmpty()) + { + if (!sendCommand(&socket, "AUTH LOGIN\r\n", 334, resp)) + { + emit finished(false, tr("AUTH LOGIN not supported by server.")); + return; + } + if (!sendCommand(&socket, m_username.toUtf8().toBase64() + "\r\n", 334, resp)) + { + emit finished(false, tr("Username rejected by server.")); + return; + } + if (!sendCommand(&socket, m_password.toUtf8().toBase64() + "\r\n", 235, resp)) + { + emit finished(false, tr("Authentication failed — check your username and password.")); + return; + } + } + + // ---- MAIL FROM ---- + if (!sendCommand(&socket, "MAIL FROM:<" + m_fromAddress.toUtf8() + ">\r\n", 250, resp)) + { + emit finished(false, tr("MAIL FROM rejected: %1").arg(QString::fromUtf8(resp))); + return; + } + + // ---- RCPT TO ---- + if (!sendCommand(&socket, "RCPT TO:<" + m_toAddress.toUtf8() + ">\r\n", 250, resp)) + { + emit finished(false, tr("Recipient address not accepted: %1").arg(QString::fromUtf8(resp))); + return; + } + + // ---- DATA ---- + if (!sendCommand(&socket, "DATA\r\n", 354, resp)) + { + emit finished(false, tr("DATA command rejected.")); + return; + } + + // ---- Send message body ---- + QByteArray mime = buildMimeMessage(); + mime += "\r\n.\r\n"; + socket.write(mime); + socket.flush(); + + if (!waitForResponse(&socket, resp) || responseCode(resp) != 250) + { + emit finished(false, tr("Message rejected by server: %1").arg(QString::fromUtf8(resp))); + return; + } + + // ---- QUIT ---- + socket.write("QUIT\r\n"); + socket.waitForBytesWritten(5000); + socket.disconnectFromHost(); + socket.waitForDisconnected(5000); + + emit finished(true, QString()); +} + +// --------------------------------------------------------------------------- +// EmailQSLService +// --------------------------------------------------------------------------- + +EmailQSLService::EmailQSLService(QObject *parent) + : QObject(parent) +{ +} + +void EmailQSLService::sendEmailQSL(const QSqlRecord &record) +{ + FCT_IDENTIFICATION; + + // Render card at full source resolution (fonts scale with image size) + const QPixmap cardPixmap = EmailQSLBase::renderCard(record); + QByteArray imageData; + QString imageName; + if (!cardPixmap.isNull()) + { + // Scale down to EMAIL_WIDTH for the attachment — keeps file size reasonable + // while still looking sharp on any screen. The full-res pixmap is only + // used when the user explicitly saves via "Save Card…". + static constexpr int EMAIL_WIDTH = 1280; + const QPixmap emailPixmap = (cardPixmap.width() > EMAIL_WIDTH) + ? cardPixmap.scaledToWidth(EMAIL_WIDTH, Qt::SmoothTransformation) + : cardPixmap; + + QBuffer buf(&imageData); + buf.open(QIODevice::WriteOnly); + emailPixmap.save(&buf, "JPEG", 90); + imageName = QStringLiteral("qsl_card.jpg"); + } + + // Merge email fields + const QString subject = EmailQSLBase::applyMergeFields( + EmailQSLBase::getSubjectTemplate(), record); + const QString body = EmailQSLBase::applyMergeFields( + EmailQSLBase::getBodyTemplate(), record); + + const QString toAddress = record.value(QStringLiteral("email")).toString().trimmed(); + + QThread *thread = new QThread(this); + SmtpWorker *worker = new SmtpWorker( + EmailQSLBase::getSmtpHost(), + EmailQSLBase::getSmtpPort(), + EmailQSLBase::getSmtpEncryption(), + EmailQSLBase::getSmtpUsername(), + EmailQSLBase::getSmtpPassword(), + EmailQSLBase::getFromAddress(), + EmailQSLBase::getFromName(), + toAddress, + subject, body, + imageData, imageName); + + worker->moveToThread(thread); + + connect(thread, &QThread::started, worker, &SmtpWorker::run); + connect(worker, &SmtpWorker::finished, this, + [this](bool ok, const QString &msg) { emit sendFinished(ok, msg); }); + connect(worker, &SmtpWorker::finished, worker, &QObject::deleteLater); + connect(worker, &SmtpWorker::finished, thread, &QThread::quit); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + + thread->start(); +} + +void EmailQSLService::testConnection(const QString &host, int port, int encryption, + const QString &username, const QString &password) +{ + FCT_IDENTIFICATION; + + // Send a dummy message to a no-op address just to verify auth works — + // actually we just connect + EHLO + AUTH and then QUIT. + QThread *thread = new QThread(this); + SmtpWorker *worker = new SmtpWorker( + host, port, encryption, username, password, + username, QStringLiteral("Test"), + username, // to = from (won't actually be sent) + QStringLiteral("QLog connection test"), + QStringLiteral("Connection test only — no message will be sent."), + QByteArray(), QString()); + + worker->moveToThread(thread); + + connect(thread, &QThread::started, worker, &SmtpWorker::run); + connect(worker, &SmtpWorker::finished, this, + [this](bool ok, const QString &msg) { emit testFinished(ok, msg); }); + connect(worker, &SmtpWorker::finished, worker, &QObject::deleteLater); + connect(worker, &SmtpWorker::finished, thread, &QThread::quit); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + + thread->start(); +} diff --git a/service/emailqsl/EmailQSLService.h b/service/emailqsl/EmailQSLService.h new file mode 100644 index 00000000..73a6f213 --- /dev/null +++ b/service/emailqsl/EmailQSLService.h @@ -0,0 +1,169 @@ +#ifndef QLOG_SERVICE_EMAILQSLSERVICE_H +#define QLOG_SERVICE_EMAILQSLSERVICE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/CredentialStore.h" + +// Describes a single overlay drawn on the QSL card image. +// type == "TEXT" → renders a merge-field value as text +// type == "BOX" → renders a filled rounded rectangle (label is optional caption) +struct EmailQSLFieldOverlay +{ + // Common + QString type = QStringLiteral("TEXT"); // "TEXT" or "BOX" + QString fieldName; // TEXT: merge key; BOX: optional caption label + int x = 0; + int y = 0; + + // TEXT-only + QString fontFamily = QStringLiteral("Arial"); + int fontSize = 14; + QString color = QStringLiteral("#000000"); // text color / BOX border color + bool bold = false; + bool italic = false; + + // BOX-only + int width = 120; + int height = 60; + QString fillColor = QStringLiteral("#FFFF99"); // box fill (no alpha — use opacity) + int opacity = 80; // fill opacity 0–100 + int cornerRadius = 8; + + QJsonObject toJson() const; + static EmailQSLFieldOverlay fromJson(const QJsonObject &obj); +}; + +// Static helpers for reading/writing all Email QSL settings. +// The SMTP password is kept in the secure credential store; +// everything else is persisted via LogParam (log_param DB table). +class EmailQSLBase : public SecureServiceBase +{ +public: + DECLARE_SECURE_SERVICE(EmailQSLBase) + static const QString SECURE_STORAGE_KEY; + + enum EncryptionType + { + ENCRYPTION_NONE = 0, + ENCRYPTION_SSL_TLS = 1, + ENCRYPTION_STARTTLS = 2 + }; + + // --- SMTP connection --- + static QString getSmtpHost(); + static void setSmtpHost(const QString &host); + static int getSmtpPort(); + static void setSmtpPort(int port); + static int getSmtpEncryption(); + static void setSmtpEncryption(int enc); + static QString getSmtpUsername(); + static void setSmtpUsername(const QString &username); + static QString getSmtpPassword(); + static void saveSmtpCredentials(const QString &username, const QString &password); + + // --- Envelope / headers --- + static QString getFromAddress(); + static void setFromAddress(const QString &addr); + static QString getFromName(); + static void setFromName(const QString &name); + static QString getSubjectTemplate(); + static void setSubjectTemplate(const QString &tmpl); + static QString getBodyTemplate(); + static void setBodyTemplate(const QString &tmpl); + + // --- QSL card image & overlays --- + static QString getCardImagePath(); + static void setCardImagePath(const QString &path); + static QList getCardFieldOverlays(); + static void setCardFieldOverlays(const QList &overlays); + + // --- Sent-tracking (stored in contacts.fields JSON) --- + static QDateTime getEmailSentDateTime(const QSqlRecord &record); + static bool hasEmailBeenSentToCallsign(const QString &callsign, int excludeId = -1); + static void recordEmailSent(int contactId, const QSqlRecord ¤tRecord); + + // --- Rendering helpers --- + // Full render using saved settings from LogParam (used when sending). + static QPixmap renderCard(const QSqlRecord &record); + // Full render using an explicit image path and overlay list + // (used by settings preview so unsaved changes are shown). + static QPixmap renderCard(const QString &imagePath, + const QSqlRecord &record, + const QList &overlays); + static QString applyMergeFields(const QString &tmpl, const QSqlRecord &record); + + // Available merge keys (for display in settings UI) + struct MergeField { QString key; QString description; }; + static QList availableMergeFields(); + +private: + static QString fieldValue(const QString &key, const QSqlRecord &record); +}; + +// Worker object that runs the SMTP protocol on a background thread. +class SmtpWorker : public QObject +{ + Q_OBJECT +public: + explicit SmtpWorker(const QString &host, int port, int encryption, + const QString &username, const QString &password, + const QString &fromAddress, const QString &fromName, + const QString &toAddress, + const QString &subject, const QString &body, + const QByteArray &imageData, const QString &imageName, + QObject *parent = nullptr); + +public slots: + void run(); + +signals: + void finished(bool success, const QString &errorMessage); + +private: + bool waitForResponse(QSslSocket *socket, QByteArray &out, int timeoutMs = 15000); + int responseCode(const QByteArray &response); + bool sendCommand(QSslSocket *socket, const QByteArray &cmd, int expectedCode, + QByteArray &response); + QByteArray buildMimeMessage(); + + QString m_host; + int m_port; + int m_encryption; + QString m_username; + QString m_password; + QString m_fromAddress; + QString m_fromName; + QString m_toAddress; + QString m_subject; + QString m_body; + QByteArray m_imageData; + QString m_imageName; +}; + +// High-level service used by the UI. Call sendEmailQSL() and connect to +// sendFinished() for result notification. +class EmailQSLService : public QObject +{ + Q_OBJECT +public: + explicit EmailQSLService(QObject *parent = nullptr); + + void sendEmailQSL(const QSqlRecord &record); + void testConnection(const QString &host, int port, int encryption, + const QString &username, const QString &password); + +signals: + void sendFinished(bool success, const QString &message); + void testFinished(bool success, const QString &message); +}; + +#endif // QLOG_SERVICE_EMAILQSLSERVICE_H diff --git a/service/lotw/Lotw.cpp b/service/lotw/Lotw.cpp index c3a29a27..0c3d6303 100644 --- a/service/lotw/Lotw.cpp +++ b/service/lotw/Lotw.cpp @@ -112,7 +112,9 @@ QString LotwBase::findTQSLPath() QDir::homePath() + "/AppData/Local/Programs/TQSL/tqsl.exe" #elif defined(Q_OS_MACOS) "/Applications/tqsl.app/Contents/MacOS/tqsl", - "/Applications/TQSL.app/Contents/MacOS/tqsl" + "/Applications/TQSL.app/Contents/MacOS/tqsl", + "/Applications/TrustedQSL/tqsl.app/Contents/MacOS/tqsl", + "/Applications/TrustedQSL/TQSL.app/Contents/MacOS/tqsl" #else "/usr/bin/tqsl", "/usr/local/bin/tqsl", @@ -580,3 +582,258 @@ LotwQSLDownloader::~LotwQSLDownloader() currentReply->deleteLater(); } } + +LotwDXCCCreditDownloader::LotwDXCCCreditDownloader(QObject *parent) : + QObject(parent), + LotwBase(), + nam(new QNetworkAccessManager(this)), + currentReply(nullptr) +{ + FCT_IDENTIFICATION; + + connect(nam, &QNetworkAccessManager::finished, + this, &LotwDXCCCreditDownloader::processReply); +} + +LotwDXCCCreditDownloader::~LotwDXCCCreditDownloader() +{ + FCT_IDENTIFICATION; + + if ( currentReply ) + { + currentReply->abort(); + currentReply->deleteLater(); + } +} + +const QString LotwDXCCCreditDownloader::dxccModeGroupFromLotw(const QString &lotwModeGroup) +{ + FCT_IDENTIFICATION; + + const QString modeGroup = lotwModeGroup.trimmed().toUpper(); + + if ( modeGroup == "DATA" || modeGroup == "IMAGE" ) + return "DIGITAL"; + + if ( modeGroup == "PHONE" || modeGroup == "CW" ) + return modeGroup; + + return QString(); +} + +void LotwDXCCCreditDownloader::downloadCredits(const QString &entity) +{ + FCT_IDENTIFICATION; + +#if 0 + // Temporary test hook for LoTW DXCC credit import. + QString testFileName = QString::fromLocal8Bit(qgetenv("QLOG_LOTW_DXCC_CREDITS_TEST_FILE")); + if ( testFileName.isEmpty() ) + testFileName = "DXCC_QSLs_20260516_174703.adi"; + + if ( QFile::exists(testFileName) ) + { + QFile testFile(testFileName); + if ( !testFile.open(QIODevice::ReadOnly) ) + { + emit downloadFailed(tr("Cannot open test LoTW DXCC credit file")); + return; + } + + const QByteArray data = testFile.readAll(); + const qint64 size = data.size(); + qCInfo(runtime) << "Using local LoTW DXCC credit test file:" << testFileName << "size:" << size; + + if ( !containsADIFTag(data, "EOH") ) + { + emit downloadFailed(plainResponseSummary(data)); + return; + } + + if ( !containsADIFTag(data, "APP_LoTW_EOF") ) + { + emit downloadFailed(tr("Incomplete LoTW DXCC credit response")); + return; + } + + QTemporaryFile tempFile; + if ( !tempFile.open() ) + { + emit downloadFailed(tr("Cannot open temporary file")); + return; + } + + tempFile.write(data); + tempFile.flush(); + tempFile.seek(0); + + emit downloadStarted(); + + QTextStream stream(&tempFile); + AdiFormat adi(stream); + + connect(&adi, &AdiFormat::importPosition, this, [this, size](qint64 position) + { + if ( size > 0 ) + { + const double progress = position * 100.0 / size; + emit downloadProgress(static_cast(progress)); + } + }); + + connect(&adi, &AdiFormat::QSLMergeFinished, this, [this](QSLMergeStat stats) + { + emit downloadComplete(stats); + }); + + adi.runDXCCCreditImport(); + + tempFile.close(); + return; + } +#endif + const QString username = getUsername(); + const QString password = getPasswd(); + + if ( username.isEmpty() || password.isEmpty() ) + { + emit downloadFailed(tr("LoTW is not configured properly")); + return; + } + + QUrlQuery query; + query.addQueryItem("login", username); + query.addQueryItem("password", password); + if ( !entity.isEmpty() ) + query.addQueryItem("entity", entity); + + QUrl url(DXCC_CREDIT_API); + url.setQuery(query); + + qCDebug(runtime) << Data::safeQueryString(query); + + if ( currentReply ) + qCWarning(runtime) << "processing a new request but the previous one hasn't been completed yet !!!"; + + currentReply = nam->get(QNetworkRequest(url)); +} + +bool LotwDXCCCreditDownloader::containsADIFTag(const QByteArray &data, const QString &tagName) +{ + const QString text = QString::fromLatin1(data); + const QRegularExpression tag(QString("<\\s*%1\\s*(:|>)") + .arg(QRegularExpression::escape(tagName)), + QRegularExpression::CaseInsensitiveOption); + return tag.match(text).hasMatch(); +} + +QString LotwDXCCCreditDownloader::plainResponseSummary(const QByteArray &data) +{ + QString text = QString::fromLatin1(data); + static const QRegularExpression re("<[^>]*>"); + text.remove(re); + text = text.simplified(); + + if ( text.isEmpty() ) + return tr("LoTW returned a non-ADIF response"); + + return text.left(500); +} + +void LotwDXCCCreditDownloader::abortDownload() +{ + FCT_IDENTIFICATION; + + if ( currentReply ) + { + currentReply->abort(); + currentReply = nullptr; + } +} + +void LotwDXCCCreditDownloader::processReply(QNetworkReply *reply) +{ + FCT_IDENTIFICATION; + + currentReply = nullptr; + + const int replyStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + auto fail = [this, reply](const QString &message) + { + emit downloadFailed(message); + reply->deleteLater(); + }; + + if ( reply->error() != QNetworkReply::NoError + || replyStatusCode < 200 + || replyStatusCode >= 300 ) + { + qCInfo(runtime) << "LoTW DXCC credit download error" << reply->errorString(); + qCDebug(runtime) << "HTTP Status Code" << replyStatusCode; + if ( reply->error() != QNetworkReply::OperationCanceledError ) + fail(reply->errorString()); + else + reply->deleteLater(); + return; + } + + const QByteArray data = reply->readAll(); + const qint64 size = data.size(); + qCDebug(runtime) << "LoTW DXCC credit reply received, size:" << size; + qCDebug(runtime) << data; + + if ( size < 10000 && data.contains("username/password you entered is not recognized") ) + { + fail(tr("Incorrect login or password")); + return; + } + + if ( !containsADIFTag(data, "EOH") ) + { + fail(plainResponseSummary(data)); + return; + } + + if ( !containsADIFTag(data, "APP_LoTW_EOF") ) + { + fail(tr("Incomplete LoTW DXCC credit response")); + return; + } + + QTemporaryFile tempFile; + + if ( !tempFile.open() ) + { + fail(tr("Cannot open temporary file")); + return; + } + + tempFile.write(data); + tempFile.flush(); + tempFile.seek(0); + + emit downloadStarted(); + + QTextStream stream(&tempFile); + AdiFormat adi(stream); + + connect(&adi, &AdiFormat::importPosition, this, [this, size](qint64 position) + { + if ( size > 0 ) + { + const double progress = position * 100.0 / size; + emit downloadProgress(static_cast(progress)); + } + }); + + connect(&adi, &AdiFormat::QSLMergeFinished, this, [this](QSLMergeStat stats) + { + emit downloadComplete(stats); + }); + + adi.runDXCCCreditImport(); + + tempFile.close(); + reply->deleteLater(); +} diff --git a/service/lotw/Lotw.h b/service/lotw/Lotw.h index 801e6f7f..4bfc607e 100644 --- a/service/lotw/Lotw.h +++ b/service/lotw/Lotw.h @@ -108,4 +108,34 @@ public slots: void get(QList> params); }; +class LotwDXCCCreditDownloader : public QObject, private LotwBase +{ + Q_OBJECT + +public: + explicit LotwDXCCCreditDownloader(QObject *parent = nullptr); + virtual ~LotwDXCCCreditDownloader(); + + static const QString dxccModeGroupFromLotw(const QString &lotwModeGroup); + void downloadCredits(const QString &entity = QString()); + +signals: + void downloadProgress(qulonglong value); + void downloadStarted(); + void downloadComplete(QSLMergeStat); + void downloadFailed(QString); + +public slots: + void abortDownload(); + +private: + QNetworkAccessManager *nam; + QNetworkReply *currentReply; + const QString DXCC_CREDIT_API = "https://lotw.arrl.org/lotwuser/logbook/qslcards.php"; + + static bool containsADIFTag(const QByteArray &data, const QString &tagName); + static QString plainResponseSummary(const QByteArray &data); + void processReply(QNetworkReply *reply); +}; + #endif // QLOG_SERVISE_LOTW_LOTW_H diff --git a/service/qrzcom/QRZ.cpp b/service/qrzcom/QRZ.cpp index b2d0c83a..30053cfd 100644 --- a/service/qrzcom/QRZ.cpp +++ b/service/qrzcom/QRZ.cpp @@ -268,6 +268,7 @@ void QRZCallbook::processReply(QNetworkReply* reply) qCDebug(runtime) << response; QXmlStreamReader xml(response); CallbookResponseData resposeData; + QString xrefCallsign; /* Reset Session Key */ /* Every response contains a valid key. If the key is not present */ @@ -315,6 +316,7 @@ void QRZCallbook::processReply(QNetworkReply* reply) if (elementName == "Key") sessionId = xml.readElementText(); else if (elementName == "call") resposeData.call = decodeHtmlEntities(xml.readElementText().toUpper()); + else if (elementName == "xref") xrefCallsign = decodeHtmlEntities(xml.readElementText().toUpper()); else if (elementName == "dxcc") resposeData.dxcc = decodeHtmlEntities(xml.readElementText()); else if (elementName == "fname") resposeData.fname = decodeHtmlEntities(xml.readElementText()); else if (elementName == "name") resposeData.lname = decodeHtmlEntities(xml.readElementText()); @@ -344,6 +346,35 @@ void QRZCallbook::processReply(QNetworkReply* reply) else if (elementName == "image") resposeData.image_url = decodeHtmlEntities(xml.readElementText()); } + const QString requestedCallsign = reply->property("queryCallsign").toString(); + const QString requestedCallsignUpper = requestedCallsign.toUpper(); + const Callsign queryCall(requestedCallsignUpper); + const QString baseCallsign = (queryCall.isValid()) ? queryCall.getBase() + : requestedCallsignUpper; + + if ( !resposeData.call.isEmpty() + && !xrefCallsign.isEmpty() + && ( xrefCallsign == requestedCallsignUpper + || xrefCallsign == baseCallsign ) ) + { + // xref points to another callsign's record; keep only partial-match fields. + CallbookResponseData xrefData; + + xrefData.call = requestedCallsign; + xrefData.fname = resposeData.fname; + xrefData.lname = resposeData.lname; + xrefData.lic_year = resposeData.lic_year; + xrefData.qsl_via = resposeData.qsl_via; + xrefData.email = resposeData.email; + xrefData.born = resposeData.born; + xrefData.url = resposeData.url; + xrefData.name_fmt = resposeData.name_fmt; + xrefData.nick = resposeData.nick; + xrefData.image_url = resposeData.image_url; + + resposeData = xrefData; + } + if (!resposeData.call.isEmpty()) emit callsignResult(resposeData); diff --git a/tests/AdiFormatTest/AdiFormatTest.pro b/tests/AdiFormatTest/AdiFormatTest.pro new file mode 100644 index 00000000..5c7d33c5 --- /dev/null +++ b/tests/AdiFormatTest/AdiFormatTest.pro @@ -0,0 +1,21 @@ +QT += testlib core sql +CONFIG += console testcase c++11 +TEMPLATE = app +TARGET = tst_adiformat + +DEFINES += VERSION=\\\"test\\\" + +INCLUDEPATH += $$PWD/../.. + +SOURCES += \ + tst_adiformat.cpp \ + test_stubs.cpp \ + ../../core/LogLocale.cpp \ + ../../data/Accents.cpp \ + ../../logformat/AdiFormat.cpp + +HEADERS += \ + ../../core/LogLocale.h \ + ../../data/Data.h \ + ../../logformat/AdiFormat.h \ + ../../logformat/LogFormat.h diff --git a/tests/AdiFormatTest/test_stubs.cpp b/tests/AdiFormatTest/test_stubs.cpp new file mode 100644 index 00000000..fb0d3784 --- /dev/null +++ b/tests/AdiFormatTest/test_stubs.cpp @@ -0,0 +1,42 @@ +#include "data/Data.h" +#include "logformat/LogFormat.h" + +LogFormat::LogFormat(QTextStream &stream) : + QObject(nullptr), + stream(stream), + exportedFields(QStringLiteral("*")), + duplicateQSOFunc(nullptr) +{ + defaults = nullptr; +} + +LogFormat::~LogFormat() = default; + +void LogFormat::setDefaults(QMap &defaults) +{ + this->defaults = &defaults; +} + +Data::Data(QObject *parent) : + QObject(parent) +{ +} + +Data::~Data() = default; + +QPair Data::legacyMode(const QString &) +{ + return {}; +} + +void Data::invalidateDXCCStatusCache(const QSqlRecord &) +{ +} + +void Data::invalidateSetOfDXCCStatusCache(const QSet &) +{ +} + +void Data::clearDXCCStatusCache() +{ +} diff --git a/tests/AdiFormatTest/tst_adiformat.cpp b/tests/AdiFormatTest/tst_adiformat.cpp new file mode 100644 index 00000000..92f3c492 --- /dev/null +++ b/tests/AdiFormatTest/tst_adiformat.cpp @@ -0,0 +1,651 @@ +#include +#include +#include +#include +#include +#include + +#include "logformat/AdiFormat.h" + +class TestAdiFormat : public AdiFormat +{ +public: + explicit TestAdiFormat(QTextStream &stream) : + AdiFormat(stream) + { + } + + bool readContactForTest(QVariantMap &contact) + { + return readContact(contact); + } + + void writeFieldForTest(const QString &name, + bool presenceCondition, + const QString &value, + const QString &type = QString()) + { + writeField(name, presenceCondition, value, type); + } + + void writeRecordForTest(const QSqlRecord &record, + QMap *applTags = nullptr) + { + writeSQLRecord(record, applTags); + } +}; + +class AdiFormatTest : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void constructorDisablesDeviceTextMode(); + void parserSkipsHeaderAndReadsTypedFields(); + void parserSkipsMixedCaseHeaderTerminator(); + void parserPreservesLengthDelimitedCRLFData(); + void parserReadsMultipleContactsAndMixedCaseEor(); + void writeFieldFormatsTagLengthAndType(); + void writeFieldNormalizesLineBreaksByFieldType(); + void exportStartWritesAdifHeader(); + void writeSqlRecordMapsKnownFields(); + void writeSqlRecordExportsApplicationTags(); + void writeSqlRecordExportsRawFieldsFiltersInvalidNames(); + void exportContactNormalizesGridAndTerminatesRecord(); + void importNextMapsAdiFieldsAndStoresUnknownFields(); + void importNextStoresZeroLengthFields(); + void importNextAcceptsLeadingZeroLengthSpecifier(); + void importNextStoresUserDefinedFieldsFromHeader(); + void importNextAppliesDefaultsForMissingFields(); + void importNextKeepsImportedValuesOverDefaults(); + void importNextFillsIntlCompanionFields(); + void importNextAppliesDefaultsBeforeIntlCompanionFields(); + +private: + static QByteArray tag(const QByteArray &name, + const QByteArray &value, + const QByteArray &type = QByteArray()); + static QByteArray writeField(const QString &name, + bool presenceCondition, + const QString &value, + const QString &type = QString()); + static void appendField(QSqlRecord &record, + const QString &name, + const QVariant &value); +}; + +void AdiFormatTest::initTestCase() +{ + QLoggingCategory::setFilterRules(QStringLiteral("*.debug=false")); +} + +QByteArray AdiFormatTest::tag(const QByteArray &name, + const QByteArray &value, + const QByteArray &type) +{ + QByteArray output("<"); + output += name; + output += ':'; + output += QByteArray::number(value.size()); + if ( !type.isEmpty() ) + { + output += ':'; + output += type; + } + output += '>'; + output += value; + return output; +} + +QByteArray AdiFormatTest::writeField(const QString &name, + bool presenceCondition, + const QString &value, + const QString &type) +{ + QByteArray output; + QBuffer buffer(&output); + buffer.open(QIODevice::WriteOnly | QIODevice::Text); + + QTextStream stream(&buffer); + TestAdiFormat format(stream); + format.writeFieldForTest(name, presenceCondition, value, type); + stream.flush(); + + return output; +} + +void AdiFormatTest::appendField(QSqlRecord &record, + const QString &name, + const QVariant &value) +{ + QSqlField field(name, value.type()); + field.setValue(value); + record.append(field); +} + +void AdiFormatTest::constructorDisablesDeviceTextMode() +{ + QByteArray input; + QBuffer buffer(&input); + QVERIFY(buffer.open(QIODevice::ReadOnly | QIODevice::Text)); + QVERIFY(buffer.isTextModeEnabled()); + + QTextStream stream(&buffer); + TestAdiFormat format(stream); + + QVERIFY(!buffer.isTextModeEnabled()); +} + +void AdiFormatTest::parserSkipsHeaderAndReadsTypedFields() +{ + QByteArray input("ADIF test header\r\n"); + input += tag("ADIF_VER", "3.1.7"); + input += "\r\n"; + input += tag("CALL", "OK1AA"); + input += tag("FREQ", "14.074", "N"); + input += tag("BAND", "20m"); + input += "\r\n"; + + QBuffer buffer(&input); + QVERIFY(buffer.open(QIODevice::ReadOnly | QIODevice::Text)); + QTextStream stream(&buffer); + TestAdiFormat format(stream); + + QVariantMap contact; + QVERIFY(format.readContactForTest(contact)); + + QCOMPARE(contact.value(QStringLiteral("call")).toString(), QStringLiteral("OK1AA")); + QCOMPARE(contact.value(QStringLiteral("freq")).toString(), QStringLiteral("14.074")); + QCOMPARE(contact.value(QStringLiteral("band")).toString(), QStringLiteral("20m")); + QVERIFY(!contact.contains(QStringLiteral("adif_ver"))); +} + +void AdiFormatTest::parserSkipsMixedCaseHeaderTerminator() +{ + QByteArray input("ADIF test header\r\n"); + input += tag("ADIF_VER", "3.1.7"); + input += "\r\n"; + input += tag("CALL", "OK1AA"); + input += "\r\n"; + + QBuffer buffer(&input); + QVERIFY(buffer.open(QIODevice::ReadOnly | QIODevice::Text)); + QTextStream stream(&buffer); + TestAdiFormat format(stream); + + QVariantMap contact; + QVERIFY(format.readContactForTest(contact)); + + QCOMPARE(contact.value(QStringLiteral("call")).toString(), QStringLiteral("OK1AA")); + QVERIFY(!contact.contains(QStringLiteral("adif_ver"))); +} + +void AdiFormatTest::parserPreservesLengthDelimitedCRLFData() +{ + const QByteArray address("Line 1\r\nLine 2\r\nLine 3"); + QByteArray input; + input += tag("ADDRESS", address); + input += tag("CALL", "OK1AA"); + input += "\r\n"; + + QBuffer buffer(&input); + QVERIFY(buffer.open(QIODevice::ReadOnly | QIODevice::Text)); + QTextStream stream(&buffer); + TestAdiFormat format(stream); + + QVariantMap contact; + QVERIFY(format.readContactForTest(contact)); + + QCOMPARE(contact.value(QStringLiteral("address")).toString(), + QString::fromLatin1(address)); + QCOMPARE(contact.value(QStringLiteral("call")).toString(), QStringLiteral("OK1AA")); +} + +void AdiFormatTest::parserReadsMultipleContactsAndMixedCaseEor() +{ + QByteArray input; + input += tag("CALL", "OK1AA"); + input += "\r\n"; + input += tag("CALL", "OK2BB"); + input += "\r\n"; + + QBuffer buffer(&input); + QVERIFY(buffer.open(QIODevice::ReadOnly | QIODevice::Text)); + QTextStream stream(&buffer); + TestAdiFormat format(stream); + + QVariantMap contact; + QVERIFY(format.readContactForTest(contact)); + QCOMPARE(contact.value(QStringLiteral("call")).toString(), QStringLiteral("OK1AA")); + + contact.clear(); + QVERIFY(format.readContactForTest(contact)); + QCOMPARE(contact.value(QStringLiteral("call")).toString(), QStringLiteral("OK2BB")); +} + +void AdiFormatTest::writeFieldFormatsTagLengthAndType() +{ + QCOMPARE(writeField(QStringLiteral("FREQ"), true, QStringLiteral("14.074"), QStringLiteral("N")), + QByteArray("14.074\n")); + QCOMPARE(writeField(QStringLiteral("CALL"), true, QString::fromUtf8("\xC5\x98" "eka")), + QByteArray("Reka\n")); + QCOMPARE(writeField(QStringLiteral("CALL"), false, QStringLiteral("OK1AA")), + QByteArray()); + QCOMPARE(writeField(QStringLiteral("CALL"), true, QString()), + QByteArray()); +} + +void AdiFormatTest::writeFieldNormalizesLineBreaksByFieldType() +{ + QCOMPARE(writeField(QStringLiteral("ADDRESS"), true, QStringLiteral("Line1\nLine2")), + QByteArray("Line1\r\nLine2\n")); + QCOMPARE(writeField(QStringLiteral("COMMENT"), true, QStringLiteral("Line1\nLine2")), + QByteArray("Line1Line2\n")); + QCOMPARE(writeField(QStringLiteral("APP_QLOG_NOTE"), true, QStringLiteral("Line1\nLine2")), + QByteArray("Line1\r\nLine2\n")); + QCOMPARE(writeField(QStringLiteral("APP_QLOG_NOTE"), true, QStringLiteral("Line1\nLine2"), QStringLiteral("S")), + QByteArray("Line1Line2\n")); + QCOMPARE(writeField(QStringLiteral("USERDEF"), true, QStringLiteral("Line1\nLine2"), QStringLiteral("M")), + QByteArray("Line1\r\nLine2\n")); +} + +void AdiFormatTest::exportStartWritesAdifHeader() +{ + QByteArray output; + QBuffer buffer(&output); + QVERIFY(buffer.open(QIODevice::WriteOnly | QIODevice::Text)); + + QTextStream stream(&buffer); + TestAdiFormat format(stream); + format.exportStart(); + stream.flush(); + + QVERIFY(output.startsWith(" 3.1.7\n")); + QVERIFY(output.contains("QLog\n")); + QVERIFY(output.contains("test\n")); + QVERIFY(output.contains("")); + QVERIFY(output.endsWith("\n\n")); +} + +void AdiFormatTest::writeSqlRecordMapsKnownFields() +{ + QSqlRecord record; + appendField(record, QStringLiteral("callsign"), QStringLiteral("OK1AA")); + appendField(record, QStringLiteral("band"), QStringLiteral("20M")); + appendField(record, QStringLiteral("iota"), QStringLiteral("eu-001")); + appendField(record, QStringLiteral("qsl_sent"), QStringLiteral("N")); + appendField(record, QStringLiteral("qsl_rcvd"), QStringLiteral("Y")); + appendField(record, QStringLiteral("notes"), QStringLiteral("Line1\nLine2")); + appendField(record, QStringLiteral("comment"), QStringLiteral("Line1\nLine2")); + appendField(record, + QStringLiteral("start_time"), + QDateTime(QDate(2026, 5, 28), QTime(12, 34, 56), Qt::UTC)); + + QByteArray output; + QBuffer buffer(&output); + QVERIFY(buffer.open(QIODevice::WriteOnly | QIODevice::Text)); + QTextStream stream(&buffer); + TestAdiFormat format(stream); + + format.writeRecordForTest(record); + stream.flush(); + + QVERIFY(output.contains("OK1AA\n")); + QVERIFY(output.contains("20m\n")); + QVERIFY(output.contains("EU-001\n")); + QVERIFY(output.contains("Y\n")); + QVERIFY(!output.contains("qsl_sent")); + QVERIFY(output.contains("Line1\r\nLine2\n")); + QVERIFY(output.contains("Line1Line2\n")); + QVERIFY(output.contains("20260528\n")); + QVERIFY(output.contains("123456\n")); +} + +void AdiFormatTest::writeSqlRecordExportsApplicationTags() +{ + QSqlRecord record; + QMap applTags; + applTags.insert(QStringLiteral("APP_QLOG_NOTE"), QStringLiteral("Line1\nLine2")); + applTags.insert(QStringLiteral("APP_QLOG_FLAG"), QStringLiteral("Y")); + + QByteArray output; + QBuffer buffer(&output); + QVERIFY(buffer.open(QIODevice::WriteOnly | QIODevice::Text)); + QTextStream stream(&buffer); + TestAdiFormat format(stream); + + format.writeRecordForTest(record, &applTags); + stream.flush(); + + QVERIFY(output.contains("Line1\r\nLine2\n")); + QVERIFY(output.contains("Y\n")); +} + +void AdiFormatTest::writeSqlRecordExportsRawFieldsFiltersInvalidNames() +{ + QSqlRecord record; + QJsonObject fields; + fields.insert(QStringLiteral("UNKNOWN_FIELD"), QStringLiteral("ok")); + fields.insert(QStringLiteral("APP_QLOG_RAW"), QStringLiteral("Line1\nLine2")); + fields.insert(QStringLiteral("BAD-NAME"), QStringLiteral("dash")); + fields.insert(QStringLiteral("1BAD"), QStringLiteral("digit")); + fields.insert(QStringLiteral("xmlBAD"), QStringLiteral("xml")); + fields.insert(QStringLiteral("BAD NAME"), QStringLiteral("space")); + fields.insert(QStringLiteral("BADok\n")); + QVERIFY(output.contains("Line1\r\nLine2\n")); + QVERIFY(!output.contains("dash")); + QVERIFY(!output.contains("digit")); + QVERIFY(!output.contains("xml")); + QVERIFY(!output.contains("space")); + QVERIFY(!output.contains("angle")); + QVERIFY(!output.contains("shortapp")); +} + +void AdiFormatTest::exportContactNormalizesGridAndTerminatesRecord() +{ + QSqlRecord record; + appendField(record, QStringLiteral("gridsquare"), QStringLiteral("jo70aa12bb")); + appendField(record, QStringLiteral("gridsquare_ext"), QString()); + + QByteArray output; + QBuffer buffer(&output); + QVERIFY(buffer.open(QIODevice::WriteOnly | QIODevice::Text)); + QTextStream stream(&buffer); + TestAdiFormat format(stream); + + format.exportContact(record); + stream.flush(); + + QVERIFY(output.contains("JO70AA12\n")); + QVERIFY(output.contains("BB\n")); + QVERIFY(output.endsWith("\n\n")); +} + +void AdiFormatTest::importNextMapsAdiFieldsAndStoresUnknownFields() +{ + QByteArray input; + input += tag("CALL", "ok1aa"); + input += tag("BAND", "20M"); + input += tag("CONT", "eu"); + input += tag("GRIDSQUARE", "jo70aa12bb"); + input += tag("QSO_DATE", "20260528"); + input += tag("TIME_ON", "123456"); + input += tag("TIME_OFF", "123500"); + input += tag("QSL_RCVD", "y"); + input += tag("QSL_SENT", "q"); + input += tag("ANT_PATH", "s"); + input += tag("APP_QLOG_TEST", "abc"); + input += tag("UNKNOWN_FIELD", "xyz"); + input += "\r\n"; + + QBuffer buffer(&input); + QVERIFY(buffer.open(QIODevice::ReadOnly | QIODevice::Text)); + QTextStream stream(&buffer); + TestAdiFormat format(stream); + + QSqlRecord record; + appendField(record, QStringLiteral("callsign"), QString()); + appendField(record, QStringLiteral("band"), QString()); + appendField(record, QStringLiteral("cont"), QString()); + appendField(record, QStringLiteral("gridsquare"), QString()); + appendField(record, QStringLiteral("gridsquare_ext"), QString()); + appendField(record, QStringLiteral("start_time"), QDateTime()); + appendField(record, QStringLiteral("end_time"), QDateTime()); + appendField(record, QStringLiteral("qsl_rcvd"), QString()); + appendField(record, QStringLiteral("qsl_sent"), QString()); + appendField(record, QStringLiteral("ant_path"), QString()); + appendField(record, QStringLiteral("fields"), QString()); + + QVERIFY(format.importNext(record)); + + QCOMPARE(record.value(QStringLiteral("callsign")).toString(), QStringLiteral("OK1AA")); + QCOMPARE(record.value(QStringLiteral("band")).toString(), QStringLiteral("20m")); + QCOMPARE(record.value(QStringLiteral("cont")).toString(), QStringLiteral("EU")); + QCOMPARE(record.value(QStringLiteral("gridsquare")).toString(), QStringLiteral("JO70AA12")); + QCOMPARE(record.value(QStringLiteral("gridsquare_ext")).toString(), QStringLiteral("BB")); + QCOMPARE(record.value(QStringLiteral("qsl_rcvd")).toString(), QStringLiteral("Y")); + QCOMPARE(record.value(QStringLiteral("qsl_sent")).toString(), QStringLiteral("Q")); + QCOMPARE(record.value(QStringLiteral("ant_path")).toString(), QStringLiteral("S")); + QCOMPARE(record.value(QStringLiteral("start_time")).toDateTime(), + QDateTime(QDate(2026, 5, 28), QTime(12, 34, 56), QTimeZone::utc())); + QCOMPARE(record.value(QStringLiteral("end_time")).toDateTime(), + QDateTime(QDate(2026, 5, 28), QTime(12, 35, 0), QTimeZone::utc())); + + const QJsonObject fields = QJsonDocument::fromJson(record.value(QStringLiteral("fields")).toByteArray()).object(); + QCOMPARE(fields.value(QStringLiteral("app_qlog_test")).toString(), QStringLiteral("abc")); + QCOMPARE(fields.value(QStringLiteral("unknown_field")).toString(), QStringLiteral("xyz")); +} + +void AdiFormatTest::importNextStoresZeroLengthFields() +{ + QByteArray input; + input += tag("CALL", "OK1AA"); + input += tag("APP_QLOG_EMPTY", QByteArray()); + input += "\r\n"; + + QBuffer buffer(&input); + QVERIFY(buffer.open(QIODevice::ReadOnly | QIODevice::Text)); + QTextStream stream(&buffer); + TestAdiFormat format(stream); + + QSqlRecord record; + appendField(record, QStringLiteral("callsign"), QString()); + appendField(record, QStringLiteral("fields"), QString()); + + QVERIFY(format.importNext(record)); + + QCOMPARE(record.value(QStringLiteral("callsign")).toString(), QStringLiteral("OK1AA")); + + const QJsonObject fields = QJsonDocument::fromJson(record.value(QStringLiteral("fields")).toByteArray()).object(); + QVERIFY(fields.contains(QStringLiteral("app_qlog_empty"))); + QCOMPARE(fields.value(QStringLiteral("app_qlog_empty")).toString(), QString()); +} + +void AdiFormatTest::importNextAcceptsLeadingZeroLengthSpecifier() +{ + QByteArray input; + input += "OK1AA"; + input += "\r\n"; + + QBuffer buffer(&input); + QVERIFY(buffer.open(QIODevice::ReadOnly | QIODevice::Text)); + QTextStream stream(&buffer); + TestAdiFormat format(stream); + + QSqlRecord record; + appendField(record, QStringLiteral("callsign"), QString()); + + QVERIFY(format.importNext(record)); + + QCOMPARE(record.value(QStringLiteral("callsign")).toString(), QStringLiteral("OK1AA")); +} + +void AdiFormatTest::importNextStoresUserDefinedFieldsFromHeader() +{ + QByteArray input("ADIF user-defined field header\r\n"); + input += tag("USERDEF1", "SweaterSize,{S,M,L}", "E"); + input += "\r\n"; + input += tag("CALL", "OK1AA"); + input += tag("SweaterSize", "M"); + input += "\r\n"; + + QBuffer buffer(&input); + QVERIFY(buffer.open(QIODevice::ReadOnly | QIODevice::Text)); + QTextStream stream(&buffer); + TestAdiFormat format(stream); + + QSqlRecord record; + appendField(record, QStringLiteral("callsign"), QString()); + appendField(record, QStringLiteral("fields"), QString()); + + QVERIFY(format.importNext(record)); + + QCOMPARE(record.value(QStringLiteral("callsign")).toString(), QStringLiteral("OK1AA")); + + const QJsonObject fields = QJsonDocument::fromJson(record.value(QStringLiteral("fields")).toByteArray()).object(); + QCOMPARE(fields.value(QStringLiteral("sweatersize")).toString(), QStringLiteral("M")); + QVERIFY(!fields.contains(QStringLiteral("userdef1"))); +} + +void AdiFormatTest::importNextAppliesDefaultsForMissingFields() +{ + QByteArray input; + input += tag("CALL", "OK1AA"); + input += "\r\n"; + + QBuffer buffer(&input); + QVERIFY(buffer.open(QIODevice::ReadOnly | QIODevice::Text)); + QTextStream stream(&buffer); + TestAdiFormat format(stream); + + QMap defaults; + defaults.insert(QStringLiteral("band"), QStringLiteral("20M")); + defaults.insert(QStringLiteral("qsl_sent"), QStringLiteral("Q")); + defaults.insert(QStringLiteral("lotw_qsl_sent"), QStringLiteral("Y")); + defaults.insert(QStringLiteral("eqsl_qsl_sent"), QStringLiteral("R")); + defaults.insert(QStringLiteral("dcl_qsl_sent"), QStringLiteral("I")); + defaults.insert(QStringLiteral("my_rig_intl"), QString::fromUtf8("R\xC3\xA1" "dio")); + format.setDefaults(defaults); + + QSqlRecord record; + appendField(record, QStringLiteral("callsign"), QString()); + appendField(record, QStringLiteral("band"), QString()); + appendField(record, QStringLiteral("qsl_sent"), QString()); + appendField(record, QStringLiteral("lotw_qsl_sent"), QString()); + appendField(record, QStringLiteral("eqsl_qsl_sent"), QString()); + appendField(record, QStringLiteral("dcl_qsl_sent"), QString()); + appendField(record, QStringLiteral("my_rig"), QString()); + appendField(record, QStringLiteral("my_rig_intl"), QString()); + + QVERIFY(format.importNext(record)); + + QCOMPARE(record.value(QStringLiteral("callsign")).toString(), QStringLiteral("OK1AA")); + QCOMPARE(record.value(QStringLiteral("band")).toString(), QStringLiteral("20m")); + QCOMPARE(record.value(QStringLiteral("qsl_sent")).toString(), QStringLiteral("Q")); + QCOMPARE(record.value(QStringLiteral("lotw_qsl_sent")).toString(), QStringLiteral("Y")); + QCOMPARE(record.value(QStringLiteral("eqsl_qsl_sent")).toString(), QStringLiteral("R")); + QCOMPARE(record.value(QStringLiteral("dcl_qsl_sent")).toString(), QStringLiteral("I")); + QCOMPARE(record.value(QStringLiteral("my_rig")).toString(), QStringLiteral("Radio")); + QCOMPARE(record.value(QStringLiteral("my_rig_intl")).toString(), QString::fromUtf8("R\xC3\xA1" "dio")); +} + +void AdiFormatTest::importNextKeepsImportedValuesOverDefaults() +{ + QByteArray input; + input += tag("CALL", "OK1AA"); + input += tag("BAND", "40M"); + input += tag("QSL_SENT", "Y"); + input += tag("LOTW_QSL_SENT", "N"); + input += tag("MY_RIG", "Imported rig"); + input += "\r\n"; + + QBuffer buffer(&input); + QVERIFY(buffer.open(QIODevice::ReadOnly | QIODevice::Text)); + QTextStream stream(&buffer); + TestAdiFormat format(stream); + + QMap defaults; + defaults.insert(QStringLiteral("band"), QStringLiteral("20M")); + defaults.insert(QStringLiteral("qsl_sent"), QStringLiteral("Q")); + defaults.insert(QStringLiteral("lotw_qsl_sent"), QStringLiteral("Y")); + defaults.insert(QStringLiteral("my_rig_intl"), QStringLiteral("Default rig")); + format.setDefaults(defaults); + + QSqlRecord record; + appendField(record, QStringLiteral("callsign"), QString()); + appendField(record, QStringLiteral("band"), QString()); + appendField(record, QStringLiteral("qsl_sent"), QString()); + appendField(record, QStringLiteral("lotw_qsl_sent"), QString()); + appendField(record, QStringLiteral("my_rig"), QString()); + appendField(record, QStringLiteral("my_rig_intl"), QString()); + + QVERIFY(format.importNext(record)); + + QCOMPARE(record.value(QStringLiteral("callsign")).toString(), QStringLiteral("OK1AA")); + QCOMPARE(record.value(QStringLiteral("band")).toString(), QStringLiteral("40m")); + QCOMPARE(record.value(QStringLiteral("qsl_sent")).toString(), QStringLiteral("Y")); + QCOMPARE(record.value(QStringLiteral("lotw_qsl_sent")).toString(), QStringLiteral("N")); + QCOMPARE(record.value(QStringLiteral("my_rig")).toString(), QStringLiteral("Imported rig")); + QCOMPARE(record.value(QStringLiteral("my_rig_intl")).toString(), QStringLiteral("Imported rig")); +} + +void AdiFormatTest::importNextFillsIntlCompanionFields() +{ + const QByteArray address("Line 1\r\nLine 2"); + const QByteArray notesIntl = QByteArray("Caf") + static_cast(0xe9) + "\r\nQTH"; + + QByteArray input; + input += tag("ADDRESS", address); + input += tag("COMMENT", "Plain comment"); + input += tag("NOTES_INTL", notesIntl); + input += "\r\n"; + + QBuffer buffer(&input); + QVERIFY(buffer.open(QIODevice::ReadOnly | QIODevice::Text)); + QTextStream stream(&buffer); + TestAdiFormat format(stream); + + QSqlRecord record; + appendField(record, QStringLiteral("address"), QString()); + appendField(record, QStringLiteral("address_intl"), QString()); + appendField(record, QStringLiteral("comment"), QString()); + appendField(record, QStringLiteral("comment_intl"), QString()); + appendField(record, QStringLiteral("notes"), QString()); + appendField(record, QStringLiteral("notes_intl"), QString()); + + QVERIFY(format.importNext(record)); + + QCOMPARE(record.value(QStringLiteral("address")).toString(), QString::fromLatin1(address)); + QCOMPARE(record.value(QStringLiteral("address_intl")).toString(), QString::fromLatin1(address)); + QCOMPARE(record.value(QStringLiteral("comment")).toString(), QStringLiteral("Plain comment")); + QCOMPARE(record.value(QStringLiteral("comment_intl")).toString(), QStringLiteral("Plain comment")); + QCOMPARE(record.value(QStringLiteral("notes")).toString(), QStringLiteral("Cafe\r\nQTH")); + QCOMPARE(record.value(QStringLiteral("notes_intl")).toString(), QString::fromLatin1(notesIntl)); +} + +void AdiFormatTest::importNextAppliesDefaultsBeforeIntlCompanionFields() +{ + QByteArray input; + input += tag("CALL", "OK1AA"); + input += "\r\n"; + + QBuffer buffer(&input); + QVERIFY(buffer.open(QIODevice::ReadOnly | QIODevice::Text)); + QTextStream stream(&buffer); + TestAdiFormat format(stream); + + QMap defaults; + defaults.insert(QStringLiteral("notes_intl"), QString::fromUtf8("Caf\xc3\xa9\nQTH")); + format.setDefaults(defaults); + + QSqlRecord record; + appendField(record, QStringLiteral("callsign"), QString()); + appendField(record, QStringLiteral("notes"), QString()); + appendField(record, QStringLiteral("notes_intl"), QString()); + + QVERIFY(format.importNext(record)); + + QCOMPARE(record.value(QStringLiteral("callsign")).toString(), QStringLiteral("OK1AA")); + QCOMPARE(record.value(QStringLiteral("notes")).toString(), QStringLiteral("Cafe\nQTH")); + QCOMPARE(record.value(QStringLiteral("notes_intl")).toString(), QString::fromUtf8("Caf\xc3\xa9\nQTH")); +} + +QTEST_APPLESS_MAIN(AdiFormatTest) + +#include "tst_adiformat.moc" diff --git a/tests/AdiImportBenchmark/AdiImportBenchmark.pro b/tests/AdiImportBenchmark/AdiImportBenchmark.pro new file mode 100644 index 00000000..02d4c895 --- /dev/null +++ b/tests/AdiImportBenchmark/AdiImportBenchmark.pro @@ -0,0 +1,24 @@ +QT += testlib core sql +CONFIG += console testcase c++11 +TEMPLATE = app +TARGET = tst_adiimportbenchmark + +DEFINES += VERSION=\\\"test\\\" + +INCLUDEPATH += $$PWD/../.. + +SOURCES += \ + tst_adiimportbenchmark.cpp \ + ../AdiFormatTest/test_stubs.cpp \ + ../../core/LogLocale.cpp \ + ../../data/Accents.cpp \ + ../../logformat/AdiFormat.cpp + +HEADERS += \ + ../../core/LogLocale.h \ + ../../data/Data.h \ + ../../logformat/AdiFormat.h \ + ../../logformat/LogFormat.h + +RESOURCES += \ + ../../res/res.qrc diff --git a/tests/AdiImportBenchmark/tst_adiimportbenchmark.cpp b/tests/AdiImportBenchmark/tst_adiimportbenchmark.cpp new file mode 100644 index 00000000..41fae4ba --- /dev/null +++ b/tests/AdiImportBenchmark/tst_adiimportbenchmark.cpp @@ -0,0 +1,720 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "logformat/AdiFormat.h" + +class TestAdiFormat : public AdiFormat +{ +public: + explicit TestAdiFormat(QTextStream &stream) : + AdiFormat(stream) + { + } + + void importStartForTest() + { + importStart(); + } + + bool readContactForTest(QVariantMap &contact) + { + return readContact(contact); + } +}; + +struct BenchmarkSample +{ + QString callsign; + QString band; + QString mode; + QString submode; + QString freq; + QString dxcc; + QString country; + QString continent; + QString cqz; + QString ituz; + QString grid; + QString name; + QString qth; + QString comment; + QString potaRef; + QString myPotaRef; + QString satName; + QDateTime startTime; +}; + +struct StageTimings +{ + qint64 parseMs = 0; + qint64 parseAndMapMs = 0; + qint64 duplicateCurrentExistingIndexesMs = 0; + qint64 insertMs = 0; + qint64 logMs = 0; + int parsed = 0; + int mapped = 0; + int duplicateHits = 0; + int inserted = 0; + int existingContactIndexCount = 0; + qsizetype logBytes = 0; +}; + +class AdiImportBenchmark : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void cleanup(); + void benchmarkImportStages_data(); + void benchmarkImportStages(); + +private: + static bool benchmarkEnabled(); + static QByteArray tag(const QByteArray &name, + const QByteArray &value, + const QByteArray &type = QByteArray()); + static QVector generateSamples(int count); + static QByteArray generateAdi(const QVector &samples); + static bool executeSqlFile(int version, QString *error); + static bool resetDatabase(QString *error); + static QStringList contactIndexNames(QString *error); + static QSqlRecord contactRecordTemplate(); + static void setRecordValue(QSqlRecord &record, const QString &field, const QVariant &value); + static void fillRecord(QSqlRecord &record, const BenchmarkSample &sample, int index); + static qint64 measureParse(const QByteArray &adi, int *parsed); + static qint64 measureParseAndMap(const QByteArray &adi, const QSqlRecord &recordTemplate, int *mapped); + static bool populateDuplicateCandidates(const QVector &samples, QString *error); + static qint64 measureDuplicateCheckCurrent(const QVector &samples, + int *duplicateHits, + QString *error); + static qint64 measureInsert(const QVector &samples, + const QSqlRecord &recordTemplate, + int *inserted, + QString *error); + static qint64 measureImportLog(const QVector &samples, + const QSqlRecord &recordTemplate, + qsizetype *logBytes); +}; + +void AdiImportBenchmark::initTestCase() +{ + QLoggingCategory::setFilterRules(QStringLiteral("*.debug=false")); + Q_INIT_RESOURCE(res); +} + +void AdiImportBenchmark::cleanup() +{ + const QString connectionName = QString::fromLatin1(QSqlDatabase::defaultConnection); + if ( !QSqlDatabase::contains(connectionName) ) + return; + + { + QSqlDatabase db = QSqlDatabase::database(); + if ( db.isValid() ) + db.close(); + } + QSqlDatabase::removeDatabase(connectionName); +} + +void AdiImportBenchmark::benchmarkImportStages_data() +{ + QTest::addColumn("recordCount"); + + QTest::newRow("50k") << 50000; +} + +void AdiImportBenchmark::benchmarkImportStages() +{ + if ( !benchmarkEnabled() ) + QSKIP("Set QLOG_RUN_ADI_IMPORT_BENCHMARK=1 to run the ADI import benchmark."); + + QFETCH(int, recordCount); + + const QVector samples = generateSamples(recordCount); + const QByteArray adi = generateAdi(samples); + + QString error; + QVERIFY2(resetDatabase(&error), qPrintable(error)); + const QSqlRecord recordTemplate = contactRecordTemplate(); + QVERIFY(recordTemplate.count() > 0); + + StageTimings timings; + timings.parseMs = measureParse(adi, &timings.parsed); + QCOMPARE(timings.parsed, recordCount); + + timings.parseAndMapMs = measureParseAndMap(adi, recordTemplate, &timings.mapped); + QCOMPARE(timings.mapped, recordCount); + + QVERIFY2(resetDatabase(&error), qPrintable(error)); + QVERIFY2(populateDuplicateCandidates(samples, &error), qPrintable(error)); + const QStringList existingContactIndexes = contactIndexNames(&error); + QVERIFY2(error.isEmpty(), qPrintable(error)); + QVERIFY(existingContactIndexes.contains(QStringLiteral("contacts_callsign_idx"))); + QVERIFY(existingContactIndexes.contains(QStringLiteral("contacts_band_idx"))); + QVERIFY(existingContactIndexes.contains(QStringLiteral("contacts_mode_idx"))); + QVERIFY(existingContactIndexes.contains(QStringLiteral("contacts_start_time_idx"))); + timings.existingContactIndexCount = existingContactIndexes.size(); + + timings.duplicateCurrentExistingIndexesMs = + measureDuplicateCheckCurrent(samples, &timings.duplicateHits, &error); + QVERIFY2(timings.duplicateCurrentExistingIndexesMs >= 0, qPrintable(error)); + + QVERIFY2(resetDatabase(&error), qPrintable(error)); + const QSqlRecord insertRecordTemplate = contactRecordTemplate(); + timings.insertMs = measureInsert(samples, insertRecordTemplate, &timings.inserted, &error); + QVERIFY2(timings.insertMs >= 0, qPrintable(error)); + QCOMPARE(timings.inserted, recordCount); + + timings.logMs = measureImportLog(samples, recordTemplate, &timings.logBytes); + + const qint64 mappingEstimate = qMax(0, timings.parseAndMapMs - timings.parseMs); + const QString report = + QStringLiteral("ADI import benchmark: records=%1, adif_kB=%2, " + "contacts_existing_index_count=%3, parse_ms=%4, parse_map_ms=%5, " + "mapping_estimate_ms=%6, duplicate_current_existing_indexes_ms=%7, " + "duplicate_hits=%8, db_insert_ms=%9, log_ms=%10, log_kB=%11") + .arg(recordCount) + .arg(adi.size() / 1024) + .arg(timings.existingContactIndexCount) + .arg(timings.parseMs) + .arg(timings.parseAndMapMs) + .arg(mappingEstimate) + .arg(timings.duplicateCurrentExistingIndexesMs) + .arg(timings.duplicateHits) + .arg(timings.insertMs) + .arg(timings.logMs) + .arg(timings.logBytes / 1024); + + qInfo().noquote() << report; +} + +bool AdiImportBenchmark::benchmarkEnabled() +{ + return qEnvironmentVariableIntValue("QLOG_RUN_ADI_IMPORT_BENCHMARK") != 0; +} + +QByteArray AdiImportBenchmark::tag(const QByteArray &name, + const QByteArray &value, + const QByteArray &type) +{ + QByteArray output("<"); + output += name; + output += ':'; + output += QByteArray::number(value.size()); + if ( !type.isEmpty() ) + { + output += ':'; + output += type; + } + output += '>'; + output += value; + return output; +} + +QVector AdiImportBenchmark::generateSamples(int count) +{ + static const struct { + const char *call; + const char *dxcc; + const char *country; + const char *continent; + const char *cqz; + const char *ituz; + const char *grid; + const char *name; + const char *qth; + } stations[] = { + {"OK1AAA", "503", "Czech Republic", "EU", "15", "28", "JO70AA", "Pavel", "Praha"}, + {"DL1ABC", "230", "Germany", "EU", "14", "28", "JO62QN", "Hans", "Berlin"}, + {"K1ABC", "291", "United States", "NA", "5", "8", "FN42AA", "John", "Boston"}, + {"JA1XYZ", "339", "Japan", "AS", "25", "45", "PM95VP", "Ken", "Tokyo"}, + {"PY2ZZZ", "108", "Brazil", "SA", "11", "15", "GG66LK", "Carlos", "Sao Paulo"}, + {"VK3AAA", "150", "Australia", "OC", "30", "59", "QF22OD", "Chris", "Melbourne"} + }; + + static const struct { + const char *band; + const char *mode; + const char *submode; + const char *freq; + } bandModes[] = { + {"40m", "CW", "", "7.025"}, + {"20m", "MFSK", "FT8", "14.074"}, + {"15m", "SSB", "", "21.285"}, + {"10m", "RTTY", "", "28.090"}, + {"17m", "MFSK", "FT4", "18.104"} + }; + + QVector samples; + samples.reserve(count); + + const QDateTime base(QDate(2024, 1, 1), QTime(0, 0, 0), QTimeZone::utc()); + + for ( int i = 0; i < count; ++i ) + { + const auto &station = stations[i % (sizeof(stations) / sizeof(stations[0]))]; + const auto &bandMode = bandModes[i % (sizeof(bandModes) / sizeof(bandModes[0]))]; + + BenchmarkSample sample; + sample.callsign = QString::fromLatin1(station.call) + QString::number(i % 90); + sample.band = QString::fromLatin1(bandMode.band); + sample.mode = QString::fromLatin1(bandMode.mode); + sample.submode = QString::fromLatin1(bandMode.submode); + sample.freq = QString::fromLatin1(bandMode.freq); + sample.dxcc = QString::fromLatin1(station.dxcc); + sample.country = QString::fromLatin1(station.country); + sample.continent = QString::fromLatin1(station.continent); + sample.cqz = QString::fromLatin1(station.cqz); + sample.ituz = QString::fromLatin1(station.ituz); + sample.grid = QString::fromLatin1(station.grid); + sample.name = QString::fromLatin1(station.name); + sample.qth = QString::fromLatin1(station.qth); + sample.comment = QStringLiteral("Contest QSO %1, propagated via realistic ADI import benchmark").arg(i); + sample.potaRef = QStringLiteral("OK-%1").arg(1000 + (i % 200), 4, 10, QLatin1Char('0')); + sample.myPotaRef = QStringLiteral("OK-%1").arg(2000 + (i % 150), 4, 10, QLatin1Char('0')); + sample.satName = (i % 97 == 0) ? QStringLiteral("QO-100") : QString(); + sample.startTime = base.addSecs(i * 73); + samples.append(sample); + } + + return samples; +} + +QByteArray AdiImportBenchmark::generateAdi(const QVector &samples) +{ + QByteArray output("QLog ADI import benchmark\r\n"); + output += tag("ADIF_VER", "3.1.7"); + output += tag("PROGRAMID", "QLog"); + output += tag("USERDEF", "APP_QLOG_BENCHMARK"); + output += "\r\n"; + output.reserve(output.size() + samples.size() * 720); + + for ( int i = 0; i < samples.size(); ++i ) + { + const BenchmarkSample &sample = samples.at(i); + const QDate date = sample.startTime.date(); + const QTime time = sample.startTime.time(); + + output += tag("CALL", sample.callsign.toLatin1()); + output += tag("QSO_DATE", date.toString(QStringLiteral("yyyyMMdd")).toLatin1(), "D"); + output += tag("TIME_ON", time.toString(QStringLiteral("hhmmss")).toLatin1(), "T"); + output += tag("TIME_OFF", time.addSecs(180 + (i % 300)).toString(QStringLiteral("hhmmss")).toLatin1(), "T"); + output += tag("BAND", sample.band.toLatin1()); + output += tag("FREQ", sample.freq.toLatin1(), "N"); + output += tag("MODE", sample.mode.toLatin1()); + if ( !sample.submode.isEmpty() ) + output += tag("SUBMODE", sample.submode.toLatin1()); + output += tag("RST_SENT", (i % 3 == 0) ? "599" : "59"); + output += tag("RST_RCVD", (i % 4 == 0) ? "599" : "59"); + output += tag("DXCC", sample.dxcc.toLatin1(), "N"); + output += tag("COUNTRY", sample.country.toLatin1()); + output += tag("CONT", sample.continent.toLatin1()); + output += tag("CQZ", sample.cqz.toLatin1(), "N"); + output += tag("ITUZ", sample.ituz.toLatin1(), "N"); + output += tag("GRIDSQUARE", sample.grid.toLatin1()); + output += tag("NAME", sample.name.toLatin1()); + output += tag("QTH", sample.qth.toLatin1()); + output += tag("COMMENT", sample.comment.toLatin1()); + output += tag("STATION_CALLSIGN", "OK1QLOG"); + output += tag("MY_DXCC", "503", "N"); + output += tag("MY_GRIDSQUARE", "JO70AA"); + output += tag("OPERATOR", "OK1QLOG"); + output += tag("TX_PWR", QByteArray::number(50 + (i % 70)), "N"); + output += tag("ANT_AZ", QByteArray::number((i * 7) % 360), "N"); + output += tag("ANT_EL", QByteArray::number((i * 3) % 90), "N"); + output += tag("QSL_SENT", (i % 13 == 0) ? "Y" : "N"); + output += tag("QSL_RCVD", (i % 17 == 0) ? "Y" : "N"); + output += tag("LOTW_QSL_SENT", (i % 5 == 0) ? "Y" : "N"); + output += tag("LOTW_QSL_RCVD", (i % 7 == 0) ? "Y" : "N"); + output += tag("EQSL_QSL_SENT", (i % 11 == 0) ? "Y" : "N"); + output += tag("EQSL_QSL_RCVD", (i % 19 == 0) ? "Y" : "N"); + output += tag("POTA_REF", sample.potaRef.toLatin1()); + output += tag("MY_POTA_REF", sample.myPotaRef.toLatin1()); + if ( !sample.satName.isEmpty() ) + output += tag("SAT_NAME", sample.satName.toLatin1()); + output += tag("APP_QLOG_BENCHMARK", QByteArray::number(i), "S"); + output += "\r\n"; + } + + return output; +} + +bool AdiImportBenchmark::executeSqlFile(int version, QString *error) +{ + const QString resourceName = QStringLiteral(":/res/sql/migration_%1.sql") + .arg(version, 3, 10, QLatin1Char('0')); + QFile sqlFile(resourceName); + if ( !sqlFile.open(QIODevice::ReadOnly | QIODevice::Text) ) + { + *error = QStringLiteral("Cannot open %1").arg(resourceName); + return false; + } + + const QString sqlContent = QTextStream(&sqlFile).readAll(); + const QStringList statements = sqlContent.split(QLatin1Char('\n')) + .join(QStringLiteral(" ")) + .split(QLatin1Char(';')); + + QSqlDatabase db = QSqlDatabase::database(); + QSqlQuery query(db); + + if ( !db.transaction() ) + { + *error = db.lastError().text(); + return false; + } + + for ( const QString &statement : statements ) + { + const QString trimmed = statement.trimmed(); + if ( trimmed.isEmpty() ) + continue; + + if ( !query.exec(trimmed) ) + { + *error = QStringLiteral("Migration %1 failed: %2\n%3") + .arg(version, 3, 10, QLatin1Char('0')) + .arg(query.lastError().text(), trimmed); + db.rollback(); + return false; + } + } + + return db.commit(); +} + +bool AdiImportBenchmark::resetDatabase(QString *error) +{ + const QString connectionName = QString::fromLatin1(QSqlDatabase::defaultConnection); + if ( QSqlDatabase::contains(connectionName) ) + { + { + QSqlDatabase db = QSqlDatabase::database(); + if ( db.isValid() ) + db.close(); + } + QSqlDatabase::removeDatabase(connectionName); + } + + QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE")); + db.setDatabaseName(QStringLiteral(":memory:")); + db.setConnectOptions(QStringLiteral("QSQLITE_ENABLE_REGEXP")); + if ( !db.open() ) + { + *error = db.lastError().text(); + return false; + } + + for ( int version = 1; version <= 39; ++version ) + { + if ( !executeSqlFile(version, error) ) + return false; + } + + return true; +} + +QStringList AdiImportBenchmark::contactIndexNames(QString *error) +{ + QStringList names; + QSqlQuery query(QStringLiteral("PRAGMA index_list('contacts')")); + if ( !query.isActive() ) + { + *error = query.lastError().text(); + return names; + } + + while ( query.next() ) + names.append(query.value(QStringLiteral("name")).toString()); + + return names; +} + +QSqlRecord AdiImportBenchmark::contactRecordTemplate() +{ + QSqlTableModel model; + model.setTable(QStringLiteral("contacts")); + model.removeColumn(model.fieldIndex(QStringLiteral("id"))); + return model.record(); +} + +void AdiImportBenchmark::setRecordValue(QSqlRecord &record, + const QString &field, + const QVariant &value) +{ + const int index = record.indexOf(field); + if ( index >= 0 ) + record.setValue(index, value); +} + +void AdiImportBenchmark::fillRecord(QSqlRecord &record, + const BenchmarkSample &sample, + int index) +{ + record.clearValues(); + + setRecordValue(record, QStringLiteral("start_time"), sample.startTime); + setRecordValue(record, QStringLiteral("end_time"), sample.startTime.addSecs(180 + (index % 300))); + setRecordValue(record, QStringLiteral("callsign"), sample.callsign.toUpper()); + setRecordValue(record, QStringLiteral("rst_sent"), (index % 3 == 0) ? QStringLiteral("599") : QStringLiteral("59")); + setRecordValue(record, QStringLiteral("rst_rcvd"), (index % 4 == 0) ? QStringLiteral("599") : QStringLiteral("59")); + setRecordValue(record, QStringLiteral("freq"), sample.freq); + setRecordValue(record, QStringLiteral("band"), sample.band); + setRecordValue(record, QStringLiteral("mode"), sample.mode); + setRecordValue(record, QStringLiteral("submode"), sample.submode); + setRecordValue(record, QStringLiteral("name"), sample.name); + setRecordValue(record, QStringLiteral("qth"), sample.qth); + setRecordValue(record, QStringLiteral("gridsquare"), sample.grid); + setRecordValue(record, QStringLiteral("dxcc"), sample.dxcc.toInt()); + setRecordValue(record, QStringLiteral("country"), sample.country); + setRecordValue(record, QStringLiteral("country_intl"), sample.country); + setRecordValue(record, QStringLiteral("cont"), sample.continent); + setRecordValue(record, QStringLiteral("cqz"), sample.cqz.toInt()); + setRecordValue(record, QStringLiteral("ituz"), sample.ituz.toInt()); + setRecordValue(record, QStringLiteral("pfx"), sample.callsign.left(3)); + setRecordValue(record, QStringLiteral("qsl_rcvd"), (index % 17 == 0) ? QStringLiteral("Y") : QStringLiteral("N")); + setRecordValue(record, QStringLiteral("qsl_sent"), (index % 13 == 0) ? QStringLiteral("Y") : QStringLiteral("N")); + setRecordValue(record, QStringLiteral("lotw_qsl_rcvd"), (index % 7 == 0) ? QStringLiteral("Y") : QStringLiteral("N")); + setRecordValue(record, QStringLiteral("lotw_qsl_sent"), (index % 5 == 0) ? QStringLiteral("Y") : QStringLiteral("N")); + setRecordValue(record, QStringLiteral("eqsl_qsl_rcvd"), (index % 19 == 0) ? QStringLiteral("Y") : QStringLiteral("N")); + setRecordValue(record, QStringLiteral("eqsl_qsl_sent"), (index % 11 == 0) ? QStringLiteral("Y") : QStringLiteral("N")); + setRecordValue(record, QStringLiteral("dcl_qsl_rcvd"), QStringLiteral("N")); + setRecordValue(record, QStringLiteral("dcl_qsl_sent"), QStringLiteral("N")); + setRecordValue(record, QStringLiteral("tx_pwr"), 50 + (index % 70)); + setRecordValue(record, QStringLiteral("comment"), sample.comment); + setRecordValue(record, QStringLiteral("station_callsign"), QStringLiteral("OK1QLOG")); + setRecordValue(record, QStringLiteral("my_dxcc"), 503); + setRecordValue(record, QStringLiteral("my_gridsquare"), QStringLiteral("JO70AA")); + setRecordValue(record, QStringLiteral("operator"), QStringLiteral("OK1QLOG")); + setRecordValue(record, QStringLiteral("ant_az"), (index * 7) % 360); + setRecordValue(record, QStringLiteral("ant_el"), (index * 3) % 90); + setRecordValue(record, QStringLiteral("pota_ref"), sample.potaRef); + setRecordValue(record, QStringLiteral("my_pota_ref"), sample.myPotaRef); + setRecordValue(record, QStringLiteral("sat_name"), sample.satName); + setRecordValue(record, QStringLiteral("fields"), + QStringLiteral("{\"app_qlog_benchmark\":\"%1\"}").arg(index)); +} + +qint64 AdiImportBenchmark::measureParse(const QByteArray &adi, int *parsed) +{ + QByteArray input(adi); + QBuffer buffer(&input); + buffer.open(QIODevice::ReadOnly | QIODevice::Text); + + QTextStream stream(&buffer); + TestAdiFormat format(stream); + format.importStartForTest(); + + QVariantMap contact; + QElapsedTimer timer; + timer.start(); + + int count = 0; + while ( format.readContactForTest(contact) ) + { + ++count; + contact.clear(); + } + + *parsed = count; + return timer.elapsed(); +} + +qint64 AdiImportBenchmark::measureParseAndMap(const QByteArray &adi, + const QSqlRecord &recordTemplate, + int *mapped) +{ + QByteArray input(adi); + QBuffer buffer(&input); + buffer.open(QIODevice::ReadOnly | QIODevice::Text); + + QTextStream stream(&buffer); + TestAdiFormat format(stream); + format.importStartForTest(); + + QSqlRecord record(recordTemplate); + QElapsedTimer timer; + timer.start(); + + int count = 0; + while ( true ) + { + record.clearValues(); + if ( !format.importNext(record) ) + break; + ++count; + } + + *mapped = count; + return timer.elapsed(); +} + +bool AdiImportBenchmark::populateDuplicateCandidates(const QVector &samples, + QString *error) +{ + QSqlQuery insert; + if ( !insert.prepare(QStringLiteral("INSERT INTO contacts " + "(callsign, mode, band, sat_name, start_time, " + "qsl_rcvd, qsl_sent, lotw_qsl_rcvd, lotw_qsl_sent, " + "eqsl_qsl_rcvd, eqsl_qsl_sent, dcl_qsl_rcvd, dcl_qsl_sent) " + "VALUES (?, ?, ?, ?, ?, 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N')")) ) + { + *error = insert.lastError().text(); + return false; + } + + QSqlDatabase::database().transaction(); + + for ( int i = 0; i < samples.size(); i += 10 ) + { + const BenchmarkSample &sample = samples.at(i); + insert.bindValue(0, sample.callsign.toUpper()); + insert.bindValue(1, sample.mode); + insert.bindValue(2, sample.band); + insert.bindValue(3, sample.satName); + insert.bindValue(4, sample.startTime.toString(QStringLiteral("yyyy-MM-dd hh:mm:ss"))); + if ( !insert.exec() ) + { + *error = insert.lastError().text(); + QSqlDatabase::database().rollback(); + return false; + } + } + + return QSqlDatabase::database().commit(); +} + +qint64 AdiImportBenchmark::measureDuplicateCheckCurrent(const QVector &samples, + int *duplicateHits, + QString *error) +{ + QSqlQuery dupQuery; + if ( !dupQuery.prepare(QStringLiteral("SELECT * FROM contacts " + "WHERE callsign=upper(:callsign) " + "AND upper(mode)=upper(:mode) " + "AND upper(band)=upper(:band) " + "AND COALESCE(sat_name, '') = COALESCE(:sat_name, '') " + "AND ABS(JULIANDAY(start_time)-JULIANDAY(datetime(:startdate)))*24*60<30")) ) + { + *error = dupQuery.lastError().text(); + return -1; + } + + int hits = 0; + QElapsedTimer timer; + timer.start(); + + for ( const BenchmarkSample &sample : samples ) + { + dupQuery.bindValue(QStringLiteral(":callsign"), sample.callsign); + dupQuery.bindValue(QStringLiteral(":mode"), sample.mode); + dupQuery.bindValue(QStringLiteral(":band"), sample.band); + dupQuery.bindValue(QStringLiteral(":startdate"), + sample.startTime.toString(QStringLiteral("yyyy-MM-dd hh:mm:ss"))); + dupQuery.bindValue(QStringLiteral(":sat_name"), sample.satName); + + if ( !dupQuery.exec() ) + { + *error = dupQuery.lastError().text(); + return -1; + } + + if ( dupQuery.next() ) + ++hits; + } + + *duplicateHits = hits; + return timer.elapsed(); +} + +qint64 AdiImportBenchmark::measureInsert(const QVector &samples, + const QSqlRecord &recordTemplate, + int *inserted, + QString *error) +{ + QSqlRecord record(recordTemplate); + QSqlQuery insertQuery; + if ( !insertQuery.prepare(QSqlDatabase::database().driver()->sqlStatement(QSqlDriver::InsertStatement, + QStringLiteral("contacts"), + record, + true)) ) + { + *error = insertQuery.lastError().text(); + return -1; + } + + QSqlDatabase::database().transaction(); + + int count = 0; + QElapsedTimer timer; + timer.start(); + + for ( int i = 0; i < samples.size(); ++i ) + { + fillRecord(record, samples.at(i), i); + + for ( int field = 0; field < record.count(); ++field ) + insertQuery.bindValue(field, record.value(field)); + + if ( !insertQuery.exec() ) + { + *error = insertQuery.lastError().text(); + QSqlDatabase::database().rollback(); + return -1; + } + ++count; + } + + const qint64 elapsed = timer.elapsed(); + QSqlDatabase::database().commit(); + + *inserted = count; + return elapsed; +} + +qint64 AdiImportBenchmark::measureImportLog(const QVector &samples, + const QSqlRecord &recordTemplate, + qsizetype *logBytes) +{ + QSqlRecord record(recordTemplate); + QString log; + log.reserve(samples.size() * 80); + QTextStream stream(&log); + + QElapsedTimer timer; + timer.start(); + + for ( int i = 0; i < samples.size(); ++i ) + { + fillRecord(record, samples.at(i), i); + stream << QStringLiteral("[QSO#%1]: ").arg(i + 1) + << QStringLiteral("INFO: ") + << QStringLiteral("Imported") + << QStringLiteral(" (%1; %2; %3)") + .arg(record.value(QStringLiteral("start_time")).toDateTime() + .toTimeZone(QTimeZone::utc()) + .toString(QStringLiteral("yyyy-MM-dd")), + record.value(QStringLiteral("callsign")).toString(), + record.value(QStringLiteral("mode")).toString()) + << QLatin1Char('\n'); + } + + stream.flush(); + const qint64 elapsed = timer.elapsed(); + *logBytes = log.size() * qsizetype(sizeof(QChar)); + return elapsed; +} + +QTEST_APPLESS_MAIN(AdiImportBenchmark) + +#include "tst_adiimportbenchmark.moc" diff --git a/tests/AdifRecoveryTest/AdifRecoveryTest.pro b/tests/AdifRecoveryTest/AdifRecoveryTest.pro new file mode 100644 index 00000000..c245c38d --- /dev/null +++ b/tests/AdifRecoveryTest/AdifRecoveryTest.pro @@ -0,0 +1,13 @@ +QT += testlib core +CONFIG += console testcase c++11 +TEMPLATE = app +TARGET = tst_adifrecovery + +INCLUDEPATH += $$PWD/../.. + +SOURCES += \ + tst_adifrecovery.cpp \ + ../../core/AdifRecovery.cpp + +HEADERS += \ + ../../core/AdifRecovery.h diff --git a/tests/AdifRecoveryTest/tst_adifrecovery.cpp b/tests/AdifRecoveryTest/tst_adifrecovery.cpp new file mode 100644 index 00000000..c8642982 --- /dev/null +++ b/tests/AdifRecoveryTest/tst_adifrecovery.cpp @@ -0,0 +1,322 @@ +#include +#include +#include +#include +#include + +#include "core/AdifRecovery.h" + +class AdifRecoveryTest : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void configSerializationRoundTrip(); + void configDeserializationDefaults(); + void stateSerializationRoundTrip(); + void invalidStateDeserializationReturnsDefault(); + void firstReadInitializesAtEnd(); + void offsetAtEndDoesNothing(); + void appendedCompleteRecordsAreReturned(); + void uppercaseEorIsAccepted(); + void incompleteTrailingRecordIsLeftUnread(); + void offsetAfterTagStartStillReadsRecord(); + void garbageAfterOffsetDoesNotAdvance(); + void missingFileReportsProblem(); + void emptyPathReportsProblem(); + void truncatedFileResetsLoadPoint(); + void tooManyRecordsMovesLoadPointToEnd(); + +private: + QString writeFile(const QByteArray &content); + AdifRecoveryScanResult scan(const QString &path, qint64 offset, int maxContacts = 0); + + QTemporaryDir tempDir; +}; + +void AdifRecoveryTest::initTestCase() +{ + QLoggingCategory::setFilterRules(QStringLiteral("*.debug=false")); + qRegisterMetaType("AdifRecoveryScanResult"); + QVERIFY(tempDir.isValid()); +} + +void AdifRecoveryTest::configSerializationRoundTrip() +{ + AdifRecoveryConfig skipped; + skipped.enabled = true; + skipped.stationProfileName = QStringLiteral("ignored"); + skipped.path = QStringLiteral(" "); + + AdifRecoveryConfig custom; + custom.enabled = false; + custom.stationProfileName = QStringLiteral("Home"); + custom.qslSentStatusDefault = QStringLiteral("custom"); + custom.path = QStringLiteral("/tmp/home.adi"); + + AdifRecoveryConfig defaultStatus; + defaultStatus.enabled = true; + defaultStatus.stationProfileName = QStringLiteral("Portable"); + defaultStatus.qslSentStatusDefault = QString(); + defaultStatus.path = QStringLiteral("/tmp/portable.adi"); + + const QList configs = AdifRecovery::deserializeConfigList( + AdifRecovery::serializeConfigList({skipped, custom, defaultStatus})); + + QCOMPARE(configs.size(), 2); + QCOMPARE(configs.at(0).enabled, false); + QCOMPARE(configs.at(0).stationProfileName, QStringLiteral("Home")); + QCOMPARE(configs.at(0).qslSentStatusDefault, QStringLiteral("custom")); + QCOMPARE(configs.at(0).path, QStringLiteral("/tmp/home.adi")); + QCOMPARE(configs.at(1).enabled, true); + QCOMPARE(configs.at(1).stationProfileName, QStringLiteral("Portable")); + QCOMPARE(configs.at(1).qslSentStatusDefault, QStringLiteral("Q")); + QCOMPARE(configs.at(1).path, QStringLiteral("/tmp/portable.adi")); +} + +void AdifRecoveryTest::configDeserializationDefaults() +{ + const QString json = QStringLiteral(R"json([ + { "path": "/tmp/defaults.adi", "stationProfileName": "Default" }, + { "path": " " }, + { "path": "/tmp/custom.adi", "enabled": false, "qslSentStatusDefault": "I" } + ])json"); + + const QList configs = AdifRecovery::deserializeConfigList(json); + + QCOMPARE(configs.size(), 2); + QCOMPARE(configs.at(0).enabled, true); + QCOMPARE(configs.at(0).stationProfileName, QStringLiteral("Default")); + QCOMPARE(configs.at(0).qslSentStatusDefault, QStringLiteral("Q")); + QCOMPARE(configs.at(0).path, QStringLiteral("/tmp/defaults.adi")); + QCOMPARE(configs.at(1).enabled, false); + QCOMPARE(configs.at(1).qslSentStatusDefault, QStringLiteral("I")); + QCOMPARE(configs.at(1).path, QStringLiteral("/tmp/custom.adi")); +} + +void AdifRecoveryTest::stateSerializationRoundTrip() +{ + AdifRecoveryState state; + state.path = QStringLiteral("/tmp/recovery.adi"); + state.offset = 123456789; + state.lastRecoveryAt = QDateTime(QDate(2026, 5, 22), QTime(8, 30, 15), Qt::UTC); + state.lastMessage = QStringLiteral("done"); + + const AdifRecoveryState restored = AdifRecovery::deserializeState(AdifRecovery::serializeState(state)); + + QCOMPARE(restored.path, state.path); + QCOMPARE(restored.offset, state.offset); + QCOMPARE(restored.lastRecoveryAt, state.lastRecoveryAt); + QCOMPARE(restored.lastMessage, state.lastMessage); +} + +void AdifRecoveryTest::invalidStateDeserializationReturnsDefault() +{ + const AdifRecoveryState state = AdifRecovery::deserializeState(QStringLiteral("not json")); + + QCOMPARE(state.path, QString()); + QCOMPARE(state.offset, static_cast(-1)); + QVERIFY(!state.lastRecoveryAt.isValid()); + QCOMPARE(state.lastMessage, QString()); +} + +QString AdifRecoveryTest::writeFile(const QByteArray &content) +{ + const QString path = tempDir.filePath(QUuid::createUuid().toString(QUuid::WithoutBraces) + QStringLiteral(".adi")); + QFile file(path); + if ( !file.open(QIODevice::WriteOnly | QIODevice::Truncate) ) + qFatal("Cannot open temporary ADIF file for writing"); + if ( file.write(content) != static_cast(content.size()) ) + qFatal("Cannot write complete temporary ADIF file"); + file.close(); + return path; +} + +AdifRecoveryScanResult AdifRecoveryTest::scan(const QString &path, qint64 offset, int maxContacts) +{ + AdifRecoveryConfig config; + config.path = path; + config.stationProfileName = QStringLiteral("default"); + + AdifRecoveryState state; + state.path = path; + state.offset = offset; + + AdifRecoveryReaderWorker worker; + QSignalSpy spy(&worker, &AdifRecoveryReaderWorker::scanFinished); + if ( !spy.isValid() ) + qFatal("Cannot create scanFinished signal spy"); + + worker.readTail(config, state, maxContacts); + + if ( spy.count() != 1 ) + qFatal("AdifRecoveryReaderWorker did not emit exactly one scanFinished signal"); + return spy.takeFirst().at(0).value(); +} + +void AdifRecoveryTest::firstReadInitializesAtEnd() +{ + const QByteArray content("AA1AA"); + const QString path = writeFile(content); + + const AdifRecoveryScanResult result = scan(path, -1); + + QCOMPARE(result.previousOffset, static_cast(-1)); + QCOMPARE(result.fileSize, static_cast(content.size())); + QCOMPARE(result.nextOffset, static_cast(content.size())); + QVERIFY(result.adifText.isEmpty()); + QVERIFY(!result.message.isEmpty()); + QVERIFY(!result.reset); + QVERIFY(!result.tooMany); +} + +void AdifRecoveryTest::offsetAtEndDoesNothing() +{ + const QByteArray content("AA1AA"); + const QString path = writeFile(content); + + const AdifRecoveryScanResult result = scan(path, content.size()); + + QCOMPARE(result.previousOffset, static_cast(content.size())); + QCOMPARE(result.nextOffset, static_cast(content.size())); + QCOMPARE(result.fileSize, static_cast(content.size())); + QVERIFY(result.adifText.isEmpty()); + QCOMPARE(result.contactCount, 0); + QVERIFY(result.message.isEmpty()); + QVERIFY(!result.reset); + QVERIFY(!result.tooMany); +} + +void AdifRecoveryTest::appendedCompleteRecordsAreReturned() +{ + const QByteArray initial("AA1AA"); + const QByteArray records("BB2BBCC3CC"); + const QByteArray content = initial + "\n" + records + "tail"; + const QString path = writeFile(content); + + const AdifRecoveryScanResult result = scan(path, initial.size()); + + QCOMPARE(result.contactCount, 2); + QCOMPARE(result.adifText, QString::fromLatin1(records)); + QCOMPARE(result.nextOffset, static_cast(initial.size() + 1 + records.size())); + QVERIFY(result.message.isEmpty()); + QVERIFY(!result.reset); + QVERIFY(!result.tooMany); +} + +void AdifRecoveryTest::uppercaseEorIsAccepted() +{ + const QByteArray content("AA1AABB2BB"); + const QString path = writeFile(content); + + const AdifRecoveryScanResult result = scan(path, 0); + + QCOMPARE(result.contactCount, 2); + QCOMPARE(result.adifText, QString::fromLatin1(content)); + QCOMPARE(result.nextOffset, static_cast(content.size())); +} + +void AdifRecoveryTest::incompleteTrailingRecordIsLeftUnread() +{ + const QByteArray initial("AA1AA"); + const QByteArray complete("BB2BB"); + const QByteArray incomplete("CC3CC"); + const QByteArray content = initial + complete + incomplete; + const QString path = writeFile(content); + + const AdifRecoveryScanResult result = scan(path, initial.size()); + + QCOMPARE(result.contactCount, 1); + QCOMPARE(result.adifText, QString::fromLatin1(complete)); + QCOMPARE(result.nextOffset, static_cast(initial.size() + complete.size())); +} + +void AdifRecoveryTest::offsetAfterTagStartStillReadsRecord() +{ + const QByteArray content("AA1AA"); + const QString path = writeFile(content); + + const AdifRecoveryScanResult result = scan(path, 1); + + QCOMPARE(result.contactCount, 1); + QCOMPARE(result.adifText, QString::fromLatin1(content)); + QCOMPARE(result.nextOffset, static_cast(content.size())); +} + +void AdifRecoveryTest::garbageAfterOffsetDoesNotAdvance() +{ + const QByteArray initial("AA1AA"); + const QByteArray garbage("this is not ADIFBB2BB"); + const QByteArray content = initial + garbage; + const QString path = writeFile(content); + + const AdifRecoveryScanResult result = scan(path, initial.size()); + + QCOMPARE(result.previousOffset, static_cast(initial.size())); + QCOMPARE(result.nextOffset, static_cast(initial.size())); + QVERIFY(result.adifText.isEmpty()); + QCOMPARE(result.contactCount, 0); + QVERIFY(result.message.isEmpty()); +} + +void AdifRecoveryTest::missingFileReportsProblem() +{ + const QString path = tempDir.filePath(QStringLiteral("missing.adi")); + + const AdifRecoveryScanResult result = scan(path, 0); + + QCOMPARE(result.previousOffset, static_cast(0)); + QCOMPARE(result.nextOffset, static_cast(0)); + QCOMPARE(result.fileSize, static_cast(-1)); + QVERIFY(result.adifText.isEmpty()); + QVERIFY(!result.message.isEmpty()); + QVERIFY(!result.reset); + QVERIFY(!result.tooMany); +} + +void AdifRecoveryTest::emptyPathReportsProblem() +{ + const AdifRecoveryScanResult result = scan(QString(), 0); + + QCOMPARE(result.previousOffset, static_cast(0)); + QCOMPARE(result.nextOffset, static_cast(0)); + QCOMPARE(result.fileSize, static_cast(-1)); + QVERIFY(result.adifText.isEmpty()); + QVERIFY(!result.message.isEmpty()); + QVERIFY(!result.reset); + QVERIFY(!result.tooMany); +} + +void AdifRecoveryTest::truncatedFileResetsLoadPoint() +{ + const QByteArray content("AA1AA"); + const QString path = writeFile(content); + + const AdifRecoveryScanResult result = scan(path, content.size() + 10); + + QVERIFY(result.reset); + QVERIFY(result.adifText.isEmpty()); + QCOMPARE(result.nextOffset, static_cast(content.size())); + QVERIFY(!result.message.isEmpty()); +} + +void AdifRecoveryTest::tooManyRecordsMovesLoadPointToEnd() +{ + const QByteArray first("AA1AA"); + const QByteArray second("BB2BB"); + const QByteArray content = first + second; + const QString path = writeFile(content); + + const AdifRecoveryScanResult result = scan(path, 0, 1); + + QVERIFY(result.tooMany); + QCOMPARE(result.contactCount, 2); + QVERIFY(result.adifText.isEmpty()); + QCOMPARE(result.nextOffset, static_cast(content.size())); + QVERIFY(!result.message.isEmpty()); +} + +QTEST_APPLESS_MAIN(AdifRecoveryTest) + +#include "tst_adifrecovery.moc" diff --git a/tests/AdxFormatTest/AdxFormatTest.pro b/tests/AdxFormatTest/AdxFormatTest.pro new file mode 100644 index 00000000..8133a31b --- /dev/null +++ b/tests/AdxFormatTest/AdxFormatTest.pro @@ -0,0 +1,23 @@ +QT += testlib core sql xml +CONFIG += console testcase c++11 +TEMPLATE = app +TARGET = tst_adxformat + +DEFINES += VERSION=\\\"test\\\" + +INCLUDEPATH += $$PWD/../.. + +SOURCES += \ + tst_adxformat.cpp \ + test_stubs.cpp \ + ../../core/LogLocale.cpp \ + ../../data/Accents.cpp \ + ../../logformat/AdxFormat.cpp \ + ../../logformat/AdiFormat.cpp + +HEADERS += \ + ../../core/LogLocale.h \ + ../../data/Data.h \ + ../../logformat/AdxFormat.h \ + ../../logformat/AdiFormat.h \ + ../../logformat/LogFormat.h diff --git a/tests/AdxFormatTest/test_stubs.cpp b/tests/AdxFormatTest/test_stubs.cpp new file mode 100644 index 00000000..fb0d3784 --- /dev/null +++ b/tests/AdxFormatTest/test_stubs.cpp @@ -0,0 +1,42 @@ +#include "data/Data.h" +#include "logformat/LogFormat.h" + +LogFormat::LogFormat(QTextStream &stream) : + QObject(nullptr), + stream(stream), + exportedFields(QStringLiteral("*")), + duplicateQSOFunc(nullptr) +{ + defaults = nullptr; +} + +LogFormat::~LogFormat() = default; + +void LogFormat::setDefaults(QMap &defaults) +{ + this->defaults = &defaults; +} + +Data::Data(QObject *parent) : + QObject(parent) +{ +} + +Data::~Data() = default; + +QPair Data::legacyMode(const QString &) +{ + return {}; +} + +void Data::invalidateDXCCStatusCache(const QSqlRecord &) +{ +} + +void Data::invalidateSetOfDXCCStatusCache(const QSet &) +{ +} + +void Data::clearDXCCStatusCache() +{ +} diff --git a/tests/AdxFormatTest/tst_adxformat.cpp b/tests/AdxFormatTest/tst_adxformat.cpp new file mode 100644 index 00000000..5794bedf --- /dev/null +++ b/tests/AdxFormatTest/tst_adxformat.cpp @@ -0,0 +1,457 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "logformat/AdxFormat.h" + +class AdxFormatTest : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void exportStartWritesAdxHeader(); + void writeSqlRecordExportsApplicationTags(); + void writeSqlRecordExportsRawFieldsFiltersInvalidNames(); + void exportContactNormalizesGridAndTerminatesRecord(); + void importNextMapsAdxFieldsAndStoresUnknownFields(); + void importNextStoresZeroLengthApplicationFields(); + void importNextAppliesDefaultsForMissingFields(); + void importNextKeepsImportedValuesOverDefaults(); + void importNextFillsIntlCompanionFields(); + void importNextAppliesDefaultsBeforeIntlCompanionFields(); + +private: + static void appendField(QSqlRecord &record, + const QString &name, + const QVariant &value); + static QByteArray xmlRecord(const QByteArray &recordBody); + static QByteArray exportRecord(const QSqlRecord &record, + QMap *applTags = nullptr); +}; + +void AdxFormatTest::initTestCase() +{ + QLoggingCategory::setFilterRules(QStringLiteral("*.debug=false")); +} + +void AdxFormatTest::appendField(QSqlRecord &record, + const QString &name, + const QVariant &value) +{ + QSqlField field(name, value.type()); + field.setValue(value); + record.append(field); +} + +QByteArray AdxFormatTest::xmlRecord(const QByteArray &recordBody) +{ + QByteArray input(""); + input += "
3.1.7
"; + input += recordBody; + input += "
"; + return input; +} + +QByteArray AdxFormatTest::exportRecord(const QSqlRecord &record, + QMap *applTags) +{ + QByteArray output; + QBuffer buffer(&output); + if ( !buffer.open(QIODevice::WriteOnly | QIODevice::Text) ) + return output; + + QTextStream stream(&buffer); + AdxFormat format(stream); + + format.exportStart(); + format.exportContact(record, applTags); + format.exportEnd(); + stream.flush(); + + return output; +} + +void AdxFormatTest::exportStartWritesAdxHeader() +{ + QByteArray output; + QBuffer buffer(&output); + QVERIFY(buffer.open(QIODevice::WriteOnly | QIODevice::Text)); + + QTextStream stream(&buffer); + AdxFormat format(stream); + format.exportStart(); + format.exportEnd(); + stream.flush(); + + QVERIFY(output.startsWith("")); + QVERIFY(output.contains("
")); + QVERIFY(output.contains("")); + + QMap headerValues; + QXmlStreamReader reader(output); + while ( !reader.atEnd() ) + { + reader.readNext(); + if ( reader.isStartElement() + && (reader.name() == QStringLiteral("ADIF_VER") + || reader.name() == QStringLiteral("PROGRAMID") + || reader.name() == QStringLiteral("PROGRAMVERSION") + || reader.name() == QStringLiteral("CREATED_TIMESTAMP")) ) + { + headerValues.insert(reader.name().toString(), reader.readElementText()); + } + } + QVERIFY(!reader.hasError()); + + QCOMPARE(headerValues.value(QStringLiteral("ADIF_VER")), QStringLiteral("3.1.7")); + QCOMPARE(headerValues.value(QStringLiteral("PROGRAMID")), QStringLiteral("QLog")); + QCOMPARE(headerValues.value(QStringLiteral("PROGRAMVERSION")), QStringLiteral("test")); + QCOMPARE(headerValues.value(QStringLiteral("CREATED_TIMESTAMP")).length(), 15); +} + +void AdxFormatTest::writeSqlRecordExportsApplicationTags() +{ + QSqlRecord record; + QJsonObject fields; + QJsonObject lotwModeGroup; + lotwModeGroup.insert(QStringLiteral("value"), QStringLiteral("DATA")); + lotwModeGroup.insert(QStringLiteral("type"), QStringLiteral("S")); + fields.insert(QStringLiteral("APP_LoTW_MODEGROUP"), lotwModeGroup); + appendField(record, + QStringLiteral("fields"), + QString(QJsonDocument(fields).toJson(QJsonDocument::Compact))); + + QMap applTags; + applTags.insert(QStringLiteral("APP_QLOG_NOTE"), QStringLiteral("Line1\nLine2")); + + const QByteArray output = exportRecord(record, &applTags); + + QVERIFY(!output.contains("APP_QLOG_NOTE")); + QVERIFY(!output.contains("APP_LoTW_MODEGROUP")); + + QMap values; + QMap types; + QXmlStreamReader reader(output); + while ( !reader.atEnd() ) + { + reader.readNext(); + if ( reader.isStartElement() && reader.name() == QStringLiteral("APP") ) + { + const QXmlStreamAttributes attributes = reader.attributes(); + const QString key = attributes.value(QStringLiteral("PROGRAMID")).toString() + + QLatin1Char('/') + + attributes.value(QStringLiteral("FIELDNAME")).toString(); + types.insert(key, attributes.value(QStringLiteral("TYPE")).toString()); + values.insert(key, reader.readElementText()); + } + } + QVERIFY(!reader.hasError()); + + QCOMPARE(values.value(QStringLiteral("QLOG/NOTE")), QStringLiteral("Line1\nLine2")); + QCOMPARE(types.value(QStringLiteral("QLOG/NOTE")), QStringLiteral("M")); + QCOMPARE(values.value(QStringLiteral("LoTW/MODEGROUP")), QStringLiteral("DATA")); + QCOMPARE(types.value(QStringLiteral("LoTW/MODEGROUP")), QStringLiteral("S")); +} + +void AdxFormatTest::writeSqlRecordExportsRawFieldsFiltersInvalidNames() +{ + QSqlRecord record; + QJsonObject fields; + fields.insert(QStringLiteral("UNKNOWN_FIELD"), QStringLiteral("ok")); + fields.insert(QStringLiteral("APP_QLOG_RAW"), QStringLiteral("Line1\nLine2")); + fields.insert(QStringLiteral("BAD-NAME"), QStringLiteral("dash")); + fields.insert(QStringLiteral("1BAD"), QStringLiteral("digit")); + fields.insert(QStringLiteral("xmlBAD"), QStringLiteral("xml")); + fields.insert(QStringLiteral("BAD NAME"), QStringLiteral("space")); + fields.insert(QStringLiteral("BAD values; + QMap appValues; + while ( !reader.atEnd() ) + { + reader.readNext(); + if ( reader.isStartElement() && reader.name() == QStringLiteral("UNKNOWN_FIELD") ) + { + values.insert(reader.name().toString(), reader.readElementText()); + } + else if ( reader.isStartElement() && reader.name() == QStringLiteral("APP") ) + { + const QXmlStreamAttributes attributes = reader.attributes(); + const QString key = attributes.value(QStringLiteral("PROGRAMID")).toString() + + QLatin1Char('/') + + attributes.value(QStringLiteral("FIELDNAME")).toString(); + appValues.insert(key, reader.readElementText()); + } + } + QVERIFY(!reader.hasError()); + + QCOMPARE(values.value(QStringLiteral("UNKNOWN_FIELD")), QStringLiteral("ok")); + QCOMPARE(appValues.value(QStringLiteral("QLOG/RAW")), QStringLiteral("Line1\nLine2")); + QVERIFY(!output.contains("dash")); + QVERIFY(!output.contains("digit")); + QVERIFY(!output.contains(">xml<")); + QVERIFY(!output.contains("space")); + QVERIFY(!output.contains("angle")); + QVERIFY(!output.contains("shortapp")); +} + +void AdxFormatTest::exportContactNormalizesGridAndTerminatesRecord() +{ + QSqlRecord record; + appendField(record, QStringLiteral("gridsquare"), QStringLiteral("jo70aa12bb")); + appendField(record, QStringLiteral("gridsquare_ext"), QString()); + + const QByteArray output = exportRecord(record); + + QVERIFY(output.contains("JO70AA12")); + QVERIFY(output.contains("BB")); + QVERIFY(!output.contains("JO70AA12BB")); + QVERIFY(output.contains("")); + QVERIFY(output.trimmed().endsWith("")); +} + +void AdxFormatTest::importNextMapsAdxFieldsAndStoresUnknownFields() +{ + QByteArray input = xmlRecord( + "ok1aa" + "20M" + "eu" + "jo70aa12bb" + "20260528" + "123456" + "123500" + "y" + "q" + "s" + "abc" + "xyz"); + + QBuffer buffer(&input); + QVERIFY(buffer.open(QIODevice::ReadOnly | QIODevice::Text)); + QTextStream stream(&buffer); + AdxFormat format(stream); + format.importStart(); + + QSqlRecord record; + appendField(record, QStringLiteral("callsign"), QString()); + appendField(record, QStringLiteral("band"), QString()); + appendField(record, QStringLiteral("cont"), QString()); + appendField(record, QStringLiteral("gridsquare"), QString()); + appendField(record, QStringLiteral("gridsquare_ext"), QString()); + appendField(record, QStringLiteral("start_time"), QDateTime()); + appendField(record, QStringLiteral("end_time"), QDateTime()); + appendField(record, QStringLiteral("qsl_rcvd"), QString()); + appendField(record, QStringLiteral("qsl_sent"), QString()); + appendField(record, QStringLiteral("ant_path"), QString()); + appendField(record, QStringLiteral("fields"), QString()); + + QVERIFY(format.importNext(record)); + format.importEnd(); + + QCOMPARE(record.value(QStringLiteral("callsign")).toString(), QStringLiteral("OK1AA")); + QCOMPARE(record.value(QStringLiteral("band")).toString(), QStringLiteral("20m")); + QCOMPARE(record.value(QStringLiteral("cont")).toString(), QStringLiteral("EU")); + QCOMPARE(record.value(QStringLiteral("gridsquare")).toString(), QStringLiteral("JO70AA12")); + QCOMPARE(record.value(QStringLiteral("gridsquare_ext")).toString(), QStringLiteral("BB")); + QCOMPARE(record.value(QStringLiteral("qsl_rcvd")).toString(), QStringLiteral("Y")); + QCOMPARE(record.value(QStringLiteral("qsl_sent")).toString(), QStringLiteral("Q")); + QCOMPARE(record.value(QStringLiteral("ant_path")).toString(), QStringLiteral("S")); + QCOMPARE(record.value(QStringLiteral("start_time")).toDateTime(), + QDateTime(QDate(2026, 5, 28), QTime(12, 34, 56), QTimeZone::utc())); + QCOMPARE(record.value(QStringLiteral("end_time")).toDateTime(), + QDateTime(QDate(2026, 5, 28), QTime(12, 35, 0), QTimeZone::utc())); + + const QJsonObject fields = QJsonDocument::fromJson(record.value(QStringLiteral("fields")).toByteArray()).object(); + const QJsonObject appField = fields.value(QStringLiteral("app_qlog_test")).toObject(); + QCOMPARE(appField.value(QStringLiteral("value")).toString(), QStringLiteral("abc")); + QCOMPARE(appField.value(QStringLiteral("type")).toString(), QStringLiteral("M")); + QCOMPARE(fields.value(QStringLiteral("unknown_field")).toString(), QStringLiteral("xyz")); +} + +void AdxFormatTest::importNextStoresZeroLengthApplicationFields() +{ + QByteArray input = xmlRecord( + "OK1AA" + ""); + + QBuffer buffer(&input); + QVERIFY(buffer.open(QIODevice::ReadOnly | QIODevice::Text)); + QTextStream stream(&buffer); + AdxFormat format(stream); + format.importStart(); + + QSqlRecord record; + appendField(record, QStringLiteral("callsign"), QString()); + appendField(record, QStringLiteral("fields"), QString()); + + QVERIFY(format.importNext(record)); + format.importEnd(); + + QCOMPARE(record.value(QStringLiteral("callsign")).toString(), QStringLiteral("OK1AA")); + + const QJsonObject fields = QJsonDocument::fromJson(record.value(QStringLiteral("fields")).toByteArray()).object(); + const QJsonObject appField = fields.value(QStringLiteral("app_qlog_empty")).toObject(); + QVERIFY(fields.contains(QStringLiteral("app_qlog_empty"))); + QCOMPARE(appField.value(QStringLiteral("value")).toString(), QString()); + QCOMPARE(appField.value(QStringLiteral("type")).toString(), QStringLiteral("M")); +} + +void AdxFormatTest::importNextAppliesDefaultsForMissingFields() +{ + QByteArray input = xmlRecord("OK1AA"); + + QBuffer buffer(&input); + QVERIFY(buffer.open(QIODevice::ReadOnly | QIODevice::Text)); + QTextStream stream(&buffer); + AdxFormat format(stream); + + QMap defaults; + defaults.insert(QStringLiteral("band"), QStringLiteral("20M")); + defaults.insert(QStringLiteral("qsl_sent"), QStringLiteral("Q")); + defaults.insert(QStringLiteral("lotw_qsl_sent"), QStringLiteral("Y")); + defaults.insert(QStringLiteral("eqsl_qsl_sent"), QStringLiteral("R")); + defaults.insert(QStringLiteral("dcl_qsl_sent"), QStringLiteral("I")); + defaults.insert(QStringLiteral("my_rig_intl"), QStringLiteral("Radio")); + format.setDefaults(defaults); + format.importStart(); + + QSqlRecord record; + appendField(record, QStringLiteral("callsign"), QString()); + appendField(record, QStringLiteral("band"), QString()); + appendField(record, QStringLiteral("qsl_sent"), QString()); + appendField(record, QStringLiteral("lotw_qsl_sent"), QString()); + appendField(record, QStringLiteral("eqsl_qsl_sent"), QString()); + appendField(record, QStringLiteral("dcl_qsl_sent"), QString()); + appendField(record, QStringLiteral("my_rig"), QString()); + appendField(record, QStringLiteral("my_rig_intl"), QString()); + + QVERIFY(format.importNext(record)); + format.importEnd(); + + QCOMPARE(record.value(QStringLiteral("callsign")).toString(), QStringLiteral("OK1AA")); + QCOMPARE(record.value(QStringLiteral("band")).toString(), QStringLiteral("20m")); + QCOMPARE(record.value(QStringLiteral("qsl_sent")).toString(), QStringLiteral("Q")); + QCOMPARE(record.value(QStringLiteral("lotw_qsl_sent")).toString(), QStringLiteral("Y")); + QCOMPARE(record.value(QStringLiteral("eqsl_qsl_sent")).toString(), QStringLiteral("R")); + QCOMPARE(record.value(QStringLiteral("dcl_qsl_sent")).toString(), QStringLiteral("I")); + QCOMPARE(record.value(QStringLiteral("my_rig")).toString(), QStringLiteral("Radio")); + QCOMPARE(record.value(QStringLiteral("my_rig_intl")).toString(), QStringLiteral("Radio")); +} + +void AdxFormatTest::importNextKeepsImportedValuesOverDefaults() +{ + QByteArray input = xmlRecord( + "OK1AA" + "40M" + "Y" + "N" + "Imported rig"); + + QBuffer buffer(&input); + QVERIFY(buffer.open(QIODevice::ReadOnly | QIODevice::Text)); + QTextStream stream(&buffer); + AdxFormat format(stream); + + QMap defaults; + defaults.insert(QStringLiteral("band"), QStringLiteral("20M")); + defaults.insert(QStringLiteral("qsl_sent"), QStringLiteral("Q")); + defaults.insert(QStringLiteral("lotw_qsl_sent"), QStringLiteral("Y")); + defaults.insert(QStringLiteral("my_rig_intl"), QStringLiteral("Default rig")); + format.setDefaults(defaults); + format.importStart(); + + QSqlRecord record; + appendField(record, QStringLiteral("callsign"), QString()); + appendField(record, QStringLiteral("band"), QString()); + appendField(record, QStringLiteral("qsl_sent"), QString()); + appendField(record, QStringLiteral("lotw_qsl_sent"), QString()); + appendField(record, QStringLiteral("my_rig"), QString()); + appendField(record, QStringLiteral("my_rig_intl"), QString()); + + QVERIFY(format.importNext(record)); + format.importEnd(); + + QCOMPARE(record.value(QStringLiteral("callsign")).toString(), QStringLiteral("OK1AA")); + QCOMPARE(record.value(QStringLiteral("band")).toString(), QStringLiteral("40m")); + QCOMPARE(record.value(QStringLiteral("qsl_sent")).toString(), QStringLiteral("Y")); + QCOMPARE(record.value(QStringLiteral("lotw_qsl_sent")).toString(), QStringLiteral("N")); + QCOMPARE(record.value(QStringLiteral("my_rig")).toString(), QStringLiteral("Imported rig")); + QCOMPARE(record.value(QStringLiteral("my_rig_intl")).toString(), QStringLiteral("Imported rig")); +} + +void AdxFormatTest::importNextFillsIntlCompanionFields() +{ + QByteArray input = xmlRecord( + "
Line 1\nLine 2
" + "Plain comment" + "Caf\xc3\xa9\nQTH"); + + QBuffer buffer(&input); + QVERIFY(buffer.open(QIODevice::ReadOnly | QIODevice::Text)); + QTextStream stream(&buffer); + AdxFormat format(stream); + format.importStart(); + + QSqlRecord record; + appendField(record, QStringLiteral("address"), QString()); + appendField(record, QStringLiteral("address_intl"), QString()); + appendField(record, QStringLiteral("comment"), QString()); + appendField(record, QStringLiteral("comment_intl"), QString()); + appendField(record, QStringLiteral("notes"), QString()); + appendField(record, QStringLiteral("notes_intl"), QString()); + + QVERIFY(format.importNext(record)); + format.importEnd(); + + QCOMPARE(record.value(QStringLiteral("address")).toString(), QStringLiteral("Line 1\nLine 2")); + QCOMPARE(record.value(QStringLiteral("address_intl")).toString(), QStringLiteral("Line 1\nLine 2")); + QCOMPARE(record.value(QStringLiteral("comment")).toString(), QStringLiteral("Plain comment")); + QCOMPARE(record.value(QStringLiteral("comment_intl")).toString(), QStringLiteral("Plain comment")); + QCOMPARE(record.value(QStringLiteral("notes")).toString(), QStringLiteral("Cafe\nQTH")); + QCOMPARE(record.value(QStringLiteral("notes_intl")).toString(), QString::fromUtf8("Caf\xc3\xa9\nQTH")); +} + +void AdxFormatTest::importNextAppliesDefaultsBeforeIntlCompanionFields() +{ + QByteArray input = xmlRecord("OK1AA"); + + QBuffer buffer(&input); + QVERIFY(buffer.open(QIODevice::ReadOnly | QIODevice::Text)); + QTextStream stream(&buffer); + AdxFormat format(stream); + + QMap defaults; + defaults.insert(QStringLiteral("notes_intl"), QString::fromUtf8("Caf\xc3\xa9\nQTH")); + format.setDefaults(defaults); + format.importStart(); + + QSqlRecord record; + appendField(record, QStringLiteral("callsign"), QString()); + appendField(record, QStringLiteral("notes"), QString()); + appendField(record, QStringLiteral("notes_intl"), QString()); + + QVERIFY(format.importNext(record)); + format.importEnd(); + + QCOMPARE(record.value(QStringLiteral("callsign")).toString(), QStringLiteral("OK1AA")); + QCOMPARE(record.value(QStringLiteral("notes")).toString(), QStringLiteral("Cafe\nQTH")); + QCOMPARE(record.value(QStringLiteral("notes_intl")).toString(), QString::fromUtf8("Caf\xc3\xa9\nQTH")); +} + +QTEST_APPLESS_MAIN(AdxFormatTest) + +#include "tst_adxformat.moc" diff --git a/tests/BandmapGuideTest/BandmapGuideTest.pro b/tests/BandmapGuideTest/BandmapGuideTest.pro new file mode 100644 index 00000000..71d0ae28 --- /dev/null +++ b/tests/BandmapGuideTest/BandmapGuideTest.pro @@ -0,0 +1,21 @@ +QT += testlib core gui sql +CONFIG += console testcase c++11 +TEMPLATE = app +TARGET = tst_bandmapguide + +INCLUDEPATH += $$PWD/../.. + +SOURCES += \ + tst_bandmapguide.cpp \ + test_stubs.cpp \ + ../../data/BandmapGuide.cpp \ + ../../data/BandPlan.cpp \ + ../../core/AdifRecovery.cpp \ + ../../core/LogParam.cpp + +HEADERS += \ + ../../data/BandmapGuide.h \ + ../../data/BandPlan.h \ + ../../data/Band.h \ + ../../core/AdifRecovery.h \ + ../../core/LogParam.h diff --git a/tests/BandmapGuideTest/test_stubs.cpp b/tests/BandmapGuideTest/test_stubs.cpp new file mode 100644 index 00000000..fbd52dfc --- /dev/null +++ b/tests/BandmapGuideTest/test_stubs.cpp @@ -0,0 +1,8 @@ +#include + +#include "core/LogDatabase.h" + +QString LogDatabase::currentPlatformId() +{ + return QStringLiteral("TestPlatform"); +} diff --git a/tests/BandmapGuideTest/tst_bandmapguide.cpp b/tests/BandmapGuideTest/tst_bandmapguide.cpp new file mode 100644 index 00000000..d2fddb6c --- /dev/null +++ b/tests/BandmapGuideTest/tst_bandmapguide.cpp @@ -0,0 +1,339 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "core/LogParam.h" +#include "data/BandmapGuide.h" + +namespace { +QString lastErrorString(const QSqlQuery &query) +{ + return query.lastError().isValid() ? query.lastError().text() : QString(); +} + +void compareRange(const BandmapGuide::Range &actual, + double from, + double to, + const QColor &color, + const QString &label) +{ + QCOMPARE(actual.from, from); + QCOMPARE(actual.to, to); + QCOMPARE(actual.color, color); + QCOMPARE(actual.label, label); +} +} + +class BandmapGuideTest : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void cleanup(); + void cleanupTestCase(); + + void profilesUseExampleGuideOnFirstRun(); + void saveProfilesRoundTripFiltersInvalidData(); + void currentProfileFallsBackToFirstProfile(); + void profileExistsRejectsEmptyAndMissingId(); + void setCurrentProfileIdEmitsChangedOnlyOnRealChange(); + void setEnabledEmitsChangedOnlyOnRealChange(); + void writeReadProfileRoundTripAssignsNewId(); + void readProfileAcceptsProfilesWrapper(); + void readProfileUsesFilenameWhenTitleMissing(); + void readProfileSupportsLegacyNameField(); + void readProfileRejectsInvalidJsonAndMissingFile(); + +private: + QString writeTextFile(const QString &fileName, const QByteArray &content); + + QTemporaryDir tempDir; +}; + +void BandmapGuideTest::initTestCase() +{ + QLoggingCategory::setFilterRules(QStringLiteral("*.debug=false")); + QVERIFY(tempDir.isValid()); + + QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); + db.setDatabaseName(QStringLiteral(":memory:")); + QVERIFY(db.open()); + + QSqlQuery query; + QVERIFY2(query.exec("CREATE TABLE log_param (name TEXT PRIMARY KEY, value TEXT)"), + qPrintable(lastErrorString(query))); +} + +void BandmapGuideTest::cleanup() +{ + LogParam::setBandmapGuideProfiles(QString()); + LogParam::setBandmapGuideCurrentProfile(QString()); + LogParam::setBandmapGuideEnabled(false); +} + +void BandmapGuideTest::cleanupTestCase() +{ + const QString connectionName = QString::fromLatin1(QSqlDatabase::defaultConnection); + { + QSqlDatabase db = QSqlDatabase::database(); + if ( db.isValid() ) + db.close(); + } + QSqlDatabase::removeDatabase(connectionName); +} + +QString BandmapGuideTest::writeTextFile(const QString &fileName, const QByteArray &content) +{ + const QString path = tempDir.filePath(fileName); + QFile file(path); + if ( !file.open(QIODevice::WriteOnly | QIODevice::Truncate) ) + qFatal("Cannot open temporary guide file for writing"); + if ( file.write(content) != static_cast(content.size()) ) + qFatal("Cannot write complete temporary guide file"); + file.close(); + return path; +} + +void BandmapGuideTest::saveProfilesRoundTripFiltersInvalidData() +{ + BandmapGuide::Profile valid; + valid.id = QStringLiteral("guide-1"); + valid.name = QStringLiteral("Region 1"); + valid.ranges << BandmapGuide::Range(14.000, 14.070, QColor(QStringLiteral("#204080")), QStringLiteral("CW")); + valid.ranges << BandmapGuide::Range(14.070, 14.070, QColor(QStringLiteral("#ff0000")), QStringLiteral("Zero")); + valid.ranges << BandmapGuide::Range(14.100, 14.200, QColor(), QStringLiteral("No color")); + + BandmapGuide::Profile invalidProfile; + invalidProfile.id = QStringLiteral("invalid"); + invalidProfile.ranges << BandmapGuide::Range(7.000, 7.030, QColor(QStringLiteral("#00ff00"))); + + QSignalSpy changedSpy(BandmapGuide::instance(), &BandmapGuide::changed); + QVERIFY(changedSpy.isValid()); + + BandmapGuide::saveProfiles({valid, invalidProfile}); + + QCOMPARE(changedSpy.count(), 1); + + const QList profiles = BandmapGuide::profiles(); + QCOMPARE(profiles.size(), 1); + QCOMPARE(profiles.first().id, QStringLiteral("guide-1")); + QCOMPARE(profiles.first().name, QStringLiteral("Region 1")); + QCOMPARE(profiles.first().ranges.size(), 1); + compareRange(profiles.first().ranges.first(), + 14.000, + 14.070, + QColor(QStringLiteral("#204080")), + QStringLiteral("CW")); +} + +void BandmapGuideTest::profilesUseExampleGuideOnFirstRun() +{ + const QList profiles = BandmapGuide::profiles(); + + QCOMPARE(profiles.size(), 1); + QCOMPARE(profiles.first().id, QStringLiteral("iaru-region-1")); + QCOMPARE(profiles.first().name, QStringLiteral("IARU Region 1")); + QVERIFY(!profiles.first().ranges.isEmpty()); + QVERIFY(BandmapGuide::profileExists(QStringLiteral("iaru-region-1"))); + QCOMPARE(BandmapGuide::currentProfile().id, profiles.first().id); +} + +void BandmapGuideTest::currentProfileFallsBackToFirstProfile() +{ + BandmapGuide::Profile first; + first.id = QStringLiteral("first"); + first.name = QStringLiteral("First Guide"); + first.ranges << BandmapGuide::Range(14.000, 14.070, QColor(QStringLiteral("#204080"))); + + BandmapGuide::Profile second; + second.id = QStringLiteral("second"); + second.name = QStringLiteral("Second Guide"); + second.ranges << BandmapGuide::Range(7.000, 7.040, QColor(QStringLiteral("#804020"))); + + BandmapGuide::saveProfiles({first, second}); + BandmapGuide::setCurrentProfileId(QStringLiteral("missing")); + + const BandmapGuide::Profile current = BandmapGuide::currentProfile(); + + QCOMPARE(current.id, first.id); + QCOMPARE(current.name, first.name); +} + +void BandmapGuideTest::profileExistsRejectsEmptyAndMissingId() +{ + BandmapGuide::Profile profile; + profile.id = QStringLiteral("guide-1"); + profile.name = QStringLiteral("Guide"); + profile.ranges << BandmapGuide::Range(14.000, 14.070, QColor(QStringLiteral("#204080"))); + BandmapGuide::saveProfiles({profile}); + + QVERIFY(BandmapGuide::profileExists(QStringLiteral("guide-1"))); + QVERIFY(!BandmapGuide::profileExists(QString())); + QVERIFY(!BandmapGuide::profileExists(QStringLiteral("missing"))); +} + +void BandmapGuideTest::setCurrentProfileIdEmitsChangedOnlyOnRealChange() +{ + QSignalSpy changedSpy(BandmapGuide::instance(), &BandmapGuide::changed); + QVERIFY(changedSpy.isValid()); + + BandmapGuide::setCurrentProfileId(QStringLiteral("guide-1")); + QCOMPARE(changedSpy.count(), 1); + + BandmapGuide::setCurrentProfileId(QStringLiteral("guide-1")); + QCOMPARE(changedSpy.count(), 1); + + BandmapGuide::setCurrentProfileId(QStringLiteral("guide-2")); + QCOMPARE(changedSpy.count(), 2); +} + +void BandmapGuideTest::setEnabledEmitsChangedOnlyOnRealChange() +{ + QSignalSpy changedSpy(BandmapGuide::instance(), &BandmapGuide::changed); + QVERIFY(changedSpy.isValid()); + + BandmapGuide::setEnabled(true); + QCOMPARE(changedSpy.count(), 1); + QVERIFY(BandmapGuide::isEnabled()); + + BandmapGuide::setEnabled(true); + QCOMPARE(changedSpy.count(), 1); + + BandmapGuide::setEnabled(false); + QCOMPARE(changedSpy.count(), 2); + QVERIFY(!BandmapGuide::isEnabled()); +} + +void BandmapGuideTest::writeReadProfileRoundTripAssignsNewId() +{ + BandmapGuide::Profile profile; + profile.id = QStringLiteral("original-id"); + profile.name = QStringLiteral("Exported Guide"); + profile.ranges << BandmapGuide::Range(7.000, 7.040, QColor(QStringLiteral("#123456")), QStringLiteral("CW")); + profile.ranges << BandmapGuide::Range(7.040, 7.074, QColor(QStringLiteral("#654321")), QStringLiteral("DIGI")); + + const QString path = tempDir.filePath(QStringLiteral("exported-guide.json")); + QString error; + QVERIFY2(BandmapGuide::writeProfileToFile(profile, path, &error), qPrintable(error)); + + const BandmapGuide::Profile imported = BandmapGuide::readProfileFromFile(path, &error); + + QVERIFY2(error.isEmpty(), qPrintable(error)); + QVERIFY(imported.isValid()); + QVERIFY(!imported.id.isEmpty()); + QVERIFY(imported.id != profile.id); + QCOMPARE(imported.name, profile.name); + QCOMPARE(imported.ranges.size(), 2); + compareRange(imported.ranges.at(0), 7.000, 7.040, QColor(QStringLiteral("#123456")), QStringLiteral("CW")); + compareRange(imported.ranges.at(1), 7.040, 7.074, QColor(QStringLiteral("#654321")), QStringLiteral("DIGI")); +} + +void BandmapGuideTest::readProfileAcceptsProfilesWrapper() +{ + const QByteArray json = R"json({ + "version": 1, + "profiles": [ + { + "id": "stored-id", + "title": "Wrapped Guide", + "ranges": [ + { "from": 14.000, "to": 14.070, "color": "#0a64c8", "label": "CW" }, + { "from": 14.070, "to": 14.070, "color": "#ff0000", "label": "Invalid width" }, + { "from": 14.100, "to": 14.200, "color": "not-a-color", "label": "Invalid color" } + ] + } + ] + })json"; + const QString path = writeTextFile(QStringLiteral("wrapped-guide.json"), json); + + QString error; + const BandmapGuide::Profile imported = BandmapGuide::readProfileFromFile(path, &error); + + QVERIFY2(error.isEmpty(), qPrintable(error)); + QVERIFY(imported.isValid()); + QVERIFY(!imported.id.isEmpty()); + QVERIFY(imported.id != QStringLiteral("stored-id")); + QCOMPARE(imported.name, QStringLiteral("Wrapped Guide")); + QCOMPARE(imported.ranges.size(), 1); + compareRange(imported.ranges.first(), + 14.000, + 14.070, + QColor(QStringLiteral("#0a64c8")), + QStringLiteral("CW")); +} + +void BandmapGuideTest::readProfileUsesFilenameWhenTitleMissing() +{ + const QByteArray json = R"json({ + "version": 1, + "ranges": [ + { "from": 3.500, "to": 3.600, "color": "#abcdef", "label": "CW" } + ] + })json"; + const QString path = writeTextFile(QStringLiteral("filename-title.json"), json); + + QString error; + const BandmapGuide::Profile imported = BandmapGuide::readProfileFromFile(path, &error); + + QVERIFY2(error.isEmpty(), qPrintable(error)); + QVERIFY(imported.isValid()); + QCOMPARE(imported.name, QStringLiteral("filename-title")); + QCOMPARE(imported.ranges.size(), 1); + compareRange(imported.ranges.first(), + 3.500, + 3.600, + QColor(QStringLiteral("#abcdef")), + QStringLiteral("CW")); +} + +void BandmapGuideTest::readProfileSupportsLegacyNameField() +{ + const QByteArray json = R"json({ + "version": 1, + "name": "Legacy Guide", + "ranges": [ + { "from": 21.000, "to": 21.070, "color": "#123abc", "label": "CW" } + ] + })json"; + const QString path = writeTextFile(QStringLiteral("legacy-name.json"), json); + + QString error; + const BandmapGuide::Profile imported = BandmapGuide::readProfileFromFile(path, &error); + + QVERIFY2(error.isEmpty(), qPrintable(error)); + QVERIFY(imported.isValid()); + QCOMPARE(imported.name, QStringLiteral("Legacy Guide")); + QCOMPARE(imported.ranges.size(), 1); + compareRange(imported.ranges.first(), + 21.000, + 21.070, + QColor(QStringLiteral("#123abc")), + QStringLiteral("CW")); +} + +void BandmapGuideTest::readProfileRejectsInvalidJsonAndMissingFile() +{ + QString error; + const BandmapGuide::Profile missing = BandmapGuide::readProfileFromFile( + tempDir.filePath(QStringLiteral("missing-guide.json")), &error); + + QVERIFY(!missing.isValid()); + QVERIFY(!error.isEmpty()); + + error.clear(); + const QString invalidPath = writeTextFile(QStringLiteral("invalid-guide.json"), + QByteArrayLiteral("{ this is not json")); + const BandmapGuide::Profile invalidJson = BandmapGuide::readProfileFromFile(invalidPath, &error); + + QVERIFY(!invalidJson.isValid()); + QVERIFY(!error.isEmpty()); +} + +QTEST_APPLESS_MAIN(BandmapGuideTest) + +#include "tst_bandmapguide.moc" diff --git a/tests/CredentialStoreTest/CredentialStoreTest.pro b/tests/CredentialStoreTest/CredentialStoreTest.pro index 03ecffef..4cfb066b 100644 --- a/tests/CredentialStoreTest/CredentialStoreTest.pro +++ b/tests/CredentialStoreTest/CredentialStoreTest.pro @@ -10,11 +10,13 @@ SOURCES += \ test_stubs.cpp \ ../../core/CredentialStore.cpp \ ../../core/PasswordCipher.cpp \ + ../../core/AdifRecovery.cpp \ ../../core/LogParam.cpp HEADERS += \ ../../core/CredentialStore.h \ ../../core/PasswordCipher.h \ + ../../core/AdifRecovery.h \ ../../core/LogParam.h # QtKeychain diff --git a/tests/DataTest/tst_data.cpp b/tests/DataTest/tst_data.cpp index 68e8b5bf..2ff79caf 100644 --- a/tests/DataTest/tst_data.cpp +++ b/tests/DataTest/tst_data.cpp @@ -115,6 +115,7 @@ private slots: void removeAccents_basic(); void removeAccents_asciiPrintable(); void removeAccents_nonPrintable(); + void removeAccents_preservesLineBreaks(); void removeAccents_limits(); void removeAccents_benchmark(); }; @@ -164,6 +165,12 @@ void DataTest::removeAccents_nonPrintable() } } +void DataTest::removeAccents_preservesLineBreaks() +{ + QCOMPARE(Data::removeAccents(QStringLiteral("řádek 1\nřádek 2\r\nřádek 3")), + QStringLiteral("radek 1\nradek 2\r\nradek 3")); +} + void DataTest::removeAccents_limits() { const QString lastInput = QString::fromUtf8("\uFFFE"); diff --git a/tests/GridsquareTest/tst_gridsquare.cpp b/tests/GridsquareTest/tst_gridsquare.cpp index 19be7fe6..1202a653 100644 --- a/tests/GridsquareTest/tst_gridsquare.cpp +++ b/tests/GridsquareTest/tst_gridsquare.cpp @@ -13,6 +13,8 @@ private slots: void invalidGridStrings(); void validGridStrings_data(); void validGridStrings(); + void mapDisplayGrid_data(); + void mapDisplayGrid(); void invalidCoordinateCtor_data(); void invalidCoordinateCtor(); void validCoordinateCtor_data(); @@ -117,6 +119,47 @@ void GridsquareTest::validGridStrings() QCOMPARE(gs.getGrid(), expectedGrid); } +void GridsquareTest::mapDisplayGrid_data() +{ + QTest::addColumn("input"); + QTest::addColumn("expectedValid"); + QTest::addColumn("expectedGrid"); + + const struct Case + { + const char *name; + const char *input; + bool expectedValid; + const char *expectedGrid; + } cases[] = { + {"valid_six", "AA11AA", true, "AA11AA"}, + {"valid_eight", "AA11AA00", true, "AA11AA00"}, + {"long_valid_prefix", "AA11AA00AA", true, "AA11AA00"}, + {"long_valid_prefix_lowercase", "aa11aa00aa", true, "AA11AA00"}, + {"long_invalid_prefix", "AA11A000AA", false, ""}, + {"short_invalid", "INVALID", false, ""} + }; + + for ( const Case &c : cases ) + { + QTest::newRow(c.name) + << QString::fromLatin1(c.input) + << c.expectedValid + << QString::fromLatin1(c.expectedGrid); + } +} + +void GridsquareTest::mapDisplayGrid() +{ + QFETCH(QString, input); + QFETCH(bool, expectedValid); + QFETCH(QString, expectedGrid); + + const Gridsquare gs = Gridsquare::mapDisplayGrid(input); + QCOMPARE(gs.isValid(), expectedValid); + QCOMPARE(gs.getGrid(), expectedGrid); +} + void GridsquareTest::invalidCoordinateCtor_data() { QTest::addColumn("latitude"); diff --git a/tests/RigctldManagerTest/tst_rigctldmanager.cpp b/tests/RigctldManagerTest/tst_rigctldmanager.cpp index 7ada9fdf..9fb8cf97 100644 --- a/tests/RigctldManagerTest/tst_rigctldmanager.cpp +++ b/tests/RigctldManagerTest/tst_rigctldmanager.cpp @@ -32,6 +32,7 @@ private slots: void start_failsWithInvalidProfile(); void getConnectHost_returnsLocalhost(); void getConnectPort_returnsConfiguredPort(); + void stop_isIdempotentWithoutStart(); // getVersion tests void getVersion_returnsInvalidForNonexistentPath(); @@ -40,6 +41,7 @@ private slots: // Integration test (skipped if rigctld not available) void start_stop_integration(); + void destructor_afterStart_stopsProcess(); private: QString findRigctld(); @@ -233,6 +235,18 @@ void RigctldManagerTest::getConnectPort_returnsConfiguredPort() QCOMPARE(manager.getConnectPort(), static_cast(4532)); } +void RigctldManagerTest::stop_isIdempotentWithoutStart() +{ + RigctldManager manager; + QSignalSpy stoppedSpy(&manager, &RigctldManager::stopped); + + manager.stop(); + manager.stop(); + + QVERIFY(!manager.isRunning()); + QCOMPARE(stoppedSpy.count(), 0); +} + // ============================================================================ // getVersion tests // ============================================================================ @@ -321,7 +335,9 @@ void RigctldManagerTest::start_stop_integration() // Stop manager.stop(); + manager.stop(); QVERIFY(!manager.isRunning()); + QCOMPARE(stoppedSpy.count(), 1); // Verify port is no longer listening QTcpSocket socket2; @@ -330,6 +346,44 @@ void RigctldManagerTest::start_stop_integration() QVERIFY(!stillConnected); } +void RigctldManagerTest::destructor_afterStart_stopsProcess() +{ + if (!rigctldAvailable) + { + QSKIP("rigctld not available for integration test"); + } + + constexpr quint16 testPort = 14535; + + { + RigctldManager manager; + QSignalSpy errorSpy(&manager, &RigctldManager::errorOccurred); + + RigProfile profile; + profile.model = 1; // Dummy rig (Hamlib model 1 = Dummy) + profile.rigctldPort = testPort; + profile.rigctldPath = rigctldPath; + + bool result = manager.start(profile); + + if (!result) + { + if (!errorSpy.isEmpty()) + { + qWarning() << "Start failed with error:" << errorSpy.first().first().toString(); + } + QSKIP("Could not start rigctld with dummy rig"); + } + + QVERIFY(manager.isRunning()); + } + + QTcpSocket socket; + socket.connectToHost("127.0.0.1", testPort); + bool stillConnected = socket.waitForConnected(1000); + QVERIFY(!stillConnected); +} + int main(int argc, char **argv) { QCoreApplication app(argc, argv); diff --git a/tests/tests.pro b/tests/tests.pro index de0a204a..23998a1f 100644 --- a/tests/tests.pro +++ b/tests/tests.pro @@ -1,11 +1,16 @@ TEMPLATE = subdirs CONFIG += ordered SUBDIRS += CallsignTest \ + AdiFormatTest \ + AdiImportBenchmark \ + AdxFormatTest \ + AdifRecoveryTest \ CredentialStoreTest \ DataTest \ FileCompressorTest \ GridsquareTest \ BandPlanTest \ + BandmapGuideTest \ AlertEvaluatorTest \ DxServerStringTest \ HostsPortStringTest \ diff --git a/ui/ActivityEditor.cpp b/ui/ActivityEditor.cpp index 91b91ed8..b2507453 100644 --- a/ui/ActivityEditor.cpp +++ b/ui/ActivityEditor.cpp @@ -7,6 +7,7 @@ #include "ui/MainWindow.h" #include "data/StationProfile.h" #include "data/AntProfile.h" +#include "data/BandmapGuide.h" #include "data/RigProfile.h" #include "data/RotProfile.h" #include "models/LogbookModel.h" @@ -154,6 +155,9 @@ void ActivityEditor::save() insertProfile(ActivityProfile::ProfileType::STATION_PROFILE, ui->stationProfileCheckbox->isChecked(), ui->stationProfileCombo->currentText()); insertProfile(ActivityProfile::ProfileType::RIG_PROFILE, ui->rigProfileCheckbox->isChecked(), ui->rigProfileCombo->currentText(), ui->rigAutoconnectCheckbox); insertProfile(ActivityProfile::ProfileType::ROT_PROFILE, ui->rotatorProfileCheckbox->isChecked(), ui->rotatorProfileCombo->currentText(), ui->rotatorAutoconnectCheckbox); + insertProfile(ActivityProfile::ProfileType::BANDMAP_GUIDE_PROFILE, + ui->bandmapGuideCombo->currentIndex() > 0, + ui->bandmapGuideCombo->currentData().toString()); insertParam(LogbookModel::COLUMN_CONTEST_ID, ui->contestIDCheckbox, ui->contestIDEdit->text()); insertParam(LogbookModel::COLUMN_PROP_MODE, ui->propagationModeCheckbox, ui->propagationModeCombo->currentText()); @@ -446,6 +450,9 @@ void ActivityEditor::setupValuesTab(const QString &activityName) assignModel(ui->propagationModeCombo, Data::instance()->propagationModesList()); assignModel(ui->satModeCombo, Data::instance()->satModeList()); + bool bandmapGuideStored = false; + QString selectedBandmapGuideId; + if ( !activityName.isEmpty() ) { const ActivityProfile &activity = ActivityProfilesManager::instance()->getProfile(activityName); @@ -453,6 +460,12 @@ void ActivityEditor::setupValuesTab(const QString &activityName) loadProfileValue(activity, ActivityProfile::ProfileType::STATION_PROFILE, ui->stationProfileCheckbox, ui->stationProfileCombo); loadProfileValue(activity, ActivityProfile::ProfileType::RIG_PROFILE, ui->rigProfileCheckbox, ui->rigProfileCombo, ui->rigAutoconnectCheckbox); loadProfileValue(activity, ActivityProfile::ProfileType::ROT_PROFILE, ui->rotatorProfileCheckbox, ui->rotatorProfileCombo, ui->rotatorAutoconnectCheckbox); + const auto bandmapGuide = activity.profiles.constFind(ActivityProfile::ProfileType::BANDMAP_GUIDE_PROFILE); + if ( bandmapGuide != activity.profiles.constEnd() ) + { + bandmapGuideStored = true; + selectedBandmapGuideId = bandmapGuide.value().name; + } for ( auto i = activity.fieldValues.begin(); i != activity.fieldValues.end(); i++ ) { @@ -489,9 +502,38 @@ void ActivityEditor::setupValuesTab(const QString &activityName) } } + populateBandmapGuideCombo(bandmapGuideStored, selectedBandmapGuideId); setValueState(); } +void ActivityEditor::populateBandmapGuideCombo(bool guideStored, const QString &selectedProfileId) +{ + FCT_IDENTIFICATION; + + ui->bandmapGuideCombo->clear(); + ui->bandmapGuideCombo->addItem(tr("Leave unchanged")); + ui->bandmapGuideCombo->addItem(tr("Off"), QString()); + + const QList profiles = BandmapGuide::profiles(); + for ( const BandmapGuide::Profile &profile : profiles ) + ui->bandmapGuideCombo->addItem(profile.name, profile.id); + + if ( !guideStored ) + { + ui->bandmapGuideCombo->setCurrentIndex(0); + return; + } + + if ( selectedProfileId.isEmpty() ) + { + ui->bandmapGuideCombo->setCurrentIndex(1); + return; + } + + const int index = ui->bandmapGuideCombo->findData(selectedProfileId); + ui->bandmapGuideCombo->setCurrentIndex(index >= 0 ? index : 0); +} + void ActivityEditor::fillWidgets(const MainLayoutProfile &profile) { FCT_IDENTIFICATION; diff --git a/ui/ActivityEditor.h b/ui/ActivityEditor.h index 5322b8c9..b773cc48 100644 --- a/ui/ActivityEditor.h +++ b/ui/ActivityEditor.h @@ -112,6 +112,7 @@ private slots: QList getFieldIndexes(StringListModel *model); void setupValuesTab(const QString &activityName); + void populateBandmapGuideCombo(bool guideStored, const QString &selectedProfileId); const QString statusUnSavedText = tr("Unsaved"); }; diff --git a/ui/ActivityEditor.ui b/ui/ActivityEditor.ui index 9d014f86..5c53be79 100644 --- a/ui/ActivityEditor.ui +++ b/ui/ActivityEditor.ui @@ -1400,6 +1400,25 @@ + + + + Bandmap Guide + + + + + + Guide + + + + + + + + + @@ -1549,6 +1568,7 @@ rotatorProfileCheckbox rotatorProfileCombo rotatorAutoconnectCheckbox + bandmapGuideCombo contestIDCheckbox contestIDEdit stxStringCheckbox diff --git a/ui/AdifRecoveryManager.cpp b/ui/AdifRecoveryManager.cpp new file mode 100644 index 00000000..e89ed8bd --- /dev/null +++ b/ui/AdifRecoveryManager.cpp @@ -0,0 +1,331 @@ +#include "AdifRecoveryManager.h" + +#include +#include +#include + +#include "core/LogParam.h" +#include "core/debug.h" +#include "data/StationProfile.h" + +MODULE_IDENTIFICATION("qlog.ui.adifrecoverymanager"); + +AdifRecoveryManager::AdifRecoveryManager(QObject *parent) : + QObject(parent) +{ + FCT_IDENTIFICATION; + + qRegisterMetaType("AdifRecoveryConfig"); + qRegisterMetaType("AdifRecoveryState"); + qRegisterMetaType("AdifRecoveryScanResult"); + + reloadSettings(); +} + +AdifRecoveryManager::~AdifRecoveryManager() +{ + FCT_IDENTIFICATION; + cleanupWorker(); +} + +void AdifRecoveryManager::reloadSettings() +{ + FCT_IDENTIFICATION; + + if ( workerThread ) + { + reloadAfterScan = true; + return; + } + + configs = LogParam::getAdifRecoveryFiles(); + configByKey.clear(); + pendingKeys.clear(); + + for ( const AdifRecoveryConfig &config : static_cast&>(configs) ) + { + if ( config.path.trimmed().isEmpty() ) + continue; + + qCDebug(runtime) << "Adding file" << config.path << config.enabled; + configByKey.insert(AdifRecovery::fileKey(config.path, config.stationProfileName), config); + } +} + +void AdifRecoveryManager::startStartupRecovery() +{ + FCT_IDENTIFICATION; + + if ( workerThread ) + return; + + const QSet profiles = stationProfiles(); + disableMissingProfileConfigs(profiles); + rebuildPendingQueue(profiles); + + startNextScan(); +} + +void AdifRecoveryManager::startNextScan() +{ + FCT_IDENTIFICATION; + + if ( workerThread ) + return; + + // Only one reader thread is allowed at a time. The loop does not start + // multiple workers; it only skips stale queue entries. A queued file can + // become stale when settings are reloaded while another file is being read. + while ( !pendingKeys.isEmpty() ) + { + const QString fileKey = pendingKeys.dequeue(); + const AdifRecoveryConfig config = configByKey.value(fileKey); + + if ( !config.enabled || config.path.trimmed().isEmpty() || config.stationProfileName.trimmed().isEmpty() ) + continue; + + AdifRecoveryState state = LogParam::getAdifRecoveryState(fileKey); + state.path = config.path; + + workerThread = new QThread(this); + worker = new AdifRecoveryReaderWorker(); + worker->moveToThread(workerThread); + + qCDebug(runtime) << "starting a new thread for " << state.path; + + connect(workerThread, &QThread::finished, worker, &QObject::deleteLater); + connect(worker, &AdifRecoveryReaderWorker::scanFinished, this, &AdifRecoveryManager::scanFinished); + connect(workerThread, &QThread::started, this, [this, config, state]() + { + QMetaObject::invokeMethod(worker, "readTail", Qt::QueuedConnection, + Q_ARG(AdifRecoveryConfig, config), + Q_ARG(AdifRecoveryState, state), + Q_ARG(int, MAX_AUTOMATIC_CONTACTS)); + }); + + workerThread->start(); + return; + } +} + +void AdifRecoveryManager::scanFinished(const AdifRecoveryScanResult &result) +{ + FCT_IDENTIFICATION; + + const AdifRecoveryConfig config = configByKey.value(result.fileKey); + const AdifRecoveryState state = LogParam::getAdifRecoveryState(result.fileKey); + + qCDebug(runtime) << "Message" << result.message; + + if ( result.tooMany ) + { + saveState(result.fileKey, config, result.nextOffset, result.message); + const QString text = tr("Startup ADI found more than %1 new QSOs in %2. Use the standard Import. Load point was moved to the end of the file.") + .arg(MAX_AUTOMATIC_CONTACTS) + .arg(QFileInfo(result.path).fileName()); + qCWarning(runtime) << "Too many QSOs"; + emit problem(text); + } + else if ( !result.message.isEmpty() && result.nextOffset >= 0 && result.nextOffset != state.offset ) + { + saveState(result.fileKey, config, result.nextOffset, result.message); + if ( result.reset ) + emit problem(result.message); + } + else if ( !result.message.isEmpty() ) + { + qCWarning(runtime) << result.message; + emit problem(result.message); + } + else if ( !result.adifText.isEmpty() ) + { + qCDebug(runtime) << "Importing contacts"; + importRecoveredContacts(result, config); + } + + cleanupWorker(); + + if ( reloadAfterScan ) + { + reloadAfterScan = false; + reloadSettings(); + startStartupRecovery(); + return; + } + + startNextScan(); +} + +void AdifRecoveryManager::importRecoveredContacts(const AdifRecoveryScanResult &result, + const AdifRecoveryConfig &config) +{ + FCT_IDENTIFICATION; + + const StationProfile stationProfile = StationProfilesManager::instance()->getProfile(config.stationProfileName); + + if ( stationProfile.profileName.isEmpty() ) + { + const QString text = tr("Startup ADI Station Profile does not exist: %1").arg(config.stationProfileName); + qCWarning(runtime) << "Profile name is empty"; + emit problem(text); + return; + } + + QString adifText = result.adifText; + QTextStream stream(&adifText, QIODevice::ReadOnly); + LogFormat *format = LogFormat::open("adi", stream); + + if ( !format ) + { + const QString text = tr("Cannot open Startup ADI records from %1").arg(config.path); + qCWarning(runtime) << "Cannot open file"; + emit problem(text); + return; + } + + QMap defaults = qslSentDefaults(config); + + if ( !defaults.isEmpty() ) + format->setDefaults(defaults); + + format->setFillMissingDxcc(true); + format->setDuplicateQSOCallback(skipAllDuplicates); + + QString importLog; + QTextStream importLogStream(&importLog); + unsigned long warnings = 0; + unsigned long errors = 0; + const int importedCount = format->runImport(importLogStream, &stationProfile, + &warnings, &errors); + format->deleteLater(); + + if ( errors == 0 ) + { + saveState(result.fileKey, config, result.nextOffset); + if ( importedCount > 0 ) + emit contactsRecovered(importedCount); + } + else + { + const QString text = tr("Startup ADI from %1 finished with %n error(s); load point was not advanced.", + "", errors).arg(QFileInfo(config.path).fileName()); + qCWarning(runtime).noquote() << text << importLog; + emit problem(text); + } + + Q_UNUSED(warnings) +} + +QMap AdifRecoveryManager::qslSentDefaults(const AdifRecoveryConfig &config) const +{ + QMap defaults; + + const QString status = config.qslSentStatusDefault.isEmpty() ? "Q" : config.qslSentStatusDefault; + + if ( status == "custom" ) + { + defaults.insert("qsl_sent", LogParam::getAdifRecoveryQslSentStatusPaper()); + defaults.insert("lotw_qsl_sent", LogParam::getAdifRecoveryQslSentStatusLoTW()); + defaults.insert("eqsl_qsl_sent", LogParam::getAdifRecoveryQslSentStatusEQSL()); + defaults.insert("dcl_qsl_sent", LogParam::getAdifRecoveryQslSentStatusDCL()); + } + else + { + defaults.insert("qsl_sent", status); + defaults.insert("lotw_qsl_sent", status); + defaults.insert("eqsl_qsl_sent", status); + defaults.insert("dcl_qsl_sent", status); + } + + return defaults; +} + +void AdifRecoveryManager::saveState(const QString &fileKey, + const AdifRecoveryConfig &config, + qint64 offset, + const QString &message) +{ + AdifRecoveryState state; + state.path = config.path; + state.offset = offset; + state.lastRecoveryAt = QDateTime::currentDateTimeUtc(); + state.lastMessage = message; + LogParam::setAdifRecoveryState(fileKey, state); +} + +void AdifRecoveryManager::cleanupWorker() +{ + if ( !workerThread ) + return; + + QThread *thread = workerThread; + workerThread = nullptr; + worker = nullptr; + thread->quit(); + thread->wait(); + thread->deleteLater(); +} + +QSet AdifRecoveryManager::stationProfiles() const +{ + const QStringList profileNames = StationProfilesManager::instance()->profileNameList(); +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + return QSet(profileNames.begin(), profileNames.end()); +#else + return QSet::fromList(profileNames); +#endif +} + +bool AdifRecoveryManager::isRunnableConfig(const AdifRecoveryConfig &config, + const QSet &profiles) const +{ + return config.enabled + && !config.path.trimmed().isEmpty() + && !config.stationProfileName.trimmed().isEmpty() + && profiles.contains(config.stationProfileName); +} + +void AdifRecoveryManager::disableMissingProfileConfigs(const QSet &profiles) +{ + QStringList missingProfiles; + + for ( AdifRecoveryConfig &config : configs ) + { + if ( !config.enabled + || config.stationProfileName.trimmed().isEmpty() + || profiles.contains(config.stationProfileName) ) + continue; + + config.enabled = false; + missingProfiles.append(config.stationProfileName); + } + + if ( missingProfiles.isEmpty() ) + return; + + LogParam::setAdifRecoveryFiles(configs); + reloadSettings(); + + const QString text = tr("Startup ADI was disabled for %n file(s) because the assigned Station Profile no longer exists.", + "", missingProfiles.count()); + qCWarning(runtime) << text << missingProfiles; + emit problem(text); +} + +void AdifRecoveryManager::rebuildPendingQueue(const QSet &profiles) +{ + pendingKeys.clear(); + + for ( const AdifRecoveryConfig &config : static_cast&>(configs) ) + { + if ( !isRunnableConfig(config, profiles) ) + continue; + + pendingKeys.enqueue(AdifRecovery::fileKey(config.path, config.stationProfileName)); + } +} + +LogFormat::duplicateQSOBehaviour AdifRecoveryManager::skipAllDuplicates(QSqlRecord *, QSqlRecord *) +{ + return LogFormat::SKIP_ALL; +} diff --git a/ui/AdifRecoveryManager.h b/ui/AdifRecoveryManager.h new file mode 100644 index 00000000..855b20ba --- /dev/null +++ b/ui/AdifRecoveryManager.h @@ -0,0 +1,60 @@ +#ifndef QLOG_UI_ADIFRECOVERYMANAGER_H +#define QLOG_UI_ADIFRECOVERYMANAGER_H + +#include +#include +#include +#include +#include + +#include "core/AdifRecovery.h" +#include "logformat/LogFormat.h" + +class AdifRecoveryManager : public QObject +{ + Q_OBJECT + +public: + explicit AdifRecoveryManager(QObject *parent = nullptr); + ~AdifRecoveryManager(); + +signals: + void contactsRecovered(int count); + void problem(const QString &text); + +public slots: + void reloadSettings(); + void startStartupRecovery(); + +private slots: + void startNextScan(); + void scanFinished(const AdifRecoveryScanResult &result); + +private: + const int MAX_AUTOMATIC_CONTACTS = 2000; + + static LogFormat::duplicateQSOBehaviour skipAllDuplicates(QSqlRecord *, QSqlRecord *); + + void importRecoveredContacts(const AdifRecoveryScanResult &result, + const AdifRecoveryConfig &config); + QMap qslSentDefaults(const AdifRecoveryConfig &config) const; + void saveState(const QString &fileKey, + const AdifRecoveryConfig &config, + qint64 offset, + const QString &message = QString()); + void cleanupWorker(); + QSet stationProfiles() const; + bool isRunnableConfig(const AdifRecoveryConfig &config, + const QSet &profiles) const; + void disableMissingProfileConfigs(const QSet &profiles); + void rebuildPendingQueue(const QSet &profiles); + + QList configs; + QMap configByKey; + QQueue pendingKeys; + QThread *workerThread = nullptr; + AdifRecoveryReaderWorker *worker = nullptr; + bool reloadAfterScan = false; +}; + +#endif // QLOG_UI_ADIFRECOVERYMANAGER_H diff --git a/ui/AwardsDialog.cpp b/ui/AwardsDialog.cpp index c327e437..c85f95a3 100644 --- a/ui/AwardsDialog.cpp +++ b/ui/AwardsDialog.cpp @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include "AwardsDialog.h" #include "ui_AwardsDialog.h" #include "models/SqlListModel.h" @@ -25,6 +27,8 @@ #include "awards/AwardNZ.h" #include "awards/AwardSpanishDME.h" #include "awards/AwardUKD.h" +#include "awards/AwardWAIP.h" +#include "awards/AwardWAAC.h" MODULE_IDENTIFICATION("qlog.ui.awardsdialog"); @@ -60,6 +64,8 @@ AwardsDialog::AwardsDialog(QWidget *parent) : ui->userFilterComboBox)); ui->userFilterComboBox->blockSignals(false); + connect(ui->rulesPushButton, &QPushButton::clicked, this, &AwardsDialog::openRulesUrl); + refreshTable(0); } @@ -81,6 +87,7 @@ void AwardsDialog::refreshTable(int) setEntityInputEnabled(award->entityInputEnabled()); setNotWorkedEnabled(award->notWorkedEnabled()); + updateRulesButton(award); if ( !award->widget() ) { @@ -159,6 +166,19 @@ QString AwardsDialog::getSelectedEntity() const return comboData; } +void AwardsDialog::openRulesUrl() const +{ + FCT_IDENTIFICATION; + + const AwardDefinition *award = currentAward(); + if ( !award ) + return; + + const QString url = award->rulesUrl(); + if ( !url.isEmpty() ) + QDesktopServices::openUrl(QUrl(url)); +} + void AwardsDialog::setEntityInputEnabled(bool enabled) { FCT_IDENTIFICATION; @@ -182,6 +202,15 @@ void AwardsDialog::setNotWorkedEnabled(bool enabled) ui->notConfirmedCheckBox->blockSignals(false); } +void AwardsDialog::updateRulesButton(const AwardDefinition *award) +{ + FCT_IDENTIFICATION; + + const bool enabled = award && !award->rulesUrl().isEmpty(); + ui->rulesPushButton->setEnabled(enabled); + ui->rulesPushButton->setToolTip(enabled ? award->rulesUrl() : QString()); +} + QList AwardsDialog::createAwards() { return { @@ -205,5 +234,7 @@ QList AwardsDialog::createAwards() new AwardNZ(), new AwardSpanishDME(), new AwardUKD(), + new AwardWAIP(), + new AwardWAAC(), }; } diff --git a/ui/AwardsDialog.h b/ui/AwardsDialog.h index 7dc79a0a..1aa7f625 100644 --- a/ui/AwardsDialog.h +++ b/ui/AwardsDialog.h @@ -32,8 +32,10 @@ public slots: AwardDefinition* currentAward() const; AwardFilterParams buildFilterParams() const; QString getSelectedEntity() const; + void openRulesUrl() const; void setEntityInputEnabled(bool); void setNotWorkedEnabled(bool); + void updateRulesButton(const AwardDefinition *award); static QList createAwards(); }; diff --git a/ui/AwardsDialog.ui b/ui/AwardsDialog.ui index b7d67735..4767608f 100644 --- a/ui/AwardsDialog.ui +++ b/ui/AwardsDialog.ui @@ -20,7 +20,7 @@ - Options + @@ -61,13 +61,23 @@ - + + + + 0 + 0 + + + + 🌐 Rules + + + + + Qt::Horizontal - - QSizePolicy::Maximum - 40 @@ -83,22 +93,6 @@ - - - - Qt::Horizontal - - - QSizePolicy::Minimum - - - - 20 - 20 - - - - @@ -110,7 +104,7 @@ - + Qt::Horizontal @@ -119,8 +113,8 @@ - 40 - 20 + 20 + 10 @@ -132,22 +126,6 @@ - - - - Qt::Horizontal - - - QSizePolicy::Maximum - - - - 20 - 20 - - - - @@ -159,10 +137,13 @@ - + Qt::Horizontal + + QSizePolicy::Maximum + 40 @@ -299,6 +280,13 @@ + + + + Double-click a row/cell to show QSOs + + + @@ -313,6 +301,7 @@ awardComboBox + rulesPushButton myEntityComboBox userFilterComboBox lotwCheckBox diff --git a/ui/BandmapGuideDialog.cpp b/ui/BandmapGuideDialog.cpp new file mode 100644 index 00000000..0a9aa6c5 --- /dev/null +++ b/ui/BandmapGuideDialog.cpp @@ -0,0 +1,515 @@ +#include "BandmapGuideDialog.h" +#include "ui_BandmapGuideDialog.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ui/component/BaseDoubleSpinBox.h" +#include "core/debug.h" +#include "data/Data.h" + +MODULE_IDENTIFICATION("qlog.ui.bandmapguidedialog"); + +BandmapGuideDialog::BandmapGuideDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::BandmapGuideDialog), + currentGuideIndex(-1) +{ + FCT_IDENTIFICATION; + + ui->setupUi(this); + + ui->mainHLayout->setStretch(0, 0); + ui->mainHLayout->setStretch(1, 1); + + QHeaderView *hdr = ui->rangesTable->horizontalHeader(); + hdr->setSectionResizeMode(QHeaderView::ResizeToContents); + + loadGuides(); + populateGuideList(); + + const QString currentId = BandmapGuide::currentProfileId(); + int selectedRow = 0; + for ( int i = 0; i < guides.size(); i++ ) + { + if ( guides[i].id == currentId ) + { + selectedRow = i; + break; + } + } + + if ( ui->guideList->count() > 0 ) + ui->guideList->setCurrentRow(selectedRow); + + int tableWidth = hdr->length() + ui->rangesTable->frameWidth() * 2; + int leftPanelWidth = ui->leftLayout->sizeHint().width(); + QMargins m = ui->mainHLayout->contentsMargins(); + int needed = m.left() + m.right() + ui->mainHLayout->spacing() + leftPanelWidth + tableWidth; + + resize(qMax(needed, width()), height()); +} + +BandmapGuideDialog::~BandmapGuideDialog() +{ + FCT_IDENTIFICATION; + + delete ui; +} + +void BandmapGuideDialog::installWheelGuard(QWidget *widget) +{ + widget->setFocusPolicy(Qt::StrongFocus); + widget->installEventFilter(this); +} + +// Prevent mouse wheel from changing combo/spin values while scrolling the table; +// wheel events are only handled when the widget has focus (after clicking on it) +bool BandmapGuideDialog::eventFilter(QObject *obj, QEvent *event) +{ + if ( event->type() == QEvent::Wheel ) + { + QWidget *widget = qobject_cast(obj); + if ( widget && !widget->hasFocus() ) + { + event->ignore(); + return true; + } + } + return QDialog::eventFilter(obj, event); +} + +void BandmapGuideDialog::loadGuides() +{ + FCT_IDENTIFICATION; + + guides = BandmapGuide::profiles(); + + if ( guides.isEmpty() ) + guides.append(BandmapGuide::exampleProfile()); +} + +void BandmapGuideDialog::populateGuideList() +{ + FCT_IDENTIFICATION; + + ui->guideList->clear(); + + for ( int i = 0; i < guides.size(); i++ ) + { + QListWidgetItem *item = new QListWidgetItem(guides[i].name, ui->guideList); + item->setData(Qt::UserRole, i); + } +} + +void BandmapGuideDialog::guideSelectionChanged() +{ + FCT_IDENTIFICATION; + + if ( currentGuideIndex >= 0 && currentGuideIndex < guides.size() ) + saveCurrentGuide(); + + QListWidgetItem *item = ui->guideList->currentItem(); + + if ( !item ) + { + currentGuideIndex = -1; + ui->nameEdit->clear(); + ui->rangesTable->setRowCount(0); + return; + } + + currentGuideIndex = item->data(Qt::UserRole).toInt(); + showGuideEditor(currentGuideIndex); +} + +void BandmapGuideDialog::guideNameChanged(const QString &text) +{ + FCT_IDENTIFICATION; + + QListWidgetItem *item = ui->guideList->currentItem(); + + if ( item ) + item->setText(text); +} + +void BandmapGuideDialog::showGuideEditor(int index) +{ + FCT_IDENTIFICATION; + + if ( index < 0 || index >= guides.size() ) + return; + + ui->nameEdit->setText(guides[index].name); + populateRangesTable(guides[index].ranges); +} + +void BandmapGuideDialog::populateRangesTable(const QList &ranges) +{ + FCT_IDENTIFICATION; + + QList sortedRanges = ranges; + sortRangesByFrom(&sortedRanges); + + ui->rangesTable->setRowCount(0); + ui->rangesTable->setRowCount(sortedRanges.size()); + + for ( int row = 0; row < sortedRanges.size(); row++ ) + setupRangeRow(row, sortedRanges[row]); + + ui->rangesTable->resizeColumnsToContents(); +} + +void BandmapGuideDialog::setupRangeRow(int row, const BandmapGuide::Range &range) +{ + FCT_IDENTIFICATION; + + BaseDoubleSpinBox *fromSpin = new BaseDoubleSpinBox(this); + installWheelGuard(fromSpin); + fromSpin->setDecimals(6); + fromSpin->setRange(0.0, 999999.999999); + fromSpin->setSingleStep(0.001); + fromSpin->setSuffix(tr(" MHz")); + fromSpin->setValue(range.from); + ui->rangesTable->setCellWidget(row, 0, fromSpin); + + BaseDoubleSpinBox *toSpin = new BaseDoubleSpinBox(this); + installWheelGuard(toSpin); + toSpin->setDecimals(6); + toSpin->setRange(0.0, 999999.999999); + toSpin->setSingleStep(0.001); + toSpin->setSuffix(tr(" MHz")); + toSpin->setValue(range.to); + ui->rangesTable->setCellWidget(row, 1, toSpin); + + QPushButton *colorButton = new QPushButton(this); + setColorButton(colorButton, range.color.isValid() ? range.color : QColor::fromRgb(0x4d8ef7)); + connect(colorButton, &QPushButton::clicked, this, [this, colorButton]() + { + chooseColor(colorButton); + }); + ui->rangesTable->setCellWidget(row, 2, colorButton); + + QLineEdit *labelEdit = new QLineEdit(range.label, this); + ui->rangesTable->setCellWidget(row, 3, labelEdit); +} + +QList BandmapGuideDialog::readRangesFromTable() const +{ + FCT_IDENTIFICATION; + + QList ranges; + + for ( int row = 0; row < ui->rangesTable->rowCount(); row++ ) + { + BandmapGuide::Range range; + + BaseDoubleSpinBox *fromSpin = dynamic_cast(ui->rangesTable->cellWidget(row, 0)); + BaseDoubleSpinBox *toSpin = dynamic_cast(ui->rangesTable->cellWidget(row, 1)); + QPushButton *colorButton = qobject_cast(ui->rangesTable->cellWidget(row, 2)); + QLineEdit *labelEdit = qobject_cast(ui->rangesTable->cellWidget(row, 3)); + + range.from = fromSpin ? fromSpin->value() : 0.0; + range.to = toSpin ? toSpin->value() : 0.0; + range.color = colorButton ? colorFromButton(colorButton) : QColor(); + range.label = labelEdit ? labelEdit->text().trimmed() : QString(); + + ranges.append(range); + } + + sortRangesByFrom(&ranges); + + return ranges; +} + +void BandmapGuideDialog::saveCurrentGuide() +{ + FCT_IDENTIFICATION; + + if ( currentGuideIndex < 0 || currentGuideIndex >= guides.size() ) + return; + + guides[currentGuideIndex].name = ui->nameEdit->text().trimmed(); + guides[currentGuideIndex].ranges = readRangesFromTable(); +} + +void BandmapGuideDialog::newGuide() +{ + FCT_IDENTIFICATION; + + BandmapGuide::Profile profile; + profile.id = BandmapGuide::newProfileId(); + profile.name = tr("New Guide"); + resolveGuideNameConflict(&profile); + profile.ranges.append(BandmapGuide::defaultRange()); + + guides.append(profile); + populateGuideList(); + ui->guideList->setCurrentRow(ui->guideList->count() - 1); +} + +void BandmapGuideDialog::copyGuide() +{ + FCT_IDENTIFICATION; + + if ( currentGuideIndex < 0 || currentGuideIndex >= guides.size() ) + return; + + saveCurrentGuide(); + + BandmapGuide::Profile profile = guides[currentGuideIndex]; + profile.id = BandmapGuide::newProfileId(); + profile.name = tr("Copy - %1").arg(profile.name); + resolveGuideNameConflict(&profile); + + guides.append(profile); + populateGuideList(); + ui->guideList->setCurrentRow(ui->guideList->count() - 1); +} + +void BandmapGuideDialog::deleteGuide() +{ + FCT_IDENTIFICATION; + + if ( currentGuideIndex < 0 || currentGuideIndex >= guides.size() ) + return; + + const QString name = guides[currentGuideIndex].name; + int ret = QMessageBox::question(this, tr("Delete Guide"), + tr("Delete guide '%1'?").arg(name), + QMessageBox::Yes | QMessageBox::No); + if ( ret != QMessageBox::Yes ) + return; + + guides.removeAt(currentGuideIndex); + currentGuideIndex = -1; + populateGuideList(); + + if ( ui->guideList->count() > 0 ) + ui->guideList->setCurrentRow(0); + else + { + ui->nameEdit->clear(); + ui->rangesTable->setRowCount(0); + } +} + +void BandmapGuideDialog::importGuide() +{ + FCT_IDENTIFICATION; + + const QString filePath = QFileDialog::getOpenFileName( + this, + tr("Import Guide"), + QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation), + tr("QLog Bandmap Guide (*.qbg);;JSON (*.json)")); + + if ( filePath.isEmpty() ) + return; + + QString error; + BandmapGuide::Profile profile = BandmapGuide::readProfileFromFile(filePath, &error); + if ( !profile.isValid() ) + { + QMessageBox::critical(this, tr("Import Failed"), error); + return; + } + + sortRangesByFrom(&profile.ranges); + resolveGuideNameConflict(&profile); + guides.append(profile); + populateGuideList(); + ui->guideList->setCurrentRow(ui->guideList->count() - 1); +} + +void BandmapGuideDialog::exportGuide() +{ + FCT_IDENTIFICATION; + + if ( currentGuideIndex < 0 || currentGuideIndex >= guides.size() ) + return; + + saveCurrentGuide(); + + const BandmapGuide::Profile &profile = guides[currentGuideIndex]; + const QString defaultName = profile.name.simplified().replace(' ', '_') + ".qbg"; + const QString filePath = QFileDialog::getSaveFileName( + this, + tr("Export Guide"), + QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + + "/" + defaultName, + tr("QLog Bandmap Guide (*.qbg)")); + + if ( filePath.isEmpty() ) + return; + + QString error; + if ( !BandmapGuide::writeProfileToFile(profile, filePath, &error) ) + QMessageBox::critical(this, tr("Export Failed"), error); +} + +void BandmapGuideDialog::addRange() +{ + FCT_IDENTIFICATION; + + int row = ui->rangesTable->rowCount(); + ui->rangesTable->setRowCount(row + 1); + setupRangeRow(row, BandmapGuide::defaultRange()); + ui->rangesTable->setCurrentCell(row, 0); +} + +void BandmapGuideDialog::removeRange() +{ + FCT_IDENTIFICATION; + + int row = ui->rangesTable->currentRow(); + if ( row >= 0 ) + ui->rangesTable->removeRow(row); +} + +void BandmapGuideDialog::sortRangesByFrom(QList *ranges) const +{ + FCT_IDENTIFICATION; + + if ( !ranges ) + return; + + std::sort(ranges->begin(), ranges->end(), + [](const BandmapGuide::Range &left, const BandmapGuide::Range &right) + { + return left.from < right.from; + }); +} + +void BandmapGuideDialog::chooseColor(QPushButton *button) +{ + FCT_IDENTIFICATION; + + const QColor selected = QColorDialog::getColor(colorFromButton(button), + this, + tr("Guide Color"), + QColorDialog::ShowAlphaChannel | QColorDialog::DontUseNativeDialog); + if ( selected.isValid() ) + setColorButton(button, selected); +} + +void BandmapGuideDialog::setColorButton(QPushButton *button, const QColor &color) +{ + FCT_IDENTIFICATION; + + const QColor safeColor = color.isValid() ? color : QColor(0x4d8ef7); + button->setProperty("color", safeColor); + + const QColor textColor = Data::textColorForBackground(safeColor, + button->palette().color(QPalette::ButtonText), + button->palette().color(QPalette::Button)); + button->setStyleSheet(QString("QPushButton { background-color: %1; color: %2; }") + .arg(safeColor.name(QColor::HexRgb), textColor.name(QColor::HexRgb))); +} + +QColor BandmapGuideDialog::colorFromButton(QPushButton *button) const +{ + FCT_IDENTIFICATION; + + return button->property("color").value(); +} + +bool BandmapGuideDialog::validateGuides() +{ + FCT_IDENTIFICATION; + + for ( int guideIndex = 0; guideIndex < guides.size(); guideIndex++ ) + { + const BandmapGuide::Profile &profile = guides[guideIndex]; + const QString profileName = profile.name.trimmed(); + if ( profileName.isEmpty() ) + { + QMessageBox::warning(this, tr("QLog Warning"), tr("Guide name cannot be empty.")); + ui->guideList->setCurrentRow(guideIndex); + ui->nameEdit->setFocus(); + return false; + } + + for ( int previousIndex = 0; previousIndex < guideIndex; previousIndex++ ) + { + if ( guides[previousIndex].name.trimmed() == profileName ) + { + QMessageBox::warning(this, tr("QLog Warning"), + tr("Guide name '%1' is already used.").arg(profileName)); + ui->guideList->setCurrentRow(guideIndex); + ui->nameEdit->setFocus(); + ui->nameEdit->selectAll(); + return false; + } + } + + for ( int rangeIndex = 0; rangeIndex < profile.ranges.size(); rangeIndex++ ) + { + const BandmapGuide::Range &range = profile.ranges[rangeIndex]; + if ( !range.isValid() ) + { + QMessageBox::warning(this, tr("QLog Warning"), + tr("Guide '%1' contains an invalid range.").arg(profile.name)); + ui->guideList->setCurrentRow(guideIndex); + ui->rangesTable->setCurrentCell(rangeIndex, 0); + return false; + } + } + } + + return true; +} + +void BandmapGuideDialog::resolveGuideNameConflict(BandmapGuide::Profile *profile) const +{ + FCT_IDENTIFICATION; + + if ( !profile ) + return; + + const QString baseName = profile->name.trimmed().isEmpty() ? tr("New Guide") : profile->name.trimmed(); + profile->name = baseName; + + int suffix = 1; + bool collision = true; + + while ( collision ) + { + collision = false; + for ( const BandmapGuide::Profile &existing : guides ) + { + if ( existing.id != profile->id && existing.name == profile->name ) + { + profile->name = QString("%1 (%2)").arg(baseName).arg(suffix++); + collision = true; + break; + } + } + } +} + +void BandmapGuideDialog::accept() +{ + FCT_IDENTIFICATION; + + saveCurrentGuide(); + + for ( int i = 0; i < guides.size(); i++ ) + sortRangesByFrom(&guides[i].ranges); + + if ( !validateGuides() ) + return; + + BandmapGuide::saveProfiles(guides); + + QDialog::accept(); +} diff --git a/ui/BandmapGuideDialog.h b/ui/BandmapGuideDialog.h new file mode 100644 index 00000000..338ff943 --- /dev/null +++ b/ui/BandmapGuideDialog.h @@ -0,0 +1,60 @@ +#ifndef QLOG_UI_BANDMAPGUIDEDIALOG_H +#define QLOG_UI_BANDMAPGUIDEDIALOG_H + +#include +#include + +#include "data/BandmapGuide.h" + +namespace Ui { +class BandmapGuideDialog; +} + +class QPushButton; +class QWidget; + +class BandmapGuideDialog : public QDialog +{ + Q_OBJECT + +public: + explicit BandmapGuideDialog(QWidget *parent = nullptr); + ~BandmapGuideDialog(); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; + +private slots: + void accept() override; + void guideSelectionChanged(); + void guideNameChanged(const QString &text); + void newGuide(); + void copyGuide(); + void deleteGuide(); + void importGuide(); + void exportGuide(); + void addRange(); + void removeRange(); + +private: + void loadGuides(); + void populateGuideList(); + void showGuideEditor(int index); + void saveCurrentGuide(); + void populateRangesTable(const QList &ranges); + void setupRangeRow(int row, const BandmapGuide::Range &range); + QList readRangesFromTable() const; + void sortRangesByFrom(QList *ranges) const; + void chooseColor(QPushButton *button); + void setColorButton(QPushButton *button, const QColor &color); + QColor colorFromButton(QPushButton *button) const; + void installWheelGuard(QWidget *widget); + bool validateGuides(); + void resolveGuideNameConflict(BandmapGuide::Profile *profile) const; + + Ui::BandmapGuideDialog *ui; + QList guides; + int currentGuideIndex; +}; + +#endif // QLOG_UI_BANDMAPGUIDEDIALOG_H diff --git a/ui/BandmapGuideDialog.ui b/ui/BandmapGuideDialog.ui new file mode 100644 index 00000000..51817036 --- /dev/null +++ b/ui/BandmapGuideDialog.ui @@ -0,0 +1,453 @@ + + + BandmapGuideDialog + + + + 0 + 0 + 760 + 500 + + + + Bandmap Guide + + + true + + + + + + + + + + Import guide + + + Import + + + + .. + + + + + + + Export guide + + + Export + + + + .. + + + + + + + + + + 0 + 0 + + + + + + + + + + New guide + + + New + + + + .. + + + + + + + Copy guide + + + Copy + + + + .. + + + + + + + Delete guide + + + Delete + + + + .. + + + + + + + + + + + + + + + Guide Name: + + + + + + + + + + + + Ranges: + + + + + + + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + QAbstractItemView::ScrollPerPixel + + + QAbstractItemView::ScrollPerPixel + + + 4 + + + true + + + false + + + 25 + + + 25 + + + + From + + + + + To + + + + + Color + + + + + Label + + + + + + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + Add range + + + Add + + + + + + + Remove selected range + + + Remove + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + nameEdit + rangesTable + addRangeButton + removeRangeButton + guideList + newButton + copyButton + deleteButton + importGuideButton + exportGuideButton + + + + + buttonBox + rejected() + BandmapGuideDialog + reject() + + + 380 + 480 + + + 380 + 250 + + + + + buttonBox + accepted() + BandmapGuideDialog + accept() + + + 380 + 480 + + + 380 + 250 + + + + + nameEdit + textChanged(QString) + BandmapGuideDialog + guideNameChanged(QString) + + + 480 + 30 + + + 380 + 250 + + + + + guideList + currentRowChanged(int) + BandmapGuideDialog + guideSelectionChanged() + + + 100 + 200 + + + 380 + 250 + + + + + newButton + clicked() + BandmapGuideDialog + newGuide() + + + 60 + 460 + + + 380 + 250 + + + + + copyButton + clicked() + BandmapGuideDialog + copyGuide() + + + 120 + 460 + + + 380 + 250 + + + + + deleteButton + clicked() + BandmapGuideDialog + deleteGuide() + + + 180 + 460 + + + 380 + 250 + + + + + importGuideButton + clicked() + BandmapGuideDialog + importGuide() + + + 70 + 25 + + + 380 + 250 + + + + + exportGuideButton + clicked() + BandmapGuideDialog + exportGuide() + + + 150 + 25 + + + 380 + 250 + + + + + addRangeButton + clicked() + BandmapGuideDialog + addRange() + + + 580 + 440 + + + 380 + 250 + + + + + removeRangeButton + clicked() + BandmapGuideDialog + removeRange() + + + 660 + 440 + + + 380 + 250 + + + + + + guideSelectionChanged() + guideNameChanged(QString) + newGuide() + copyGuide() + deleteGuide() + importGuide() + exportGuide() + addRange() + removeRange() + + diff --git a/ui/BandmapWidget.cpp b/ui/BandmapWidget.cpp index ca6a2d92..c0a899cf 100644 --- a/ui/BandmapWidget.cpp +++ b/ui/BandmapWidget.cpp @@ -9,15 +9,19 @@ #include #include #include +#include #include "BandmapWidget.h" #include "ui_BandmapWidget.h" #include "data/Data.h" #include "data/BandPlan.h" +#include "data/BandmapGuide.h" #include "core/debug.h" #include "rig/macros.h" #include "core/LogParam.h" #include "core/EmergencyFrequency.h" +#include "core/IBPBeacon.h" +#include "ui/BandmapGuideDialog.h" MODULE_IDENTIFICATION("qlog.ui.bandmapwidget"); @@ -41,6 +45,7 @@ BandmapWidget::BandmapWidget(const QString &widgetID, txMark(nullptr), keepRXCenter(true), showEmergencyMarkers(true), + showIBPMarkers(true), pendingSpots(0), lastStationUpdate(0), bandmapAnimation(true), @@ -61,6 +66,7 @@ BandmapWidget::BandmapWidget(const QString &widgetID, keepRXCenter = LogParam::getBandmapCenterRX(objectName()); showEmergencyMarkers = LogParam::getBandmapShowEmergency(objectName()); + showIBPMarkers = LogParam::getBandmapShowIBP(objectName()); if ( isNonVfo ) { @@ -82,8 +88,12 @@ BandmapWidget::BandmapWidget(const QString &widgetID, connect(bandmapScene, &GraphicsScene::spotClicked, this, &BandmapWidget::spotClicked); + connect(bandmapScene, &GraphicsScene::markerClicked, + this, &BandmapWidget::markerClicked); connect(ui->scrollArea->verticalScrollBar(), &QScrollBar::rangeChanged, this, &BandmapWidget::focusZoomFreq); + connect(BandmapGuide::instance(), &BandmapGuide::changed, + this, [this]() { update(); }); ui->graphicsView->setScene(bandmapScene); ui->graphicsView->installEventFilter(this); @@ -129,6 +139,7 @@ void BandmapWidget::update() determineStepDigits(step, digits); const int steps = static_cast(round((currentBand.end - currentBand.start) / step)); + const QString endFreqDigits = QString::number(currentBand.end + step * steps, 'f', digits); minHeight = steps * PIXELSPERSTEP + 30; ui->graphicsView->setFixedSize(270, minHeight); @@ -139,6 +150,8 @@ void BandmapWidget::update() const QPen gridPen(QColor(192, 192, 192)); const QBrush highlightBrush(QColor(102, 153, 255, 100)); + drawGuideOverlay(step, endFreqDigits); + for ( int i = 0; i <= steps; i++ ) { double plottedFreq = currentBand.start + step * i; @@ -162,7 +175,6 @@ void BandmapWidget::update() } } - const QString &endFreqDigits= QString::number(currentBand.end + step*steps, 'f', digits); bandmapScene->setSceneRect(135 - (endFreqDigits.size() * PIXELSPERSTEP), 0, 0, @@ -173,10 +185,11 @@ void BandmapWidget::update() /************************/ drawTXRXMarks(step); - /*****************************/ - /* Draw Emergency Freq Marks */ - /*****************************/ + /********************************/ + /* Draw Special Frequency Marks */ + /********************************/ drawEmergencyMarkers(step); + drawIBPMarkers(step); /***************** * Draw Stations * @@ -259,6 +272,7 @@ void BandmapWidget::updateStations() QGraphicsTextItem* text = bandmapScene->addText(callsignTmp + " @ " + timeTmp); text->document()->setDocumentMargin(0); + text->setCursor(Qt::PointingHandCursor); qreal halfHeight = text->boundingRect().height() / 2; text->setPos(40, text_y - halfHeight); @@ -460,6 +474,140 @@ void BandmapWidget::drawTXRXMarks(double step) } } +void BandmapWidget::drawLabeledFrequencyMarker(double frequency, + double step, + const FrequencyMarkerStyle &style, + const QString &mode, + const QString &submode) +{ + FCT_IDENTIFICATION; + + if ( frequency < currentBand.start || frequency > currentBand.end ) + return; + + QFont markerFont; + markerFont.setPointSize(7); + markerFont.setBold(true); + + const qreal pillX = 157.0; + const qreal pillH = 14.0; + const qreal glowH = qMin((style.glowWidthMHz / step) * PIXELSPERSTEP, 15.0); + const qreal y = ((frequency - currentBand.start) / step) * PIXELSPERSTEP; + + QColor glowTransparent(style.glowColor); + glowTransparent.setAlpha(0); + QColor glowLow(style.glowColor); + glowLow.setAlpha(70); + QColor glowMid(style.glowColor); + glowMid.setAlpha(115); + + QLinearGradient glow(0.0, y - glowH, 0.0, y + glowH); + glow.setColorAt(0.0, glowTransparent); + glow.setColorAt(0.35, glowLow); + glow.setColorAt(0.5, glowMid); + glow.setColorAt(0.65, glowLow); + glow.setColorAt(1.0, glowTransparent); + bandmapScene->addRect(0, y - glowH, pillX, 2.0 * glowH, + QPen(Qt::NoPen), QBrush(glow)); + + bandmapScene->addLine(0, y, pillX, y, QPen(style.lineColor, 2)); + + QGraphicsSimpleTextItem *textItem = bandmapScene->addSimpleText(style.label, markerFont); + textItem->setBrush(QBrush(readableMarkerTextColor(style.pillColor))); + const QRectF textRect = textItem->boundingRect(); + const qreal pillW = textRect.width() + 10.0; + const qreal pillY = y - pillH / 2.0; + + QPainterPath pillPath; + pillPath.addRoundedRect(QRectF(pillX, pillY, pillW, pillH), 4, 4); + QGraphicsPathItem *pillItem = bandmapScene->addPath(pillPath, + QPen(Qt::NoPen), + QBrush(style.pillColor)); + + textItem->setPos(pillX + (pillW - textRect.width()) / 2.0, + pillY + (pillH - textRect.height()) / 2.0); + + pillItem->setZValue(1); + textItem->setZValue(2); + + setMarkerTuneData(pillItem, frequency, mode, submode); + setMarkerTuneData(textItem, frequency, mode, submode); +} + +void BandmapWidget::setMarkerTuneData(QGraphicsItem *item, + double frequency, + const QString &mode, + const QString &submode) const +{ + FCT_IDENTIFICATION; + + if ( !item ) + return; + + item->setCursor(Qt::PointingHandCursor); + item->setData(GraphicsScene::MarkerFrequencyRole, frequency); + item->setData(GraphicsScene::MarkerModeRole, mode); + item->setData(GraphicsScene::MarkerSubmodeRole, submode); +} + +QColor BandmapWidget::readableMarkerTextColor(const QColor &background) const +{ + return Data::textColorForBackground(background, + palette().color(QPalette::Text), + palette().color(QPalette::Base)); +} + +void BandmapWidget::drawGuideOverlay(double step, const QString &widestFreqText) +{ + FCT_IDENTIFICATION; + + if ( !BandmapGuide::isEnabled() ) + return; + + const BandmapGuide::Profile profile = BandmapGuide::currentProfile(); + if ( profile.ranges.isEmpty() ) + return; + + QFontMetrics metrics(qApp->font()); +#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) + const qreal labelWidth = metrics.horizontalAdvance(widestFreqText) + 8.0; +#else + const qreal labelWidth = metrics.width(widestFreqText) + 8.0; +#endif + const qreal guideRight = -12.0; + const qreal guideX = guideRight - labelWidth; + + for ( const BandmapGuide::Range &range : profile.ranges ) + { + if ( range.to <= currentBand.start || range.from >= currentBand.end ) + continue; + + const double from = qMax(range.from, currentBand.start); + const double to = qMin(range.to, currentBand.end); + const qreal y1 = ((from - currentBand.start) / step) * PIXELSPERSTEP; + const qreal y2 = ((to - currentBand.start) / step) * PIXELSPERSTEP; + + QColor color(range.color); + color.setAlpha(100); + + QGraphicsRectItem *item = bandmapScene->addRect(guideX, + y1, + labelWidth, + qMax(y2 - y1, 1.0), + QPen(Qt::NoPen), + QBrush(color)); + item->setZValue(-20); + + if ( !range.label.isEmpty() ) + { + item->setToolTip(QString("%1
%2 - %3 MHz") + .arg(range.label, + QString::number(range.from, 'f', 6), + QString::number(range.to, 'f', 6))); + } + } +} + void BandmapWidget::removeDuplicates(DxSpot &spot) { FCT_IDENTIFICATION; @@ -975,9 +1123,62 @@ void BandmapWidget::showContextMenu(const QPoint &point) emergencyAction->setChecked(showEmergencyMarkers); connect(emergencyAction, &QAction::triggered, this, &BandmapWidget::emergencyMarkersActionChecked); + QAction* ibpAction = new QAction(tr("Show IBP Frequencies"), &contextMenu); + ibpAction->setCheckable(true); + ibpAction->setChecked(showIBPMarkers); + connect(ibpAction, &QAction::triggered, this, &BandmapWidget::ibpMarkersActionChecked); + + QMenu guideMenu(tr("Show Guide"), &contextMenu); + + QAction* guideOffAction = new QAction(tr("Off"), &guideMenu); + guideOffAction->setCheckable(true); + guideOffAction->setChecked(!BandmapGuide::isEnabled()); + connect(guideOffAction, &QAction::triggered, this, []() + { + BandmapGuide::setEnabled(false); + refreshAllBandmaps(); + }); + guideMenu.addAction(guideOffAction); + + const QList guides = BandmapGuide::profiles(); + QString currentGuideId = BandmapGuide::currentProfileId(); + if ( currentGuideId.isEmpty() && !guides.isEmpty() ) + currentGuideId = guides.first().id; + + if ( guides.isEmpty() ) + { + QAction* noGuideAction = new QAction(tr("No Guide"), &guideMenu); + noGuideAction->setEnabled(false); + guideMenu.addAction(noGuideAction); + } + else + { + guideMenu.addSeparator(); + for ( const BandmapGuide::Profile &guide : guides ) + { + QAction* action = new QAction(guide.name, &guideMenu); + action->setCheckable(true); + action->setChecked(BandmapGuide::isEnabled() && guide.id == currentGuideId); + connect(action, &QAction::triggered, this, [guide]() + { + BandmapGuide::setCurrentProfileId(guide.id); + BandmapGuide::setEnabled(true); + refreshAllBandmaps(); + }); + guideMenu.addAction(action); + } + } + + QAction* editGuideAction = new QAction(tr("Edit Guide..."), &contextMenu); + connect(editGuideAction, &QAction::triggered, this, &BandmapWidget::editGuide); + contextMenu.addMenu(&bandsMenu); contextMenu.addAction(centerRXAction); contextMenu.addAction(emergencyAction); + contextMenu.addAction(ibpAction); + contextMenu.addSeparator(); + contextMenu.addMenu(&guideMenu); + contextMenu.addAction(editGuideAction); contextMenu.exec(ui->graphicsView->mapToGlobal(point)); } @@ -1057,55 +1258,62 @@ void BandmapWidget::drawEmergencyMarkers(double step) if ( !showEmergencyMarkers ) return; - QFont emergencyFont; - emergencyFont.setPointSize(7); - emergencyFont.setBold(true); - - const QColor lineColor(220, 40, 40); - const QColor pillColor(185, 28, 28); - const qreal pillX = 157.0; - const qreal pillH = 14.0; - const qreal tolerancePx = (EmergencyFrequency::TOLERANCE_MHZ / step) * PIXELSPERSTEP; - const qreal glowH = qMin(tolerancePx, 15.0); - const EmergencyFreqEntry *entry = EmergencyFrequency::inBand(currentBand.start, currentBand.end); if ( !entry ) return; - const qreal y = ((entry->frequency - currentBand.start) / step) * PIXELSPERSTEP; + const FrequencyMarkerStyle style = + { + tr("SOS"), + QColor(220, 40, 40), + QColor(185, 28, 28), + QColor(220, 30, 30), + EmergencyFrequency::TOLERANCE_MHZ + }; + + const QString mode = (entry->mode == QLatin1String("LSB") + || entry->mode == QLatin1String("USB")) + ? QStringLiteral("SSB") + : entry->mode; + const QString submode = (mode == QLatin1String("SSB")) ? entry->mode : QString(); + drawLabeledFrequencyMarker(entry->frequency, step, style, mode, submode); +} - // Gradient glow — height proportional to zoom, fades to transparent at edges - QLinearGradient glow(0.0, y - glowH, 0.0, y + glowH); - glow.setColorAt(0.0, QColor(220, 30, 30, 0)); - glow.setColorAt(0.35, QColor(220, 30, 30, 70)); - glow.setColorAt(0.5, QColor(220, 30, 30, 115)); - glow.setColorAt(0.65, QColor(220, 30, 30, 70)); - glow.setColorAt(1.0, QColor(220, 30, 30, 0)); - bandmapScene->addRect(0, y - glowH, pillX, 2.0 * glowH, - QPen(Qt::NoPen), QBrush(glow)); +void BandmapWidget::drawIBPMarkers(double step) +{ + FCT_IDENTIFICATION; - // Sharp centre line — runs from the scale up to the SOS pill - bandmapScene->addLine(0, y, pillX, y, QPen(lineColor, 2)); + if ( !showIBPMarkers ) + return; - // Pill label — sized dynamically around the text - QGraphicsSimpleTextItem *textItem = bandmapScene->addSimpleText(tr("SOS"), emergencyFont); - textItem->setBrush(QBrush(Qt::white)); - const QRectF textRect = textItem->boundingRect(); - const qreal pillW = textRect.width() + 10.0; - const qreal pillY = y - pillH / 2.0; + const FrequencyMarkerStyle style = + { + tr("IBP"), + QColor(80, 170, 255), + QColor(30, 136, 229), + QColor(80, 170, 255), + 0.001 + }; + + for ( const IBPBeacon::Band &band : IBPBeacon::bands() ) + { + if ( band.frequency >= currentBand.start && band.frequency <= currentBand.end ) + drawLabeledFrequencyMarker(band.frequency, + step, + style, + QStringLiteral("CW")); + } +} - QPainterPath pillPath; - pillPath.addRoundedRect(QRectF(pillX, pillY, pillW, pillH), 4, 4); - QGraphicsPathItem *pillItem = bandmapScene->addPath(pillPath, - QPen(Qt::NoPen), - QBrush(pillColor)); +void BandmapWidget::markerClicked(double frequency, const QString &mode, const QString &submode) +{ + FCT_IDENTIFICATION; - textItem->setPos(pillX + (pillW - textRect.width()) / 2.0, - pillY + (pillH - textRect.height()) / 2.0); + qCDebug(function_parameters) << frequency << mode << submode; - pillItem->setZValue(1); - textItem->setZValue(2); + Rig::instance()->setFrequency(MHz(frequency)); + Rig::instance()->setMode(mode, submode); } void BandmapWidget::drawMarkers(double frequency) @@ -1314,6 +1522,36 @@ void BandmapWidget::emergencyMarkersActionChecked(bool state) update(); } +void BandmapWidget::ibpMarkersActionChecked(bool state) +{ + FCT_IDENTIFICATION; + + showIBPMarkers = state; + LogParam::setBandmapShowIBP(objectName(), showIBPMarkers); + update(); +} + +void BandmapWidget::editGuide() +{ + FCT_IDENTIFICATION; + + BandmapGuideDialog dialog(this); + if ( dialog.exec() == QDialog::Accepted ) + refreshAllBandmaps(); +} + +void BandmapWidget::refreshAllBandmaps() +{ + FCT_IDENTIFICATION; + + if ( vfoWidget ) + vfoWidget->update(); + + for ( BandmapWidget *widget : static_cast>(nonVfoWidgets) ) + if ( widget ) + widget->update(); +} + void GraphicsScene::mousePressEvent(QGraphicsSceneMouseEvent *evt) { @@ -1322,12 +1560,22 @@ void GraphicsScene::mousePressEvent(QGraphicsSceneMouseEvent *evt) if ( evt->button() & Qt::LeftButton ) { QGraphicsItem *item = itemAt(evt->scenePos(), QTransform()); - QGraphicsTextItem *focusedSpot = dynamic_cast(item); - if ( focusedSpot && focusedSpot->property("freq").isValid() ) - emit spotClicked(focusedSpot->toPlainText().split(" ").first(), - focusedSpot->property("freq").toDouble(), - static_cast(focusedSpot->property("bandmode").toInt())); + if ( item && item->data(MarkerFrequencyRole).isValid() ) + { + emit markerClicked(item->data(MarkerFrequencyRole).toDouble(), + item->data(MarkerModeRole).toString(), + item->data(MarkerSubmodeRole).toString()); + } + else + { + QGraphicsTextItem *focusedSpot = dynamic_cast(item); + + if ( focusedSpot && focusedSpot->property("freq").isValid() ) + emit spotClicked(focusedSpot->toPlainText().split(" ").first(), + focusedSpot->property("freq").toDouble(), + static_cast(focusedSpot->property("bandmode").toInt())); + } } evt->accept(); } diff --git a/ui/BandmapWidget.h b/ui/BandmapWidget.h index daa86330..337cb841 100644 --- a/ui/BandmapWidget.h +++ b/ui/BandmapWidget.h @@ -28,9 +28,16 @@ class GraphicsScene : public QGraphicsScene public: explicit GraphicsScene(QObject *parent = nullptr) : QGraphicsScene(parent){}; + enum + { + MarkerFrequencyRole = Qt::UserRole + 1, + MarkerModeRole, + MarkerSubmodeRole + }; signals: void spotClicked(QString, double, BandPlan::BandPlanMode mode); + void markerClicked(double frequency, const QString &mode, const QString &submode); protected: void mousePressEvent (QGraphicsSceneMouseEvent *evt) override; @@ -48,6 +55,7 @@ class BandmapWidget : public QWidget, public ShutdownAwareWidget ~BandmapWidget(); const Band& getBand() const {return currentBand;}; const QList getNonVfoWidgetList() {return nonVfoWidgets;}; + static void refreshAllBandmaps(); enum BandmapZoom { ZOOM_100HZ = 6, ZOOM_250HZ = 5, @@ -89,12 +97,33 @@ public slots: void removeDuplicates(DxSpot &spot); void spotAging(); + struct FrequencyMarkerStyle + { + QString label; + QColor lineColor; + QColor pillColor; + QColor glowColor; + double glowWidthMHz; + }; + void determineStepDigits(double &step, int &digits) const; void clearAllCallsignFromScene(); void clearFreqMark(QGraphicsPolygonItem **); void drawFreqMark(const double, const double, const QColor&, QGraphicsPolygonItem **); void drawTXRXMarks(double); + void drawLabeledFrequencyMarker(double frequency, + double step, + const FrequencyMarkerStyle &style, + const QString &mode, + const QString &submode = QString()); + void setMarkerTuneData(QGraphicsItem *item, + double frequency, + const QString &mode, + const QString &submode) const; + QColor readableMarkerTextColor(const QColor &background) const; + void drawGuideOverlay(double step, const QString &widestFreqText); void drawEmergencyMarkers(double step); + void drawIBPMarkers(double step); void drawMarkers(double frequency); void resizeEvent(QResizeEvent * event) override; bool eventFilter(QObject *obj, QEvent *event) override; @@ -116,7 +145,10 @@ public slots: private slots: void centerRXActionChecked(bool); void emergencyMarkersActionChecked(bool); + void ibpMarkersActionChecked(bool); + void editGuide(); void spotClicked(const QString&, double, BandPlan::BandPlanMode); + void markerClicked(double frequency, const QString &mode, const QString &submode); void showContextMenu(const QPoint&); void updateStationTimer(); void focusZoomFreq(int, int); @@ -141,6 +173,7 @@ private slots: QGraphicsPolygonItem* txMark; bool keepRXCenter; bool showEmergencyMarkers; + bool showIBPMarkers; LogLocale locale; quint32 pendingSpots; qint64 lastStationUpdate; diff --git a/ui/CabrilloTemplateDialog.cpp b/ui/CabrilloTemplateDialog.cpp index 85146d9e..3359d353 100644 --- a/ui/CabrilloTemplateDialog.cpp +++ b/ui/CabrilloTemplateDialog.cpp @@ -3,7 +3,6 @@ #include #include -#include #include #include #include @@ -20,7 +19,7 @@ #include "core/debug.h" #include "data/Data.h" -#include "models/LogbookModel.h" +#include "ui/component/LogbookFieldComboBox.h" MODULE_IDENTIFICATION("qlog.ui.cabrillotemplatedialog"); @@ -41,30 +40,6 @@ CabrilloTemplateDialog::CabrilloTemplateDialog(QWidget *parent) : QHeaderView *hdr = ui->columnsTable->horizontalHeader(); hdr->setSectionResizeMode(QHeaderView::ResizeToContents); - // Populate available DB fields with translated names - QSqlRecord contactsRecord = QSqlDatabase::database().record("contacts"); - - for ( int i = LogbookModel::ColumnID::COLUMN_ID; i < LogbookModel::ColumnID::COLUMN_LAST_ELEMENT; ++i ) - { - LogbookModel::ColumnID columnID = static_cast(i); - const QString translation = LogbookModel::getFieldNameTranslation(columnID); - if ( translation.isEmpty() ) - continue; - - const QString dbField = contactsRecord.fieldName(i); - if ( dbField.isEmpty() ) - continue; - - dbFieldItems.append({dbField, translation}); - } - - std::sort(dbFieldItems.begin(), dbFieldItems.end(), - [](const CabrilloFormat::CategoryItem &a, - const CabrilloFormat::CategoryItem &b) - { - return a.label.localeAwareCompare(b.label) < 0; - }); - formatterItems = CabrilloFormat::formatterTypes(); for ( const CabrilloFormat::CategoryItem &item : static_cast>(CabrilloFormat::modeCategories()) ) @@ -262,16 +237,11 @@ void CabrilloTemplateDialog::setupColumnRow(int row, const CabrilloFormat::Colum ui->columnsTable->setItem(row, 0, posItem); // DB Field (ComboBox with translated names) - QComboBox *fieldCombo = new QComboBox(this); + LogbookFieldComboBox *fieldCombo = new LogbookFieldComboBox(this); installWheelGuard(fieldCombo); - fieldCombo->addItem(QString(), QLatin1String("")); // empty entry for non-DB fields - for ( const CabrilloFormat::CategoryItem &item : static_cast&>(dbFieldItems) ) - fieldCombo->addItem(item.label, item.value); - - int fieldIdx = fieldCombo->findData(col.dbField); - - if ( fieldIdx >= 0 ) - fieldCombo->setCurrentIndex(fieldIdx); + fieldCombo->populate(LogbookFieldComboBox::ValueMode::DbFieldName, + LogbookFieldComboBox::EmptyMode::Blank); + fieldCombo->setCurrentDbFieldName(col.dbField); ui->columnsTable->setCellWidget(row, 1, fieldCombo); diff --git a/ui/CabrilloTemplateDialog.h b/ui/CabrilloTemplateDialog.h index 559df0aa..769886e9 100644 --- a/ui/CabrilloTemplateDialog.h +++ b/ui/CabrilloTemplateDialog.h @@ -74,7 +74,6 @@ private slots: Ui::CabrilloTemplateDialog *ui; QList templates; int currentTemplateIndex; - QList dbFieldItems; QList formatterItems; }; diff --git a/ui/CabrilloTemplateDialog.ui b/ui/CabrilloTemplateDialog.ui index d0a9387f..a466dc98 100644 --- a/ui/CabrilloTemplateDialog.ui +++ b/ui/CabrilloTemplateDialog.ui @@ -30,7 +30,8 @@ Import
- + + ..
@@ -43,7 +44,8 @@ Export - + + .. @@ -70,7 +72,8 @@ New - + + .. @@ -83,7 +86,8 @@ Copy - + + .. @@ -96,7 +100,8 @@ Delete - + + .. @@ -189,6 +194,12 @@ false + + 25 + + + 25 + Seq. diff --git a/ui/ClockWidget.cpp b/ui/ClockWidget.cpp index e2444a00..838026a5 100644 --- a/ui/ClockWidget.cpp +++ b/ui/ClockWidget.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include #include "ClockWidget.h" #include "ui_ClockWidget.h" @@ -7,29 +9,126 @@ #include "data/Gridsquare.h" #include "data/StationProfile.h" -#define EARTH_RADIUS 6371 -#define EARTH_CIRCUM 40075 - #define MSECS_PER_DAY (24.0 * 60.0 * 60.0 * 1000.0) +#define MSECS_PER_DAY_INT (24 * 60 * 60 * 1000) + MODULE_IDENTIFICATION("qlog.ui.clockwidget"); +SunTimelineWidget::SunTimelineWidget(QWidget *parent) : + QWidget(parent), + sunState(NoSunTimes) +{ + setMinimumHeight(18); +} + +QSize SunTimelineWidget::sizeHint() const +{ + return QSize(200, 18); +} + +QSize SunTimelineWidget::minimumSizeHint() const +{ + return QSize(120, 18); +} + +void SunTimelineWidget::setSunTimes(const QTime &newSunrise, + const QTime &newSunset, + SunState newSunState) +{ + sunrise = newSunrise; + sunset = newSunset; + sunState = newSunState; + update(); +} + +void SunTimelineWidget::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, true); + + const QRectF bar = rect().adjusted(4.0, 5.0, -4.0, -5.0); + if ( bar.width() <= 0.0 || bar.height() <= 0.0 ) + return; + + const QColor nightColor(42, 68, 90); + const QColor dayColor(255, 255, 0); + const QColor twilightColor(232, 105, 72); + const QColor currentColor(220, 60, 55); + + QPainterPath clipPath; + clipPath.addRoundedRect(bar, bar.height() / 2.0, bar.height() / 2.0); + + painter.fillPath(clipPath, nightColor); + painter.setClipPath(clipPath); + + const auto drawDaySegment = [&](qreal start, qreal end, bool sunriseAtStart, bool sunsetAtEnd) + { + if ( end <= start ) + return; + + const qreal width = end - start; + const qreal edge = qMin(0.35, 9.0 / width); + + QLinearGradient gradient(start, 0.0, end, 0.0); + gradient.setColorAt(0.0, sunriseAtStart ? twilightColor : dayColor); + gradient.setColorAt(edge, dayColor); + gradient.setColorAt(1.0 - edge, dayColor); + gradient.setColorAt(1.0, sunsetAtEnd ? twilightColor : dayColor); + + painter.fillRect(QRectF(start, bar.top(), width, bar.height()), gradient); + }; + + if ( sunState == AllDay ) + { + painter.fillRect(bar, dayColor); + } + else if ( sunState == NormalSunTimes && sunrise.isValid() && sunset.isValid() ) + { + const qreal rise = bar.left() + sunrise.msecsSinceStartOfDay() / MSECS_PER_DAY * bar.width(); + const qreal set = bar.left() + sunset.msecsSinceStartOfDay() / MSECS_PER_DAY * bar.width(); + + if ( set > rise ) + { + drawDaySegment(rise, set, true, true); + } + else + { + drawDaySegment(bar.left(), set, false, true); + drawDaySegment(rise, bar.right(), true, false); + } + } + + painter.setClipping(false); + painter.setPen(QPen(palette().color(QPalette::Mid), 1.0)); + painter.drawPath(clipPath); + + const qreal current = bar.left() + + QDateTime::currentDateTimeUtc().time().msecsSinceStartOfDay() + / MSECS_PER_DAY * bar.width(); + QPen currentPen(currentColor); + currentPen.setWidthF(1.5); + painter.setPen(currentPen); + painter.drawLine(QPointF(current, bar.top() - 3.0), + QPointF(current, bar.bottom() + 3.0)); +} + ClockWidget::ClockWidget(QWidget *parent) : QWidget(parent), ui(new Ui::ClockWidget), - sunScene(new QGraphicsScene(this)), + timer(new QTimer(this)), clockScene(new QGraphicsScene(this)), - clockItem(new QGraphicsTextItem) + clockItem(new QGraphicsTextItem), + sunState(SunTimelineWidget::NoSunTimes) { FCT_IDENTIFICATION; ui->setupUi(this); - QTimer *timer = new QTimer(this); + timer->setSingleShot(true); + timer->setTimerType(Qt::PreciseTimer); connect(timer, &QTimer::timeout, this, &ClockWidget::updateClock); - timer->start(500); - - sunScene->setSceneRect(0, 0, 200, 10); - ui->sunGraphicsView->setScene(sunScene.data()); QFont font = clockItem->font(); font.setPointSize(20); @@ -37,9 +136,18 @@ ClockWidget::ClockWidget(QWidget *parent) : clockScene->addItem(clockItem.data()); ui->clockGraphicsView->setScene(clockScene.data()); - updateClock(); updateSun(); updateSunGraph(); + updateClock(); +} + +void ClockWidget::scheduleClockUpdate() +{ + FCT_IDENTIFICATION; + + const QTime now = QDateTime::currentDateTimeUtc().time(); + int msecsToNextSecond = 1000 - now.msec(); + timer->start(msecsToNextSecond); } void ClockWidget::updateClock() @@ -51,12 +159,14 @@ void ClockWidget::updateClock() clockItem->setDefaultTextColor(textColor); clockItem->setPlainText(locale.toString(now, locale.formatTimeLongWithoutTZ())); - if (now.time().second() == 0) + if ( now.time().second() == 0 ) { updateSun(); updateSunGraph(); } + scheduleClockUpdate(); + /* Use only in case when you want to debug which widget is focussed*/ //#define SHOW_FOCUS #ifdef SHOW_FOCUS @@ -77,7 +187,7 @@ void ClockWidget::updateSun() double lat = myGrid.getLatitude(); double lon = myGrid.getLongitude(); - qint64 julianDay = QDate::currentDate().toJulianDay(); + qint64 julianDay = QDateTime::currentDateTimeUtc().date().toJulianDay(); double n = static_cast(julianDay) - 2451545.0 + 0.0008; double Js = n - lon / 360.0; @@ -87,13 +197,42 @@ void ClockWidget::updateSun() double Jt = 2451545.0 + Js + 0.0053 * sin(M / 180.0 * M_PI) - 0.0069 * sin(2 * L / 180.0 * M_PI); double sind = sin(L / 180.0 * M_PI) * sin(23.44 / 180.0 * M_PI); double cosd = cos(asin(sind)); - double w = acos((sin(-0.83 / 180.0 * M_PI) - sin(lat / 180.0 * M_PI) * sind) / (cos(lat / 180.0 * M_PI) * cosd)); + const double cosHourAngleNumerator = sin(-0.83 / 180.0 * M_PI) + - sin(lat / 180.0 * M_PI) * sind; + const double cosHourAngleDenominator = cos(lat / 180.0 * M_PI) * cosd; + double cosHourAngle = cosHourAngleNumerator / cosHourAngleDenominator; + + if ( !qIsFinite(cosHourAngle) ) + cosHourAngle = (cosHourAngleNumerator <= 0.0) ? -2.0 : 2.0; + + if ( cosHourAngle > 1.0 ) + { + sunrise = QTime(); + sunset = QTime(); + sunState = SunTimelineWidget::AllNight; + ui->sunRiseLabel->setText(tr("N/A")); + ui->sunSetLabel->setText(tr("N/A")); + return; + } + + if ( cosHourAngle < -1.0 ) + { + sunrise = QTime(); + sunset = QTime(); + sunState = SunTimelineWidget::AllDay; + ui->sunRiseLabel->setText(tr("N/A")); + ui->sunSetLabel->setText(tr("N/A")); + return; + } + + double w = acos(qBound(-1.0, cosHourAngle, 1.0)); double Jrise = Jt - w / (2*M_PI) + 0.5; double Jset = Jt + w / (2*M_PI) + 0.5; - sunrise = QTime::fromMSecsSinceStartOfDay(static_cast(fmod(Jrise, 1.0) * MSECS_PER_DAY)); - sunset = QTime::fromMSecsSinceStartOfDay(static_cast(fmod(Jset, 1.0) * MSECS_PER_DAY)); + sunrise = timeFromJulianDayFraction(Jrise); + sunset = timeFromJulianDayFraction(Jset); + sunState = SunTimelineWidget::NormalSunTimes; ui->sunRiseLabel->setText(locale.toString(sunrise, locale.formatTimeShort())); ui->sunSetLabel->setText(locale.toString(sunset, locale.formatTimeShort())); @@ -102,41 +241,31 @@ void ClockWidget::updateSun() { ui->sunRiseLabel->setText(tr("N/A")); ui->sunSetLabel->setText(tr("N/A")); + sunrise = QTime(); + sunset = QTime(); + sunState = SunTimelineWidget::NoSunTimes; } } -void ClockWidget::updateSunGraph() +QTime ClockWidget::timeFromJulianDayFraction(double julianDay) { FCT_IDENTIFICATION; - QColor dayColor(255, 253, 59); - QColor nightColor(33, 150, 243); - QColor currentColor(229, 57, 53); + if ( !qIsFinite(julianDay) ) + return QTime(); - qreal width = sunScene->width(); + const double fraction = julianDay - qFloor(julianDay); + qint64 msecs = qRound64(fraction * MSECS_PER_DAY); + msecs %= MSECS_PER_DAY_INT; - double rise = sunrise.msecsSinceStartOfDay() / MSECS_PER_DAY * width; - double set = sunset.msecsSinceStartOfDay() / MSECS_PER_DAY * width; - double cur = QDateTime::currentDateTimeUtc().time().msecsSinceStartOfDay() / MSECS_PER_DAY * width; - - sunScene->clear(); + return QTime::fromMSecsSinceStartOfDay(static_cast(msecs)); +} - if ( set > rise ) - { - sunScene->addRect(0, 0, rise, 10, QPen(Qt::NoPen), QBrush(nightColor, Qt::SolidPattern)); - sunScene->addRect(rise, 0, set-rise, 10, QPen(Qt::NoPen), QBrush(dayColor, Qt::SolidPattern)); - sunScene->addRect(set, 0, width-set, 10, QPen(Qt::NoPen), QBrush(nightColor, Qt::SolidPattern)); - } - else - { - sunScene->addRect(0, 0, set, 10, QPen(Qt::NoPen), QBrush(dayColor, Qt::SolidPattern)); - sunScene->addRect(set, 0, rise-set, 10, QPen(Qt::NoPen), QBrush(nightColor, Qt::SolidPattern)); - sunScene->addRect(rise, 0, width-rise, 10, QPen(Qt::NoPen), QBrush(dayColor, Qt::SolidPattern)); - } +void ClockWidget::updateSunGraph() +{ + FCT_IDENTIFICATION; - QPen currentPen(currentColor); - currentPen.setWidthF(2.0); - sunScene->addLine(cur, 0, cur, 10, currentPen); + ui->sunTimelineWidget->setSunTimes(sunrise, sunset, sunState); } ClockWidget::~ClockWidget() diff --git a/ui/ClockWidget.h b/ui/ClockWidget.h index d1971f07..540e2d56 100644 --- a/ui/ClockWidget.h +++ b/ui/ClockWidget.h @@ -3,14 +3,42 @@ #include #include -#include "core/LogLocale.h" +#include #include +#include "core/LogLocale.h" + namespace Ui { class ClockWidget; } class QGraphicsScene; +class QTimer; + +class SunTimelineWidget : public QWidget +{ +public: + enum SunState + { + NoSunTimes, + NormalSunTimes, + AllDay, + AllNight + }; + + explicit SunTimelineWidget(QWidget *parent = nullptr); + QSize sizeHint() const override; + QSize minimumSizeHint() const override; + void setSunTimes(const QTime &sunrise, const QTime &sunset, SunState state); + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + QTime sunrise; + QTime sunset; + SunState sunState; +}; class ClockWidget : public QWidget { @@ -26,13 +54,17 @@ public slots: void updateSunGraph(); private: + static QTime timeFromJulianDayFraction(double julianDay); + void scheduleClockUpdate(); + Ui::ClockWidget *ui; - QScopedPointer sunScene; + QTimer *timer; QScopedPointer clockScene; QScopedPointer clockItem; LogLocale locale; QTime sunrise; QTime sunset; + SunTimelineWidget::SunState sunState; }; #endif // QLOG_UI_CLOCKWIDGET_H diff --git a/ui/ClockWidget.ui b/ui/ClockWidget.ui index c3a6503c..477c4798 100644 --- a/ui/ClockWidget.ui +++ b/ui/ClockWidget.ui @@ -69,41 +69,23 @@ - - + + + + + 0 + 18 + + - 16777215 + 200 20 Qt::NoFocus - - background-color: transparent; - - - QFrame::NoFrame - - - QFrame::Plain - - - 0 - - - Qt::ScrollBarAlwaysOff - - - Qt::ScrollBarAlwaysOff - - - false - - - QPainter::TextAntialiasing - @@ -324,6 +306,13 @@ + + + SunTimelineWidget + QWidget +
ui/ClockWidget.h
+
+
diff --git a/ui/ColumnSettingDialog.cpp b/ui/ColumnSettingDialog.cpp index 48bbe09d..a58ba203 100644 --- a/ui/ColumnSettingDialog.cpp +++ b/ui/ColumnSettingDialog.cpp @@ -1,4 +1,5 @@ #include +#include #include #include "ColumnSettingDialog.h" #include "ui_ColumnSettingDialog.h" @@ -77,8 +78,13 @@ void ColumnSettingDialog::setupDialog() connect(columnCheckbox, &QCheckBox::stateChanged, this, [columnIndex, this](int state) #endif { - emit columnChanged(columnIndex, state); - if ( table ) table->setColumnHidden(columnIndex, !table->isColumnHidden(columnIndex)); + const bool columnVisible = state == Qt::Checked; + + emit columnChanged(columnIndex, columnVisible); + + // The checkbox is the source of truth. Toggling the current table + // state can get out of sync after restoring an older header state. + setColumnVisible(columnIndex, columnVisible); }); switch ( columnIndex ) @@ -265,6 +271,17 @@ ColumnSettingDialog::~ColumnSettingDialog() delete ui; } +void ColumnSettingDialog::setColumnVisible(int columnIndex, bool visible) +{ + if ( !table ) + return; + + table->setColumnHidden(columnIndex, !visible); + + if ( visible && table->columnWidth(columnIndex) == 0 ) + table->setColumnWidth(columnIndex, table->horizontalHeader()->defaultSectionSize()); +} + ColumnSettingGenericDialog::ColumnSettingGenericDialog(const QAbstractItemModel *model, QWidget *parent) : QDialog(parent), @@ -368,13 +385,18 @@ ColumnSettingSimpleDialog::ColumnSettingSimpleDialog(QTableView *table, QWidget columnCheckbox->setText(columnNameString); #if (QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)) - connect(columnCheckbox, &QCheckBox::checkStateChanged, this, [columnIndex, table, this](Qt::CheckState state) + connect(columnCheckbox, &QCheckBox::checkStateChanged, this, [columnIndex, this](Qt::CheckState state) #else - connect(columnCheckbox, &QCheckBox::stateChanged, this, [columnIndex, table, this](int state) + connect(columnCheckbox, &QCheckBox::stateChanged, this, [columnIndex, this](int state) #endif { - emit columnChanged(columnIndex, state); - if ( table) table->setColumnHidden(columnIndex, !table->isColumnHidden(columnIndex)); + const bool columnVisible = state == Qt::Checked; + + emit columnChanged(columnIndex, columnVisible); + + // Keep the simple dialog deterministic as well: checked means + // visible, unchecked means hidden. + setColumnVisible(columnIndex, columnVisible); }); checkboxList.append(columnCheckbox); @@ -390,3 +412,14 @@ ColumnSettingSimpleDialog::~ColumnSettingSimpleDialog() FCT_IDENTIFICATION; delete ui; } + +void ColumnSettingSimpleDialog::setColumnVisible(int columnIndex, bool visible) +{ + if ( !table ) + return; + + table->setColumnHidden(columnIndex, !visible); + + if ( visible && table->columnWidth(columnIndex) == 0 ) + table->setColumnWidth(columnIndex, table->horizontalHeader()->defaultSectionSize()); +} diff --git a/ui/ColumnSettingDialog.h b/ui/ColumnSettingDialog.h index 1bead3ce..a0f7dded 100644 --- a/ui/ColumnSettingDialog.h +++ b/ui/ColumnSettingDialog.h @@ -42,6 +42,7 @@ class ColumnSettingSimpleDialog : public ColumnSettingGenericDialog ~ColumnSettingSimpleDialog(); private: + void setColumnVisible(int columnIndex, bool visible); Ui::ColumnSettingSimpleDialog *ui; QTableView *table; }; @@ -62,6 +63,7 @@ class ColumnSettingDialog : public ColumnSettingGenericDialog private: void setupDialog(); + void setColumnVisible(int columnIndex, bool visible); Ui::ColumnSettingDialog *ui; QTableView *table; QSet defaultColumnsState; diff --git a/ui/DxWidget.cpp b/ui/DxWidget.cpp index 26b95935..34dcfe3b 100644 --- a/ui/DxWidget.cpp +++ b/ui/DxWidget.cpp @@ -178,6 +178,12 @@ QVariant DxTableModel::data(const QModelIndex& index, int role) const { return Data::statusToColor(spot.status, spot.dupeCount, QColor(Qt::transparent)); } + else if (index.column() == 1 && role == Qt::ForegroundRole) + { + return Data::textColorForBackground(Data::statusToColor(spot.status, + spot.dupeCount, + QColor(Qt::transparent))); + } else if (index.column() == 1 && role == Qt::ToolTipRole) { return QCoreApplication::translate("DBStrings", spot.dxcc.country.toUtf8().constData()) + " [" + Data::statusToText(spot.status) + "]"; @@ -245,6 +251,16 @@ void DxTableModel::clear() endResetModel(); } +void DxTableModel::refreshStatusColors() +{ + if ( dxData.isEmpty() ) + return; + + emit dataChanged(createIndex(0, 1), + createIndex(dxData.size() - 1, 1), + {Qt::BackgroundRole, Qt::ForegroundRole}); +} + int WCYTableModel::rowCount(const QModelIndex&) const { return wcyData.count(); @@ -1373,6 +1389,13 @@ void DxWidget::reloadSetting() ui->filteredLabel->setHidden(!isFilterEnabled()); } +void DxWidget::refreshStatusColors() +{ + FCT_IDENTIFICATION; + + dxTableModel->refreshStatusColors(); +} + void DxWidget::prepareQSOSpot(QSqlRecord qso) { FCT_IDENTIFICATION; diff --git a/ui/DxWidget.h b/ui/DxWidget.h index 96708989..718a547a 100644 --- a/ui/DxWidget.h +++ b/ui/DxWidget.h @@ -44,6 +44,7 @@ class DxTableModel : public QAbstractTableModel { double dedup_freq_tolerance = DEDUPLICATION_FREQ_TOLERANCE); const DxSpot getSpot(const QModelIndex& index) const {return dxData.at(index.row());}; void clear(); + void refreshStatusColors(); private: QList dxData; @@ -141,6 +142,7 @@ public slots: void serverSelectChanged(int); void setLastQSO(QSqlRecord); void reloadSetting(); + void refreshStatusColors(); void prepareQSOSpot(QSqlRecord); void setSearch(const QString &); void setSearchStatus(bool); diff --git a/ui/EmailQSLDialog.cpp b/ui/EmailQSLDialog.cpp new file mode 100644 index 00000000..3eb4fe98 --- /dev/null +++ b/ui/EmailQSLDialog.cpp @@ -0,0 +1,234 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "EmailQSLDialog.h" +#include "ui_EmailQSLDialog.h" +#include "core/debug.h" + +MODULE_IDENTIFICATION("qlog.ui.emailqsldialog"); + +EmailQSLDialog::EmailQSLDialog(const QSqlRecord &record, QWidget *parent) + : QDialog(parent), + ui(new Ui::EmailQSLDialog), + m_record(record), + m_service(new EmailQSLService(this)) +{ + FCT_IDENTIFICATION; + + ui->setupUi(this); + + // Add action buttons next to Cancel + QPushButton *previewBtn = ui->buttonBox->addButton(tr("Preview Card…"), QDialogButtonBox::ActionRole); + connect(previewBtn, &QPushButton::clicked, this, &EmailQSLDialog::previewAndPrintCard); + + QPushButton *sendBtn = ui->buttonBox->addButton(tr("Send"), QDialogButtonBox::AcceptRole); + connect(sendBtn, &QPushButton::clicked, this, &EmailQSLDialog::sendClicked); + connect(m_service, &EmailQSLService::sendFinished, + this, &EmailQSLDialog::onSendFinished); + + populateInfo(); + buildWarnings(); +} + +EmailQSLDialog::~EmailQSLDialog() +{ + delete ui; +} + +void EmailQSLDialog::populateInfo() +{ + FCT_IDENTIFICATION; + + const QString callsign = m_record.value(QStringLiteral("callsign")).toString().toUpper(); + const QString email = m_record.value(QStringLiteral("email")).toString(); + const QDateTime dt = m_record.value(QStringLiteral("start_time")).toDateTime().toUTC(); + const QString band = m_record.value(QStringLiteral("band")).toString().toUpper(); + const QString mode = m_record.value(QStringLiteral("mode")).toString().toUpper(); + + ui->callValueLabel->setText(callsign.isEmpty() ? tr("(unknown)") : callsign); + ui->emailValueLabel->setText(email.isEmpty() + ? tr("No email address on record") + : email); + ui->dateValueLabel->setText(dt.isValid() ? dt.toString(Qt::RFC2822Date) : tr("(unknown)")); + ui->bandModeValueLabel->setText( + QString("%1 / %2").arg(band.isEmpty() ? QStringLiteral("?") : band, + mode.isEmpty() ? QStringLiteral("?") : mode)); + + // Subject / body previews + ui->subjectPreviewLabel->setText( + EmailQSLBase::applyMergeFields(EmailQSLBase::getSubjectTemplate(), m_record)); + ui->bodyPreviewEdit->setPlainText( + EmailQSLBase::applyMergeFields(EmailQSLBase::getBodyTemplate(), m_record)); + + // Card thumbnail + const QPixmap card = EmailQSLBase::renderCard(m_record); + if (!card.isNull()) + ui->cardPreviewLabel->setPixmap(card.scaled( + ui->cardPreviewLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); + else + ui->cardPreviewLabel->setText(tr("No card image")); +} + +void EmailQSLDialog::buildWarnings() +{ + FCT_IDENTIFICATION; + + QStringList warnings; + + // 1) No email address + const QString emailAddr = m_record.value(QStringLiteral("email")).toString().trimmed(); + if (emailAddr.isEmpty()) + { + warnings << tr("This contact has no email address — the message cannot be sent."); + // Disable the Send button + for (QAbstractButton *btn : ui->buttonBox->buttons()) + if (ui->buttonBox->buttonRole(btn) == QDialogButtonBox::AcceptRole) + btn->setEnabled(false); + } + + // 2) Already sent for this specific QSO + const QDateTime prevSent = EmailQSLBase::getEmailSentDateTime(m_record); + if (prevSent.isValid()) + { + warnings << tr("An Email QSL was already sent for this QSO on %1 UTC.") + .arg(prevSent.toString(Qt::RFC2822Date)); + } + + // 3) Previously sent to the same callsign (different QSO) + const QString callsign = m_record.value(QStringLiteral("callsign")).toString().toUpper(); + const int id = m_record.value(QStringLiteral("id")).toInt(); + if (!callsign.isEmpty() && EmailQSLBase::hasEmailBeenSentToCallsign(callsign, id)) + { + warnings << tr("Note: you have previously sent an Email QSL to %1 for a different QSO.") + .arg(callsign); + } + + // 4) No SMTP host configured + if (EmailQSLBase::getSmtpHost().isEmpty()) + warnings << tr("SMTP server is not configured. Go to Settings → Email QSL."); + + if (!warnings.isEmpty()) + ui->warningLabel->setText(warnings.join(QStringLiteral("\n"))); +} + +void EmailQSLDialog::sendClicked() +{ + FCT_IDENTIFICATION; + + // Disable buttons while sending + for (QAbstractButton *btn : ui->buttonBox->buttons()) + btn->setEnabled(false); + + ui->statusLabel->setText(tr("Sending…")); + + m_service->sendEmailQSL(m_record); +} + +void EmailQSLDialog::onSendFinished(bool success, const QString &message) +{ + FCT_IDENTIFICATION; + + if (success) + { + // Record the sent timestamp in contacts.fields + const int id = m_record.value(QStringLiteral("id")).toInt(); + EmailQSLBase::recordEmailSent(id, m_record); + + // Auto-close — the log table refresh is handled by the finished() signal + accept(); + } + else + { + ui->statusLabel->setStyleSheet(QStringLiteral("color: red; font-weight: bold;")); + ui->statusLabel->setText(tr("Send failed: %1").arg(message)); + + // Rename Cancel → Close and disable Send so the user can only dismiss + for (QAbstractButton *btn : ui->buttonBox->buttons()) + { + const QDialogButtonBox::ButtonRole role = ui->buttonBox->buttonRole(btn); + if (role == QDialogButtonBox::RejectRole) + { + btn->setText(tr("Close")); + btn->setEnabled(true); + } + else + { + btn->setEnabled(false); // keep Send / Preview disabled + } + } + } +} + +void EmailQSLDialog::previewAndPrintCard() +{ + FCT_IDENTIFICATION; + + const QPixmap card = EmailQSLBase::renderCard(m_record); + if (card.isNull()) + { + QMessageBox::warning(this, tr("Preview"), + tr("Could not render the card image.\n" + "Please check Settings → Email QSL and make sure a card image is selected.")); + return; + } + + QDialog *dlg = new QDialog(this); + dlg->setAttribute(Qt::WA_DeleteOnClose); + dlg->setWindowTitle(tr("Card Preview — %1") + .arg(m_record.value(QStringLiteral("callsign")).toString().toUpper())); + QVBoxLayout *lay = new QVBoxLayout(dlg); + + QScrollArea *scroll = new QScrollArea(dlg); + QLabel *lbl = new QLabel(scroll); + const QPixmap scaled = card.scaled(QSize(800, 600), Qt::KeepAspectRatio, Qt::SmoothTransformation); + lbl->setPixmap(scaled); + lbl->adjustSize(); + scroll->setWidget(lbl); + scroll->setMinimumSize(qMin(scaled.width() + 20, 820), + qMin(scaled.height() + 20, 620)); + lay->addWidget(scroll); + + QDialogButtonBox *bb = new QDialogButtonBox(QDialogButtonBox::Close, dlg); + QPushButton *saveBtn = bb->addButton(tr("Save Card…"), QDialogButtonBox::ActionRole); + + connect(saveBtn, &QPushButton::clicked, dlg, [card, this]() + { + const QString callsign = m_record.value(QStringLiteral("callsign")).toString().toUpper(); + const QString rawDate = m_record.value(QStringLiteral("start_time")).toString(); + const QDateTime dt = QDateTime::fromString(rawDate, Qt::ISODate); + const QString date = dt.isValid() ? dt.toUTC().toString(QStringLiteral("yyyyMMdd")) : QStringLiteral("date"); + const QString time = dt.isValid() ? dt.toUTC().toString(QStringLiteral("HHmm")) : QStringLiteral("time"); + const QString band = m_record.value(QStringLiteral("band")).toString().toUpper().replace(QLatin1Char(' '), QLatin1Char('_')); + const QString mode = m_record.value(QStringLiteral("mode")).toString().toUpper(); + const QString defaultName = QStringLiteral("QSL_%1_%2_%3_%4_%5.png") + .arg(callsign, date, time, band, mode); + const QString defaultDir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); + const QString path = QFileDialog::getSaveFileName( + this, + tr("Save QSL Card"), + defaultDir + QStringLiteral("/") + defaultName, + tr("PNG Image (*.png);;JPEG Image (*.jpg *.jpeg)")); + + if (path.isEmpty()) + return; + + if (!card.save(path)) + { + QMessageBox::warning(this, tr("Save Failed"), + tr("Could not save the card image to:\n%1").arg(path)); + } + }); + + connect(bb, &QDialogButtonBox::rejected, dlg, &QDialog::close); + lay->addWidget(bb); + dlg->show(); // non-modal +} diff --git a/ui/EmailQSLDialog.h b/ui/EmailQSLDialog.h new file mode 100644 index 00000000..c2a609bb --- /dev/null +++ b/ui/EmailQSLDialog.h @@ -0,0 +1,37 @@ +#ifndef QLOG_UI_EMAILQSLDIALOG_H +#define QLOG_UI_EMAILQSLDIALOG_H + +#include +#include + +#include "service/emailqsl/EmailQSLService.h" + +namespace Ui { +class EmailQSLDialog; +} + +// Shown before sending an Email QSL. Displays contact details, the rendered +// card thumbnail, warnings about duplicate sends, and lets the user confirm. +class EmailQSLDialog : public QDialog +{ + Q_OBJECT + +public: + explicit EmailQSLDialog(const QSqlRecord &record, QWidget *parent = nullptr); + ~EmailQSLDialog(); + +private slots: + void sendClicked(); + void onSendFinished(bool success, const QString &message); + void previewAndPrintCard(); + +private: + void populateInfo(); + void buildWarnings(); + + Ui::EmailQSLDialog *ui; + QSqlRecord m_record; + EmailQSLService *m_service; +}; + +#endif // QLOG_UI_EMAILQSLDIALOG_H diff --git a/ui/EmailQSLDialog.ui b/ui/EmailQSLDialog.ui new file mode 100644 index 00000000..1489fbae --- /dev/null +++ b/ui/EmailQSLDialog.ui @@ -0,0 +1,135 @@ + + + EmailQSLDialog + + + 00520500 + + + Send Email QSL Card + + + + + + + + + 200130 + 260170 + QFrame::Box + Qt::AlignCenter + No card image + true + + + + + + QSO Details + + + Callsign: + + + + + + To address: + + + + + true + + + + Date / Time: + + + + + + Band / Mode: + + + + + + + + + + + + + + + true + + QLabel { color: #cc6600; font-weight: bold; } + + + + + + + + Subject + + + + + true + + + + + + + + + + Email Body (preview) + + + + true + 0100 + + + + + + + + + + + Qt::AlignCenter + + + + + + + + QDialogButtonBox::Cancel + + + + + + + + + + buttonBoxrejected() + EmailQSLDialogreject() + + 260480 + 260240 + + + + diff --git a/ui/EmailQSLSettingsWidget.cpp b/ui/EmailQSLSettingsWidget.cpp new file mode 100644 index 00000000..0163da9a --- /dev/null +++ b/ui/EmailQSLSettingsWidget.cpp @@ -0,0 +1,1051 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "EmailQSLSettingsWidget.h" +#include "ui_EmailQSLSettingsWidget.h" +#include "service/emailqsl/EmailQSLService.h" +#include "ui/component/CardEditorWidget.h" +#include "core/debug.h" + +MODULE_IDENTIFICATION("qlog.ui.emailqslsettingswidget"); + +// --------------------------------------------------------------------------- +// Column indices for the overlay table +// TEXT columns: TYPE FIELD X Y FONT SIZE COLOR BOLD ITALIC (BOX cols greyed) +// BOX columns: TYPE FIELD X Y W H FILL OPACITY RADIUS BORDER +// We use one wide model and show/grey non-applicable cells per row. +// --------------------------------------------------------------------------- +enum OverlayCol +{ + OC_TYPE = 0, + OC_FIELD = 1, // TEXT: merge key; BOX: optional caption + OC_X = 2, + OC_Y = 3, + OC_FONT = 4, // TEXT only + OC_SIZE = 5, // TEXT only (also used for BOX caption font size) + OC_COLOR = 6, // TEXT: text color; BOX: border color + OC_BOLD = 7, // TEXT only + OC_ITALIC = 8, // TEXT only + OC_W = 9, // BOX only + OC_H = 10, // BOX only + OC_FILL = 11, // BOX only + OC_OPACITY = 12, // BOX only + OC_RADIUS = 13, // BOX only + OC_COUNT = 14 +}; + +// --------------------------------------------------------------------------- +// Local delegate: merge-field combobox for the Field column +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// Delegate: Type combo (TEXT / BOX) +// --------------------------------------------------------------------------- +class TypeDelegate : public QStyledItemDelegate +{ +public: + using QStyledItemDelegate::QStyledItemDelegate; + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &, + const QModelIndex &) const override + { + QComboBox *cb = new QComboBox(parent); + cb->addItem(QStringLiteral("TEXT")); + cb->addItem(QStringLiteral("BOX")); + cb->addItem(QStringLiteral("LABEL")); + return cb; + } + void setEditorData(QWidget *editor, const QModelIndex &index) const override + { + QComboBox *cb = static_cast(editor); + cb->setCurrentIndex(cb->findText(index.data(Qt::DisplayRole).toString())); + } + void setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const override + { + model->setData(index, static_cast(editor)->currentText(), Qt::EditRole); + } + void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, + const QModelIndex &) const override + { editor->setGeometry(option.rect); } +}; + +// --------------------------------------------------------------------------- +// Delegate: merge-field combobox (TEXT Field column) +// --------------------------------------------------------------------------- +class MergeFieldDelegate : public QStyledItemDelegate +{ +public: + using QStyledItemDelegate::QStyledItemDelegate; + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &, + const QModelIndex &) const override + { + QComboBox *cb = new QComboBox(parent); + cb->setEditable(true); // also allow free text (for BOX captions) + for (const EmailQSLBase::MergeField &f : EmailQSLBase::availableMergeFields()) + cb->addItem(f.key); + return cb; + } + void setEditorData(QWidget *editor, const QModelIndex &index) const override + { + QComboBox *cb = static_cast(editor); + const QString v = index.data(Qt::DisplayRole).toString(); + const int idx = cb->findText(v); + if (idx >= 0) cb->setCurrentIndex(idx); + else cb->setEditText(v); + } + void setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const override + { + model->setData(index, static_cast(editor)->currentText(), Qt::EditRole); + } + void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, + const QModelIndex &) const override + { editor->setGeometry(option.rect); } +}; + +// --------------------------------------------------------------------------- +// Delegate: QFontComboBox for the Font column +// --------------------------------------------------------------------------- +class FontFamilyDelegate : public QStyledItemDelegate +{ +public: + using QStyledItemDelegate::QStyledItemDelegate; + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &, + const QModelIndex &) const override + { return new QFontComboBox(parent); } + void setEditorData(QWidget *editor, const QModelIndex &index) const override + { + static_cast(editor)->setCurrentFont( + QFont(index.data(Qt::DisplayRole).toString())); + } + void setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const override + { + model->setData(index, + static_cast(editor)->currentFont().family(), Qt::EditRole); + } + void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, + const QModelIndex &) const override + { editor->setGeometry(option.rect); } +}; + +// --------------------------------------------------------------------------- +// Delegate: colored swatch — double-click opens QColorDialog +// --------------------------------------------------------------------------- +class ColorSwatchDelegate : public QStyledItemDelegate +{ +public: + using QStyledItemDelegate::QStyledItemDelegate; + + void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override + { + const QString hex = index.data(Qt::DisplayRole).toString(); + const QColor c(hex.isEmpty() ? QStringLiteral("#000000") : hex); + const QRect r = option.rect.adjusted(2, 2, -2, -2); + painter->fillRect(r, c); + painter->setPen(QPen(Qt::gray, 1)); + painter->drawRect(r); + painter->setPen(c.lightness() > 128 ? Qt::black : Qt::white); + QFont f = painter->font(); f.setPointSize(8); painter->setFont(f); + painter->drawText(option.rect, Qt::AlignCenter, hex); + } + + QWidget *createEditor(QWidget *, const QStyleOptionViewItem &, + const QModelIndex &) const override { return nullptr; } + + bool editorEvent(QEvent *event, QAbstractItemModel *model, + const QStyleOptionViewItem &, const QModelIndex &index) override + { + if (event->type() != QEvent::MouseButtonDblClick) return false; + const QString cur = index.data(Qt::DisplayRole).toString(); + const QColor chosen = QColorDialog::getColor( + QColor(cur.isEmpty() ? QStringLiteral("#000000") : cur), + nullptr, tr("Choose Color")); + if (chosen.isValid()) + model->setData(index, chosen.name(), Qt::EditRole); + return true; + } +}; + +// --------------------------------------------------------------------------- +// Delegate: compact QSpinBox — editor width constrained to cell +// --------------------------------------------------------------------------- +class SpinBoxDelegate : public QStyledItemDelegate +{ + int m_min, m_max; +public: + SpinBoxDelegate(int min, int max, QObject *parent) + : QStyledItemDelegate(parent), m_min(min), m_max(max) {} + + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, + const QModelIndex &) const override + { + QSpinBox *sb = new QSpinBox(parent); + sb->setRange(m_min, m_max); + // Constrain width so the spinbox doesn't expand beyond the cell + sb->setFixedWidth(option.rect.width()); + sb->setButtonSymbols(QAbstractSpinBox::UpDownArrows); + return sb; + } + void setEditorData(QWidget *editor, const QModelIndex &index) const override + { + static_cast(editor)->setValue(index.data(Qt::DisplayRole).toInt()); + } + void setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const override + { + model->setData(index, static_cast(editor)->value(), Qt::EditRole); + } + void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, + const QModelIndex &) const override + { editor->setGeometry(option.rect); } +}; + +// --------------------------------------------------------------------------- +// SMTP presets +// --------------------------------------------------------------------------- +struct SmtpPreset +{ + QString label; + QString host; + int port; + int encryption; + QString hint; +}; + +static QList smtpPresets() +{ + return { + { QObject::tr("-- Select preset --"), {}, 587, 2, {} }, + { QStringLiteral("Gmail"), + QStringLiteral("smtp.gmail.com"), 587, 2, + QObject::tr("Gmail: go to myaccount.google.com → Security → 2-Step Verification → App Passwords.\n" + "Create an App Password for 'Mail'. Use the 16-character code as your password here.") }, + { QStringLiteral("Gmail (SSL/TLS port 465)"), + QStringLiteral("smtp.gmail.com"), 465, 1, + QObject::tr("Gmail SSL variant (port 465). Use your Gmail App Password.") }, + { QStringLiteral("Outlook / Hotmail / Microsoft 365"), + QStringLiteral("smtp-mail.outlook.com"), 587, 2, + QObject::tr("Outlook: use your full Microsoft email and password.\n" + "If MFA is on, generate an App Password at account.microsoft.com → Security.") }, + { QStringLiteral("Yahoo Mail"), + QStringLiteral("smtp.mail.yahoo.com"), 587, 2, + QObject::tr("Yahoo: go to Account Security and click 'Generate app password'.") }, + { QStringLiteral("iCloud Mail"), + QStringLiteral("smtp.mail.me.com"), 587, 2, + QObject::tr("iCloud: generate an App-Specific Password at appleid.apple.com → Sign-In and Security.") }, + { QStringLiteral("Zoho Mail"), + QStringLiteral("smtp.zoho.com"), 587, 2, + QObject::tr("Zoho: enable SMTP in Zoho Mail Settings → Mail Accounts → IMAP/POP/SMTP.") }, + { QStringLiteral("Custom"), {}, 587, 2, {} }, + }; +} + +// --------------------------------------------------------------------------- +// Helper — build a dummy QSqlRecord for previewing merge fields +// --------------------------------------------------------------------------- +static QSqlRecord buildDummyRecord() +{ + static const QStringList cols = { + "callsign","start_time","freq","band","mode","submode", + "rst_sent","rst_rcvd","name_intl","name","qth_intl","qth", + "country_intl","country","gridsquare","dxcc","cqz","ituz", + "tx_pwr","email","station_callsign","my_gridsquare","operator", + "comment_intl","comment","sota_ref","pota_ref","wwff_ref", + "iota","sig_intl","contest_id" + }; + QSqlRecord r; + for (const QString &c : cols) + { + QSqlField f(c, QVariant::String); + r.append(f); + } + r.setValue("callsign", QStringLiteral("W1AW")); + r.setValue("start_time", QDateTime::currentDateTimeUtc().toString(Qt::ISODate)); + r.setValue("freq", QStringLiteral("14.225")); + r.setValue("band", QStringLiteral("20M")); + r.setValue("mode", QStringLiteral("SSB")); + r.setValue("rst_sent", QStringLiteral("59")); + r.setValue("rst_rcvd", QStringLiteral("59")); + r.setValue("name_intl", QStringLiteral("ARRL HQ")); + r.setValue("station_callsign", QStringLiteral("AA5SH")); + r.setValue("my_gridsquare", QStringLiteral("EM20")); + r.setValue("gridsquare", QStringLiteral("FN31")); + r.setValue("country_intl", QStringLiteral("United States")); + return r; +} + +// --------------------------------------------------------------------------- +// Helper — build a default overlay +// --------------------------------------------------------------------------- +static EmailQSLFieldOverlay makeOverlay(const QString &field, + int x, int y, + int fontSize, + bool bold, + const QString &color = QStringLiteral("#000000"), + const QString &font = QStringLiteral("Arial")) +{ + EmailQSLFieldOverlay ov; + ov.fieldName = field; + ov.x = x; + ov.y = y; + ov.fontSize = fontSize; + ov.bold = bold; + ov.fontFamily = font; + ov.color = color; + return ov; +} + +// --------------------------------------------------------------------------- +// EmailQSLSettingsWidget +// --------------------------------------------------------------------------- + +EmailQSLSettingsWidget::EmailQSLSettingsWidget(QWidget *parent) + : QWidget(parent), + ui(new Ui::EmailQSLSettingsWidget), + m_overlayModel(new QStandardItemModel(0, OC_COUNT, this)), + m_testService(new EmailQSLService(this)), + m_syncing(false) +{ + FCT_IDENTIFICATION; + + ui->setupUi(this); + + // ---- Overlay table ---- + m_overlayModel->setHorizontalHeaderLabels({ + tr("Type"), tr("Field / Caption"), + tr("X"), tr("Y"), + tr("Font"), tr("Pt"), + tr("Color"), tr("B"), tr("I"), + tr("W"), tr("H"), tr("Fill"), tr("Opacity%"), tr("Radius") + }); + ui->overlayTableView->setModel(m_overlayModel); + + // Delegates + auto *spinCoord = new SpinBoxDelegate(0, 99999, this); + auto *spinSz = new SpinBoxDelegate(4, 300, this); + auto *spinDim = new SpinBoxDelegate(1, 99999, this); + auto *spinOpac = new SpinBoxDelegate(0, 100, this); + auto *spinRadius = new SpinBoxDelegate(0, 500, this); + + ui->overlayTableView->setItemDelegateForColumn(OC_TYPE, new TypeDelegate(this)); + ui->overlayTableView->setItemDelegateForColumn(OC_FIELD, new MergeFieldDelegate(this)); + ui->overlayTableView->setItemDelegateForColumn(OC_X, spinCoord); + ui->overlayTableView->setItemDelegateForColumn(OC_Y, spinCoord); + ui->overlayTableView->setItemDelegateForColumn(OC_FONT, new FontFamilyDelegate(this)); + ui->overlayTableView->setItemDelegateForColumn(OC_SIZE, spinSz); + ui->overlayTableView->setItemDelegateForColumn(OC_COLOR, new ColorSwatchDelegate(this)); + ui->overlayTableView->setItemDelegateForColumn(OC_W, spinDim); + ui->overlayTableView->setItemDelegateForColumn(OC_H, spinDim); + ui->overlayTableView->setItemDelegateForColumn(OC_FILL, new ColorSwatchDelegate(this)); + ui->overlayTableView->setItemDelegateForColumn(OC_OPACITY, spinOpac); + ui->overlayTableView->setItemDelegateForColumn(OC_RADIUS, spinRadius); + + // Column widths — fixed-width numeric columns stay compact + auto *hdr = ui->overlayTableView->horizontalHeader(); + hdr->setSectionResizeMode(OC_TYPE, QHeaderView::ResizeToContents); + hdr->setSectionResizeMode(OC_FIELD, QHeaderView::Stretch); + hdr->setSectionResizeMode(OC_X, QHeaderView::Fixed); + hdr->setSectionResizeMode(OC_Y, QHeaderView::Fixed); + hdr->setSectionResizeMode(OC_FONT, QHeaderView::Stretch); + hdr->setSectionResizeMode(OC_SIZE, QHeaderView::Fixed); + hdr->setSectionResizeMode(OC_COLOR, QHeaderView::Fixed); + hdr->setSectionResizeMode(OC_BOLD, QHeaderView::Fixed); + hdr->setSectionResizeMode(OC_ITALIC, QHeaderView::Fixed); + hdr->setSectionResizeMode(OC_W, QHeaderView::Fixed); + hdr->setSectionResizeMode(OC_H, QHeaderView::Fixed); + hdr->setSectionResizeMode(OC_FILL, QHeaderView::Fixed); + hdr->setSectionResizeMode(OC_OPACITY, QHeaderView::Fixed); + hdr->setSectionResizeMode(OC_RADIUS, QHeaderView::Fixed); + // Set pixel widths for fixed columns + ui->overlayTableView->setColumnWidth(OC_X, 55); + ui->overlayTableView->setColumnWidth(OC_Y, 55); + ui->overlayTableView->setColumnWidth(OC_SIZE, 40); + ui->overlayTableView->setColumnWidth(OC_COLOR, 60); + ui->overlayTableView->setColumnWidth(OC_BOLD, 24); + ui->overlayTableView->setColumnWidth(OC_ITALIC, 24); + ui->overlayTableView->setColumnWidth(OC_W, 55); + ui->overlayTableView->setColumnWidth(OC_H, 55); + ui->overlayTableView->setColumnWidth(OC_FILL, 60); + ui->overlayTableView->setColumnWidth(OC_OPACITY, 55); + ui->overlayTableView->setColumnWidth(OC_RADIUS, 50); + + // ---- Merge field reference table ---- + const auto fields = EmailQSLBase::availableMergeFields(); + ui->mergeFieldRefTable->setRowCount(fields.size()); + for (int i = 0; i < fields.size(); ++i) + { + ui->mergeFieldRefTable->setItem( + i, 0, new QTableWidgetItem(QLatin1Char('{') + fields[i].key + QLatin1Char('}'))); + ui->mergeFieldRefTable->setItem( + i, 1, new QTableWidgetItem(fields[i].description)); + } + ui->mergeFieldRefTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + ui->mergeFieldRefTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + + // ---- Merge field combos (subject/body insert) ---- + for (const EmailQSLBase::MergeField &f : fields) + { + const QString display = QLatin1Char('{') + f.key + QLatin1Char('}'); + ui->bodyFieldCombo->addItem(display); + ui->subjectFieldCombo->addItem(display); + } + + // ---- SMTP presets ---- + ui->smtpPresetCombo->blockSignals(true); + for (const SmtpPreset &p : smtpPresets()) + ui->smtpPresetCombo->addItem(p.label); + ui->smtpPresetCombo->blockSignals(false); + + // ---- Connections ---- + connect(ui->browseCardImageButton, &QPushButton::clicked, + this, &EmailQSLSettingsWidget::browseCardImage); + connect(ui->cardImagePathEdit, &QLineEdit::textChanged, + this, &EmailQSLSettingsWidget::onCardImageChanged); + + connect(ui->smtpPresetCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, &EmailQSLSettingsWidget::onSmtpPresetChanged); + connect(ui->testConnectionButton, &QPushButton::clicked, + this, &EmailQSLSettingsWidget::testConnection); + connect(m_testService, &EmailQSLService::testFinished, + this, &EmailQSLSettingsWidget::onTestFinished); + + connect(ui->addOverlayButton, &QPushButton::clicked, + this, &EmailQSLSettingsWidget::addOverlay); + connect(ui->addBoxButton, &QPushButton::clicked, + this, &EmailQSLSettingsWidget::addBox); + connect(ui->addLabelButton, &QPushButton::clicked, + this, &EmailQSLSettingsWidget::addLabel); + connect(ui->addDefaultFieldsButton, &QPushButton::clicked, + this, &EmailQSLSettingsWidget::addDefaultOverlays); + connect(ui->removeOverlayButton, &QPushButton::clicked, + this, &EmailQSLSettingsWidget::removeOverlay); + connect(ui->previewCardButton, &QPushButton::clicked, + this, &EmailQSLSettingsWidget::previewCard); + + connect(ui->insertBodyFieldButton, &QPushButton::clicked, + this, &EmailQSLSettingsWidget::insertMergeFieldBody); + connect(ui->insertSubjectFieldButton, &QPushButton::clicked, + this, &EmailQSLSettingsWidget::insertMergeFieldSubject); + + // Table ↔ editor bidirectional sync + connect(m_overlayModel, &QStandardItemModel::dataChanged, + this, &EmailQSLSettingsWidget::onTableDataChanged); + connect(m_overlayModel, &QStandardItemModel::rowsRemoved, + this, &EmailQSLSettingsWidget::onTableDataChanged); + + connect(ui->cardEditorWidget, &CardEditorWidget::overlayPositionChanged, + this, &EmailQSLSettingsWidget::onEditorPositionChanged); + connect(ui->cardEditorWidget, &CardEditorWidget::overlaySizeChanged, + this, &EmailQSLSettingsWidget::onEditorSizeChanged); + connect(ui->cardEditorWidget, &CardEditorWidget::overlaySelected, + this, &EmailQSLSettingsWidget::onEditorOverlaySelected); + + connect(ui->overlayTableView->selectionModel(), + &QItemSelectionModel::currentRowChanged, + this, [this](const QModelIndex ¤t, const QModelIndex &) { + ui->cardEditorWidget->setSelectedIndex( + current.isValid() ? current.row() : -1); + ui->removeOverlayButton->setEnabled(current.isValid()); + }); +} + +EmailQSLSettingsWidget::~EmailQSLSettingsWidget() +{ + delete ui; +} + +// --------------------------------------------------------------------------- +// readSettings / writeSettings +// --------------------------------------------------------------------------- + +void EmailQSLSettingsWidget::readSettings() +{ + FCT_IDENTIFICATION; + + ui->smtpHostEdit->setText(EmailQSLBase::getSmtpHost()); + ui->smtpPortSpin->setValue(EmailQSLBase::getSmtpPort()); + ui->encryptionCombo->setCurrentIndex(EmailQSLBase::getSmtpEncryption()); + ui->smtpUsernameEdit->setText(EmailQSLBase::getSmtpUsername()); + ui->smtpPasswordEdit->setText(EmailQSLBase::getSmtpPassword()); + ui->fromAddressEdit->setText(EmailQSLBase::getFromAddress()); + ui->fromNameEdit->setText(EmailQSLBase::getFromName()); + ui->subjectTemplateEdit->setText(EmailQSLBase::getSubjectTemplate()); + ui->bodyTemplateEdit->setPlainText(EmailQSLBase::getBodyTemplate()); + ui->cardImagePathEdit->setText(EmailQSLBase::getCardImagePath()); + + m_overlays = EmailQSLBase::getCardFieldOverlays(); + listToOverlayTable(); + + // Trigger image load into the editor (textChanged may not fire if value unchanged) + onCardImageChanged(ui->cardImagePathEdit->text()); +} + +void EmailQSLSettingsWidget::writeSettings() +{ + FCT_IDENTIFICATION; + + EmailQSLBase::setSmtpHost(ui->smtpHostEdit->text().trimmed()); + EmailQSLBase::setSmtpPort(ui->smtpPortSpin->value()); + EmailQSLBase::setSmtpEncryption(ui->encryptionCombo->currentIndex()); + EmailQSLBase::saveSmtpCredentials(ui->smtpUsernameEdit->text().trimmed(), + ui->smtpPasswordEdit->text()); + EmailQSLBase::setFromAddress(ui->fromAddressEdit->text().trimmed()); + EmailQSLBase::setFromName(ui->fromNameEdit->text().trimmed()); + EmailQSLBase::setSubjectTemplate(ui->subjectTemplateEdit->text()); + EmailQSLBase::setBodyTemplate(ui->bodyTemplateEdit->toPlainText()); + EmailQSLBase::setCardImagePath(ui->cardImagePathEdit->text().trimmed()); + + overlayTableToList(); + EmailQSLBase::setCardFieldOverlays(m_overlays); +} + +// --------------------------------------------------------------------------- +// Overlay table ↔ m_overlays ↔ card editor +// --------------------------------------------------------------------------- + +// Helper — make a greyed-out non-editable placeholder for N/A cells +static QStandardItem *naItem() +{ + QStandardItem *si = new QStandardItem(QStringLiteral("—")); + si->setFlags(si->flags() & ~Qt::ItemIsEditable); + si->setForeground(QColor(0xbb, 0xbb, 0xbb)); + si->setTextAlignment(Qt::AlignCenter); + return si; +} + +void EmailQSLSettingsWidget::listToOverlayTable() +{ + // Guard against re-entrant onTableDataChanged() firing during setRowCount(0) + // which would wipe m_overlays before we finish rebuilding the table. + m_syncing = true; + m_overlayModel->setRowCount(0); + for (const EmailQSLFieldOverlay &ov : m_overlays) + { + const bool isBox = (ov.type == QLatin1String("BOX")); + + auto item = [](const QString &text) { return new QStandardItem(text); }; + auto checkItem = [](bool checked) { + QStandardItem *si = new QStandardItem(); + si->setCheckable(true); + si->setCheckState(checked ? Qt::Checked : Qt::Unchecked); + si->setTextAlignment(Qt::AlignCenter); + return si; + }; + + QList row; + row << item(ov.type); // OC_TYPE + row << item(ov.fieldName); // OC_FIELD + row << item(QString::number(ov.x)); // OC_X + row << item(QString::number(ov.y)); // OC_Y + // TEXT-only + row << (isBox ? naItem() : item(ov.fontFamily)); // OC_FONT + row << (isBox && ov.fieldName.isEmpty() + ? naItem() : item(QString::number(ov.fontSize))); // OC_SIZE + row << item(ov.color); // OC_COLOR (border/text) + row << (isBox ? naItem() : checkItem(ov.bold)); // OC_BOLD + row << (isBox ? naItem() : checkItem(ov.italic)); // OC_ITALIC + // BOX-only + row << (isBox ? item(QString::number(ov.width)) : naItem()); // OC_W + row << (isBox ? item(QString::number(ov.height)) : naItem()); // OC_H + row << (isBox ? item(ov.fillColor) : naItem()); // OC_FILL + row << (isBox ? item(QString::number(ov.opacity)): naItem()); // OC_OPACITY + row << (isBox ? item(QString::number(ov.cornerRadius)) : naItem()); // OC_RADIUS + + m_overlayModel->appendRow(row); + } + m_syncing = false; + ui->cardEditorWidget->setOverlays(m_overlays); +} + +void EmailQSLSettingsWidget::overlayTableToList() +{ + m_overlays.clear(); + for (int r = 0; r < m_overlayModel->rowCount(); ++r) + { + EmailQSLFieldOverlay ov; + auto txt = [&](int col) { return m_overlayModel->item(r, col)->text(); }; + + ov.type = txt(OC_TYPE); + ov.fieldName = txt(OC_FIELD); + ov.x = txt(OC_X).toInt(); + ov.y = txt(OC_Y).toInt(); + ov.color = txt(OC_COLOR); + + if (ov.type == QLatin1String("BOX")) + { + ov.fontFamily = QStringLiteral("Arial"); + ov.fontSize = txt(OC_SIZE) == QStringLiteral("—") ? 11 : txt(OC_SIZE).toInt(); + ov.width = txt(OC_W).toInt(); + ov.height = txt(OC_H).toInt(); + ov.fillColor = txt(OC_FILL); + ov.opacity = txt(OC_OPACITY).toInt(); + ov.cornerRadius = txt(OC_RADIUS).toInt(); + } + else + { + ov.fontFamily = txt(OC_FONT); + ov.fontSize = txt(OC_SIZE).toInt(); + ov.bold = m_overlayModel->item(r, OC_BOLD)->checkState() == Qt::Checked; + ov.italic = m_overlayModel->item(r, OC_ITALIC)->checkState() == Qt::Checked; + } + m_overlays.append(ov); + } +} + +void EmailQSLSettingsWidget::onTableDataChanged() +{ + FCT_IDENTIFICATION; + + if (m_syncing) return; + m_syncing = true; + overlayTableToList(); + ui->cardEditorWidget->setOverlays(m_overlays); + m_syncing = false; +} + +void EmailQSLSettingsWidget::onEditorPositionChanged(int index, int x, int y) +{ + FCT_IDENTIFICATION; + + if (m_syncing || index < 0 || index >= m_overlayModel->rowCount()) return; + m_syncing = true; + m_overlayModel->item(index, OC_X)->setText(QString::number(x)); + m_overlayModel->item(index, OC_Y)->setText(QString::number(y)); + if (index < m_overlays.size()) + { m_overlays[index].x = x; m_overlays[index].y = y; } + m_syncing = false; +} + +void EmailQSLSettingsWidget::onEditorSizeChanged(int index, int w, int h) +{ + FCT_IDENTIFICATION; + + if (m_syncing || index < 0 || index >= m_overlayModel->rowCount()) return; + m_syncing = true; + auto *wi = m_overlayModel->item(index, OC_W); + auto *hi = m_overlayModel->item(index, OC_H); + if (wi && wi->text() != QStringLiteral("—")) wi->setText(QString::number(w)); + if (hi && hi->text() != QStringLiteral("—")) hi->setText(QString::number(h)); + if (index < m_overlays.size()) + { m_overlays[index].width = w; m_overlays[index].height = h; } + m_syncing = false; +} + +void EmailQSLSettingsWidget::onEditorOverlaySelected(int index) +{ + if (index >= 0) + ui->overlayTableView->selectRow(index); + else + ui->overlayTableView->clearSelection(); + ui->removeOverlayButton->setEnabled(index >= 0); +} + +// --------------------------------------------------------------------------- +// Slots: image +// --------------------------------------------------------------------------- + +void EmailQSLSettingsWidget::browseCardImage() +{ + FCT_IDENTIFICATION; + + const QString path = QFileDialog::getOpenFileName( + this, tr("Select QSL Card Image"), + ui->cardImagePathEdit->text(), + tr("Images (*.jpg *.jpeg *.png *.bmp);;All files (*)")); + if (!path.isEmpty()) + ui->cardImagePathEdit->setText(path); +} + +void EmailQSLSettingsWidget::onCardImageChanged(const QString &path) +{ + FCT_IDENTIFICATION; + + QPixmap pm(path); + + // If there are existing overlays and a previous image, rescale all stored + // pixel values (positions, font sizes, box dimensions) proportionally so + // the layout stays in the same visual position on the new image. + const QPixmap &prev = ui->cardEditorWidget->image(); + if (!prev.isNull() && !pm.isNull() + && prev.size() != pm.size()) + { + overlayTableToList(); // make sure m_overlays is current + + if (!m_overlays.isEmpty()) + { + const double sx = static_cast(pm.width()) / prev.width(); + const double sy = static_cast(pm.height()) / prev.height(); + + for (EmailQSLFieldOverlay &ov : m_overlays) + { + ov.x = qRound(ov.x * sx); + ov.y = qRound(ov.y * sy); + // Font size tracks horizontal scale (same axis as card width) + ov.fontSize = qMax(1, qRound(ov.fontSize * sx)); + if (ov.type == QLatin1String("BOX")) + { + ov.width = qMax(20, qRound(ov.width * sx)); + ov.height = qMax(10, qRound(ov.height * sy)); + } + } + + listToOverlayTable(); + } + } + + ui->cardEditorWidget->setImage(pm); +} + +// --------------------------------------------------------------------------- +// Slots: overlays +// --------------------------------------------------------------------------- + +void EmailQSLSettingsWidget::addOverlay() +{ + FCT_IDENTIFICATION; + + // Default position: top-left area of image (or 50,50 if no image) + const QPixmap &img = ui->cardEditorWidget->image(); + const int cx = img.isNull() ? 50 : img.width() / 2; + const int cy = img.isNull() ? 50 : img.height() / 4; + + overlayTableToList(); + m_overlays.append(makeOverlay(QStringLiteral("CALLSIGN"), cx, cy, 14, false)); + listToOverlayTable(); + + const int newRow = m_overlayModel->rowCount() - 1; + ui->overlayTableView->setCurrentIndex(m_overlayModel->index(newRow, 0)); + ui->overlayTableView->scrollToBottom(); + ui->cardEditorWidget->setSelectedIndex(newRow); +} + +void EmailQSLSettingsWidget::addBox() +{ + FCT_IDENTIFICATION; + + const QPixmap &img = ui->cardEditorWidget->image(); + const int W = img.isNull() ? 1000 : img.width(); + const int H = img.isNull() ? 700 : img.height(); + + EmailQSLFieldOverlay ov; + ov.type = QStringLiteral("BOX"); + ov.fieldName = QString(); // no caption by default + ov.x = qRound(W * 0.05); + ov.y = qRound(H * 0.60); + ov.width = qRound(W * 0.25); + ov.height = qRound(H * 0.22); + ov.fillColor = QStringLiteral("#FFFF99"); + ov.color = QStringLiteral("#888800"); + ov.opacity = 80; + ov.cornerRadius = qMax(4, qRound(H * 0.02)); + ov.fontSize = 0; // no caption + + overlayTableToList(); + m_overlays.append(ov); + listToOverlayTable(); + + const int newRow = m_overlayModel->rowCount() - 1; + ui->overlayTableView->setCurrentIndex(m_overlayModel->index(newRow, 0)); + ui->overlayTableView->scrollToBottom(); + ui->cardEditorWidget->setSelectedIndex(newRow); +} + +void EmailQSLSettingsWidget::addLabel() +{ + FCT_IDENTIFICATION; + + const QPixmap &img = ui->cardEditorWidget->image(); + const int cx = img.isNull() ? 50 : img.width() / 2; + const int cy = img.isNull() ? 50 : img.height() / 4; + + EmailQSLFieldOverlay ov; + ov.type = QStringLiteral("LABEL"); + ov.fieldName = tr("Label Text"); + ov.x = cx; + ov.y = cy; + ov.fontFamily = QStringLiteral("Arial"); + ov.fontSize = 14; + ov.color = QStringLiteral("#000000"); + ov.bold = false; + ov.italic = false; + + overlayTableToList(); + m_overlays.append(ov); + listToOverlayTable(); + + const int newRow = m_overlayModel->rowCount() - 1; + ui->overlayTableView->setCurrentIndex(m_overlayModel->index(newRow, 0)); + ui->overlayTableView->scrollToBottom(); + ui->cardEditorWidget->setSelectedIndex(newRow); +} + +void EmailQSLSettingsWidget::addDefaultOverlays() +{ + FCT_IDENTIFICATION; + + const QPixmap &img = ui->cardEditorWidget->image(); + const int W = img.isNull() ? 1280 : img.width(); + const int H = img.isNull() ? 896 : img.height(); + + // Font sizes are designed for a 1280-px-wide reference card and scaled + // linearly to the actual image width so they always look proportional. + // The same scale is applied to horizontal positions; vertical positions + // use the image height fraction directly. + static constexpr double REF_W = 1280.0; + const double fs = W / REF_W; // font scale (also used for x/horizontal) + + auto scalePt = [&](int refPt) { return qMax(1, qRound(refPt * fs)); }; + + // Helper lambda to make a LABEL (static text) overlay + auto makeLabel = [&](const QString &text, int x, int y, int refFontPt, bool bold, + const QString &color = QStringLiteral("#333333")) -> EmailQSLFieldOverlay + { + EmailQSLFieldOverlay ov; + ov.type = QStringLiteral("LABEL"); + ov.fieldName = text; + ov.x = x; + ov.y = y; + ov.fontSize = scalePt(refFontPt); + ov.bold = bold; + ov.fontFamily = QStringLiteral("Arial"); + ov.color = color; + return ov; + }; + + // Reference font sizes at 1280 px width + const int refCallsignPt = 80; // MY_CALLSIGN prominent header + const int refDxCallPt = 40; // DX station callsign + const int refDataPt = 20; // QSO data values + const int refLabelPt = 12; // small caption labels above each field + + // Positions as fractions of card dimensions — same as before + const int labelOffY = qRound(H * 0.045); + + const int rowName = qRound(H * 0.60); + const int rowCall = qRound(H * 0.72); + const int rowData = qRound(H * 0.84); + + const int col1 = qRound(W * 0.08); + const int col2 = qRound(W * 0.35); + const int col3 = qRound(W * 0.55); + const int col4 = qRound(W * 0.68); + const int col5 = qRound(W * 0.80); + const int col6 = qRound(W * 0.90); + const int colGrid = qRound(W * 0.40); + + const QList defaults = { + // My callsign centred near top — no label needed + makeOverlay("MY_CALLSIGN", qRound(W * 0.50), qRound(H * 0.18), scalePt(refCallsignPt), true), + + // Name row + makeLabel(tr("Name:"), col1, rowName - labelOffY, refLabelPt, false), + makeOverlay("NAME", col1, rowName, scalePt(refDataPt), false), + + // Callsign + grid row + makeLabel(tr("Callsign:"), col1, rowCall - labelOffY, refLabelPt, false), + makeOverlay("CALLSIGN", col1, rowCall, scalePt(refDxCallPt), true), + makeLabel(tr("Grid:"), colGrid, rowCall - labelOffY, refLabelPt, false), + makeOverlay("GRIDSQUARE", colGrid, rowCall, scalePt(refDataPt), false), + + // QSO detail row + makeLabel(tr("Date:"), col1, rowData - labelOffY, refLabelPt, false), + makeOverlay("QSO_DATE", col1, rowData, scalePt(refDataPt), false), + makeLabel(tr("Time (UTC):"), col2, rowData - labelOffY, refLabelPt, false), + makeOverlay("TIME_ON", col2, rowData, scalePt(refDataPt), false), + makeLabel(tr("Band:"), col3, rowData - labelOffY, refLabelPt, false), + makeOverlay("BAND", col3, rowData, scalePt(refDataPt), false), + makeLabel(tr("Mode:"), col4, rowData - labelOffY, refLabelPt, false), + makeOverlay("MODE", col4, rowData, scalePt(refDataPt), false), + makeLabel(tr("RST Snt:"), col5, rowData - labelOffY, refLabelPt, false), + makeOverlay("RST_SENT", col5, rowData, scalePt(refDataPt), false), + makeLabel(tr("RST Rcvd:"), col6, rowData - labelOffY, refLabelPt, false), + makeOverlay("RST_RCVD", col6, rowData, scalePt(refDataPt), false), + }; + + overlayTableToList(); + for (const EmailQSLFieldOverlay &ov : defaults) + m_overlays.append(ov); + listToOverlayTable(); + ui->overlayTableView->scrollToBottom(); +} + +void EmailQSLSettingsWidget::removeOverlay() +{ + FCT_IDENTIFICATION; + + const QModelIndexList sel = ui->overlayTableView->selectionModel()->selectedRows(); + if (sel.isEmpty()) + return; + + overlayTableToList(); + m_overlays.removeAt(sel.first().row()); + listToOverlayTable(); +} + +// --------------------------------------------------------------------------- +// Full-size preview — renders using current UI state (not saved LogParam values) +// --------------------------------------------------------------------------- + +QPixmap EmailQSLSettingsWidget::renderPreviewPixmap(const QSqlRecord &record) +{ + FCT_IDENTIFICATION; + + overlayTableToList(); + return EmailQSLBase::renderCard(ui->cardImagePathEdit->text().trimmed(), + record, m_overlays); +} + +void EmailQSLSettingsWidget::previewCard() +{ + FCT_IDENTIFICATION; + + const QSqlRecord dummy = buildDummyRecord(); + const QPixmap preview = renderPreviewPixmap(dummy); + + if (preview.isNull()) + { + QMessageBox::warning(this, tr("Preview"), + tr("Could not load the card image.\n" + "Please check the path and make sure the file exists.")); + return; + } + + QDialog *dlg = new QDialog(this); + dlg->setAttribute(Qt::WA_DeleteOnClose); + dlg->setWindowTitle(tr("Card Preview (test data)")); + QVBoxLayout *lay = new QVBoxLayout(dlg); + + QScrollArea *scroll = new QScrollArea(dlg); + QLabel *lbl = new QLabel(scroll); + const QPixmap scaled = preview.scaled( + QSize(800, 600), Qt::KeepAspectRatio, Qt::SmoothTransformation); + lbl->setPixmap(scaled); + lbl->adjustSize(); + scroll->setWidget(lbl); + scroll->setMinimumSize(qMin(scaled.width() + 20, 820), + qMin(scaled.height() + 20, 620)); + lay->addWidget(scroll); + + QDialogButtonBox *bb = new QDialogButtonBox(QDialogButtonBox::Close, dlg); + QPushButton *saveBtn = bb->addButton(tr("Save Card…"), QDialogButtonBox::ActionRole); + + connect(saveBtn, &QPushButton::clicked, this, [preview, this]() + { + const QString defaultDir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); + const QString path = QFileDialog::getSaveFileName( + this, + tr("Save QSL Card"), + defaultDir + QStringLiteral("/QSL_preview.png"), + tr("PNG Image (*.png);;JPEG Image (*.jpg *.jpeg)")); + + if (path.isEmpty()) + return; + + if (!preview.save(path)) + { + QMessageBox::warning(this, tr("Save Failed"), + tr("Could not save the card image to:\n%1").arg(path)); + } + }); + + connect(bb, &QDialogButtonBox::rejected, dlg, &QDialog::close); + lay->addWidget(bb); + dlg->show(); // non-modal +} + +// --------------------------------------------------------------------------- +// SMTP +// --------------------------------------------------------------------------- + +void EmailQSLSettingsWidget::onSmtpPresetChanged(int index) +{ + FCT_IDENTIFICATION; + + const QList presets = smtpPresets(); + if (index <= 0 || index >= presets.size()) + return; + + const SmtpPreset &p = presets.at(index); + if (!p.host.isEmpty()) + { + ui->smtpHostEdit->setText(p.host); + ui->smtpPortSpin->setValue(p.port); + ui->encryptionCombo->setCurrentIndex(p.encryption); + } + if (!p.hint.isEmpty()) + QMessageBox::information(this, tr("Provider Notes"), p.hint); +} + +void EmailQSLSettingsWidget::testConnection() +{ + FCT_IDENTIFICATION; + + ui->testResultLabel->setText(tr("Testing…")); + ui->testConnectionButton->setEnabled(false); + + m_testService->testConnection( + ui->smtpHostEdit->text().trimmed(), + ui->smtpPortSpin->value(), + ui->encryptionCombo->currentIndex(), + ui->smtpUsernameEdit->text().trimmed(), + ui->smtpPasswordEdit->text()); +} + +void EmailQSLSettingsWidget::onTestFinished(bool success, const QString &message) +{ + FCT_IDENTIFICATION; + + ui->testConnectionButton->setEnabled(true); + if (success) + { + ui->testResultLabel->setStyleSheet(QStringLiteral("color: green; font-weight: bold;")); + ui->testResultLabel->setText(tr("Connection OK")); + } + else + { + ui->testResultLabel->setStyleSheet(QStringLiteral("color: red;")); + ui->testResultLabel->setText(tr("Failed: %1").arg(message)); + } +} + +// --------------------------------------------------------------------------- +// Template insertion +// --------------------------------------------------------------------------- + +void EmailQSLSettingsWidget::insertMergeFieldBody() +{ + FCT_IDENTIFICATION; + + const QString text = ui->bodyFieldCombo->currentText(); + if (!text.isEmpty()) + ui->bodyTemplateEdit->insertPlainText(text); +} + +void EmailQSLSettingsWidget::insertMergeFieldSubject() +{ + FCT_IDENTIFICATION; + + const QString text = ui->subjectFieldCombo->currentText(); + if (!text.isEmpty()) + ui->subjectTemplateEdit->insert(text); +} diff --git a/ui/EmailQSLSettingsWidget.h b/ui/EmailQSLSettingsWidget.h new file mode 100644 index 00000000..a987e0f0 --- /dev/null +++ b/ui/EmailQSLSettingsWidget.h @@ -0,0 +1,63 @@ +#ifndef QLOG_UI_EMAILQSLSETTINGSWIDGET_H +#define QLOG_UI_EMAILQSLSETTINGSWIDGET_H + +#include +#include +#include +#include +#include + +#include "service/emailqsl/EmailQSLService.h" + +namespace Ui { +class EmailQSLSettingsWidget; +} + +class EmailQSLSettingsWidget : public QWidget +{ + Q_OBJECT + +public: + explicit EmailQSLSettingsWidget(QWidget *parent = nullptr); + ~EmailQSLSettingsWidget(); + + void readSettings(); + void writeSettings(); + + // Render card using the current UI state (not saved LogParam values). + // Used by the preview dialog so the user can see results before saving. + QPixmap renderPreviewPixmap(const QSqlRecord &record); + +public slots: + void browseCardImage(); + void onCardImageChanged(const QString &path); + void onSmtpPresetChanged(int index); + void testConnection(); + void onTestFinished(bool success, const QString &message); + void addOverlay(); + void addBox(); + void addLabel(); + void addDefaultOverlays(); + void removeOverlay(); + void insertMergeFieldBody(); + void insertMergeFieldSubject(); + void previewCard(); + + // Bidirectional sync between the overlay table and the card editor widget + void onTableDataChanged(); + void onEditorPositionChanged(int index, int newX, int newY); + void onEditorSizeChanged(int index, int newW, int newH); + void onEditorOverlaySelected(int index); + +private: + void overlayTableToList(); + void listToOverlayTable(); + + Ui::EmailQSLSettingsWidget *ui; + QList m_overlays; + QStandardItemModel *m_overlayModel; + EmailQSLService *m_testService; + bool m_syncing; +}; + +#endif // QLOG_UI_EMAILQSLSETTINGSWIDGET_H diff --git a/ui/EmailQSLSettingsWidget.ui b/ui/EmailQSLSettingsWidget.ui new file mode 100644 index 00000000..ae3d019c --- /dev/null +++ b/ui/EmailQSLSettingsWidget.ui @@ -0,0 +1,384 @@ + + + EmailQSLSettingsWidget + + + 00820720 + + Email QSL Settings + + + + + + Configure Email QSL card sending. Use {FIELD} placeholders in subject, body and card overlays. + + true + + + + + + + + + + + SMTP Server + + + + + Quick Presets + + + + Provider preset: + + + + + + Select a preset to auto-fill host, port and encryption. + +Gmail: enable 2-FA, then create an App Password at myaccount.google.com → Security → App Passwords. Use the 16-character app password (not your Google password). +Outlook / Hotmail / Microsoft 365: use your Microsoft account email and password. If MFA is enabled generate an App Password at account.microsoft.com → Security. +Yahoo Mail: go to Account Security and generate an App Password. +iCloud Mail: generate an App-Specific Password at appleid.apple.com → Sign-In and Security. + + + + + + Qt::Horizontal + + + + + + + + + Server + + + SMTP host: + + + + e.g. smtp.gmail.com + + + + Port: + + + + 1 + 65535 + 587 + + + + Encryption: + + + + None + SSL/TLS (implicit, port 465) + STARTTLS (explicit, port 587) + + + + + + + + + Authentication + + + Username: + + + + your.address@gmail.com + + + + Password / App password: + + + + QLineEdit::Password + App password recommended (16 chars for Gmail) + + + + From address: + + + + your.address@gmail.com + + + + From display name: + + + + AA5SH Amateur Radio + + + + + + + + + + + Test Connection + + + + + + + + + Qt::Horizontal + + + + + + Qt::Vertical + + + + + + + + + + Card Image & Overlays + + + + + + + Card image: + + + + Path to base QSL card image (JPEG or PNG) + + + + + Browse… + + + + + + + + + + 300220 + + + Click an overlay to select it. Drag to reposition it on the card. + + + + + + + + Click to select · Drag to move · BOX: drag bottom-right handle to resize. + + QLabel { color: #555; font-style: italic; } + + + + + + + Overlays + + + + 16777215170 + QAbstractItemView::SingleSelection + QAbstractItemView::SelectRows + + QAbstractItemView::DoubleClicked|QAbstractItemView::SelectedClicked|QAbstractItemView::EditKeyPressed + + true + + + + + + + Add Text + Add a merge-field text overlay + + + + + Add Box + Add a filled rounded rectangle (like the yellow boxes on traditional QSL cards) + + + + + Add Static Text + Add a free-form text label (literal text, not a merge field) + + + + + Default QSL Fields + + Insert a pre-built set of text overlays (callsign, date, time, band, mode, RST, name, grid) positioned across the lower area of the card. + + + + + + Remove + false + + + + Qt::Horizontal + + + + Full-Size Preview… + + + + + + + + TEXT: Field=merge key. BOX: Field=optional caption, drag corner to resize. LABEL: Field=literal text printed as-is. Double-click Color/Fill to pick color. + + true + QLabel { color: #555; } + + + + + + + + + + + + + + Email Template + + + + + Subject Line + + + + QSL Card from {MY_CALLSIGN} for our QSO on {QSO_DATE} + + + + + Choose a merge field to insert + + + + + Insert + + + + + + + + + Email Body + + + + + Insert field: + + + + Choose a merge field to insert at the cursor + + + + + Insert + + + + Qt::Horizontal + + + + + + 0180 + Enter email body here. Use {FIELD} placeholders. + + + + + + + + + Available Merge Fields + + + + 16777215150 + 2 + QAbstractItemView::NoEditTriggers + QAbstractItemView::NoSelection + Placeholder + Description + + + + + + + + + + + + + + + + + CardEditorWidget + QWidget +
ui/component/CardEditorWidget.h
+
+
+ + +
diff --git a/ui/ExportDialog.cpp b/ui/ExportDialog.cpp index 333c9787..aa2fe87d 100644 --- a/ui/ExportDialog.cpp +++ b/ui/ExportDialog.cpp @@ -301,7 +301,8 @@ void ExportDialog::showColumnsSetting() FCT_IDENTIFICATION; // don't want to export QSO ID because it is not a ADIF field - QList excludeFilter({LogbookModel::COLUMN_ID}); + QList excludeFilter({LogbookModel::COLUMN_ID, + LogbookModel::COLUMN_MODE_SUBMODE}); ColumnSettingDialog dialog(&logbookmodel, exportedColumns, diff --git a/ui/ImportDialog.cpp b/ui/ImportDialog.cpp index 11576192..8358e5e8 100644 --- a/ui/ImportDialog.cpp +++ b/ui/ImportDialog.cpp @@ -1,14 +1,56 @@ +#include #include #include +#include #include "ImportDialog.h" #include "ui_ImportDialog.h" #include "logformat/LogFormat.h" #include "core/debug.h" +#include "core/LogParam.h" #include "data/StationProfile.h" #include "data/RigProfile.h" MODULE_IDENTIFICATION("qlog.ui.importdialog"); +void ImportDialog::setQslSentStatusValue(QComboBox *combo, const QString &value) +{ + if ( !combo ) return; + + int index = combo->findData(value); + + if ( index < 0 ) + index = combo->findData(QStringLiteral("Q")); + + if ( index >= 0 ) + combo->setCurrentIndex(index); +} + +void ImportDialog::setupQslSentStatusComboData() +{ + FCT_IDENTIFICATION; + + /* do not use Data::qslSentBox — different ordering */ + ui->qslSentStatusCombo->clear(); + ui->qslSentStatusCombo->addItem(tr("Queued (ready to send)"), "Q"); + ui->qslSentStatusCombo->addItem(tr("Ignored (do not track)"), "I"); + ui->qslSentStatusCombo->addItem(tr("Requested (requested again)"), "R"); + ui->qslSentStatusCombo->addItem(tr("Yes (already sent)"), "Y"); + ui->qslSentStatusCombo->addItem(tr("Custom..."), "custom"); + + for ( QComboBox* combo : { ui->qslSentStatusPaperCombo, + ui->qslSentStatusLotwCombo, + ui->qslSentStatusEqslCombo, + ui->qslSentStatusDclCombo }) + { + combo->clear(); + combo->addItem(tr("Queued"), "Q"); + combo->addItem(tr("Requested"), "R"); + combo->addItem(tr("Ignored"), "I"); + combo->addItem(tr("No"), "N"); + combo->addItem(tr("Yes"), "Y"); + } +} + ImportDialog::ImportDialog(QWidget *parent) : QDialog(parent), ui(new Ui::ImportDialog) @@ -16,6 +58,7 @@ ImportDialog::ImportDialog(QWidget *parent) : FCT_IDENTIFICATION; ui->setupUi(this); + setupQslSentStatusComboData(); ui->allCheckBox->setChecked(true); ui->startDateEdit->setDisplayFormat(locale.formatDateShortWithYYYY()); @@ -47,8 +90,13 @@ ImportDialog::ImportDialog(QWidget *parent) : ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("&Import")); + loadQslSentStatusCustomDefaults(); + updateQslSentStatusDefaultWidgets(); + // TODO: disabled #983 - If everything is OK, then delete it over time. ui->optionBox->setVisible(false); + + updateWidgetSize(); } void ImportDialog::browse() @@ -70,7 +118,7 @@ void ImportDialog::browse() // More information: // https://stackoverflow.com/questions/34858220/qt-how-to-set-a-case-insensitive-filter-on-qfiledialog // https://bugreports.qt.io/browse/QTBUG-51712 - // But Raspberry pi crashes when DontUseNativeDialog is used therefore use the native for it. + // But Raspberry pi crashes when DontUseNativeDialog is used therefore use the native for it. QFileDialog::DontUseNativeDialog #else QFileDialog::Options() @@ -121,6 +169,58 @@ void ImportDialog::toggleComment() commentChanged(ui->commentEdit->text()); } +void ImportDialog::qslSentStatusDefaultChanged(int) +{ + FCT_IDENTIFICATION; + + updateQslSentStatusDefaultWidgets(); +} + +void ImportDialog::updateQslSentStatusDefaultWidgets() +{ + const bool enabled = ui->qslSentStatusCheckBox->isChecked(); + const bool custom = ui->qslSentStatusCombo->currentData().toString() == QLatin1String("custom"); + + ui->qslSentStatusCombo->setEnabled(enabled); + ui->qslSentStatusCustomWidget->setVisible(enabled && custom); + + updateWidgetSize(); +} + +void ImportDialog::loadQslSentStatusCustomDefaults() +{ + FCT_IDENTIFICATION; + + setQslSentStatusValue(ui->qslSentStatusPaperCombo, LogParam::getImportQslSentStatusPaper()); + setQslSentStatusValue(ui->qslSentStatusLotwCombo, LogParam::getImportQslSentStatusLoTW()); + setQslSentStatusValue(ui->qslSentStatusEqslCombo, LogParam::getImportQslSentStatusEQSL()); + setQslSentStatusValue(ui->qslSentStatusDclCombo, LogParam::getImportQslSentStatusDCL()); +} + +void ImportDialog::saveQslSentStatusCustomDefaults() const +{ + FCT_IDENTIFICATION; + + LogParam::setImportQslSentStatusPaper(ui->qslSentStatusPaperCombo->currentData().toString()); + LogParam::setImportQslSentStatusLoTW(ui->qslSentStatusLotwCombo->currentData().toString()); + LogParam::setImportQslSentStatusEQSL(ui->qslSentStatusEqslCombo->currentData().toString()); + LogParam::setImportQslSentStatusDCL(ui->qslSentStatusDclCombo->currentData().toString()); +} + +void ImportDialog::updateWidgetSize() +{ + FCT_IDENTIFICATION; + + QTimer::singleShot(0, this, [this]() + { + if ( layout() ) + layout()->activate(); + + const QSize targetSize = sizeHint(); + resize(qMax(width(), targetSize.width()), targetSize.height()); + }); +} + void ImportDialog::computeProgress(qint64 position) { FCT_IDENTIFICATION; @@ -263,6 +363,25 @@ void ImportDialog::runImport() defaults["comment_intl"] = ui->commentEdit->text(); } + if (ui->qslSentStatusCheckBox->isChecked()) + { + if ( ui->qslSentStatusCombo->currentData().toString() == "custom" ) + { + defaults["qsl_sent"] = ui->qslSentStatusPaperCombo->currentData().toString(); + defaults["lotw_qsl_sent"] = ui->qslSentStatusLotwCombo->currentData().toString(); + defaults["eqsl_qsl_sent"] = ui->qslSentStatusEqslCombo->currentData().toString(); + defaults["dcl_qsl_sent"] = ui->qslSentStatusDclCombo->currentData().toString(); + } + else + { + const QString qslSentStatusDefault = ui->qslSentStatusCombo->currentData().toString(); + defaults["qsl_sent"] = qslSentStatusDefault; + defaults["lotw_qsl_sent"] = qslSentStatusDefault; + defaults["eqsl_qsl_sent"] = qslSentStatusDefault; + defaults["dcl_qsl_sent"] = qslSentStatusDefault; + } + } + LogFormat* format = LogFormat::open(ui->typeSelect->currentText(), in); if (!format) { @@ -294,6 +413,9 @@ void ImportDialog::runImport() ui->rigSelect->setEnabled(false); ui->commentCheckBox->setEnabled(false); ui->commentEdit->setEnabled(false); + ui->qslSentStatusCheckBox->setEnabled(false); + ui->qslSentStatusCombo->setEnabled(false); + ui->qslSentStatusCustomWidget->setEnabled(false); ui->fillMissingDxccCheckBox->setEnabled(false); QString s; @@ -352,5 +474,7 @@ ImportDialog::~ImportDialog() { FCT_IDENTIFICATION; + saveQslSentStatusCustomDefaults(); + delete ui; } diff --git a/ui/ImportDialog.h b/ui/ImportDialog.h index d16ff4f1..f050b8b2 100644 --- a/ui/ImportDialog.h +++ b/ui/ImportDialog.h @@ -7,6 +7,8 @@ #include "data/StationProfile.h" #include "core/LogLocale.h" +class QComboBox; + namespace Ui { class ImportDialog; } @@ -30,6 +32,7 @@ private slots: void toggleMyProfile(); void toggleMyRig(); void commentChanged(const QString&); + void qslSentStatusDefaultChanged(int); private: Ui::ImportDialog *ui; @@ -38,6 +41,14 @@ private slots: LogLocale locale; static LogFormat::duplicateQSOBehaviour showDuplicateDialog(QSqlRecord *, QSqlRecord *); + + static void setQslSentStatusValue(QComboBox *, const QString &); + + void setupQslSentStatusComboData(); + void updateQslSentStatusDefaultWidgets(); + void loadQslSentStatusCustomDefaults(); + void saveQslSentStatusCustomDefaults() const; + void updateWidgetSize(); void saveImportDetails(const QString &importDetail, const QString &filename, const int count, diff --git a/ui/ImportDialog.ui b/ui/ImportDialog.ui index c104d33b..bc1a2240 100644 --- a/ui/ImportDialog.ui +++ b/ui/ImportDialog.ui @@ -2,6 +2,14 @@ ImportDialog + + + 0 + 0 + 560 + 660 + + Import @@ -11,6 +19,12 @@ + + + 0 + 0 + + File @@ -61,6 +75,12 @@ + + + 0 + 0 + + @@ -136,11 +156,30 @@ + + + + Qt::Horizontal + + + + 40 + 20 + + + +
+ + + 0 + 0 + + The value is used when an input record does not contain the ADIF value @@ -148,7 +187,36 @@ Defaults - + + + + + 0 + 0 + + + + Values are used only for fields that are missing in the import file. Existing values are preserved. + + + true + + + + + + + + 0 + 0 + + + + <p>⚠ Missing QSL Sent fields are set to <b>"N"</b> (do not send) by default in ADIF. + + + + @@ -164,7 +232,14 @@ - + + + + false + + + + @@ -177,7 +252,7 @@ - + false @@ -193,7 +268,7 @@ - + @@ -206,7 +281,7 @@ - + false @@ -222,11 +297,119 @@ - - + + + + + 0 + 0 + + + + Used only for missing QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT, and DCL_QSL_SENT fields where default is "N"; otherwise, the value from the input is used. + + + QSL Sent status + + + false + + + + + false + + + 0 + 0 + + + + Used only for missing QSL_SENT, LOTW_QSL_SENT, EQSL_QSL_SENT, and DCL_QSL_SENT fields where default is "N"; otherwise, the value from the input is used.<p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + + + + + + + Used only when the imported ADIF record does not contain the selected field. Explicit ADIF values are kept. + + + + 0 + + + 0 + + + 0 + + + 0 + + + 12 + + + + + Default value for missing DCL_QSL_SENT. <p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + + + + + + + Default value for missing EQSL_QSL_SENT. <p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + + + + + + + Default value for missing LOTW_QSL_SENT. <p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + + + + + + + LoTW + + + + + + + DCL + + + + + + + Paper QSL + + + + + + + eQSL + + + + + + + Default value for missing QSL_SENT.<p><b>Queued</b> (ready), <b>No</b> (do not send), <b>Ignore</b> (do not track), <b>Requested</b> (requested), <b>Yes</b> (already sent). + + + + @@ -297,6 +480,12 @@ rigSelect commentCheckBox commentEdit + qslSentStatusCheckBox + qslSentStatusCombo + qslSentStatusPaperCombo + qslSentStatusLotwCombo + qslSentStatusEqslCombo + qslSentStatusDclCombo fillMissingDxccCheckBox @@ -461,6 +650,38 @@ + + qslSentStatusCheckBox + stateChanged(int) + ImportDialog + qslSentStatusDefaultChanged(int) + + + 70 + 306 + + + 214 + 218 + + + + + qslSentStatusCombo + currentIndexChanged(int) + ImportDialog + qslSentStatusDefaultChanged(int) + + + 265 + 306 + + + 214 + 218 + + + browse() @@ -473,5 +694,6 @@ stationProfileTextChanged(QString) rigProfileTextChanged(QString) commentChanged(QString) + qslSentStatusDefaultChanged(int) diff --git a/ui/KSTChatWidget.cpp b/ui/KSTChatWidget.cpp index 6a355669..4fa688f0 100644 --- a/ui/KSTChatWidget.cpp +++ b/ui/KSTChatWidget.cpp @@ -707,6 +707,12 @@ QVariant UserListModel::data(const QModelIndex &index, int role) const { return Data::statusToColor(userInfo.status, userInfo.dupeCount, QColor(Qt::transparent)); } + else if ( index.column() == 0 && role == Qt::ForegroundRole) + { + return Data::textColorForBackground(Data::statusToColor(userInfo.status, + userInfo.dupeCount, + QColor(Qt::transparent))); + } else if (index.column() == 0 && role == Qt::ToolTipRole) { return QCoreApplication::translate("DBStrings", userInfo.dxcc.country.toUtf8().constData()) + " [" + Data::statusToText(userInfo.status) + "]"; diff --git a/ui/LogbookWidget.cpp b/ui/LogbookWidget.cpp index 2e29aba5..56c4ffbc 100644 --- a/ui/LogbookWidget.cpp +++ b/ui/LogbookWidget.cpp @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include #include #include @@ -11,6 +13,7 @@ #include #include #include +#include #include "logformat/AdiFormat.h" #include "models/LogbookModel.h" @@ -24,6 +27,7 @@ #include "ui/ColumnSettingDialog.h" #include "data/Data.h" #include "ui/ExportDialog.h" +#include "ui/component/ModeSubmodeDelegate.h" #include "service/eqsl/Eqsl.h" #include "ui/PaperQSLDialog.h" #include "ui/QSODetailDialog.h" @@ -31,6 +35,7 @@ #include "service/GenericCallbook.h" #include "core/QSOFilterManager.h" #include "core/LogParam.h" +#include "ui/EmailQSLDialog.h" MODULE_IDENTIFICATION("qlog.ui.logbookwidget"); @@ -131,6 +136,7 @@ LogbookWidget::LogbookWidget(QWidget *parent) : ui->contactTable->addAction(ui->actionFilter); ui->contactTable->addAction(ui->actionLookup); ui->contactTable->addAction(ui->actionSendDXCSpot); + ui->contactTable->addAction(ui->actionSendEmailQSL); ui->contactTable->addAction(separator); ui->contactTable->addAction(ui->actionExportAs); ui->contactTable->addAction(ui->actionCallbookLookup); @@ -155,6 +161,8 @@ LogbookWidget::LogbookWidget(QWidget *parent) : ui->contactTable->setItemDelegateForColumn(LogbookModel::COLUMN_FREQUENCY, new UnitFormatDelegate("", 6, 0.001, ui->contactTable)); ui->contactTable->setItemDelegateForColumn(LogbookModel::COLUMN_BAND, new ComboFormatDelegate(new SqlListModel("SELECT name FROM bands ORDER BY start_freq", " ", ui->contactTable), ui->contactTable)); ui->contactTable->setItemDelegateForColumn(LogbookModel::COLUMN_MODE, new ComboFormatDelegate(new SqlListModel("SELECT name FROM modes", " ", ui->contactTable), ui->contactTable)); + ui->contactTable->setItemDelegateForColumn(LogbookModel::COLUMN_SUBMODE, new SubmodeDelegate(ui->contactTable)); + ui->contactTable->setItemDelegateForColumn(LogbookModel::COLUMN_MODE_SUBMODE, new ModeSubmodeDelegate(ui->contactTable)); ui->contactTable->setItemDelegateForColumn(LogbookModel::COLUMN_CONTINENT, new ComboFormatDelegate(QStringList() << " " << Data::getContinentList(), ui->contactTable)); ui->contactTable->setItemDelegateForColumn(LogbookModel::COLUMN_QSL_SENT, new ComboFormatDelegate(Data::instance()->qslSentEnum, ui->contactTable)); ui->contactTable->setItemDelegateForColumn(LogbookModel::COLUMN_QSL_SENT_VIA, new ComboFormatDelegate(Data::instance()->qslSentViaEnum, ui->contactTable)); @@ -236,10 +244,9 @@ LogbookWidget::LogbookWidget(QWidget *parent) : else { /* Hide all */ - for ( int i = 0; i < LogbookModel::COLUMN_LAST_ELEMENT; i++ ) - { + for ( int i = 0; i < model->columnCount(); i++ ) ui->contactTable->hideColumn(i); - } + /* Set a basic set of columns */ ui->contactTable->showColumn(LogbookModel::COLUMN_TIME_ON); ui->contactTable->showColumn(LogbookModel::COLUMN_CALL); @@ -247,6 +254,7 @@ LogbookWidget::LogbookWidget(QWidget *parent) : ui->contactTable->showColumn(LogbookModel::COLUMN_RST_SENT); ui->contactTable->showColumn(LogbookModel::COLUMN_FREQUENCY); ui->contactTable->showColumn(LogbookModel::COLUMN_MODE); + ui->contactTable->showColumn(LogbookModel::COLUMN_SUBMODE); ui->contactTable->showColumn(LogbookModel::COLUMN_NAME_INTL); ui->contactTable->showColumn(LogbookModel::COLUMN_QTH_INTL); ui->contactTable->showColumn(LogbookModel::COLUMN_COMMENT_INTL); @@ -1067,6 +1075,14 @@ void LogbookWidget::saveTableHeaderState() LogParam::setLogbookState(ui->contactTable->horizontalHeader()->saveState()); } +void LogbookWidget::setContactTableColumnVisible(int columnIndex, bool visible) +{ + ui->contactTable->setColumnHidden(columnIndex, !visible); + + if ( visible && ui->contactTable->columnWidth(columnIndex) == 0 ) + ui->contactTable->setColumnWidth(columnIndex, ui->contactTable->horizontalHeader()->defaultSectionSize()); +} + void LogbookWidget::showTableHeaderContextMenu(const QPoint& point) { FCT_IDENTIFICATION; @@ -1079,9 +1095,9 @@ void LogbookWidget::showTableHeaderContextMenu(const QPoint& point) action->setCheckable(true); action->setChecked(!ui->contactTable->isColumnHidden(i)); - connect(action, &QAction::triggered, this, [this, i]() + connect(action, &QAction::triggered, this, [this, i](bool checked) { - ui->contactTable->setColumnHidden(i, !ui->contactTable->isColumnHidden(i)); + setContactTableColumnVisible(i, checked); saveTableHeaderState(); }); @@ -1244,6 +1260,131 @@ void LogbookWidget::sendDXCSpot() emit sendDXSpotContactReq(model->record(selectedIndexes.at(0).row())); } +void LogbookWidget::sendEmailQSL() +{ + FCT_IDENTIFICATION; + + const QModelIndexList selectedIndexes = ui->contactTable->selectionModel()->selectedRows(); + if (selectedIndexes.isEmpty()) + return; + + // Collect records, skip those without an email address silently (report at end) + QList toSend; + int skippedNoEmail = 0; + + // Single resend decision covers both "already sent for this QSO" and + // "already sent to this callsign for a different QSO" — one prompt per + // batch regardless of the mix of duplicate types. + // -1 = undecided, 0 = skip all duplicates, 1 = resend all duplicates + int resendDecision = -1; + + const bool batchMode = (selectedIndexes.count() > 1); + + for (const QModelIndex &idx : selectedIndexes) + { + const QSqlRecord rec = model->record(idx.row()); + const QString email = rec.value(QStringLiteral("email")).toString().trimmed(); + + if (email.isEmpty()) + { + ++skippedNoEmail; + continue; + } + + const QString callsign = rec.value(QStringLiteral("callsign")).toString().toUpper(); + const int id = rec.value(QStringLiteral("id")).toInt(); + const QDateTime prevSent = EmailQSLBase::getEmailSentDateTime(rec); + const bool sentQso = prevSent.isValid(); + const bool sentCall = !sentQso && EmailQSLBase::hasEmailBeenSentToCallsign(callsign, id); + + if (sentQso || sentCall) + { + if (resendDecision == 0) + { + continue; // "No to All" already chosen + } + else if (resendDecision == -1) + { + QString detail = sentQso + ? tr("An Email QSL was already sent for %1 on %2 UTC.") + .arg(callsign, prevSent.toString(Qt::RFC2822Date)) + : tr("An Email QSL was previously sent to %1 for a different QSO.") + .arg(callsign); + + QString msg = detail + QStringLiteral("

") + tr("Do you want to send again?"); + if (batchMode) + msg += QStringLiteral("
") + + tr("\"Yes to All\" / \"No to All\" applies to every duplicate in this batch.") + + QStringLiteral(""); + + QMessageBox mb(this); + mb.setWindowTitle(tr("Already Sent — Send Again?")); + mb.setTextFormat(Qt::RichText); + mb.setText(msg); + mb.setIcon(QMessageBox::Question); + + QPushButton *yesBtn = mb.addButton(tr("Yes"), QMessageBox::YesRole); + QPushButton *yesAllBtn = batchMode ? mb.addButton(tr("Yes to All"), QMessageBox::YesRole) : nullptr; + QPushButton *noBtn = mb.addButton(tr("No"), QMessageBox::NoRole); + QPushButton *noAllBtn = batchMode ? mb.addButton(tr("No to All"), QMessageBox::NoRole) : nullptr; + Q_UNUSED(yesBtn) + + mb.exec(); + QAbstractButton *clicked = mb.clickedButton(); + + if (clicked == noAllBtn) { resendDecision = 0; continue; } + else if (clicked == noBtn) { continue; } + else if (clicked == yesAllBtn) { resendDecision = 1; } + // else "Yes" — include this one, leave decision open for next + } + // resendDecision == 1 → fall through and add + } + + toSend.append(rec); + } + + if (toSend.isEmpty()) + { + if (skippedNoEmail > 0) + QMessageBox::information(this, tr("Email QSL"), + tr("%n contact(s) skipped — no email address on file.", "", skippedNoEmail)); + return; + } + + // Show dialogs one at a time so they never pile up behind the main window + showNextEmailQSLDialog(new QList(toSend)); + + if (skippedNoEmail > 0) + QMessageBox::information(this, tr("Email QSL"), + tr("%n contact(s) skipped — no email address on file.", "", skippedNoEmail)); +} + +void LogbookWidget::showNextEmailQSLDialog(QList *queue) +{ + FCT_IDENTIFICATION; + + if (queue->isEmpty()) + { + delete queue; + model->select(); // refresh the log table once the whole batch is done + return; + } + + const QSqlRecord rec = queue->takeFirst(); + EmailQSLDialog *dialog = new EmailQSLDialog(rec, this); + dialog->setAttribute(Qt::WA_DeleteOnClose); + + // When this dialog closes (for any reason), open the next one + connect(dialog, &QDialog::finished, this, [this, queue]() + { + showNextEmailQSLDialog(queue); + }); + + dialog->show(); + dialog->raise(); + dialog->activateWindow(); +} + void LogbookWidget::setDefaultSort() { FCT_IDENTIFICATION; diff --git a/ui/LogbookWidget.h b/ui/LogbookWidget.h index 3d79947f..3f8bce2f 100644 --- a/ui/LogbookWidget.h +++ b/ui/LogbookWidget.h @@ -87,6 +87,7 @@ public slots: void focusSearchCallsign(); void reloadSetting(); void sendDXCSpot(); + void sendEmailQSL(); void setDefaultSort(); void actionCallbookLookup(); void callsignFound(const CallbookResponseData &data); @@ -129,8 +130,10 @@ public slots: void updateQSORecordFromCallbook(const CallbookResponseData &data); void queryNextQSOLookupBatch(); void finishQSOLookupBatch(); + void showNextEmailQSLDialog(QList *queue); void clearSearchText(); void setupSearchMenu(); + void setContactTableColumnVisible(int columnIndex, bool visible); void updateSelectedRows(std::function updater); QModelIndexList callbookLookupBatch; QModelIndex currLookupIndex; diff --git a/ui/LogbookWidget.ui b/ui/LogbookWidget.ui index cca6bd5f..1ab80107 100644 --- a/ui/LogbookWidget.ui +++ b/ui/LogbookWidget.ui @@ -333,6 +333,14 @@ Logbook - Send DX Spot + + + Send Email QSL Card + + + Send an Email QSL card to this contact + + true @@ -567,6 +575,16 @@ + + actionSendEmailQSL + triggered() + LogbookWidget + sendEmailQSL() + + -1-1 + 404168 + + actionSendDXCSpot triggered() @@ -715,6 +733,7 @@ exportContact() clubFilterChanged() sendDXCSpot() + sendEmailQSL() setCallsignSearch() setGridsquareSearch() setPotaSearch() diff --git a/ui/MainWindow.cpp b/ui/MainWindow.cpp index 271bd6a0..b3b4120c 100644 --- a/ui/MainWindow.cpp +++ b/ui/MainWindow.cpp @@ -5,6 +5,11 @@ #include #include #include +#include +#include +#include +#include +#include #include "MainWindow.h" #include "ui_MainWindow.h" @@ -32,10 +37,13 @@ #include "data/Data.h" #include "data/ActivityProfile.h" #include "data/AntProfile.h" +#include "data/StationProfile.h" #include "data/RigProfile.h" #include "data/RotProfile.h" #include "ui/DownloadQSLDialog.h" #include "ui/UploadQSODialog.h" +#include "ui/QSLImportStatDialog.h" +#include "service/lotw/Lotw.h" #include "core/LogParam.h" #include "core/PotaQE.h" #include "data/WsjtxEntry.h" @@ -48,6 +56,7 @@ #include "ui/PlatformSettingsDialog.h" #include "ui/QSLGalleryDialog.h" #include "ui/QSLPrintLabelDialog.h" +#include "ui/AdifRecoveryManager.h" #include #include #include @@ -59,7 +68,8 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow), stats(new StatisticsWidget), - clublogRT(new ClubLogUploader(this)) + clublogRT(new ClubLogUploader(this)), + adifRecoveryManager(new AdifRecoveryManager(this)) { FCT_IDENTIFICATION; @@ -289,6 +299,13 @@ MainWindow::MainWindow(QWidget* parent) : FldigiTCPServer* fldigi = new FldigiTCPServer(this); connect(fldigi, &FldigiTCPServer::addContact, ui->newContactWidget, &NewContactWidget::saveExternalContact); + connect(adifRecoveryManager, &AdifRecoveryManager::contactsRecovered, ui->logbookWidget, &LogbookWidget::updateTable); + connect(adifRecoveryManager, &AdifRecoveryManager::problem, this, [this](const QString &message) + { + if ( !message.isEmpty() ) + QMessageBox::warning(this, tr("Startup ADI"), message); + }, Qt::QueuedConnection); + wsjtx = new WsjtxUDPReceiver(this); connect(wsjtx, &WsjtxUDPReceiver::statusReceived, ui->wsjtxWidget, &WsjtxWidget::statusReceived); connect(wsjtx, &WsjtxUDPReceiver::decodeReceived, ui->wsjtxWidget, &WsjtxWidget::decodeReceived); @@ -308,6 +325,7 @@ MainWindow::MainWindow(QWidget* parent) : connect(ui->wsjtxWidget, &WsjtxWidget::modeChanged, ui->newContactWidget, &NewContactWidget::changeModefromRig); connect(this, &MainWindow::settingsChanged, wsjtx, &WsjtxUDPReceiver::reloadSetting); + connect(this, &MainWindow::settingsChanged, adifRecoveryManager, &AdifRecoveryManager::reloadSettings); connect(this, &MainWindow::settingsChanged, ui->rotatorWidget, &RotatorWidget::reloadSettings); connect(this, &MainWindow::settingsChanged, ui->rigWidget, &RigWidget::reloadSettings); connect(this, &MainWindow::settingsChanged, ui->cwconsoleWidget, &CWConsoleWidget::reloadSettings); @@ -315,6 +333,8 @@ MainWindow::MainWindow(QWidget* parent) : connect(this, &MainWindow::settingsChanged, ui->onlineMapWidget, &OnlineMapWidget::flyToMyQTH); connect(this, &MainWindow::settingsChanged, ui->logbookWidget, &LogbookWidget::reloadSetting); connect(this, &MainWindow::settingsChanged, ui->dxWidget, &DxWidget::reloadSetting); + connect(this, &MainWindow::settingsChanged, ui->dxWidget, &DxWidget::refreshStatusColors); + connect(this, &MainWindow::settingsChanged, ui->wsjtxWidget, &WsjtxWidget::refreshStatusColors); connect(this, &MainWindow::settingsChanged, ui->bandmapWidget, &BandmapWidget::recalculateDxccStatus); connect(this, &MainWindow::settingsChanged, ui->alertsWidget, &AlertWidget::recalculateDxccStatus); connect(this, &MainWindow::settingsChanged, ui->chatWidget, &ChatWidget::recalculateDxccStatus); @@ -468,6 +488,8 @@ MainWindow::MainWindow(QWidget* parent) : //restoreConnectionStates(); setupActivitiesMenu(); + + QTimer::singleShot(10000, adifRecoveryManager, &AdifRecoveryManager::startStartupRecovery); } void MainWindow::closeEvent(QCloseEvent* event) @@ -1099,6 +1121,126 @@ void MainWindow::showServiceDownloadQSL() ui->logbookWidget->updateTable(); } +void MainWindow::showServiceDownloadLotwDXCCCredits() +{ + FCT_IDENTIFICATION; + + if ( LotwBase::getUsername().isEmpty() ) + { + QMessageBox::warning(this, + tr("QLog Warning"), + tr("LoTW is not configured properly.

Please, use Settings dialog to configure it.

")); + return; + } + + QSqlQuery entityQuery; + if ( !entityQuery.exec("SELECT my_dxcc, " + " MAX(NULLIF(TRIM(my_country_intl), '')) AS my_country, " + " CASE WHEN LENGTH(GROUP_CONCAT(station_callsign, ', ')) > 50 " + " THEN SUBSTR(GROUP_CONCAT(station_callsign, ', '), 1, 50) || '...' " + " ELSE GROUP_CONCAT(station_callsign, ', ') END AS station_callsigns " + "FROM (" + " SELECT DISTINCT my_dxcc, " + " my_country_intl, " + " UPPER(NULLIF(TRIM(station_callsign), '')) AS station_callsign " + " FROM contacts " + " WHERE my_dxcc IS NOT NULL AND my_dxcc > 0" + ") " + "GROUP BY my_dxcc " + "ORDER BY my_dxcc") ) + { + QMessageBox::critical(this, + tr("QLog Error"), + tr("Cannot load local DXCC entities from the logbook: ") + entityQuery.lastError().text()); + return; + } + + QStringList items; + QMap itemToEntity; + + while ( entityQuery.next() ) + { + const QString entity = entityQuery.value(0).toString(); + const QString country = entityQuery.value(1).toString(); + const QString callsigns = entityQuery.value(2).toString(); + + QString item = QString("%1 - %2").arg(entity, + country.isEmpty() ? tr("Unknown DXCC Entity") : country); + if ( !callsigns.isEmpty() ) + item += QString(" (%1)").arg(callsigns); + + items << item; + itemToEntity.insert(item, entity); + } + + if ( items.isEmpty() ) + { + QMessageBox::warning(this, + tr("QLog Warning"), + tr("Cannot determine a local DXCC entity from logbook contacts.")); + return; + } + + bool ok = false; + const QString selectedItem = QInputDialog::getItem(this, + tr("LoTW DXCC Credits"), + tr("Select the local DXCC entity for which LoTW DXCC credits will be downloaded:"), + items, + 0, + false, + &ok); + if ( !ok ) + return; + + const QString selectedEntity = itemToEntity.value(selectedItem); + + LotwDXCCCreditDownloader *downloader = new LotwDXCCCreditDownloader(this); + QProgressDialog *progressDialog = new QProgressDialog("", tr("Cancel"), 0, 0, this); + progressDialog->setWindowModality(Qt::WindowModal); + progressDialog->setRange(0, 0); + progressDialog->setAttribute(Qt::WA_DeleteOnClose, true); + progressDialog->setLabelText(tr("Downloading LoTW DXCC credits")); + progressDialog->show(); + + connect(downloader, &LotwDXCCCreditDownloader::downloadProgress, + progressDialog, &QProgressDialog::setValue); + + connect(downloader, &LotwDXCCCreditDownloader::downloadStarted, this, [progressDialog] + { + progressDialog->setLabelText(tr("Processing LoTW DXCC credits")); + progressDialog->setRange(0, 100); + }); + + connect(downloader, &LotwDXCCCreditDownloader::downloadComplete, this, + [this, downloader, progressDialog](const QSLMergeStat &stats) + { + progressDialog->done(QDialog::Accepted); + downloader->deleteLater(); + ui->logbookWidget->updateTable(); + + QSLImportStatDialog statDialog(stats, this); + statDialog.setWindowTitle(tr("LoTW DXCC Credit Import Summary")); + statDialog.exec(); + }); + + connect(downloader, &LotwDXCCCreditDownloader::downloadFailed, this, + [this, downloader, progressDialog](const QString &error) + { + progressDialog->done(QDialog::Accepted); + QMessageBox::critical(this, tr("QLog Error"), tr("LoTW DXCC credit import failed: ") + error); + downloader->deleteLater(); + }); + + connect(progressDialog, &QProgressDialog::canceled, this, [downloader]() + { + qCDebug(runtime) << "Operation canceled"; + downloader->abortDownload(); + downloader->deleteLater(); + }); + + downloader->downloadCredits(selectedEntity); +} + void MainWindow::showQSLGallery() { FCT_IDENTIFICATION; @@ -2090,14 +2232,15 @@ MainWindow::~MainWindow() //saveEquipmentConnOptions(); - Rig::instance()->close(); - Rotator::instance()->close(); - CWKeyer::instance()->close(); - QThread::msleep(500); + // These singletons live on worker threads. Disconnect them before shutdown + // so queued signals cannot land on widgets that are about to be deleted. + QObject::disconnect(CWKeyer::instance(), nullptr, nullptr, nullptr); + QObject::disconnect(Rotator::instance(), nullptr, nullptr, nullptr); + QObject::disconnect(Rig::instance(), nullptr, nullptr, nullptr); - Rig::instance()->stopTimer(); - Rotator::instance()->stopTimer(); - CWKeyer::instance()->stopTimer(); + CWKeyer::instance()->shutdown(); + Rotator::instance()->shutdown(); + Rig::instance()->shutdown(); conditions->deleteLater(); conditionsLabel->deleteLater(); diff --git a/ui/MainWindow.h b/ui/MainWindow.h index 4cfd796a..143664da 100644 --- a/ui/MainWindow.h +++ b/ui/MainWindow.h @@ -16,6 +16,7 @@ class MainWindow; class QLabel; class WsjtxUDPReceiver; +class AdifRecoveryManager; class MainWindow : public QMainWindow { Q_OBJECT @@ -78,6 +79,7 @@ private slots: void showEditLayout(); void showServiceUpload(); void showServiceDownloadQSL(); + void showServiceDownloadLotwDXCCCredits(); void showDumpDB(); void showLoadDB(); void showQSLGallery(); @@ -117,6 +119,7 @@ private slots: PropConditions *conditions; bool isFusionStyle; ClubLogUploader* clublogRT; + AdifRecoveryManager* adifRecoveryManager; WsjtxUDPReceiver* wsjtx; QActionGroup *seqGroup; QActionGroup *dupeGroup; diff --git a/ui/MainWindow.ui b/ui/MainWindow.ui index e75c9479..59a69f2b 100644 --- a/ui/MainWindow.ui +++ b/ui/MainWindow.ui @@ -127,6 +127,8 @@
+ +
@@ -512,8 +514,11 @@ + + + - Print QSL &Labels + Print QS&L QAction::NoRole @@ -1071,6 +1076,17 @@ true + + + Download LoTW DXCC Credits + + + Service - Download LoTW DXCC Credits + + + QAction::NoRole + + true @@ -1899,6 +1915,22 @@ + + actionServiceDownloadLotwDXCCCredits + triggered() + MainWindow + showServiceDownloadLotwDXCCCredits() + + + -1 + -1 + + + 456 + 278 + + + actionWhatsNew triggered() @@ -2008,6 +2040,7 @@ setEquipmentKeepOptions(bool) stopContest() showServiceDownloadQSL() + showServiceDownloadLotwDXCCCredits() showServiceUpload() showWhatsNew() showDumpDB() diff --git a/ui/MapLayer.h b/ui/MapLayer.h new file mode 100644 index 00000000..9f5291dc --- /dev/null +++ b/ui/MapLayer.h @@ -0,0 +1,26 @@ +#ifndef QLOG_UI_MAPLAYER_H +#define QLOG_UI_MAPLAYER_H + +#include + +class MapLayer +{ +public: + enum Layer + { + Grid = 0x0001, + Grayline = 0x0002, + Aurora = 0x0004, + Muf = 0x0008, + Ibp = 0x0010, + Beam = 0x0020, + Chat = 0x0040, + Wsjtx = 0x0080, + Path = 0x0100 + }; + Q_DECLARE_FLAGS(Layers, Layer) +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(MapLayer::Layers) + +#endif // QLOG_UI_MAPLAYER_H diff --git a/ui/MapPageController.cpp b/ui/MapPageController.cpp new file mode 100644 index 00000000..d9a7f1dc --- /dev/null +++ b/ui/MapPageController.cpp @@ -0,0 +1,586 @@ +#include "MapPageController.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/debug.h" +#include "core/IBPBeacon.h" +#include "core/LogParam.h" +#include "ui/WebEnginePage.h" + +MODULE_IDENTIFICATION("qlog.ui.mappagecontroller"); + +QString MapPageController::jsonArray(const QJsonArray &array) +{ + return QString::fromUtf8(QJsonDocument(array).toJson(QJsonDocument::Compact)); +} + +QString MapPageController::jsonObject(const QJsonObject &object) +{ + return QString::fromUtf8(QJsonDocument(object).toJson(QJsonDocument::Compact)); +} + +QString MapPageController::jsonString(const QString &string) +{ + QJsonArray array; + array.append(string); + + QString ret = jsonArray(array); + ret.remove(0, 1); + ret.chop(1); + return ret; +} + +QJsonObject MapPageController::coordinateObject(const MapCoordinate &coordinate) +{ + QJsonObject object; + object.insert(QStringLiteral("lat"), coordinate.latitude); + object.insert(QStringLiteral("lng"), coordinate.longitude); + return object; +} + +QJsonObject MapPageController::pointObject(const MapPoint &point) +{ + QJsonObject object; + object.insert(QStringLiteral("label"), point.label); + object.insert(QStringLiteral("lat"), point.coordinate.latitude); + object.insert(QStringLiteral("lng"), point.coordinate.longitude); + object.insert(QStringLiteral("icon"), point.icon); + return object; +} + +QJsonArray MapPageController::coordinateArray(const MapCoordinate &coordinate) +{ + QJsonArray array; + array.append(coordinate.latitude); + array.append(coordinate.longitude); + return array; +} + +QJsonArray MapPageController::pointsArray(const QList &points) +{ + QJsonArray array; + + for ( const MapPoint &point : points ) + array.append(pointObject(point)); + + return array; +} + +QJsonArray MapPageController::coordinatesArray(const QList &coordinates) +{ + QJsonArray array; + + for ( const MapCoordinate &coordinate : coordinates ) + array.append(coordinateArray(coordinate)); + + return array; +} + +QJsonArray MapPageController::pathsArray(const QList &paths) +{ + QJsonArray array; + + for ( const MapPath &path : paths ) + { + QJsonObject item; + item.insert(QStringLiteral("from"), coordinateObject(path.from)); + item.insert(QStringLiteral("to"), coordinateObject(path.to)); + array.append(item); + } + + return array; +} + +QJsonArray MapPageController::stringArray(const QStringList &strings) +{ + QJsonArray array; + + for ( const QString &string : strings ) + array.append(string); + + return array; +} + +QJsonArray MapPageController::heatPointsArray(const QList &points) +{ + QJsonArray array; + + for ( const MapHeatPoint &point : points ) + { + QJsonObject item; + item.insert(QStringLiteral("lat"), point.coordinate.latitude); + item.insert(QStringLiteral("lng"), point.coordinate.longitude); + item.insert(QStringLiteral("count"), point.value); + array.append(item); + } + + return array; +} + +QJsonArray MapPageController::ibpBandsArray() +{ + QJsonArray array; + + for ( const IBPBeacon::Band &band : IBPBeacon::bands() ) + { + QJsonObject item; + item.insert(QStringLiteral("name"), band.name); + item.insert(QStringLiteral("frequency"), band.frequency); + array.append(item); + } + + return array; +} + +QJsonArray MapPageController::ibpBeaconsArray() +{ + QJsonArray array; + + for ( const IBPBeacon::Station &beacon : IBPBeacon::beacons() ) + { + QJsonObject item; + item.insert(QStringLiteral("callsign"), beacon.callsign); + item.insert(QStringLiteral("lat"), beacon.latitude); + item.insert(QStringLiteral("lon"), beacon.longitude); + item.insert(QStringLiteral("active"), beacon.active); + array.append(item); + } + + return array; +} + +MapPageController::MapPageController(const QString &configID, + QObject *parent) + : QObject(parent), + configID(configID), + mainPage(new WebEnginePage(this)), + pageLoaded(false) +{ + channel.registerObject("mapBridge", this); +} + +MapPageController::~MapPageController() +{ + if ( attachedView && attachedView->page() == mainPage ) + attachedView->setPage(nullptr); + + if ( mainPage ) + mainPage->setWebChannel(nullptr); + + channel.deregisterObject(this); +} + +void MapPageController::attach(QWebEngineView *view, + MapLayer::Layers layers) +{ + FCT_IDENTIFICATION; + + if ( !view ) + return; + + if ( attachedView ) + { + disconnect(attachedView.data(), &QWebEngineView::loadFinished, + this, &MapPageController::finishLoading); + if ( attachedView->page() == mainPage ) + attachedView->setPage(nullptr); + } + + attachedView = view; + mapLayers = layers; + mainPage->setWebChannel(&channel); + view->setPage(mainPage); + connect(view, &QWebEngineView::loadFinished, + this, &MapPageController::finishLoading, + Qt::UniqueConnection); + mainPage->load(QUrl(QStringLiteral("qrc:/res/map/onlinemap.html"))); + view->setFocusPolicy(Qt::ClickFocus); +} + +void MapPageController::runJavaScript(const QString &js) +{ + FCT_IDENTIFICATION; + + qCDebug(function_parameters) << js; + + if ( !pageLoaded ) + postponedScripts.append(js); + else + mainPage->runJavaScript(js); +} + +void MapPageController::setDarkTheme(bool isDark) +{ + FCT_IDENTIFICATION; + + const QString js = isDark + ? QLatin1String("if (typeof setMapDarkMode === \"function\") setMapDarkMode(true);") + : QLatin1String("if (typeof setMapDarkMode === \"function\") setMapDarkMode(false);"); + runJavaScript(js); +} + +void MapPageController::setStaticMapTime(const QDateTime &dateTime) +{ + FCT_IDENTIFICATION; + + runJavaScript(QStringLiteral("setStaticMapTime(new Date(%1));") + .arg(dateTime.toMSecsSinceEpoch())); +} + +void MapPageController::drawPoints(const QList &points) +{ + FCT_IDENTIFICATION; + + runJavaScript(QStringLiteral("drawPoints(%1);") + .arg(jsonArray(pointsArray(points)))); +} + +void MapPageController::drawPointsBusy(const QList &points, + const QString &text) +{ + FCT_IDENTIFICATION; + + runJavaScript(QStringLiteral("drawPointsBusy(%1, %2);") + .arg(jsonArray(pointsArray(points)), + jsonString(text))); +} + +void MapPageController::drawPointsAndShortPathsBusy(const QList &points, + const QList &paths, + const QString &text) +{ + FCT_IDENTIFICATION; + + runJavaScript(QStringLiteral("drawPointsAndShortPathsBusy(%1, %2, %3);") + .arg(jsonArray(pointsArray(points)), + jsonArray(pathsArray(paths)), + jsonString(text))); +} + +void MapPageController::drawHomePoints(const QList &points) +{ + FCT_IDENTIFICATION; + + runJavaScript(QStringLiteral("drawPointsGroup2(%1);") + .arg(jsonArray(pointsArray(points)))); +} + +void MapPageController::drawChatPoints(const QList &points) +{ + FCT_IDENTIFICATION; + + runJavaScript(QStringLiteral("drawPointsGroup3(%1);") + .arg(jsonArray(pointsArray(points)))); +} + +void MapPageController::flyToPoint(const MapPoint &point, int zoom) +{ + FCT_IDENTIFICATION; + + runJavaScript(QStringLiteral("flyToPoint(%1, %2);") + .arg(jsonObject(pointObject(point))) + .arg(zoom)); +} + +void MapPageController::drawPath(const QList &points) +{ + FCT_IDENTIFICATION; + + runJavaScript(QStringLiteral("drawPath(%1);") + .arg(jsonArray(coordinatesArray(points)))); +} + +void MapPageController::clearPath() +{ + FCT_IDENTIFICATION; + + drawPath(QList()); +} + +void MapPageController::drawShortPaths(const QList &paths) +{ + FCT_IDENTIFICATION; + + runJavaScript(QStringLiteral("drawShortPaths(%1);") + .arg(jsonArray(pathsArray(paths)))); +} + +void MapPageController::drawShortPathsBusy(const QList &paths, + const QString &text) +{ + FCT_IDENTIFICATION; + + runJavaScript(QStringLiteral("drawShortPathsBusy(%1, %2);") + .arg(jsonArray(pathsArray(paths)), + jsonString(text))); +} + +void MapPageController::drawAntPath(const MapCoordinate &from, + double distance, + double azimuth, + double antAngle) +{ + FCT_IDENTIFICATION; + + runJavaScript(QStringLiteral("drawAntPath(%1, %2, %3, %4);") + .arg(jsonObject(coordinateObject(from))) + .arg(distance) + .arg(azimuth) + .arg(antAngle)); +} + +void MapPageController::clearAntPath() +{ + FCT_IDENTIFICATION; + + runJavaScript(QLatin1String("drawAntPath({}, 0, 0, 0);")); +} + +void MapPageController::setGridLayers(const QStringList &confirmedGrids, + const QStringList &workedGrids) +{ + FCT_IDENTIFICATION; + + runJavaScript(QStringLiteral("grids_confirmed = %1;" + "grids_worked = %2;") + .arg(jsonArray(stringArray(confirmedGrids)), + jsonArray(stringArray(workedGrids)))); +} + +void MapPageController::clearGridLayers() +{ + FCT_IDENTIFICATION; + + setGridLayers(QStringList(), QStringList()); +} + +void MapPageController::redrawGridLayer() +{ + FCT_IDENTIFICATION; + + runJavaScript(QLatin1String("maidenheadConfWorked.redraw();")); +} + +void MapPageController::invalidateSize() +{ + FCT_IDENTIFICATION; + + runJavaScript(QLatin1String("if (typeof map !== 'undefined') " + "setTimeout(function() { " + "map.invalidateSize({pan: false, animate: false}); " + "}, 0);")); +} + +void MapPageController::panToBoundsLongitudeCenter(const QList &coordinates) +{ + FCT_IDENTIFICATION; + + if ( coordinates.isEmpty() ) + return; + + runJavaScript(QStringLiteral("map.panTo([0, L.latLngBounds(%1).getCenter().lng]);") + .arg(jsonArray(coordinatesArray(coordinates)))); +} + +void MapPageController::setAuroraData(const QList &points) +{ + FCT_IDENTIFICATION; + + runJavaScript(QStringLiteral("auroraLayer.setData({max: 100, data:%1});") + .arg(jsonArray(heatPointsArray(points)))); +} + +void MapPageController::drawMuf(const QList &points) +{ + FCT_IDENTIFICATION; + + runJavaScript(QStringLiteral("drawMuf(%1);") + .arg(jsonArray(pointsArray(points)))); +} + +void MapPageController::setCurrentBand(const QString &band) +{ + FCT_IDENTIFICATION; + + runJavaScript(QStringLiteral("setIbpCurrentBand(%1);") + .arg(jsonString(band))); +} + +void MapPageController::addWsjtxSpot(const MapPoint &point, + const QString &color, + const QString &textColor) +{ + FCT_IDENTIFICATION; + + runJavaScript(QStringLiteral("addWSJTXSpot(%1, %2, %3);") + .arg(jsonObject(pointObject(point)), + jsonString(color), + jsonString(textColor))); +} + +void MapPageController::clearWsjtxSpots() +{ + FCT_IDENTIFICATION; + + runJavaScript(QLatin1String("clearWSJTXSpots();")); +} + +QString MapPageController::generateIbpDataJS() +{ + FCT_IDENTIFICATION; + + return QStringLiteral("configureIbpData(%1, %2);") + .arg(jsonArray(ibpBandsArray()), + jsonArray(ibpBeaconsArray())); +} + +QString MapPageController::generateLayerControlJS(MapLayer::Layers layers) +{ + FCT_IDENTIFICATION; + QJsonArray options; + + auto appendOption = [&options, layers](MapLayer::Layer layer, + const QString &label, + const QString &key) + { + if ( !layers.testFlag(layer) ) + return; + + QJsonObject option; + option.insert(QStringLiteral("label"), label); + option.insert(QStringLiteral("key"), key); + options.append(option); + }; + + appendOption(MapLayer::Aurora, tr("Aurora"), QStringLiteral("auroraLayer")); + + appendOption(MapLayer::Beam, tr("Beam"), QStringLiteral("antPathLayer")); + + appendOption(MapLayer::Chat, tr("Chat"), QStringLiteral("chatStationsLayer")); + + appendOption(MapLayer::Grid, tr("Grid"), QStringLiteral("maidenheadConfWorked")); + + appendOption(MapLayer::Grayline, tr("Gray-Line"), QStringLiteral("grayline")); + + appendOption(MapLayer::Ibp, tr("IBP"), QStringLiteral("IBPLayer")); + + appendOption(MapLayer::Muf, tr("MUF"), QStringLiteral("mufLayer")); + + appendOption(MapLayer::Wsjtx, tr("WSJTX - CQ"), QStringLiteral("wsjtxStationsLayer")); + + appendOption(MapLayer::Path, tr("Path"), QStringLiteral("pathLayer")); + + QString ret = QStringLiteral("configureLayerControl(%1);") + .arg(jsonArray(options)); + + qCDebug(runtime) << ret; + + return ret; +} + +void MapPageController::restoreLayerControlStates() +{ + FCT_IDENTIFICATION; + + QJsonArray layerStates; + + const QStringList &keys = LogParam::getMapLayerStates(configID); + + for ( const QString &key : keys ) + { + qCDebug(runtime) << "key:" << key << "value:" << LogParam::getMapLayerState(configID, key); + + QJsonObject layerState; + layerState.insert(QStringLiteral("key"), key); + layerState.insert(QStringLiteral("visible"), LogParam::getMapLayerState(configID, key)); + layerStates.append(layerState); + } + + const QString js = QStringLiteral("restoreQLogLayerStates(%1);") + .arg(jsonArray(layerStates)); + qCDebug(runtime) << js; + + mainPage->runJavaScript(js); + + connectWebChannel(); +} + +void MapPageController::connectWebChannel() +{ + FCT_IDENTIFICATION; + + QFile file(":/qtwebchannel/qwebchannel.js"); + + if ( !file.open(QIODevice::ReadOnly) ) + { + qCInfo(runtime) << "Cannot read qwebchannel.js"; + return; + } + + QTextStream stream(&file); + QString js; + + js.append(stream.readAll()); + js += " var webChannel = new QWebChannel(qt.webChannelTransport, function(channel) " + "{ " + " window.mapBridge = channel.objects.mapBridge; " + " if (window.connectQtMapBridge) " + " window.connectQtMapBridge(window.mapBridge); " + "});"; + mainPage->runJavaScript(js); +} + +void MapPageController::handleLayerSelectionChanged(const QVariant &data, const QVariant &state) +{ + FCT_IDENTIFICATION; + + qCDebug(function_parameters) << data << state; + + LogParam::setMapLayerState(configID, data.toString(), + (state.toString().toLower() == "on") ? true : false); +} + +void MapPageController::chatCallsignClicked(const QVariant &data) +{ + FCT_IDENTIFICATION; + + emit chatCallsignPressed(data.toString()); +} + +void MapPageController::wsjtxCallsignClicked(const QVariant &data) +{ + FCT_IDENTIFICATION; + + emit wsjtxCallsignPressed(data.toString()); +} + +void MapPageController::IBPCallsignClicked(const QVariant &callsign, const QVariant &freq) +{ + FCT_IDENTIFICATION; + + emit IBPPressed(callsign.toString(), freq.toDouble()); +} + +void MapPageController::finishLoading(bool ok) +{ + FCT_IDENTIFICATION; + + if ( pageLoaded || !ok ) + return; + + pageLoaded = true; + postponedScripts.append(generateIbpDataJS()); + postponedScripts.append(generateLayerControlJS(mapLayers)); + mainPage->runJavaScript(postponedScripts.join(QLatin1Char('\n'))); + postponedScripts.clear(); + + restoreLayerControlStates(); + emit loaded(); +} diff --git a/ui/MapPageController.h b/ui/MapPageController.h new file mode 100644 index 00000000..86952600 --- /dev/null +++ b/ui/MapPageController.h @@ -0,0 +1,177 @@ +#ifndef QLOG_UI_MAPPAGECONTROLLER_H +#define QLOG_UI_MAPPAGECONTROLLER_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ui/MapLayer.h" + +class WebEnginePage; +class QJsonArray; +class QJsonObject; +class QWebEngineView; + +struct MapCoordinate +{ + MapCoordinate() = default; + MapCoordinate(double inLatitude, double inLongitude) + : latitude(inLatitude), + longitude(inLongitude) + { + } + + double latitude = 0.0; + double longitude = 0.0; +}; + +struct MapPoint +{ + MapPoint() = default; + MapPoint(const QString &inLabel, + double inLatitude, + double inLongitude, + const QString &inIcon = QString()) + : label(inLabel), + coordinate(inLatitude, inLongitude), + icon(inIcon) + { + } + + QString label; + MapCoordinate coordinate; + QString icon; +}; + +struct MapPath +{ + MapPath() = default; + MapPath(const MapCoordinate &inFrom, + const MapCoordinate &inTo) + : from(inFrom), + to(inTo) + { + } + + MapCoordinate from; + MapCoordinate to; +}; + +struct MapHeatPoint +{ + MapHeatPoint() = default; + MapHeatPoint(double inLatitude, + double inLongitude, + double inValue) + : coordinate(inLatitude, inLongitude), + value(inValue) + { + } + + MapCoordinate coordinate; + double value = 0.0; +}; + +class MapPageController : public QObject +{ + Q_OBJECT + +public: + explicit MapPageController(const QString &configID, + QObject *parent = nullptr); + ~MapPageController() override; + + void attach(QWebEngineView *view, + MapLayer::Layers layers); + + void setDarkTheme(bool isDark); + void setStaticMapTime(const QDateTime &dateTime); + + void drawPoints(const QList &points); + void drawPointsBusy(const QList &points, + const QString &text); + void drawPointsAndShortPathsBusy(const QList &points, + const QList &paths, + const QString &text); + void drawHomePoints(const QList &points); + void drawChatPoints(const QList &points); + void flyToPoint(const MapPoint &point, int zoom); + + void drawPath(const QList &points); + void clearPath(); + void drawShortPaths(const QList &paths); + void drawShortPathsBusy(const QList &paths, + const QString &text); + void drawAntPath(const MapCoordinate &from, + double distance, + double azimuth, + double antAngle); + void clearAntPath(); + + void setGridLayers(const QStringList &confirmedGrids, + const QStringList &workedGrids); + void clearGridLayers(); + void redrawGridLayer(); + void invalidateSize(); + void panToBoundsLongitudeCenter(const QList &coordinates); + + void setAuroraData(const QList &points); + void drawMuf(const QList &points); + void setCurrentBand(const QString &band); + + void addWsjtxSpot(const MapPoint &point, + const QString &color, + const QString &textColor); + void clearWsjtxSpots(); + +signals: + void loaded(); + void chatCallsignPressed(QString callsign); + void wsjtxCallsignPressed(QString callsign); + void IBPPressed(QString callsign, double frequency); + +public slots: + void handleLayerSelectionChanged(const QVariant &data, + const QVariant &state); + void chatCallsignClicked(const QVariant &data); + void wsjtxCallsignClicked(const QVariant &data); + void IBPCallsignClicked(const QVariant &callsign, + const QVariant &freq); + +private: + static QString jsonArray(const QJsonArray &array); + static QString jsonObject(const QJsonObject &object); + static QString jsonString(const QString &string); + static QJsonObject pointObject(const MapPoint &point); + static QJsonObject coordinateObject(const MapCoordinate &coordinate); + static QJsonArray coordinateArray(const MapCoordinate &coordinate); + static QJsonArray pointsArray(const QList &points); + static QJsonArray coordinatesArray(const QList &coordinates); + static QJsonArray pathsArray(const QList &paths); + static QJsonArray stringArray(const QStringList &strings); + static QJsonArray heatPointsArray(const QList &points); + static QJsonArray ibpBandsArray(); + static QJsonArray ibpBeaconsArray(); + + QString generateIbpDataJS(); + QString generateLayerControlJS(MapLayer::Layers layers); + void restoreLayerControlStates(); + void connectWebChannel(); + void finishLoading(bool ok); + void runJavaScript(const QString &js); + + QString configID; + WebEnginePage *mainPage; + QPointer attachedView; + bool pageLoaded; + QStringList postponedScripts; + QWebChannel channel; + MapLayer::Layers mapLayers; +}; + +#endif // QLOG_UI_MAPPAGECONTROLLER_H diff --git a/ui/MapWebChannelHandler.cpp b/ui/MapWebChannelHandler.cpp deleted file mode 100644 index 4d0e04c2..00000000 --- a/ui/MapWebChannelHandler.cpp +++ /dev/null @@ -1,199 +0,0 @@ -#include - -#include "MapWebChannelHandler.h" -#include "core/debug.h" -#include "core/LogParam.h" - -MODULE_IDENTIFICATION("qlog.ui.maplayercontrolhandler"); - -MapWebChannelHandler::MapWebChannelHandler(const QString &configID, - QObject *parent) - : QObject(parent), configID(configID) -{ -} - -void MapWebChannelHandler::connectWebChannel(QWebEnginePage *page) -{ - FCT_IDENTIFICATION; - - QFile file(":/qtwebchannel/qwebchannel.js"); - - if (!file.open(QIODevice::ReadOnly)) - { - qCInfo(runtime) << "Cannot read qwebchannel.js"; - return; - } - - QTextStream stream(&file); - QString js; - - js.append(stream.readAll()); - js += " var webChannel = new QWebChannel(qt.webChannelTransport, function(channel) " - "{ window.foo = channel.objects.layerControlHandler; });" - " map.on('overlayadd', function(e){ " - " switch (e.name) " - " { " - " case '" + tr("Grid") + "': " - " foo.handleLayerSelectionChanged('maidenheadConfWorked', 'on'); " - " break; " - " case '" + tr("Gray-Line") + "': " - " foo.handleLayerSelectionChanged('grayline', 'on'); " - " break; " - " case '" + tr("Beam") + "': " - " foo.handleLayerSelectionChanged('antPathLayer', 'on'); " - " break; " - " case '" + tr("Aurora") + "': " - " foo.handleLayerSelectionChanged('auroraLayer', 'on'); " - " break; " - " case '" + tr("MUF") + "': " - " foo.handleLayerSelectionChanged('mufLayer', 'on'); " - " break; " - " case '" + tr("IBP") + "': " - " foo.handleLayerSelectionChanged('IBPLayer', 'on'); " - " break; " - " case '" + tr("Chat") + "': " - " foo.handleLayerSelectionChanged('chatStationsLayer', 'on'); " - " break; " - " case '" + tr("WSJTX - CQ") + "': " - " foo.handleLayerSelectionChanged('wsjtxStationsLayer', 'on'); " - " break; " - " case '" + tr("Path") + "': " - " foo.handleLayerSelectionChanged('pathLayer', 'on'); " - " break; " - " } " - "});" - "map.on('overlayremove', function(e){ " - " switch (e.name) " - " { " - " case '" + tr("Grid") + "': " - " foo.handleLayerSelectionChanged('maidenheadConfWorked', 'off'); " - " break; " - " case '" + tr("Gray-Line") + "': " - " foo.handleLayerSelectionChanged('grayline', 'off'); " - " break; " - " case '" + tr("Beam") + "': " - " foo.handleLayerSelectionChanged('antPathLayer', 'off'); " - " break; " - " case '" + tr("Aurora") + "': " - " foo.handleLayerSelectionChanged('auroraLayer', 'off'); " - " break; " - " case '" + tr("MUF") + "': " - " foo.handleLayerSelectionChanged('mufLayer', 'off'); " - " break; " - " case '" + tr("IBP") + "': " - " foo.handleLayerSelectionChanged('IBPLayer', 'off'); " - " break; " - " case '" + tr("Chat") + "': " - " foo.handleLayerSelectionChanged('chatStationsLayer', 'off'); " - " break; " - " case '" + tr("WSJTX - CQ") + "': " - " foo.handleLayerSelectionChanged('wsjtxStationsLayer', 'off'); " - " break; " - " case '" + tr("Path") + "': " - " foo.handleLayerSelectionChanged('pathLayer', 'off'); " - " break; " - " } " - "});"; - page->runJavaScript(js); -} - -void MapWebChannelHandler::restoreLayerControlStates(QWebEnginePage *page) -{ - FCT_IDENTIFICATION; - - QString js; - - const QStringList &keys = LogParam::getMapLayerStates(configID); - - for ( const QString &key : keys) - { - qCDebug(runtime) << "key:" << key << "value:" << LogParam::getMapLayerState(configID, key); - - js += ( LogParam::getMapLayerState(configID, key) ) ? QString("map.addLayer(%1);").arg(key) - : QString("map.removeLayer(%1);").arg(key); - } - qCDebug(runtime) << js; - - page->runJavaScript(js); - - connectWebChannel(page); -} - -QString MapWebChannelHandler::generateMapMenuJS(bool gridLayer, - bool grayline, - bool aurora, - bool muf, - bool ibp, - bool antpath, - bool chatStations, - bool wsjtxStations, - bool paths) -{ - FCT_IDENTIFICATION; - QStringList options; - - if ( aurora ) - options << "\"" + tr("Aurora") + "\": auroraLayer"; - - if ( antpath ) - options << "\"" + tr("Beam") + "\": antPathLayer"; - - if ( chatStations ) - options << "\"" + tr("Chat") + "\": chatStationsLayer"; - - if ( gridLayer ) - options << "\"" + tr("Grid") + "\": maidenheadConfWorked"; - - if ( grayline ) - options << "\"" + tr("Gray-Line") + "\": grayline"; - - if ( ibp ) - options << "\"" + tr("IBP") + "\": IBPLayer"; - - if ( muf ) - options << "\"" + tr("MUF") + "\": mufLayer"; - - if ( wsjtxStations ) - options << "\"" + tr("WSJTX - CQ") + "\": wsjtxStationsLayer"; - - if ( paths ) - options << "\"" + tr("Path") + "\": pathLayer"; - - QString ret = QString("var layerControl = new L.Control.Layers(null," - "{ %1 },{}).addTo(map);").arg(options.join(",")); - - qCDebug(runtime) << ret; - - return ret; -} - -void MapWebChannelHandler::handleLayerSelectionChanged(const QVariant &data, const QVariant &state) -{ - FCT_IDENTIFICATION; - - qCDebug(function_parameters) << data << state; - - LogParam::setMapLayerState(configID, data.toString(), - (state.toString().toLower() == "on") ? true : false); -} - -void MapWebChannelHandler::chatCallsignClicked(const QVariant &data) -{ - FCT_IDENTIFICATION; - - emit chatCallsignPressed(data.toString()); -} - -void MapWebChannelHandler::wsjtxCallsignClicked(const QVariant &data) -{ - FCT_IDENTIFICATION; - - emit wsjtxCallsignPressed(data.toString()); -} - -void MapWebChannelHandler::IBPCallsignClicked(const QVariant &callsign, const QVariant &freq) -{ - FCT_IDENTIFICATION; - - emit IBPPressed(callsign.toString(), freq.toDouble()); -} diff --git a/ui/MapWebChannelHandler.h b/ui/MapWebChannelHandler.h deleted file mode 100644 index c29032a2..00000000 --- a/ui/MapWebChannelHandler.h +++ /dev/null @@ -1,42 +0,0 @@ -#ifndef QLOG_UI_MAPWEBCHANNELHANDLER_H -#define QLOG_UI_MAPWEBCHANNELHANDLER_H - -#include -#include - -class MapWebChannelHandler : public QObject -{ - Q_OBJECT -public: - explicit MapWebChannelHandler(const QString &configID, - QObject *parent = nullptr); - void restoreLayerControlStates(QWebEnginePage *page); - QString generateMapMenuJS(bool gridLayer = true, - bool grayline = false, - bool aurora = false, - bool muf = false, - bool ibp = false, - bool antpath = false, - bool chatStations = false, - bool wsjtxStations = false, - bool paths = false); - -signals: - void chatCallsignPressed(QString); - void wsjtxCallsignPressed(QString); - void IBPPressed(QString, double); - -public slots: - void handleLayerSelectionChanged(const QVariant &data, - const QVariant &state); - void chatCallsignClicked(const QVariant &data); - void wsjtxCallsignClicked(const QVariant &data); - void IBPCallsignClicked(const QVariant &callsign, - const QVariant &freq); -private: - QString configID; - - void connectWebChannel(QWebEnginePage *page); -}; - -#endif // QLOG_UI_MAPWEBCHANNELHANDLER_H diff --git a/ui/MapWidget.cpp b/ui/MapWidget.cpp index 43a0f371..9f5b5626 100644 --- a/ui/MapWidget.cpp +++ b/ui/MapWidget.cpp @@ -61,7 +61,7 @@ void MapWidget::clear() i.remove(); } - Gridsquare myGrid(StationProfilesManager::instance()->getCurProfile1().locator); + const Gridsquare myGrid = Gridsquare::mapDisplayGrid(StationProfilesManager::instance()->getCurProfile1().locator); if ( myGrid.isValid() ) { @@ -298,7 +298,7 @@ void MapWidget::setTarget(double lat, double lon) if ( qIsNaN(lat) || qIsNaN(lon) ) return; - Gridsquare myGrid(StationProfilesManager::instance()->getCurProfile1().locator); + const Gridsquare myGrid = Gridsquare::mapDisplayGrid(StationProfilesManager::instance()->getCurProfile1().locator); QPoint point = coordToPoint(lat, lon); drawPoint(point); diff --git a/ui/ModeSelectionController.cpp b/ui/ModeSelectionController.cpp index 7d72e5bf..90a967b9 100644 --- a/ui/ModeSelectionController.cpp +++ b/ui/ModeSelectionController.cpp @@ -1,7 +1,7 @@ -#include #include #include "ModeSelectionController.h" #include "core/debug.h" +#include "data/Data.h" MODULE_IDENTIFICATION("qlog.ui.modeselectioncontroller"); @@ -66,8 +66,7 @@ void ModeSelectionController::applyCurrentMode() return; } - const QString submodes = record.value("submodes").toString(); - const QStringList submodeList = QJsonDocument::fromJson(submodes.toUtf8()).toVariant().toStringList(); + const QStringList submodeList = Data::instance()->submodesForMode(record.value("name").toString()); applySubmodes(submodeList); emit defaultReportChanged(record.value("rprt").toString()); diff --git a/ui/NewContactWidget.cpp b/ui/NewContactWidget.cpp index e8eced84..095518b5 100644 --- a/ui/NewContactWidget.cpp +++ b/ui/NewContactWidget.cpp @@ -702,14 +702,17 @@ void NewContactWidget::setMembershipList(const QString &in_callsign, QString memberText; QMapIterator clubs(data); - QPalette palette; while ( clubs.hasNext() ) { clubs.next(); - const QColor &color = Data::statusToColor(static_cast(clubs.value().status), false, palette.color(QPalette::Text)); - //"Hello World" - memberText.append(QString("%2   ").arg(Data::colorToHTMLColor(color), clubs.key())); + const QColor color = Data::statusToColor(static_cast(clubs.value().status), false, QColor()); + const QString clubName = clubs.key().toHtmlEscaped(); + + if ( color.isValid() && color.alpha() > 0 ) + memberText.append(QString("%2   ").arg(Data::colorToHTMLColor(color), clubName)); + else + memberText.append(QString("%1   ").arg(clubName)); if ( clubs.key().toUpper() == "SKCC" && uiDynamic->skccEdit->text().isEmpty() @@ -1975,6 +1978,10 @@ void NewContactWidget::saveExternalContact(QSqlRecord record) record.setValue("cont", dxcc.cont); } + // Issue #1028: raw WSJT/JTDX messages can + // contain GRIDSQUARE + GRIDSQUARE_EXT in one field. + AdiFormat::normalizeGridFields(record); + // add information from callbook if it is a known callsign // based on the poll #420, QLog adds more information from callbook if ( savedCallsign == ui->callsignEdit->text() ) @@ -2004,7 +2011,8 @@ void NewContactWidget::saveExternalContact(QSqlRecord record) record.setValue("darc_dok", uiDynamic->dokEdit->text()); // information depending on QTH (Grid) - const QString &savedGrid = record.value("gridsquare").toString(); + const QString savedGrid = record.value("gridsquare").toString(); + if ( savedGrid.startsWith(uiDynamic->gridEdit->text(), Qt::CaseSensitivity::CaseInsensitive) || uiDynamic->gridEdit->text().startsWith(savedGrid, Qt::CaseSensitivity::CaseInsensitive ) ) { @@ -2288,11 +2296,19 @@ void NewContactWidget::updateDxccStatus() ui->dxccStatus->clear(); } - QPalette palette; - palette.setColor(QPalette::Text, Data::statusToColor(status, - ui->dupeLabel->isVisible(), - palette.color(QPalette::Text))); - ui->callsignEdit->setPalette(palette); + const QColor statusColor = Data::statusToColor(status, + ui->dupeLabel->isVisible(), + QColor()); + if ( statusColor.isValid() && statusColor.alpha() > 0 ) + { + QPalette palette = ui->callsignEdit->palette(); + palette.setColor(QPalette::Text, statusColor); + ui->callsignEdit->setPalette(palette); + } + else + { + ui->callsignEdit->setPalette(QPalette()); + } } @@ -2589,20 +2605,27 @@ void NewContactWidget::setNearestSpotColor() if ( nearestSpot.callsign.isEmpty() ) { ui->nearStationLabel->clear(); + ui->nearStationLabel->setPalette(QPalette()); return; } - QPalette palette; - const DxccEntity &spotEntity = Data::instance()->lookupDxcc(nearestSpot.callsign); const DxccStatus &status = Data::instance()->dxccStatus(spotEntity.dxcc, ui->bandRXLabel->text(), ui->modeEdit->currentText()); - palette.setColor(QPalette::WindowText, - Data::statusToColor(status, - nearestSpot.dupeCount, - palette.color(QPalette::Text))); - ui->nearStationLabel->setPalette(palette); + const QColor statusColor = Data::statusToColor(status, + nearestSpot.dupeCount, + QColor()); + if ( statusColor.isValid() && statusColor.alpha() > 0 ) + { + QPalette palette = ui->nearStationLabel->palette(); + palette.setColor(QPalette::WindowText, statusColor); + ui->nearStationLabel->setPalette(palette); + } + else + { + ui->nearStationLabel->setPalette(QPalette()); + } ui->nearStationLabel->setText(nearestSpot.callsign); } diff --git a/ui/NewContactWidget.h b/ui/NewContactWidget.h index 12cb2f75..27e9b019 100644 --- a/ui/NewContactWidget.h +++ b/ui/NewContactWidget.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include "data/DxSpot.h" diff --git a/ui/OnlineMapWidget.cpp b/ui/OnlineMapWidget.cpp index ba1a44f9..1e5608ae 100644 --- a/ui/OnlineMapWidget.cpp +++ b/ui/OnlineMapWidget.cpp @@ -25,9 +25,7 @@ MODULE_IDENTIFICATION("qlog.ui.onlinemapwidget"); OnlineMapWidget::OnlineMapWidget(QWidget *parent): QWebEngineView(parent), - main_page(new WebEnginePage(this)), - isMainPageLoaded(false), - webChannelHandler("onlinemap",parent), + mapController(new MapPageController(QStringLiteral("onlinemap"), this)), prop_cond(nullptr), contact(nullptr), lastSeenAzimuth(0.0), @@ -36,24 +34,28 @@ OnlineMapWidget::OnlineMapWidget(QWidget *parent): { FCT_IDENTIFICATION; - main_page->setWebChannel(&channel); - - setPage(main_page); - main_page->load(QUrl(QLatin1String("qrc:/res/map/onlinemap.html"))); - connect(this, &OnlineMapWidget::loadFinished, this, &OnlineMapWidget::finishLoading); - + mapController->attach(this, + MapLayer::Grid + | MapLayer::Grayline + | MapLayer::Aurora + | MapLayer::Muf + | MapLayer::Ibp + | MapLayer::Beam + | MapLayer::Chat + | MapLayer::Wsjtx); + connect(mapController.data(), &MapPageController::loaded, + this, &OnlineMapWidget::finishLoading); setFocusPolicy(Qt::ClickFocus); setContextMenuPolicy(Qt::NoContextMenu); - channel.registerObject("layerControlHandler", &webChannelHandler); double freq = LogParam::getNewContactFreq(); freq += RigProfilesManager::instance()->getCurProfile1().ritOffset; setIBPBand(VFO1, 0.0, freq, 0.0); - connect(&webChannelHandler, &MapWebChannelHandler::chatCallsignPressed, this, &OnlineMapWidget::chatCallsignTrigger); - connect(&webChannelHandler, &MapWebChannelHandler::wsjtxCallsignPressed, this, &OnlineMapWidget::wsjtxCallsignTrigger); - connect(&webChannelHandler, &MapWebChannelHandler::IBPPressed, this, &OnlineMapWidget::IBPCallsignTrigger); + connect(mapController.data(), &MapPageController::chatCallsignPressed, this, &OnlineMapWidget::chatCallsignTrigger); + connect(mapController.data(), &MapPageController::wsjtxCallsignPressed, this, &OnlineMapWidget::wsjtxCallsignTrigger); + connect(mapController.data(), &MapPageController::IBPPressed, this, &OnlineMapWidget::IBPCallsignTrigger); } void OnlineMapWidget::setTarget(double lat, double lon) @@ -62,25 +64,26 @@ void OnlineMapWidget::setTarget(double lat, double lon) qCDebug(function_parameters) << lat << " " << lon; - QString targetJavaScript; - if ( ! qIsNaN(lat) && ! qIsNaN(lon) ) { /* Draw a new path */ - Gridsquare myGrid(StationProfilesManager::instance()->getCurProfile1().locator); + const Gridsquare myGrid = Gridsquare::mapDisplayGrid(StationProfilesManager::instance()->getCurProfile1().locator); if ( myGrid.isValid() ) { - targetJavaScript += QString("drawPath([{lat: %1, lng: %2}, {lat: %3, lng: %4}]);").arg(myGrid.getLatitude()) - .arg(myGrid.getLongitude()) - .arg(lat) - .arg(lon); + mapController->drawPath(QList() + << MapCoordinate(myGrid.getLatitude(), myGrid.getLongitude()) + << MapCoordinate(lat, lon)); + } + else + { + mapController->clearPath(); } } else - targetJavaScript = QLatin1String("drawPath([]);"); - - runJavaScript(targetJavaScript); + { + mapController->clearPath(); + } // redraw ant path because QSO distance can change antPositionChanged(lastSeenAzimuth, lastSeenElevation); @@ -92,24 +95,15 @@ void OnlineMapWidget::changeTheme(int theme, bool isDark) qCDebug(function_parameters) << theme << isDark; - QString themeJavaScript; - //theme == 1 dart - themeJavaScript - = (isDark == 1) - ? QLatin1String( - "map.getPanes().tilePane.style.webkitFilter=\"brightness(0.6) invert(1) " - "contrast(3) hue-rotate(200deg) saturate(0.3) brightness(0.9)\";") - : QLatin1String("map.getPanes().tilePane.style.webkitFilter=\"\";"); - - runJavaScript(themeJavaScript); + mapController->setDarkTheme(isDark); } void OnlineMapWidget::auroraDataUpdate() { FCT_IDENTIFICATION; - QStringList mapPoints; + QList mapPoints; if ( !prop_cond ) return; @@ -121,24 +115,20 @@ void OnlineMapWidget::auroraDataUpdate() { if ( point.value > 10 ) { - mapPoints << QString("{lat: %1, lng: %2, count: %3}").arg(point.latitude) - .arg(point.longitude) - .arg(point.value) - << QString("{lat: %1, lng: %2, count: %3}").arg(point.latitude) - .arg(point.longitude - 360) - .arg(point.value); + mapPoints << MapHeatPoint(point.latitude, point.longitude, point.value) + << MapHeatPoint(point.latitude, point.longitude - 360, point.value); } } } - runJavaScript(QString(" auroraLayer.setData({max: 100, data:[%1]});").arg(mapPoints.join(","))); + mapController->setAuroraData(mapPoints); } void OnlineMapWidget::mufDataUpdate() { FCT_IDENTIFICATION; - QStringList mapPoints; + QList mapPoints; if ( !prop_cond ) return; @@ -148,16 +138,13 @@ void OnlineMapWidget::mufDataUpdate() for ( const GenericValueMap::MapPoint &point : points ) { - mapPoints << QString("['%1', %2, %3]").arg(QString::number(point.value,'f',0)) - .arg(point.latitude) - .arg(point.longitude) - << QString("['%1', %2, %3]").arg(QString::number(point.value,'f',0)) - .arg(point.latitude) - .arg(point.longitude - 360); + const QString label = QString::number(point.value, 'f', 0); + mapPoints << MapPoint(label, point.latitude, point.longitude) + << MapPoint(label, point.latitude, point.longitude - 360); } } - runJavaScript(QString(" drawMuf([%1]);").arg(mapPoints.join(","))); + mapController->drawMuf(mapPoints); } void OnlineMapWidget::setIBPBand(VFOID vfoid, double, double ritFreq, double) @@ -168,7 +155,7 @@ void OnlineMapWidget::setIBPBand(VFOID vfoid, double, double ritFreq, double) if ( vfoid == VFO2 ) return; - runJavaScript(QString("currentBand=\"%1\";").arg(BandPlan::freq2Band(ritFreq).name)); + mapController->setCurrentBand(BandPlan::freq2Band(ritFreq).name); } void OnlineMapWidget::antPositionChanged(double in_azimuth, double in_elevation) @@ -180,12 +167,11 @@ void OnlineMapWidget::antPositionChanged(double in_azimuth, double in_elevation) if ( ! isRotConnected ) return; - QString targetJavaScript; lastSeenAzimuth = in_azimuth; lastSeenElevation = in_elevation; /* Draw a new path */ - Gridsquare myGrid(StationProfilesManager::instance()->getCurProfile1().locator); + const Gridsquare myGrid = Gridsquare::mapDisplayGrid(StationProfilesManager::instance()->getCurProfile1().locator); if ( myGrid.isValid() ) { @@ -200,19 +186,16 @@ void OnlineMapWidget::antPositionChanged(double in_azimuth, double in_elevation) beamLen = newBeamLen; } } - targetJavaScript += QString("drawAntPath({lat: %1, lng: %2}, %3, %4, %5);").arg(myGrid.getLatitude()) - .arg(myGrid.getLongitude()) - .arg(beamLen) - .arg(in_azimuth) - .arg(azimuthBeamWidth); + mapController->drawAntPath(MapCoordinate(myGrid.getLatitude(), myGrid.getLongitude()), + beamLen, + in_azimuth, + azimuthBeamWidth); } else { // clean paths - targetJavaScript = QLatin1String("drawAntPath({}, 0, 0, 0);"); + mapController->clearAntPath(); } - - runJavaScript(targetJavaScript); } void OnlineMapWidget::rotConnected() @@ -230,25 +213,13 @@ void OnlineMapWidget::rotDisconnected() isRotConnected = false; // clear the Ant Path - runJavaScript(QLatin1String("drawAntPath({}, 0, 0, 0);")); + mapController->clearAntPath(); } -void OnlineMapWidget::finishLoading(bool) +void OnlineMapWidget::finishLoading() { FCT_IDENTIFICATION; - if ( isMainPageLoaded ) - return; - - isMainPageLoaded = true; - - /* which layers will be active */ - postponedScripts += webChannelHandler.generateMapMenuJS(true, true, true, true, true, true, true, true); - main_page->runJavaScript(postponedScripts); - postponedScripts = QString(); - - webChannelHandler.restoreLayerControlStates(main_page); - flyToMyQTH(); auroraDataUpdate(); } @@ -281,30 +252,20 @@ void OnlineMapWidget::IBPCallsignTrigger(const QString &callsign, double freq) Rig::instance()->setMode("CW", QString()); } -void OnlineMapWidget::runJavaScript(const QString &js) -{ - FCT_IDENTIFICATION; - - qCDebug(function_parameters) << js; - - if ( !isMainPageLoaded ) - postponedScripts.append(js); - else - main_page->runJavaScript(js); -} - void OnlineMapWidget::flyToMyQTH() { FCT_IDENTIFICATION; /* focus current location */ - Gridsquare myGrid(StationProfilesManager::instance()->getCurProfile1().locator); + const Gridsquare myGrid = Gridsquare::mapDisplayGrid(StationProfilesManager::instance()->getCurProfile1().locator); if ( myGrid.isValid() ) { - QString currentProfilePosition(QString("[\"\", %1, %2, yellowIcon]").arg(myGrid.getLatitude()) - .arg(myGrid.getLongitude())); - runJavaScript(QString("flyToPoint(%1, 4);").arg(currentProfilePosition)); + mapController->flyToPoint(MapPoint(QString(), + myGrid.getLatitude(), + myGrid.getLongitude(), + QStringLiteral("yellowIcon")), + 4); } // redraw ant path because QSO distance can change antPositionChanged(lastSeenAzimuth, lastSeenElevation); @@ -314,33 +275,37 @@ void OnlineMapWidget::drawChatUsers(const QList &list) { FCT_IDENTIFICATION; - QList chatUsers; + QList chatUsers; for ( const KSTUsersInfo &user : list ) { if ( user.grid.isValid() ) { - chatUsers.append(QString("[\"%1\", %2, %3, %4]").arg(user.callsign) - .arg(user.grid.getLatitude()) - .arg(user.grid.getLongitude()) - .arg("yellowIcon")); + chatUsers << MapPoint(user.callsign, + user.grid.getLatitude(), + user.grid.getLongitude(), + QStringLiteral("yellowIcon")); } } - runJavaScript(QString("drawPointsGroup3([%1]);").arg(chatUsers.join(","))); + mapController->drawChatPoints(chatUsers); } void OnlineMapWidget::drawWSJTXSpot(const WsjtxEntry &spot) { FCT_IDENTIFICATION; - Gridsquare spotGrid(spot.grid); + const Gridsquare spotGrid = Gridsquare::mapDisplayGrid(spot.grid); if ( spotGrid.isValid() ) { - runJavaScript(QString("addWSJTXSpot(%1, %2, \"%3\", \"%4\");").arg(spotGrid.getLatitude()) - .arg(spotGrid.getLongitude()) - .arg(spot.callsign, Data::colorToHTMLColor(Data::statusToColor(spot.status, spot.dupeCount, QColor(Qt::white))))); + const QColor background = Data::statusToColor(spot.status, spot.dupeCount, QColor(Qt::white)); + mapController->addWsjtxSpot(MapPoint(spot.callsign, + spotGrid.getLatitude(), + spotGrid.getLongitude()), + Data::colorToHTMLColor(background), + Data::colorToHTMLColor(Data::textColorForBackground(background, + QColor(Qt::black)))); } } @@ -348,14 +313,12 @@ void OnlineMapWidget::clearWSJTXSpots() { FCT_IDENTIFICATION; - runJavaScript(QLatin1String("clearWSJTXSpots();")); + mapController->clearWsjtxSpots(); } OnlineMapWidget::~OnlineMapWidget() { FCT_IDENTIFICATION; - - main_page->deleteLater(); } void OnlineMapWidget::assignPropConditions(PropConditions *conditions) diff --git a/ui/OnlineMapWidget.h b/ui/OnlineMapWidget.h index b6bb1057..0e89eccd 100644 --- a/ui/OnlineMapWidget.h +++ b/ui/OnlineMapWidget.h @@ -3,10 +3,9 @@ #include #include -#include -#include "ui/MapWebChannelHandler.h" +#include +#include "ui/MapPageController.h" #include "core/PropConditions.h" -#include "ui/WebEnginePage.h" #include "rig/Rig.h" #include "ui/NewContactWidget.h" #include "service/kstchat/KSTChat.h" @@ -46,24 +45,18 @@ public slots: void clearWSJTXSpots(); protected slots: - void finishLoading(bool); + void finishLoading(); void chatCallsignTrigger(const QString&); void wsjtxCallsignTrigger(const QString&); void IBPCallsignTrigger(const QString&, double); private: - WebEnginePage *main_page; - bool isMainPageLoaded; - QString postponedScripts; - QWebChannel channel; - MapWebChannelHandler webChannelHandler; + QScopedPointer mapController; PropConditions *prop_cond; const NewContactWidget *contact; double lastSeenAzimuth, lastSeenElevation; bool isRotConnected; - - void runJavaScript(const QString &); }; #endif // QLOG_UI_ONLINEMAPWIDGET_H diff --git a/ui/QSLImportStatDialog.cpp b/ui/QSLImportStatDialog.cpp index c6256746..80f258ab 100644 --- a/ui/QSLImportStatDialog.cpp +++ b/ui/QSLImportStatDialog.cpp @@ -72,32 +72,22 @@ void QSLImportStatDialog::showStat(const quint64 updated, ui->unmatchedNumber->setText(QString::number(unmatched)); ui->errorsNumber->setText(QString::number(errors)); - if ( !newQSLText.isEmpty() ) - { - ui->detailsText->moveCursor(QTextCursor::End); - ui->detailsText->insertPlainText("*** " + tr("New QSLs: ") + "\n"); - ui->detailsText->moveCursor(QTextCursor::End); - ui->detailsText->insertPlainText(newQSLText); - ui->detailsText->moveCursor(QTextCursor::End); - ui->detailsText->moveCursor(QTextCursor::End); - } + QStringList sections; - if ( !updatedQSLText.isEmpty() ) + auto appendSection = [§ions](const QString &title, const QString &text) { - ui->detailsText->insertPlainText("*** " + tr("Updated QSOs: ") + "\n"); - ui->detailsText->moveCursor(QTextCursor::End); - ui->detailsText->insertPlainText(updatedQSLText); - ui->detailsText->moveCursor(QTextCursor::End); - ui->detailsText->moveCursor(QTextCursor::End); - } + const QString body = text.trimmed(); + if ( body.isEmpty() ) + return; - if ( !unmatchedQSLText.isEmpty() ) - { - ui->detailsText->insertPlainText("*** " + tr("Unmatched QSLs: ") + "\n"); - ui->detailsText->moveCursor(QTextCursor::End); - ui->detailsText->insertPlainText (unmatchedQSLText); - ui->detailsText->moveCursor(QTextCursor::End); - } + sections << "*** " + title + "\n" + body; + }; + + appendSection(tr("New QSLs:"), newQSLText); + appendSection(tr("Updated QSOs:"), updatedQSLText); + appendSection(tr("Unmatched QSLs:"), unmatchedQSLText); + + ui->detailsText->setPlainText(sections.join("\n\n")); } QSLImportStatDialog::~QSLImportStatDialog() diff --git a/ui/QSLPrintLabelDialog.cpp b/ui/QSLPrintLabelDialog.cpp index f16f8cde..b8220ba3 100644 --- a/ui/QSLPrintLabelDialog.cpp +++ b/ui/QSLPrintLabelDialog.cpp @@ -2,6 +2,8 @@ #include "ui_QSLPrintLabelDialog.h" #include +#include +#include #include #include #include @@ -9,7 +11,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -21,6 +25,7 @@ #include "data/StationProfile.h" #include "models/SqlListModel.h" #include "data/Data.h" +#include "ui/component/LogbookFieldComboBox.h" MODULE_IDENTIFICATION("qlog.ui.qslprintlabeldialog"); @@ -41,6 +46,12 @@ QSLPrintLabelDialog::QSLPrintLabelDialog(QWidget *parent) : * so that their width still contributes to the scroll area's sizeHint */ ui->templateSectionContent->setMaximumHeight(0); ui->printOptionsSectionContent->setMaximumHeight(0); + ui->cardSectionContent->setMaximumHeight(0); + + ui->printModeComboBox->addItem(tr("Label Sheet"), static_cast(QSLPrintMode::LabelSheet)); + ui->printModeComboBox->addItem(tr("QSL Card"), static_cast(QSLPrintMode::DirectCard)); + ui->printPageSizeComboBox->addItem("A4", static_cast(QPageSize::A4)); + ui->printPageSizeComboBox->addItem("Letter", static_cast(QPageSize::Letter)); /* Populate template combo: predefined + Custom */ const QList templates = QSLPrintLabelRenderer::predefinedTemplates(); @@ -51,6 +62,8 @@ QSLPrintLabelDialog::QSLPrintLabelDialog(QWidget *parent) : /* Populate page size combo */ ui->pageSizeComboBox->addItem("A4", static_cast(QPageSize::A4)); ui->pageSizeComboBox->addItem("Letter", static_cast(QPageSize::Letter)); + ui->pageSizeLabel->setVisible(false); + ui->pageSizeComboBox->setVisible(false); /* Populate callsign combo */ ui->myCallsignComboBox->setModel(new SqlListModel("SELECT DISTINCT UPPER(station_callsign) FROM contacts ORDER BY station_callsign", @@ -96,6 +109,11 @@ QSLPrintLabelDialog::QSLPrintLabelDialog(QWidget *parent) : ui->templateSectionButton->setArrowType(checked ? Qt::DownArrow : Qt::RightArrow); ui->templateSectionContent->setMaximumHeight(checked ? QWIDGETSIZE_MAX : 0); }); + connect(ui->cardSectionButton, &QToolButton::toggled, this, [this](bool checked) + { + ui->cardSectionButton->setArrowType(checked ? Qt::DownArrow : Qt::RightArrow); + ui->cardSectionContent->setMaximumHeight(checked ? QWIDGETSIZE_MAX : 0); + }); connect(ui->printOptionsSectionButton, &QToolButton::toggled, this, [this](bool checked) { ui->printOptionsSectionButton->setArrowType(checked ? Qt::DownArrow : Qt::RightArrow); @@ -113,6 +131,8 @@ QSLPrintLabelDialog::QSLPrintLabelDialog(QWidget *parent) : QScroller::grabGesture(ui->previewScrollArea->viewport(), QScroller::LeftMouseButtonGesture); ui->previewScrollArea->viewport()->installEventFilter(this); + + connect(ui->exportImagesButton, &QPushButton::clicked, this, &QSLPrintLabelDialog::exportCardImages); } QSLPrintLabelDialog::~QSLPrintLabelDialog() @@ -140,6 +160,11 @@ void QSLPrintLabelDialog::loadSettings() ui->zoomSlider->setValue(zoomPercent); ui->zoomSpinBox->setValue(zoomPercent); + int printModeIdx = ui->printModeComboBox->findData(LogParam::getQslLabelPrintMode()); + ui->printModeComboBox->setCurrentIndex(printModeIdx >= 0 ? printModeIdx : 0); + int outputPageSizeIdx = ui->printPageSizeComboBox->findData(LogParam::getQslLabelPageSize()); + ui->printPageSizeComboBox->setCurrentIndex(outputPageSizeIdx >= 0 ? outputPageSizeIdx : 0); + ui->printBordersCheckBox->setChecked(LogParam::getQslLabelPrintBorders()); ui->dateFormatEdit->setText(LogParam::getQslLabelDateFormat()); @@ -151,6 +176,8 @@ void QSLPrintLabelDialog::loadSettings() const QString savedMonoFont = LogParam::getQslLabelMonoFont(); if ( !savedMonoFont.isEmpty() ) ui->monoFontComboBox->setCurrentFont(QFont(savedMonoFont)); + labelTextColor = LogParam::getQslLabelTextColor(); + updateLabelTextColorUi(); /* extraColumnComboBox is populated in constructor before loadSettings() is called */ const QString savedExtraCol = LogParam::getQslLabelExtraColumn(); @@ -183,6 +210,19 @@ void QSLPrintLabelDialog::loadSettings() ui->hSpacingSpinBox->setValue(LogParam::getQslLabelCustomHSpacing()); ui->vSpacingSpinBox->setValue(LogParam::getQslLabelCustomVSpacing()); + ui->cardWidthSpinBox->setValue(LogParam::getQslLabelCardWidth()); + ui->cardHeightSpinBox->setValue(LogParam::getQslLabelCardHeight()); + ui->cardGapSpinBox->setValue(LogParam::getQslLabelCardGap()); + ui->cardLabelWidthSpinBox->setValue(LogParam::getQslLabelCardLabelWidth()); + ui->cardLabelHeightSpinBox->setValue(LogParam::getQslLabelCardLabelHeight()); + ui->cardLabelOffsetXSpinBox->setValue(LogParam::getQslLabelCardLabelOffsetX()); + ui->cardLabelOffsetYSpinBox->setValue(LogParam::getQslLabelCardLabelOffsetY()); + ui->cardLabelOpaqueBackgroundCheckBox->setChecked(LogParam::getQslLabelCardLabelOpaqueBackground()); + cardLabelBackgroundColor = LogParam::getQslLabelCardLabelBackgroundColor(); + updateCardLabelBackgroundColorUi(); + cardBackgroundImageData = LogParam::getQslLabelCardBackgroundImage(); + updateCardBackgroundUi(); + /* Apply template fields state based on selected template */ const QList templates = QSLPrintLabelRenderer::predefinedTemplates(); if ( templateIndex >= 0 && templateIndex < templates.size() ) @@ -194,6 +234,8 @@ void QSLPrintLabelDialog::loadSettings() { setTemplateFieldsEnabled(true); } + + updatePrintModeUi(); } void QSLPrintLabelDialog::saveSettings() @@ -205,10 +247,13 @@ void QSLPrintLabelDialog::saveSettings() LogParam::setQslLabelFooterRight(ui->footerRightEdit->text()); LogParam::setQslLabelSkip(ui->skipSpinBox->value()); LogParam::setQslLabelZoom(zoomPercent); + LogParam::setQslLabelPrintMode(ui->printModeComboBox->currentData().toInt()); + LogParam::setQslLabelPageSize(static_cast(currentOutputPageSize())); LogParam::setQslLabelPrintBorders(ui->printBordersCheckBox->isChecked()); LogParam::setQslLabelDateFormat(ui->dateFormatEdit->text()); LogParam::setQslLabelSansFont(ui->sansFontComboBox->currentFont().family()); LogParam::setQslLabelMonoFont(ui->monoFontComboBox->currentFont().family()); + LogParam::setQslLabelTextColor(labelTextColor); LogParam::setQslLabelExtraColumn(ui->extraColumnComboBox->currentData().toString()); LogParam::setQslLabelExtraColumnHeader(ui->columnHeaderEdit->text()); LogParam::setQslLabelToRadioText(ui->toRadioTextEdit->text()); @@ -233,22 +278,31 @@ void QSLPrintLabelDialog::saveSettings() LogParam::setQslLabelCustomTopMargin(ui->topMarginSpinBox->value()); LogParam::setQslLabelCustomHSpacing(ui->hSpacingSpinBox->value()); LogParam::setQslLabelCustomVSpacing(ui->vSpacingSpinBox->value()); + LogParam::setQslLabelCardWidth(ui->cardWidthSpinBox->value()); + LogParam::setQslLabelCardHeight(ui->cardHeightSpinBox->value()); + LogParam::setQslLabelCardGap(ui->cardGapSpinBox->value()); + LogParam::setQslLabelCardLabelWidth(ui->cardLabelWidthSpinBox->value()); + LogParam::setQslLabelCardLabelHeight(ui->cardLabelHeightSpinBox->value()); + LogParam::setQslLabelCardLabelOffsetX(ui->cardLabelOffsetXSpinBox->value()); + LogParam::setQslLabelCardLabelOffsetY(ui->cardLabelOffsetYSpinBox->value()); + LogParam::setQslLabelCardLabelOpaqueBackground(ui->cardLabelOpaqueBackgroundCheckBox->isChecked()); + LogParam::setQslLabelCardLabelBackgroundColor(cardLabelBackgroundColor); + LogParam::setQslLabelCardBackgroundImage(cardBackgroundImageData); } void QSLPrintLabelDialog::populateTemplateFields(const LabelTemplate &tmpl) { FCT_IDENTIFICATION; - /* Block signals to avoid triggering customTemplateFieldChanged */ - ui->pageSizeComboBox->blockSignals(true); - ui->colsSpinBox->blockSignals(true); - ui->rowsSpinBox->blockSignals(true); - ui->labelWidthSpinBox->blockSignals(true); - ui->labelHeightSpinBox->blockSignals(true); - ui->leftMarginSpinBox->blockSignals(true); - ui->topMarginSpinBox->blockSignals(true); - ui->hSpacingSpinBox->blockSignals(true); - ui->vSpacingSpinBox->blockSignals(true); + const QSignalBlocker pageSizeBlocker(ui->pageSizeComboBox); + const QSignalBlocker colsBlocker(ui->colsSpinBox); + const QSignalBlocker rowsBlocker(ui->rowsSpinBox); + const QSignalBlocker labelWidthBlocker(ui->labelWidthSpinBox); + const QSignalBlocker labelHeightBlocker(ui->labelHeightSpinBox); + const QSignalBlocker leftMarginBlocker(ui->leftMarginSpinBox); + const QSignalBlocker topMarginBlocker(ui->topMarginSpinBox); + const QSignalBlocker hSpacingBlocker(ui->hSpacingSpinBox); + const QSignalBlocker vSpacingBlocker(ui->vSpacingSpinBox); int pageSizeIdx = ui->pageSizeComboBox->findData(static_cast(tmpl.pageSize)); if ( pageSizeIdx >= 0 ) @@ -262,16 +316,6 @@ void QSLPrintLabelDialog::populateTemplateFields(const LabelTemplate &tmpl) ui->topMarginSpinBox->setValue(tmpl.topMarginMm); ui->hSpacingSpinBox->setValue(tmpl.hSpacingMm); ui->vSpacingSpinBox->setValue(tmpl.vSpacingMm); - - ui->pageSizeComboBox->blockSignals(false); - ui->colsSpinBox->blockSignals(false); - ui->rowsSpinBox->blockSignals(false); - ui->labelWidthSpinBox->blockSignals(false); - ui->labelHeightSpinBox->blockSignals(false); - ui->leftMarginSpinBox->blockSignals(false); - ui->topMarginSpinBox->blockSignals(false); - ui->hSpacingSpinBox->blockSignals(false); - ui->vSpacingSpinBox->blockSignals(false); } void QSLPrintLabelDialog::setTemplateFieldsEnabled(bool enabled) @@ -296,7 +340,7 @@ LabelTemplate QSLPrintLabelDialog::buildCustomTemplate() const LabelTemplate tmpl; tmpl.name = tr("Custom"); tmpl.orientation = QPageLayout::Portrait; - tmpl.pageSize = static_cast(ui->pageSizeComboBox->currentData().toInt()); + tmpl.pageSize = currentOutputPageSize(); tmpl.cols = ui->colsSpinBox->value(); tmpl.rows = ui->rowsSpinBox->value(); tmpl.labelWidthMm = ui->labelWidthSpinBox->value(); @@ -308,37 +352,219 @@ LabelTemplate QSLPrintLabelDialog::buildCustomTemplate() const return tmpl; } -void QSLPrintLabelDialog::populateExtraColumnCombo() +LabelTemplate QSLPrintLabelDialog::currentLabelTemplate() const { FCT_IDENTIFICATION; - ui->extraColumnComboBox->addItem(tr("Empty"), QString()); + const QList templates = QSLPrintLabelRenderer::predefinedTemplates(); + const int templateIndex = ui->templateComboBox->currentIndex(); - QSqlRecord contactsRecord = QSqlDatabase::database().record("contacts"); - QList> dbFieldItems; - for ( int i = LogbookModel::ColumnID::COLUMN_ID; i < LogbookModel::ColumnID::COLUMN_LAST_ELEMENT; ++i ) - { - LogbookModel::ColumnID columnID = static_cast(i); - const QString translation = LogbookModel::getFieldNameTranslation(columnID); - if ( translation.isEmpty() ) - continue; + if ( templateIndex >= 0 && templateIndex < templates.size() ) + return templates.at(templateIndex); - const QString dbField = contactsRecord.fieldName(i); - if ( dbField.isEmpty() ) - continue; + return buildCustomTemplate(); +} + +QSLPrintMode QSLPrintLabelDialog::currentPrintMode() const +{ + FCT_IDENTIFICATION; + + return static_cast(ui->printModeComboBox->currentData().toInt()); +} + +QPageSize::PageSizeId QSLPrintLabelDialog::currentOutputPageSize() const +{ + FCT_IDENTIFICATION; + + return static_cast(ui->printPageSizeComboBox->currentData().toInt()); +} + +QSLCardLayout QSLPrintLabelDialog::buildCardLayout() const +{ + FCT_IDENTIFICATION; + + QSLCardLayout layout; + layout.cardWidthMm = ui->cardWidthSpinBox->value(); + layout.cardHeightMm = ui->cardHeightSpinBox->value(); + layout.cardGapMm = ui->cardGapSpinBox->value(); + layout.labelWidthMm = ui->cardLabelWidthSpinBox->value(); + layout.labelHeightMm = ui->cardLabelHeightSpinBox->value(); + layout.labelOffsetXMm = ui->cardLabelOffsetXSpinBox->value(); + layout.labelOffsetYMm = ui->cardLabelOffsetYSpinBox->value(); + layout.labelOpaqueBackground = ui->cardLabelOpaqueBackgroundCheckBox->isChecked(); + layout.labelBackgroundColor = cardLabelBackgroundColor; + return layout; +} + +LabelStyleOptions QSLPrintLabelDialog::buildStyleOptions() const +{ + FCT_IDENTIFICATION; + + LabelStyleOptions styleOpts; + styleOpts.sansFontFamily = ui->sansFontComboBox->currentFont().family(); + styleOpts.monoFontFamily = ui->monoFontComboBox->currentFont().family(); + styleOpts.textColor = labelTextColor; + styleOpts.toRadioFontSize = ui->toRadioSizeSpinBox->value(); + styleOpts.callsignFontSize = ui->callsignSizeSpinBox->value(); + styleOpts.headerFontSize = ui->headerSizeSpinBox->value(); + styleOpts.dataFontSize = ui->dataSizeSpinBox->value(); + + const QString extraCol = ui->extraColumnComboBox->currentText(); + const QString customHeader = ui->columnHeaderEdit->text().trimmed(); + styleOpts.extraColumnHeader = (extraCol.isEmpty() || ui->extraColumnComboBox->currentIndex() == 0) ? QString() + : (customHeader.isEmpty() ? extraCol : customHeader); + styleOpts.maxQsoRows = ui->maxRowsSpinBox->value(); + + const QString toRadioText = ui->toRadioTextEdit->text().trimmed(); + if ( !toRadioText.isEmpty() ) + styleOpts.toRadioText = toRadioText; - dbFieldItems.append({translation, dbField}); + const QString hdrDate = ui->hdrDateEdit->text().trimmed(); + if ( !hdrDate.isEmpty() ) + styleOpts.hdrDate = hdrDate; + + const QString hdrTime = ui->hdrTimeEdit->text().trimmed(); + if ( !hdrTime.isEmpty() ) + styleOpts.hdrTime = hdrTime; + + const QString hdrBand = ui->hdrBandEdit->text().trimmed(); + if ( !hdrBand.isEmpty() ) + styleOpts.hdrBand = hdrBand; + + const QString hdrMode = ui->hdrModeEdit->text().trimmed(); + if ( !hdrMode.isEmpty() ) + styleOpts.hdrMode = hdrMode; + + const QString hdrQsl = ui->hdrQslEdit->text().trimmed(); + if ( !hdrQsl.isEmpty() ) + styleOpts.hdrQsl = hdrQsl; + + return styleOpts; +} + +QString QSLPrintLabelDialog::buttonContrastTextColor(const QColor &backgroundColor) const +{ + FCT_IDENTIFICATION; + + return backgroundColor.lightness() < 128 ? QStringLiteral("white") : QStringLiteral("black"); +} + +QString QSLPrintLabelDialog::imageExportFileName(const QSLLabelData &label, int index) const +{ + FCT_IDENTIFICATION; + + QString base = label.callsign.trimmed().toUpper(); + + if ( base.isEmpty() ) + base = QStringLiteral("qsl_card"); + + if ( !label.qsos.isEmpty() ) + { + const QSLLabelData::QsoRow &firstQso = label.qsos.first(); + + if ( !firstQso.date.isEmpty() ) + base += "_" + firstQso.date; + if ( !firstQso.time.isEmpty() ) + base += "_" + firstQso.time; } - std::sort(dbFieldItems.begin(), dbFieldItems.end(), - [](const QPair &a, - const QPair &b) + QString sanitized; + sanitized.reserve(base.size()); + + for ( const QChar &ch : base ) { - return a.first.localeAwareCompare(b.first) < 0; - }); + if ( ch.isLetterOrNumber() || ch == '_' || ch == '-' ) + sanitized += ch; + else if ( ch.isSpace() || ch == '/' || ch == ':' || ch == '.' ) + sanitized += '_'; + } + + while ( sanitized.contains("__") ) + sanitized.replace("__", "_"); + + sanitized = sanitized.trimmed(); - for ( const QPair &item : static_cast>&>(dbFieldItems) ) - ui->extraColumnComboBox->addItem(item.first, item.second); + if ( sanitized.startsWith("_") ) + sanitized.remove(0, 1); + if ( sanitized.endsWith("_") ) + sanitized.chop(1); + if ( sanitized.isEmpty() ) + sanitized = QStringLiteral("qsl_card"); + + return QString("%1_%2.jpg").arg(sanitized, QString::number(index + 1).rightJustified(3, '0')); +} + +void QSLPrintLabelDialog::updateRendererOptions() +{ + FCT_IDENTIFICATION; + + renderer.setPrintMode(currentPrintMode()); + renderer.setPageSize(currentOutputPageSize()); + renderer.setFooterLeft(ui->footerLeftEdit->text()); + renderer.setFooterRight(ui->footerRightEdit->text()); + renderer.setPrintBorders(ui->printBordersCheckBox->isChecked()); + renderer.setCardLayout(buildCardLayout()); + renderer.setCardBackgroundImage(cardBackgroundImageData); + renderer.setStyleOptions(buildStyleOptions()); +} + +void QSLPrintLabelDialog::updatePrintModeUi() +{ + FCT_IDENTIFICATION; + + const bool directCard = currentPrintMode() == QSLPrintMode::DirectCard; + + ui->templateSectionButton->setEnabled(!directCard); + ui->templateSectionContent->setEnabled(!directCard); + ui->cardSectionButton->setEnabled(directCard); + ui->cardSectionContent->setEnabled(directCard); + ui->templateSectionButton->setChecked(!directCard); + ui->cardSectionButton->setChecked(directCard); + ui->exportImagesButton->setVisible(directCard); + + ui->templateSectionButton->setArrowType(ui->templateSectionButton->isChecked() ? Qt::DownArrow : Qt::RightArrow); + ui->templateSectionContent->setMaximumHeight((!directCard && ui->templateSectionButton->isChecked()) ? QWIDGETSIZE_MAX : 0); + ui->cardSectionButton->setArrowType(ui->cardSectionButton->isChecked() ? Qt::DownArrow : Qt::RightArrow); + ui->cardSectionContent->setMaximumHeight((directCard && ui->cardSectionButton->isChecked()) ? QWIDGETSIZE_MAX : 0); +} + +void QSLPrintLabelDialog::updateLabelTextColorUi() +{ + FCT_IDENTIFICATION; + + const QColor color = labelTextColor.isValid() ? labelTextColor : QColor(Qt::black); + const QString buttonTextColor = buttonContrastTextColor(color); + ui->labelTextColorButton->setStyleSheet(QStringLiteral("background-color: %1; color: %2;") + .arg(color.name(QColor::HexArgb), buttonTextColor)); +} + +void QSLPrintLabelDialog::updateCardLabelBackgroundColorUi() +{ + FCT_IDENTIFICATION; + + const QColor color = cardLabelBackgroundColor.isValid() ? cardLabelBackgroundColor : QColor(Qt::white); + const QString buttonTextColor = buttonContrastTextColor(color); + ui->cardLabelBackgroundColorButton->setEnabled(ui->cardLabelOpaqueBackgroundCheckBox->isChecked()); + ui->cardLabelBackgroundColorButton->setStyleSheet(QStringLiteral("background-color: %1; color: %2;") + .arg(color.name(QColor::HexArgb), buttonTextColor)); +} + +void QSLPrintLabelDialog::updateCardBackgroundUi() +{ + FCT_IDENTIFICATION; + + const bool hasImage = !cardBackgroundImageData.isNull(); + ui->clearCardBackgroundButton->setEnabled(hasImage); +} + +void QSLPrintLabelDialog::populateExtraColumnCombo() +{ + FCT_IDENTIFICATION; + + LogbookFieldComboBox::populateCombo(ui->extraColumnComboBox, + LogbookFieldComboBox::ValueMode::DbFieldName, + LogbookFieldComboBox::EmptyMode::EmptyLabel, + tr("Empty")); } void QSLPrintLabelDialog::populateQSLSentCombo() @@ -423,7 +649,14 @@ void QSLPrintLabelDialog::templateChanged(int index) if ( index >= 0 && index < templates.size() ) { /* Predefined template selected - show values, disable fields */ - populateTemplateFields(templates.at(index)); + const LabelTemplate &tmpl = templates.at(index); + populateTemplateFields(tmpl); + const int pageSizeIdx = ui->printPageSizeComboBox->findData(static_cast(tmpl.pageSize)); + if ( pageSizeIdx >= 0 ) + { + const QSignalBlocker pageSizeBlocker(ui->printPageSizeComboBox); + ui->printPageSizeComboBox->setCurrentIndex(pageSizeIdx); + } setTemplateFieldsEnabled(false); } else @@ -467,6 +700,98 @@ void QSLPrintLabelDialog::customTemplateFieldChanged() refreshData(); } +void QSLPrintLabelDialog::printModeChanged(int index) +{ + FCT_IDENTIFICATION; + qCDebug(function_parameters) << index; + + updatePrintModeUi(); + refreshData(); +} + +void QSLPrintLabelDialog::cardLayoutChanged() +{ + FCT_IDENTIFICATION; + + updateCardLabelBackgroundColorUi(); + refreshData(); +} + +void QSLPrintLabelDialog::selectLabelTextColor() +{ + FCT_IDENTIFICATION; + + const QColor color = QColorDialog::getColor(labelTextColor.isValid() ? labelTextColor : QColor(Qt::black), + this, + tr("Select Label Text Color"), + QColorDialog::ShowAlphaChannel | QColorDialog::DontUseNativeDialog); + if ( !color.isValid() ) + return; + + labelTextColor = color; + updateLabelTextColorUi(); + updatePreview(); +} + +void QSLPrintLabelDialog::selectCardLabelBackgroundColor() +{ + FCT_IDENTIFICATION; + + const QColor color = QColorDialog::getColor(cardLabelBackgroundColor.isValid() ? cardLabelBackgroundColor : QColor(Qt::white), + this, + tr("Select Label Background Color"), + QColorDialog::ShowAlphaChannel | QColorDialog::DontUseNativeDialog); + if ( !color.isValid() ) + return; + + cardLabelBackgroundColor = color; + updateCardLabelBackgroundColorUi(); + updatePreview(); +} + +void QSLPrintLabelDialog::selectCardBackgroundImage() +{ + FCT_IDENTIFICATION; + + const QString filename = QFileDialog::getOpenFileName(this, + tr("Select QSL Card Background"), + QDir::homePath(), + tr("Images (*.png *.jpg *.jpeg *.bmp)")); + if ( filename.isEmpty() ) + return; + + QFile file(filename); + if ( !file.open(QIODevice::ReadOnly) ) + { + QMessageBox::warning(this, + tr("Select QSL Card Background"), + tr("Cannot read selected image file.")); + return; + } + + QImage image; + if ( !image.loadFromData(file.readAll()) ) + { + QMessageBox::warning(this, + tr("Select QSL Card Background"), + tr("Selected file is not a valid image.")); + return; + } + + cardBackgroundImageData = image; + updateCardBackgroundUi(); + updatePreview(); +} + +void QSLPrintLabelDialog::clearCardBackgroundImage() +{ + FCT_IDENTIFICATION; + + cardBackgroundImageData = QImage(); + updateCardBackgroundUi(); + updatePreview(); +} + void QSLPrintLabelDialog::resizeEvent(QResizeEvent *event) { FCT_IDENTIFICATION; @@ -696,15 +1021,7 @@ void QSLPrintLabelDialog::refreshData() buildLabels(); - /* Set template on renderer */ - const QList templates = QSLPrintLabelRenderer::predefinedTemplates(); - int templateIndex = ui->templateComboBox->currentIndex(); - - if ( templateIndex >= 0 && templateIndex < templates.size() ) - renderer.setTemplate(templates.at(templateIndex)); - else - renderer.setTemplate(buildCustomTemplate()); - + renderer.setTemplate(currentLabelTemplate()); renderer.setLabels(labelsData); renderer.setSkipLabels(ui->skipSpinBox->value()); currentPage = 0; @@ -716,48 +1033,7 @@ void QSLPrintLabelDialog::updatePreview() { FCT_IDENTIFICATION; - renderer.setFooterLeft(ui->footerLeftEdit->text()); - renderer.setFooterRight(ui->footerRightEdit->text()); - renderer.setPrintBorders(ui->printBordersCheckBox->isChecked()); - - LabelStyleOptions styleOpts; - styleOpts.sansFontFamily = ui->sansFontComboBox->currentFont().family(); - styleOpts.monoFontFamily = ui->monoFontComboBox->currentFont().family(); - styleOpts.toRadioFontSize = ui->toRadioSizeSpinBox->value(); - styleOpts.callsignFontSize = ui->callsignSizeSpinBox->value(); - styleOpts.headerFontSize = ui->headerSizeSpinBox->value(); - styleOpts.dataFontSize = ui->dataSizeSpinBox->value(); - const QString extraCol = ui->extraColumnComboBox->currentText(); - const QString customHeader = ui->columnHeaderEdit->text().trimmed(); - styleOpts.extraColumnHeader = (extraCol.isEmpty() || ui->extraColumnComboBox->currentIndex() == 0) ? QString() - : (customHeader.isEmpty() ? extraCol : customHeader); - styleOpts.maxQsoRows = ui->maxRowsSpinBox->value(); - - const QString toRadioText = ui->toRadioTextEdit->text().trimmed(); - if ( !toRadioText.isEmpty() ) - styleOpts.toRadioText = toRadioText; - - const QString hdrDate = ui->hdrDateEdit->text().trimmed(); - if ( !hdrDate.isEmpty() ) - styleOpts.hdrDate = hdrDate; - - const QString hdrTime = ui->hdrTimeEdit->text().trimmed(); - if ( !hdrTime.isEmpty() ) - styleOpts.hdrTime = hdrTime; - - const QString hdrBand = ui->hdrBandEdit->text().trimmed(); - if ( !hdrBand.isEmpty() ) - styleOpts.hdrBand = hdrBand; - - const QString hdrMode = ui->hdrModeEdit->text().trimmed(); - if ( !hdrMode.isEmpty() ) - styleOpts.hdrMode = hdrMode; - - const QString hdrQsl = ui->hdrQslEdit->text().trimmed(); - if ( !hdrQsl.isEmpty() ) - styleOpts.hdrQsl = hdrQsl; - - renderer.setStyleOptions(styleOpts); + updateRendererOptions(); int totalPages = renderer.pageCount(); int totalLabels = renderer.labelCount(); @@ -765,10 +1041,16 @@ void QSLPrintLabelDialog::updatePreview() bool hasLabels = ( totalLabels > 0 ); ui->printButton->setEnabled(hasLabels); ui->exportPdfButton->setEnabled(hasLabels); + ui->exportImagesButton->setEnabled(hasLabels && currentPrintMode() == QSLPrintMode::DirectCard); - ui->statusLabel->setText(tr("Labels: %1 (%2 pages)") - .arg(totalLabels) - .arg(totalPages)); + if ( currentPrintMode() == QSLPrintMode::DirectCard ) + ui->statusLabel->setText(tr("Cards: %1 (%2 pages)") + .arg(totalLabels) + .arg(totalPages)); + else + ui->statusLabel->setText(tr("Labels: %1 (%2 pages)") + .arg(totalLabels) + .arg(totalPages)); updatePageNavigation(); @@ -846,6 +1128,7 @@ void QSLPrintLabelDialog::print() if ( printDialog.exec() == QDialog::Accepted ) { + updateRendererOptions(); renderer.printAll(&printer); askAndMarkQslSent(); } @@ -871,10 +1154,106 @@ void QSLPrintLabelDialog::exportPdf() printer.setOutputFormat(QPrinter::PdfFormat); printer.setOutputFileName(filename); + updateRendererOptions(); renderer.printAll(&printer); askAndMarkQslSent(); } +void QSLPrintLabelDialog::exportCardImages() +{ + FCT_IDENTIFICATION; + + if ( currentPrintMode() != QSLPrintMode::DirectCard || labelsData.isEmpty() ) + return; + + const QString lastPath = LogParam::getQslLabelImageExportPath(QDir::homePath()); + const QString dirPath = QFileDialog::getExistingDirectory(this, + tr("Export QSL Card Images"), + lastPath); + + if ( dirPath.isEmpty() ) + return; + + LogParam::setQslLabelImageExportPath(dirPath); + + updateRendererOptions(); + + const QDir dir(dirPath); + QStringList fileNames; + bool hasExistingFiles = false; + + for ( int i = 0; i < labelsData.size(); ++i ) + { + const QString fileName = imageExportFileName(labelsData.at(i), i); + fileNames.append(fileName); + + if ( QFileInfo::exists(dir.filePath(fileName)) ) + hasExistingFiles = true; + } + + if ( hasExistingFiles ) + { + const QMessageBox::StandardButton answer = QMessageBox::question( + this, + tr("Export QSL Card Images"), + tr("Some image files already exist. Overwrite them?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + + if ( answer != QMessageBox::Yes ) + return; + } + + int saved = 0; + QStringList failedFiles; + + for ( int i = 0; i < labelsData.size(); ++i ) + { + const QImage cardImage = renderer.renderDirectCard(i); + const QString fileName = fileNames.at(i); + + if ( cardImage.isNull() ) + { + failedFiles.append(fileName); + continue; + } + + const QString filePath = dir.filePath(fileName); + + if ( cardImage.save(filePath, "JPG", 95) ) + { + ++saved; + } + else + { + failedFiles.append(fileName); + qCWarning(runtime) << "Cannot save QSL card image" << filePath; + } + } + + const QString dialogTitle = tr("Export QSL Card Images"); + + if ( failedFiles.isEmpty() ) + { + QMessageBox::information(this, + dialogTitle, + tr("Exported %n QSL card image(s).", "", saved)); + askAndMarkQslSent(); + return; + } + + QMessageBox messageBox(QMessageBox::Warning, + dialogTitle, + tr("Exported %1 of %2 QSL card images.") + .arg(saved) + .arg(labelsData.size()), + QMessageBox::Ok, + this); + messageBox.setInformativeText(tr("QSOs were not marked as sent.")); + messageBox.setDetailedText(failedFiles.join('\n')); + messageBox.exec(); +} + void QSLPrintLabelDialog::askAndMarkQslSent() { FCT_IDENTIFICATION; diff --git a/ui/QSLPrintLabelDialog.h b/ui/QSLPrintLabelDialog.h index a77aa77e..31d4ee66 100644 --- a/ui/QSLPrintLabelDialog.h +++ b/ui/QSLPrintLabelDialog.h @@ -1,7 +1,9 @@ #ifndef QLOG_UI_QSLPRINTLABELDIALOG_H #define QLOG_UI_QSLPRINTLABELDIALOG_H +#include #include +#include #include "core/LogLocale.h" #include "core/QSLPrintLabelRenderer.h" @@ -32,10 +34,17 @@ private slots: void nextPage(); void print(); void exportPdf(); + void exportCardImages(); void templateChanged(int index); void skipChanged(int value); void zoomChanged(int value); void customTemplateFieldChanged(); + void printModeChanged(int index); + void cardLayoutChanged(); + void selectLabelTextColor(); + void selectCardLabelBackgroundColor(); + void selectCardBackgroundImage(); + void clearCardBackgroundImage(); protected: void resizeEvent(QResizeEvent* event) override; @@ -46,6 +55,9 @@ private slots: LogLocale locale; QSLPrintLabelRenderer renderer; QList labelsData; + QImage cardBackgroundImageData; + QColor labelTextColor; + QColor cardLabelBackgroundColor; int currentPage = 0; int zoomPercent = 100; @@ -59,6 +71,18 @@ private slots: void populateTemplateFields(const LabelTemplate &tmpl); void setTemplateFieldsEnabled(bool enabled); LabelTemplate buildCustomTemplate() const; + LabelTemplate currentLabelTemplate() const; + QSLPrintMode currentPrintMode() const; + QPageSize::PageSizeId currentOutputPageSize() const; + QSLCardLayout buildCardLayout() const; + LabelStyleOptions buildStyleOptions() const; + QString buttonContrastTextColor(const QColor &backgroundColor) const; + QString imageExportFileName(const QSLLabelData &label, int index) const; + void updateRendererOptions(); + void updatePrintModeUi(); + void updateLabelTextColorUi(); + void updateCardLabelBackgroundColorUi(); + void updateCardBackgroundUi(); void populateExtraColumnCombo(); void populateQSLSentCombo(); }; diff --git a/ui/QSLPrintLabelDialog.ui b/ui/QSLPrintLabelDialog.ui index 9a9eba7f..3934d034 100644 --- a/ui/QSLPrintLabelDialog.ui +++ b/ui/QSLPrintLabelDialog.ui @@ -11,7 +11,7 @@ - Print QSL Labels + Print QSL Labels / Cards true @@ -48,7 +48,7 @@ 0 0 393 - 1300 + 1778 @@ -221,6 +221,361 @@
+ + + + Print Mode + + + true + + + + 0 + + + 0 + + + 0 + + + 4 + + + + + Mode: + + + + + + + + + + Page Size: + + + + + + + + + + + + + + 0 + 0 + + + + QSL Card + + + true + + + false + + + Qt::ToolButtonTextBesideIcon + + + Qt::RightArrow + + + + + + + + + + true + + + + 0 + + + 0 + + + 0 + + + 4 + + + + + Card Width: + + + + + + + mm + + + 1 + + + 50.000000000000000 + + + 300.000000000000000 + + + 0.500000000000000 + + + 140.000000000000000 + + + + + + + Card Height: + + + + + + + mm + + + 1 + + + 50.000000000000000 + + + 300.000000000000000 + + + 0.500000000000000 + + + 90.000000000000000 + + + + + + + Card Gap: + + + + + + + mm + + + 1 + + + 0.000000000000000 + + + 20.000000000000000 + + + 0.500000000000000 + + + 2.000000000000000 + + + + + + + Label Width: + + + + + + + mm + + + 1 + + + 10.000000000000000 + + + 300.000000000000000 + + + 0.500000000000000 + + + 70.000000000000000 + + + + + + + Label Height: + + + + + + + mm + + + 1 + + + 10.000000000000000 + + + 300.000000000000000 + + + 0.500000000000000 + + + 35.000000000000000 + + + + + + + Label X Offset: + + + + + + + mm + + + 1 + + + -300.000000000000000 + + + 300.000000000000000 + + + 0.500000000000000 + + + 5.000000000000000 + + + + + + + Label Y Offset: + + + + + + + mm + + + 1 + + + -300.000000000000000 + + + 300.000000000000000 + + + 0.500000000000000 + + + 5.000000000000000 + + + + + + + Label Background: + + + + + + + + + Fill under label + + + true + + + + + + + Color + + + + + + + + + Background Image: + + + + + + + + + Browse + + + + + + + + + + Clear + + + + + + + + + + + @@ -668,13 +1023,27 @@ + + + Text Color: + + + + + + + Color + + + + Callsign Size: - + pt @@ -696,14 +1065,14 @@ - + "To Radio" Size: - + pt @@ -725,28 +1094,28 @@ - + "To Radio" Text: - + To Radio - + Header Size: - + pt @@ -768,14 +1137,14 @@ - + Data Size: - + pt @@ -797,112 +1166,112 @@ - + Date Header Text: - + Date - + Date Format: - + yyyy-MM-dd - + Time Header Text: - + Time - + Band Header Text: - + Band - + Mode Header Text: - + Mode - + QSL Header Text: - + QSL - + Extra Column: - + QComboBox::AdjustToContents - + Extra Column Text - + (DB column name) @@ -1163,6 +1532,36 @@ + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 40 + 20 + + + + + + + + false + + + Export as Images + + + + .. + + + @@ -1778,6 +2177,22 @@ + + labelTextColorButton + clicked() + QSLPrintLabelDialog + selectLabelTextColor() + + + 290 + 925 + + + 553 + 349 + + + toRadioSizeSpinBox valueChanged(double) @@ -1986,6 +2401,214 @@ + + printModeComboBox + currentIndexChanged(int) + QSLPrintLabelDialog + printModeChanged(int) + + + 290 + 244 + + + 553 + 349 + + + + + printPageSizeComboBox + currentIndexChanged(int) + QSLPrintLabelDialog + refreshData() + + + 290 + 276 + + + 553 + 349 + + + + + cardWidthSpinBox + valueChanged(double) + QSLPrintLabelDialog + cardLayoutChanged() + + + 290 + 317 + + + 553 + 349 + + + + + cardGapSpinBox + valueChanged(double) + QSLPrintLabelDialog + cardLayoutChanged() + + + 290 + 388 + + + 553 + 349 + + + + + cardLabelOpaqueBackgroundCheckBox + toggled(bool) + QSLPrintLabelDialog + cardLayoutChanged() + + + 290 + 532 + + + 553 + 349 + + + + + cardLabelBackgroundColorButton + clicked() + QSLPrintLabelDialog + selectCardLabelBackgroundColor() + + + 360 + 532 + + + 553 + 349 + + + + + cardHeightSpinBox + valueChanged(double) + QSLPrintLabelDialog + cardLayoutChanged() + + + 290 + 352 + + + 553 + 349 + + + + + cardLabelWidthSpinBox + valueChanged(double) + QSLPrintLabelDialog + cardLayoutChanged() + + + 290 + 388 + + + 553 + 349 + + + + + cardLabelHeightSpinBox + valueChanged(double) + QSLPrintLabelDialog + cardLayoutChanged() + + + 290 + 424 + + + 553 + 349 + + + + + cardLabelOffsetXSpinBox + valueChanged(double) + QSLPrintLabelDialog + cardLayoutChanged() + + + 290 + 460 + + + 553 + 349 + + + + + cardLabelOffsetYSpinBox + valueChanged(double) + QSLPrintLabelDialog + cardLayoutChanged() + + + 290 + 496 + + + 553 + 349 + + + + + selectCardBackgroundButton + clicked() + QSLPrintLabelDialog + selectCardBackgroundImage() + + + 250 + 532 + + + 553 + 349 + + + + + clearCardBackgroundButton + clicked() + QSLPrintLabelDialog + clearCardBackgroundImage() + + + 330 + 532 + + + 553 + 349 + + + toggleDateRange() @@ -2003,5 +2626,11 @@ exportPdf() zoomChanged(int) customTemplateFieldChanged() + printModeChanged(int) + cardLayoutChanged() + selectLabelTextColor() + selectCardLabelBackgroundColor() + selectCardBackgroundImage() + clearCardBackgroundImage() diff --git a/ui/QSODetailDialog.cpp b/ui/QSODetailDialog.cpp index 25cabfda..27aa3778 100644 --- a/ui/QSODetailDialog.cpp +++ b/ui/QSODetailDialog.cpp @@ -34,9 +34,7 @@ QSODetailDialog::QSODetailDialog(const QSqlRecord &qso, mapper(new QDataWidgetMapper(this)), model(new LogbookModelPrivate(this)), editedRecord(new QSqlRecord(qso)), - isMainPageLoaded(false), - main_page(new WebEnginePage(this)), - layerControlHandler("qsodetail", parent) + mapController(new MapPageController(QStringLiteral("qsodetail"), this)) { FCT_IDENTIFICATION; @@ -48,12 +46,12 @@ QSODetailDialog::QSODetailDialog(const QSqlRecord &qso, connect(model, &QSqlTableModel::beforeUpdate, this, &QSODetailDialog::handleBeforeUpdate); /* mapView setting */ - main_page->setWebChannel(&channel); - ui->mapView->setPage(main_page); - main_page->load(QUrl(QStringLiteral("qrc:/res/map/onlinemap.html"))); - ui->mapView->setFocusPolicy(Qt::ClickFocus); - connect(ui->mapView, &QWebEngineView::loadFinished, this, &QSODetailDialog::mapLoaded); - channel.registerObject("layerControlHandler", &layerControlHandler); + mapController->attach(ui->mapView, + MapLayer::Grid + | MapLayer::Grayline + | MapLayer::Path); + connect(mapController.data(), &MapPageController::loaded, + this, &QSODetailDialog::mapLoaded); /* Edit Button */ editButton = new QPushButton(getButtonText(EDIT_BUTTON_TEXT)); @@ -410,8 +408,8 @@ QSODetailDialog::QSODetailDialog(const QSqlRecord &qso, setReadOnlyMode(true); - drawDXOnMap(ui->callsignEdit->text(), Gridsquare(ui->gridEdit->text())); - drawMyQTHOnMap(ui->myCallsignEdit->text(), Gridsquare(ui->myGridEdit->text())); + drawDXOnMap(ui->callsignEdit->text(), Gridsquare::mapDisplayGrid(ui->gridEdit->text())); + drawMyQTHOnMap(ui->myCallsignEdit->text(), Gridsquare::mapDisplayGrid(ui->myGridEdit->text())); setStaticMapTime(ui->dateTimeOnEdit->dateTime()); refreshDXStatTabs(); @@ -1090,44 +1088,23 @@ void QSODetailDialog::doValidationDouble(double) doValidation(); } -void QSODetailDialog::mapLoaded(bool) +void QSODetailDialog::mapLoaded() { FCT_IDENTIFICATION; - isMainPageLoaded = true; - - /* which layers will be active */ - postponedScripts += layerControlHandler.generateMapMenuJS(true, - true, - false, - false, - false, - false, - false, - false, - true); - - main_page->runJavaScript(postponedScripts); - const QPalette &defaultPalette = this->palette(); const QColor &text = defaultPalette.color(QPalette::WindowText); const QColor &window = defaultPalette.color(QPalette::Window); bool isDark = text.lightness() > window.lightness(); - if ( isDark ) - { - QString themeJavaScript = "map.getPanes().tilePane.style.webkitFilter=\"brightness(0.6) invert(1) contrast(3) hue-rotate(200deg) saturate(0.3) brightness(0.9)\";"; - main_page->runJavaScript(themeJavaScript); - } - - layerControlHandler.restoreLayerControlStates(main_page); + mapController->setDarkTheme(isDark); } void QSODetailDialog::myGridChanged(const QString &newGrid) { FCT_IDENTIFICATION; - drawMyQTHOnMap(ui->myCallsignEdit->text(), Gridsquare(newGrid)); + drawMyQTHOnMap(ui->myCallsignEdit->text(), Gridsquare::mapDisplayGrid(newGrid)); return; } @@ -1136,7 +1113,7 @@ void QSODetailDialog::DXGridChanged(const QString &newGrid) { FCT_IDENTIFICATION; - drawDXOnMap(ui->callsignEdit->text(), Gridsquare(newGrid)); + drawDXOnMap(ui->callsignEdit->text(), Gridsquare::mapDisplayGrid(newGrid)); return; } @@ -1261,7 +1238,7 @@ void QSODetailDialog::mySotaChanged(const QString &newSOTA) if ( newSOTA.length() >= 3 ) { - ui->mySOTAEdit->setCompleter(sotaCompleter.data()); + ui->mySOTAEdit->setCompleter(mySotaCompleter.data()); } else { @@ -1275,7 +1252,7 @@ void QSODetailDialog::myPOTAChanged(const QString &newPOTA) if ( newPOTA.length() >= 3 ) { - ui->myPOTAEdit->setCompleter(potaCompleter.data()); + ui->myPOTAEdit->setCompleter(myPotaCompleter.data()); } else { @@ -1289,7 +1266,7 @@ void QSODetailDialog::myWWFFChanged(const QString &newWWFF) if ( newWWFF.length() >= 3 ) { - ui->myWWFFEdit->setCompleter(wwffCompleter.data()); + ui->myWWFFEdit->setCompleter(myWWFFCompleter.data()); } else { @@ -1312,14 +1289,16 @@ void QSODetailDialog::clubQueryResult(const QString &in_callsign, QMapIterator clubs(data); - QPalette palette; - - //"Hello World" while ( clubs.hasNext() ) { clubs.next(); - QColor color = Data::statusToColor(static_cast(clubs.value().status), false, palette.color(QPalette::Text)); - memberText.append(QString("%2   ").arg(Data::colorToHTMLColor(color), clubs.key())); + const QColor color = Data::statusToColor(static_cast(clubs.value().status), false, QColor()); + const QString clubName = clubs.key().toHtmlEscaped(); + + if ( color.isValid() && color.alpha() > 0 ) + memberText.append(QString("%2   ").arg(Data::colorToHTMLColor(color), clubName)); + else + memberText.append(QString("%1   ").arg(clubName)); } ui->memberListLabel->setText(memberText); } @@ -1392,7 +1371,7 @@ void QSODetailDialog::drawDXOnMap(const QString &label, const Gridsquare &dxGrid QString stationString; QString popupString = label; - Gridsquare myGrid = Gridsquare(ui->myGridEdit->text()); + Gridsquare myGrid = Gridsquare::mapDisplayGrid(ui->myGridEdit->text()); double distance = 0; if (dxGrid.distanceTo(myGrid, distance)) @@ -1404,39 +1383,28 @@ void QSODetailDialog::drawDXOnMap(const QString &label, const Gridsquare &dxGrid double lat = dxGrid.getLatitude(); double lon = dxGrid.getLongitude(); - // do not wrap the points - double delta = lon - myGrid.getLongitude(); - if ( delta > 180 ) - lon -= 360; - if ( delta < -180 ) - lon += 360; - - stationString.append(QString("[[\"%1\", %2, %3, yellowIcon]]").arg(popupString).arg(lat).arg(lon)); - - QString shortPath = QString("[%1, %2, %3, %4]") - .arg(myGrid.getLatitude()) - .arg(myGrid.getLongitude()) - .arg(lat) - .arg(lon); - - QString javaScript = QString("grids_confirmed = [];" - "grids_worked = [];" - "drawPoints(%1);" - "drawShortPaths([%2]);" - "maidenheadConfWorked.redraw();" - "flyToPoint(%3[0], 6);") - .arg(stationString, shortPath, stationString); - qCDebug(runtime) << javaScript; + QList shortPaths; - if ( !isMainPageLoaded ) - { - postponedScripts.append(javaScript); - } - else + if ( myGrid.isValid() ) { - main_page->runJavaScript(javaScript); + // do not wrap the points + double delta = lon - myGrid.getLongitude(); + if ( delta > 180 ) + lon -= 360; + if ( delta < -180 ) + lon += 360; + + shortPaths << MapPath(MapCoordinate(myGrid.getLatitude(), myGrid.getLongitude()), + MapCoordinate(lat, lon)); } + + const MapPoint dxPoint(popupString, lat, lon, QStringLiteral("yellowIcon")); + mapController->clearGridLayers(); + mapController->drawPoints(QList() << dxPoint); + mapController->drawShortPaths(shortPaths); + mapController->redrawGridLayer(); + mapController->flyToPoint(dxPoint, 6); } void QSODetailDialog::drawMyQTHOnMap(const QString &label, const Gridsquare &myGrid) @@ -1450,26 +1418,12 @@ void QSODetailDialog::drawMyQTHOnMap(const QString &label, const Gridsquare &myG return; } - QString stationString; double lat = myGrid.getLatitude(); double lon = myGrid.getLongitude(); - stationString.append(QString("[[\"%1\", %2, %3, homeIcon]]").arg(label).arg(lat).arg(lon)); - - QString javaScript = QString("grids_confirmed = [];" - "grids_worked = [];" - "drawPointsGroup2(%1);" - "maidenheadConfWorked.redraw();").arg(stationString); - - qCDebug(runtime) << javaScript; - - if ( !isMainPageLoaded ) - { - postponedScripts.append(javaScript); - } - else - { - main_page->runJavaScript(javaScript); - } + const MapPoint myPoint(label, lat, lon, QStringLiteral("homeIcon")); + mapController->clearGridLayers(); + mapController->drawHomePoints(QList() << myPoint); + mapController->redrawGridLayer(); } void QSODetailDialog::setStaticMapTime(const QDateTime &dateTime) @@ -1478,15 +1432,7 @@ void QSODetailDialog::setStaticMapTime(const QDateTime &dateTime) qCDebug(function_parameters) << dateTime; - QString javaScript = QString("setStaticMapTime(new Date(%1));").arg(dateTime.toMSecsSinceEpoch()); - - qCDebug(runtime) << javaScript; - - if (!isMainPageLoaded) { - postponedScripts.append(javaScript); - } else { - main_page->runJavaScript(javaScript); - } + mapController->setStaticMapTime(dateTime); } void QSODetailDialog::enableWidgetChangeHandlers() diff --git a/ui/QSODetailDialog.h b/ui/QSODetailDialog.h index bf2178d0..f09131ca 100644 --- a/ui/QSODetailDialog.h +++ b/ui/QSODetailDialog.h @@ -7,13 +7,12 @@ #include #include #include -#include +#include #include "models/LogbookModel.h" #include "data/Gridsquare.h" #include "core/CallbookManager.h" -#include "ui/MapWebChannelHandler.h" -#include "ui/WebEnginePage.h" +#include "ui/MapPageController.h" #include "core/MembershipQE.h" #include "core/LogLocale.h" #include "ui/component/MultiselectCompleter.h" @@ -78,7 +77,7 @@ private slots: bool doValidation(); void doValidationDateTime(const QDateTime&); void doValidationDouble(double); - void mapLoaded(bool); + void mapLoaded(); void myGridChanged(const QString&); void DXGridChanged(const QString&); void callsignFound(const CallbookResponseData &data); @@ -141,9 +140,7 @@ private slots: QPointer lookupButtonMovie; qint64 timeLockDiff; double freqLockDiff; - bool isMainPageLoaded; - QPointer main_page; - QString postponedScripts; + QScopedPointer mapController; CallbookManager callbookManager; QScopedPointer iotaCompleter; QScopedPointer myIotaCompleter; @@ -156,8 +153,6 @@ private slots: QScopedPointer sigCompleter; QScopedPointer countyCompleter; QScopedPointer modeController; - QWebChannel channel; - MapWebChannelHandler layerControlHandler; LogLocale locale; }; diff --git a/ui/QSOFilterDetail.cpp b/ui/QSOFilterDetail.cpp index 3bb3b775..f5b9830e 100644 --- a/ui/QSOFilterDetail.cpp +++ b/ui/QSOFilterDetail.cpp @@ -7,6 +7,7 @@ #include "core/debug.h" #include "data/Data.h" #include "core/QSOFilterManager.h" +#include "ui/component/LogbookFieldComboBox.h" MODULE_IDENTIFICATION("qlog.ui.qsofilterdetail"); @@ -48,7 +49,7 @@ void QSOFilterDetail::addCondition(int fieldIdx, int operatorId, QString value) /***************/ /* Field Combo */ /***************/ - QComboBox* fieldNameCombo = new QComboBox(this); + LogbookFieldComboBox* fieldNameCombo = new LogbookFieldComboBox(this); fieldNameCombo->setObjectName(QString::fromUtf8("fieldNameCombo%1").arg(condCount)); QSizePolicy sizePolicy1(QSizePolicy::Maximum, QSizePolicy::Fixed); sizePolicy1.setHorizontalStretch(0); @@ -56,22 +57,7 @@ void QSOFilterDetail::addCondition(int fieldIdx, int operatorId, QString value) sizePolicy1.setHeightForWidth(fieldNameCombo->sizePolicy().hasHeightForWidth()); fieldNameCombo->setSizePolicy(sizePolicy1); - QList> items; - - for ( int i = LogbookModel::ColumnID::COLUMN_ID; i < LogbookModel::ColumnID::COLUMN_LAST_ELEMENT; ++i ) - { - LogbookModel::ColumnID columnID = static_cast(i); - items.append({columnID, LogbookModel::getFieldNameTranslation(columnID)}); - } - - std::sort(items.begin(), items.end(), [](const QPair& a, - const QPair& b) - { - return a.second.localeAwareCompare(b.second) < 0; - }); - - for (const auto& item : items) - fieldNameCombo->addItem(item.second, item.first); + fieldNameCombo->populate(LogbookFieldComboBox::ValueMode::ColumnId); /* Do not set combo value here because we will connect signal Change later */ conditionLayout->addWidget(fieldNameCombo); diff --git a/ui/RigWidget.cpp b/ui/RigWidget.cpp index 5e3cfd6e..32670f54 100644 --- a/ui/RigWidget.cpp +++ b/ui/RigWidget.cpp @@ -2,8 +2,11 @@ #include "ui_RigWidget.h" #include "rig/macros.h" #include "core/debug.h" +#include "core/EmergencyFrequency.h" +#include "core/IBPBeacon.h" #include "data/Data.h" #include "service/hrdlog/HRDLog.h" +#include "data/BandmapGuide.h" #include "data/BandPlan.h" MODULE_IDENTIFICATION("qlog.ui.rigwidget"); @@ -19,6 +22,12 @@ RigWidget::RigWidget(QWidget *parent) : FCT_IDENTIFICATION; ui->setupUi(this); + ui->bandmapGuideLabel->setVisible(false); + ui->emergencyFrequencyLabel->setVisible(false); + ui->ibpBeaconLabel->setVisible(false); + connect(BandmapGuide::instance(), &BandmapGuide::changed, + this, [this]() { updateFrequencyInfoLabels(lastSeenFreq); }); + ui->freqLabel->setSelectionModeEnabled(false); ui->freqLabel->setDebounceEnabled(true); ui->freqLabel->setDebounceIntervalMs(250); @@ -101,6 +110,7 @@ void RigWidget::updateFrequency(VFOID vfoid, double vfoFreq, double ritFreq, dou ui->bandComboBox->blockSignals(false); } lastSeenFreq = vfoFreq; + updateFrequencyInfoLabels(vfoFreq); } void RigWidget::updateSplit(VFOID, bool enabled) @@ -320,6 +330,7 @@ void RigWidget::reloadSettings() FCT_IDENTIFICATION; refreshRigProfileCombo(); + updateFrequencyInfoLabels(lastSeenFreq); } void RigWidget::rigConnected() @@ -338,6 +349,7 @@ void RigWidget::rigConnected() ui->txFreqLabel->setReadOnly(false); ui->pttLabel->setVisible(RigProfilesManager::instance()->getCurProfile1().getPTTInfo); refreshModeCombo(); + updateFrequencyInfoLabels(lastSeenFreq); } void RigWidget::rigDisconnected() @@ -361,6 +373,7 @@ void RigWidget::rigDisconnected() ui->freqLabel->setReadOnly(true); ui->txFreqLabel->setReadOnly(true); ui->pttLabel->setVisible(false); + updateFrequencyInfoLabels(0.0); } void RigWidget::bandUp() @@ -443,6 +456,134 @@ void RigWidget::resetRigInfo() updatePTT(VFO1, false); } +QString RigWidget::readableLabelTextColor(const QColor &background) const +{ + return Data::textColorForBackground(background, + palette().color(QPalette::WindowText), + palette().color(QPalette::Window)).name(QColor::HexRgb); +} + +void RigWidget::clearFrequencyInfoLabel(QLabel *label) +{ + label->clear(); + label->setToolTip(QString()); + label->setStyleSheet(QString()); + label->setVisible(false); +} + +void RigWidget::updateFrequencyInfoLabels(double frequency) +{ + FCT_IDENTIFICATION; + + updateBandmapGuideLabel(frequency); + updateImportantFrequencyLabels(frequency); +} + +void RigWidget::updateBandmapGuideLabel(double frequency) +{ + FCT_IDENTIFICATION; + + if ( !rigOnline || frequency <= 0.0 || !BandmapGuide::isEnabled() ) + { + clearFrequencyInfoLabel(ui->bandmapGuideLabel); + return; + } + + const BandmapGuide::Profile profile = BandmapGuide::currentProfile(); + bool hasValidRange = false; + bool insideGuideRange = false; + for ( const BandmapGuide::Range &range : profile.ranges ) + { + if ( !range.isValid() ) + continue; + + hasValidRange = true; + + if ( frequency < range.from || frequency > range.to ) + continue; + + insideGuideRange = true; + + const QString label = range.label.trimmed(); + if ( label.isEmpty() ) + continue; + + ui->bandmapGuideLabel->setText(label); + ui->bandmapGuideLabel->setToolTip(QString("%1: %2 - %3 MHz") + .arg(label, + QString::number(range.from, 'f', 6), + QString::number(range.to, 'f', 6))); + ui->bandmapGuideLabel->setStyleSheet(QString("QLabel { color: %1; background-color: %2; border-radius: 3px; padding: 1px 5px; font-weight: bold; }") + .arg(readableLabelTextColor(range.color), + range.color.name())); + ui->bandmapGuideLabel->setVisible(true); + return; + } + + if ( hasValidRange && !insideGuideRange ) + { + const QColor outColor(QStringLiteral("#ffd45a")); + ui->bandmapGuideLabel->setText(tr("OUT")); + ui->bandmapGuideLabel->setToolTip(tr("Outside Bandmap Guide range")); + ui->bandmapGuideLabel->setStyleSheet(QString("QLabel { color: %1; background-color: %2; border-radius: 3px; padding: 1px 5px; font-weight: bold; }") + .arg(readableLabelTextColor(outColor), + outColor.name())); + ui->bandmapGuideLabel->setVisible(true); + } + else + { + clearFrequencyInfoLabel(ui->bandmapGuideLabel); + } +} + +void RigWidget::updateImportantFrequencyLabels(double frequency) +{ + FCT_IDENTIFICATION; + + if ( !rigOnline || frequency <= 0.0 ) + { + clearFrequencyInfoLabel(ui->emergencyFrequencyLabel); + clearFrequencyInfoLabel(ui->ibpBeaconLabel); + return; + } + + const EmergencyFreqEntry *emergency = EmergencyFrequency::findEmergency(frequency); + if ( emergency ) + { + const QColor emergencyColor(QStringLiteral("#b91c1c")); + ui->emergencyFrequencyLabel->setText(tr("SOS")); + ui->emergencyFrequencyLabel->setToolTip(tr("Emergency frequency: %1 MHz") + .arg(QString::number(emergency->frequency, 'f', 3))); + ui->emergencyFrequencyLabel->setStyleSheet(QString("QLabel { color: %1; background-color: %2; border-radius: 3px; padding: 1px 5px; font-weight: bold; }") + .arg(readableLabelTextColor(emergencyColor), + emergencyColor.name())); + ui->emergencyFrequencyLabel->setVisible(true); + } + else + { + clearFrequencyInfoLabel(ui->emergencyFrequencyLabel); + } + + const double ibpToleranceMHz = 0.001; + for ( const IBPBeacon::Band &band : IBPBeacon::bands() ) + { + if ( qAbs(frequency - band.frequency) > ibpToleranceMHz ) + continue; + + const QColor ibpColor(QStringLiteral("#1e88e5")); + ui->ibpBeaconLabel->setText(tr("IBP")); + ui->ibpBeaconLabel->setToolTip(tr("International Beacon Project: %1 MHz") + .arg(QString::number(band.frequency, 'f', 3))); + ui->ibpBeaconLabel->setStyleSheet(QString("QLabel { color: %1; background-color: %2; border-radius: 3px; padding: 1px 5px; font-weight: bold; }") + .arg(readableLabelTextColor(ibpColor), + ibpColor.name())); + ui->ibpBeaconLabel->setVisible(true); + return; + } + + clearFrequencyInfoLabel(ui->ibpBeaconLabel); +} + void RigWidget::saveLastSeenFreq() { FCT_IDENTIFICATION; diff --git a/ui/RigWidget.h b/ui/RigWidget.h index 45c7da5e..50530b3f 100644 --- a/ui/RigWidget.h +++ b/ui/RigWidget.h @@ -5,6 +5,9 @@ #include "rig/Rig.h" #include "service/hrdlog/HRDLog.h" +class QColor; +class QLabel; + namespace Ui { class RigWidget; } @@ -55,6 +58,12 @@ private slots: void resetRigInfo(); void saveLastSeenFreq(); + void updateFrequencyInfoLabels(double frequency); + void updateBandmapGuideLabel(double frequency); + void updateImportantFrequencyLabels(double frequency); + void clearFrequencyInfoLabel(QLabel *label); + QString readableLabelTextColor(const QColor &background) const; + double lastSeenFreq; QString lastSeenMode; bool rigOnline; diff --git a/ui/RigWidget.ui b/ui/RigWidget.ui index d4b2028e..16da6266 100644 --- a/ui/RigWidget.ui +++ b/ui/RigWidget.ui @@ -6,8 +6,8 @@ 0 0 - 354 - 112 + 383 + 128 @@ -148,6 +148,60 @@ + + + + + 0 + 0 + + + + + 10 + + + + + + + + + + + + 0 + 0 + + + + + 10 + + + + + + + + + + + + 0 + 0 + + + + + 10 + + + + + + + @@ -281,7 +335,8 @@ QAbstractSpinBox::lineEdit { background: transparent; border: none; padding: 0px - + + .. diff --git a/ui/SettingsDialog.cpp b/ui/SettingsDialog.cpp index 54956235..339c49d5 100644 --- a/ui/SettingsDialog.cpp +++ b/ui/SettingsDialog.cpp @@ -1,10 +1,19 @@ #include #include +#include #include +#include #include #include +#include +#include #include #include +#include +#include +#include +#include +#include #include "SettingsDialog.h" #include "ui_SettingsDialog.h" @@ -33,6 +42,7 @@ #include "core/LogParam.h" #include "data/Callsign.h" #include "core/MembershipQE.h" +#include "data/BandmapGuide.h" #include "models/SqlListModel.h" #include "service/kstchat/KSTChat.h" #include "data/HostsPortString.h" @@ -40,11 +50,42 @@ #include "ui/component/StyleItemDelegate.h" #include "data/SerialPort.h" #include "service/cloudlog/Cloudlog.h" +#include "ui/BandmapGuideDialog.h" +#include "ui/BandmapWidget.h" #include "ui/RigctldAdvancedDialog.h" #include "cwkey/drivers/CWWinKey.h" +#include "ui/EmailQSLSettingsWidget.h" MODULE_IDENTIFICATION("qlog.ui.settingsdialog"); +namespace +{ + constexpr int ADIF_FILE_EXISTS_ROLE = Qt::UserRole + 1; + + class AdifRecoveryPathDelegate : public QStyledItemDelegate + { + public: + using QStyledItemDelegate::QStyledItemDelegate; + + void paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override + { + QStyleOptionViewItem opt(option); + initStyleOption(&opt, index); + + const bool exists = index.data(ADIF_FILE_EXISTS_ROLE).toBool(); + opt.text = QStringLiteral("%1 %2").arg(QString(QChar(exists ? 0x2713 : 0x2717)), + index.data(Qt::EditRole).toString()); + opt.palette.setColor(QPalette::Text, exists ? Qt::darkGreen : Qt::red); + opt.palette.setColor(QPalette::HighlightedText, exists ? Qt::darkGreen : Qt::red); + + QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); + style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget); + } + }; +} + void SettingsDialog::refreshProfileView(QAbstractItemView *view, const QStringList &names) { QStringListModel *model = static_cast(view->model()); @@ -114,6 +155,242 @@ void SettingsDialog::deleteSelectedProfiles(QAbstractItemView *view, view->clearSelection(); } +QList SettingsDialog::qsoStatusColorRows() const +{ + return QList + { + {Data::QSO_STATUS_COLOR_DUPE_KEY, tr("Duplicate"), tr("Already worked QSO")}, + {Data::QSO_STATUS_COLOR_NEW_ENTITY_KEY, tr("New Entity"), tr("DXCC entity not worked yet")}, + {Data::QSO_STATUS_COLOR_NEW_BAND_MODE_KEY, tr("New Band / Mode"), tr("New band, mode, or band and mode")}, + {Data::QSO_STATUS_COLOR_NEW_SLOT_KEY, tr("New Slot"), tr("New band and mode combination")}, + {Data::QSO_STATUS_COLOR_WORKED_KEY, tr("Worked"), tr("Worked but not confirmed")}, + {Data::QSO_STATUS_COLOR_CONFIRMED_KEY, tr("Confirmed"), tr("Confirmed QSO; no highlight by default")} + }; +} + +void SettingsDialog::setupQsoStatusColorsTable() +{ + FCT_IDENTIFICATION; + + ui->qsoStatusColorsTable->setColumnCount(QsoStatusColorColumnCount); + ui->qsoStatusColorsTable->setHorizontalHeaderLabels(QStringList() << tr("Status") + << tr("Description") + << tr("Color")); + ui->qsoStatusColorsTable->verticalHeader()->setVisible(false); + ui->qsoStatusColorsTable->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + ui->qsoStatusColorsTable->horizontalHeader()->setSectionResizeMode(QsoStatusColumn, QHeaderView::ResizeToContents); + ui->qsoStatusColorsTable->horizontalHeader()->setSectionResizeMode(QsoStatusMeaningColumn, QHeaderView::Stretch); + ui->qsoStatusColorsTable->horizontalHeader()->setSectionResizeMode(QsoStatusColorColumn, QHeaderView::ResizeToContents); + + const QList rows = qsoStatusColorRows(); + ui->qsoStatusColorsTable->setRowCount(rows.size()); + + for ( int row = 0; row < rows.size(); ++row ) + { + const QsoStatusColorRow &colorRow = rows.at(row); + + QTableWidgetItem *statusItem = new QTableWidgetItem(colorRow.status); + statusItem->setFlags(Qt::ItemIsEnabled); + ui->qsoStatusColorsTable->setItem(row, QsoStatusColumn, statusItem); + + QTableWidgetItem *meaningItem = new QTableWidgetItem(colorRow.meaning); + meaningItem->setFlags(Qt::ItemIsEnabled); + ui->qsoStatusColorsTable->setItem(row, QsoStatusMeaningColumn, meaningItem); + + QPushButton *button = new QPushButton(ui->qsoStatusColorsTable); + button->setMinimumWidth(75); + button->setProperty("qsoStatusColorKey", colorRow.key); + connect(button, &QPushButton::clicked, this, [this, button]() + { + chooseQsoStatusColor(button); + }); + ui->qsoStatusColorsTable->setCellWidget(row, QsoStatusColorColumn, button); + } + + const int tableHeight = ui->qsoStatusColorsTable->horizontalHeader()->sizeHint().height() + + ui->qsoStatusColorsTable->verticalHeader()->length() + + ui->qsoStatusColorsTable->frameWidth() * 2; + + ui->qsoStatusColorsTable->setFixedHeight(tableHeight); +} + +void SettingsDialog::loadQsoStatusColors() +{ + FCT_IDENTIFICATION; + + const QVariantMap colors = LogParam::getQsoStatusColors(); + for ( int row = 0; row < ui->qsoStatusColorsTable->rowCount(); ++row ) + { + QPushButton *button = qsoStatusColorButton(row); + if ( !button ) + continue; + + setQsoStatusColorButton(button, qsoStatusColorFromSettings(qsoStatusColorKey(button), colors)); + } +} + +void SettingsDialog::saveQsoStatusColors() const +{ + FCT_IDENTIFICATION; + + QVariantMap colors; + for ( int row = 0; row < ui->qsoStatusColorsTable->rowCount(); ++row ) + { + QPushButton *button = qsoStatusColorButton(row); + if ( !button ) + continue; + + const QString key = qsoStatusColorKey(button); + const QColor color = qsoStatusColorFromButton(button); + const QColor defaultColor = Data::defaultQsoStatusColor(key); + + if ( !color.isValid() ) + continue; + + if ( !qsoStatusColorsEqual(color, defaultColor) ) + colors.insert(key, color.name(QColor::HexArgb)); + } + + LogParam::setQsoStatusColors(colors); +} + +void SettingsDialog::chooseQsoStatusColor(QPushButton *button) +{ + FCT_IDENTIFICATION; + + QMenu menu(this); + const QAction *chooseColorAction = menu.addAction(tr("Choose Color...")); + const QAction *defaultColorAction = menu.addAction(tr("Default")); + const QAction *noColorAction = menu.addAction(tr("No Color")); + const QAction *selectedAction = menu.exec(button->mapToGlobal(QPoint(0, button->height()))); + + if ( !selectedAction ) + return; + + const QString key = qsoStatusColorKey(button); + + if ( selectedAction == defaultColorAction ) + { + setQsoStatusColorButton(button, Data::defaultQsoStatusColor(key)); + return; + } + + if ( selectedAction == noColorAction ) + { + setQsoStatusColorButton(button, qsoStatusNoColor()); + return; + } + + if ( selectedAction != chooseColorAction ) + return; + + const QColor currentColor = qsoStatusColorFromButton(button); + const QColor defaultColor = Data::defaultQsoStatusColor(key); + const QColor initialColor = (currentColor.isValid() && currentColor.alpha() > 0) + ? currentColor + : (defaultColor.isValid() && defaultColor.alpha() > 0) + ? defaultColor + : qApp->palette().highlight().color(); + const QColor selected = QColorDialog::getColor(initialColor, + this, + tr("Status Color"), + QColorDialog::ShowAlphaChannel | QColorDialog::DontUseNativeDialog); + if ( selected.isValid() ) + setQsoStatusColorButton(button, selected); +} + +QColor SettingsDialog::qsoStatusNoColor() const +{ + return QColor(0, 0, 0, 0); +} + +void SettingsDialog::setQsoStatusColorButton(QPushButton *button, const QColor &color) const +{ + FCT_IDENTIFICATION; + + button->setProperty("color", color); + + if ( !color.isValid() || color.alpha() == 0 ) + { + button->setText(tr("No color")); + button->setToolTip(tr("No highlight. Click to choose a color or set no color.")); + button->setStyleSheet(QString()); + return; + } + + const QColor textColor = Data::textColorForBackground(color, + button->palette().color(QPalette::ButtonText), + button->palette().color(QPalette::Button)); + button->setText(QString()); + button->setToolTip(tr("Click to change color or set no color.")); + button->setStyleSheet(QStringLiteral("QPushButton { background-color: %1; color: %2; }") + .arg(qsoStatusColorStyleValue(color), textColor.name(QColor::HexRgb))); +} + +QColor SettingsDialog::qsoStatusColorFromButton(QPushButton *button) const +{ + FCT_IDENTIFICATION; + + return button->property("color").value(); +} + +void SettingsDialog::restoreDefaultQsoStatusColors() +{ + FCT_IDENTIFICATION; + + for ( int row = 0; row < ui->qsoStatusColorsTable->rowCount(); ++row ) + { + QPushButton *button = qsoStatusColorButton(row); + if ( !button ) + continue; + + setQsoStatusColorButton(button, Data::defaultQsoStatusColor(qsoStatusColorKey(button))); + } +} + +QPushButton *SettingsDialog::qsoStatusColorButton(int row) const +{ + return qobject_cast(ui->qsoStatusColorsTable->cellWidget(row, QsoStatusColorColumn)); +} + +QString SettingsDialog::qsoStatusColorKey(QPushButton *button) const +{ + return button ? button->property("qsoStatusColorKey").toString() : QString(); +} + +QColor SettingsDialog::qsoStatusColorFromSettings(const QString &key, const QVariantMap &colors) const +{ + const QString value = colors.value(key).toString().trimmed(); + if ( !value.isEmpty() ) + { + const QColor color(value); + if ( color.isValid() ) + return color; + } + + return Data::defaultQsoStatusColor(key); +} + +bool SettingsDialog::qsoStatusColorsEqual(const QColor &left, const QColor &right) const +{ + const bool leftEmpty = !left.isValid() || left.alpha() == 0; + const bool rightEmpty = !right.isValid() || right.alpha() == 0; + + if ( leftEmpty || rightEmpty ) + return leftEmpty == rightEmpty; + + return left.rgba() == right.rgba(); +} + +QString SettingsDialog::qsoStatusColorStyleValue(const QColor &color) const +{ + return QStringLiteral("rgba(%1, %2, %3, %4)") + .arg(color.red()) + .arg(color.green()) + .arg(color.blue()) + .arg(color.alpha()); +} + SettingsDialog::SettingsDialog(MainWindow *parent) : QDialog(parent), stationProfManager(StationProfilesManager::instance()), @@ -128,11 +405,15 @@ SettingsDialog::SettingsDialog(MainWindow *parent) : sotaFallback(false), potaFallback(false), wwffFallback(false), - tqslVersionTimer(new QTimer(this)) + tqslVersionTimer(new QTimer(this)), + adifRecoveryModel(nullptr) { FCT_IDENTIFICATION; ui->setupUi(this); + setupAdifRecoveryTab(); + setupQsoStatusColorsTable(); + refreshBandmapGuideCombo(); ui->dateFormatResultLabel->setVisible(false); ui->dateFormatStringEdit->setVisible(false); @@ -280,19 +561,19 @@ SettingsDialog::SettingsDialog(MainWindow *parent) : iotaCompleter->setModelSorting(QCompleter::CaseSensitivelySortedModel); ui->stationIOTAEdit->setCompleter(iotaCompleter); - sotaCompleter = new QCompleter(Data::instance()->sotaIDList(), ui->stationSOTAEdit); + sotaCompleter = new QCompleter(Data::instance()->sotaIDList(), this); sotaCompleter->setCaseSensitivity(Qt::CaseInsensitive); sotaCompleter->setFilterMode(Qt::MatchStartsWith); sotaCompleter->setModelSorting(QCompleter::CaseSensitivelySortedModel); ui->stationSOTAEdit->setCompleter(nullptr); - wwffCompleter = new QCompleter(Data::instance()->wwffIDList(), ui->stationWWFFEdit); + wwffCompleter = new QCompleter(Data::instance()->wwffIDList(), this); wwffCompleter->setCaseSensitivity(Qt::CaseInsensitive); wwffCompleter->setFilterMode(Qt::MatchStartsWith); wwffCompleter->setModelSorting(QCompleter::CaseSensitivelySortedModel); ui->stationWWFFEdit->setCompleter(nullptr); - potaCompleter = new MultiselectCompleter(Data::instance()->potaIDList(), ui->stationPOTAEdit); + potaCompleter = new MultiselectCompleter(Data::instance()->potaIDList(), this); potaCompleter->setCaseSensitivity(Qt::CaseInsensitive); potaCompleter->setFilterMode(Qt::MatchStartsWith); potaCompleter->setModelSorting(QCompleter::CaseSensitivelySortedModel); @@ -1007,10 +1288,10 @@ void SettingsDialog::doubleClickRotUsrButtonsProfile(QModelIndex i) QLineEdit * const buttonEdits[] = { ui->rotUsrButton1Edit, ui->rotUsrButton2Edit, ui->rotUsrButton3Edit, ui->rotUsrButton4Edit }; QSpinBox * const spinBoxes[] = { ui->rotUsrButtonSpinBox1, ui->rotUsrButtonSpinBox2, ui->rotUsrButtonSpinBox3, ui->rotUsrButtonSpinBox4 }; - for ( int i = 0; i < 4; ++i ) + for ( int x = 0; x < 4; ++x ) { - buttonEdits[i]->setText(profile.shortDescs[i]); - spinBoxes[i]->setValue(profile.bearings[i]); + buttonEdits[x]->setText(profile.shortDescs[x]); + spinBoxes[x]->setValue(profile.bearings[x]); } ui->rotUsrButtonAddProfileButton->setText(tr("Modify")); @@ -1357,10 +1638,10 @@ void SettingsDialog::doubleClickCWShortcutProfile(QModelIndex i) QLineEdit * const shortEdits[] = { ui->cwShortcutF1ShortEdit, ui->cwShortcutF2ShortEdit, ui->cwShortcutF3ShortEdit, ui->cwShortcutF4ShortEdit, ui->cwShortcutF5ShortEdit, ui->cwShortcutF6ShortEdit, ui->cwShortcutF7ShortEdit }; QLineEdit * const macroEdits[] = { ui->cwShortcutF1MacroEdit, ui->cwShortcutF2MacroEdit, ui->cwShortcutF3MacroEdit, ui->cwShortcutF4MacroEdit, ui->cwShortcutF5MacroEdit, ui->cwShortcutF6MacroEdit, ui->cwShortcutF7MacroEdit }; - for ( int i = 0; i < 7; ++i ) + for ( int x = 0; x < 7; ++x ) { - shortEdits[i]->setText(profile.shortDescs[i]); - macroEdits[i]->setText(profile.macros[i]); + shortEdits[x]->setText(profile.shortDescs[x]); + macroEdits[x]->setText(profile.macros[x]); } ui->cwShortcutAddProfileButton->setText(tr("Modify")); @@ -1394,6 +1675,8 @@ void SettingsDialog::refreshStationProfilesView() { FCT_IDENTIFICATION; refreshProfileView(ui->stationProfilesListView, stationProfManager->profileNameList()); + refreshAdifRecoveryStationProfileDelegate(); + validateAdifRecoveryStationProfiles(); } void SettingsDialog::addStationProfile() @@ -2199,6 +2482,39 @@ void SettingsDialog::showRigctldAdvanced() } } +void SettingsDialog::editBandmapGuide() +{ + FCT_IDENTIFICATION; + + BandmapGuideDialog dialog(this); + if ( dialog.exec() == QDialog::Accepted ) + { + refreshBandmapGuideCombo(); + BandmapWidget::refreshAllBandmaps(); + } +} + +void SettingsDialog::bandmapGuideChanged(int index) +{ + FCT_IDENTIFICATION; + + if ( index < 0 ) + return; + + const QString profileId = ui->bandmapGuideComboBox->itemData(index).toString(); + if ( profileId.isEmpty() ) + { + BandmapGuide::setEnabled(false); + } + else + { + BandmapGuide::setCurrentProfileId(profileId); + BandmapGuide::setEnabled(true); + } + + BandmapWidget::refreshAllBandmaps(); +} + void SettingsDialog::rigShareChanged(int) { FCT_IDENTIFICATION; @@ -2233,6 +2549,45 @@ void SettingsDialog::updateRigShareEnabled() ui->rigShareCheckBox->setToolTip(tr("Start rigctld daemon to share rig with other applications (e.g. WSJT-X)")); } +void SettingsDialog::refreshBandmapGuideCombo() +{ + FCT_IDENTIFICATION; + + QSignalBlocker blocker(ui->bandmapGuideComboBox); + const QList profiles = BandmapGuide::profiles(); + const QString currentProfileId = BandmapGuide::currentProfileId(); + const bool enabled = BandmapGuide::isEnabled(); + int selectedIndex = 0; + int firstProfileIndex = -1; + + ui->bandmapGuideComboBox->clear(); + ui->bandmapGuideComboBox->addItem(tr("Off"), QString()); + + for ( const BandmapGuide::Profile &profile : profiles ) + { + ui->bandmapGuideComboBox->addItem(profile.name, profile.id); + + const int itemIndex = ui->bandmapGuideComboBox->count() - 1; + if ( firstProfileIndex < 0 ) + firstProfileIndex = itemIndex; + if ( enabled && profile.id == currentProfileId ) + selectedIndex = itemIndex; + } + + if ( enabled && selectedIndex == 0 && firstProfileIndex > 0 ) + selectedIndex = firstProfileIndex; + + ui->bandmapGuideComboBox->setCurrentIndex(selectedIndex); + + if ( enabled ) + { + if ( selectedIndex > 0 ) + BandmapGuide::setCurrentProfileId(ui->bandmapGuideComboBox->itemData(selectedIndex).toString()); + else + BandmapGuide::setEnabled(false); + } +} + void SettingsDialog::qrzAddCallsignAPIKey() { FCT_IDENTIFICATION; @@ -2355,6 +2710,11 @@ void SettingsDialog::readSettings() ui->hrdlogUploadCodeEdit->setText(HRDLogBase::getUploadCode()); ui->hrdlogOnAirCheckBox->setChecked(HRDLogBase::getOnAirEnabled()); + /*****************/ + /* Startup ADI */ + /*****************/ + loadAdifRecoveryTable(); + /***********/ /* QRZ.COM */ /***********/ @@ -2418,6 +2778,12 @@ void SettingsDialog::readSettings() bool unitFormatMetric = locale.getSettingUseMetric(); ui->unitFormatMetricRadioButton->setChecked(unitFormatMetric); ui->unitFormatImperialRadioButton->setChecked(!unitFormatMetric); + loadQsoStatusColors(); + + /***************/ + /* Email QSL */ + /***************/ + ui->emailQSLSettingsWidget->readSettings(); /******************/ /* END OF Reading */ @@ -2479,6 +2845,11 @@ void SettingsDialog::writeSettings() ui->hrdlogUploadCodeEdit->text()); HRDLogBase::saveOnAirEnabled(ui->hrdlogOnAirCheckBox->isChecked()); + /*****************/ + /* Startup ADI */ + /*****************/ + saveAdifRecoveryTable(); + /***********/ /* QRZ.COM */ /***********/ @@ -2545,6 +2916,382 @@ void SettingsDialog::writeSettings() locale.setSettingDateFormat(ui->dateFormatStringEdit->text()); locale.setSettingUseMetric(ui->unitFormatMetricRadioButton->isChecked()); + saveQsoStatusColors(); + Data::reloadQsoStatusColors(); + + /***************/ + /* Email QSL */ + /***************/ + ui->emailQSLSettingsWidget->writeSettings(); +} + +void SettingsDialog::setupAdifRecoveryTab() +{ + FCT_IDENTIFICATION; + + adifRecoveryModel = new QStandardItemModel(this); + adifRecoveryModel->setHorizontalHeaderLabels({tr("Enabled"), + tr("Path"), + tr("Station Profile"), tr("Missing QSL Sent"), + tr("Last Recovery")}); + connect(adifRecoveryModel, &QStandardItemModel::itemChanged, this, [this](QStandardItem *item) + { + if ( !item ) + return; + + if ( item->column() == ADIF_FILE_COLUMN_QSL_SENT ) + { + const QString status = adifRecoveryQslSentStatusFromText(item->text()); + if ( item->data(Qt::UserRole).toString() != status ) + item->setData(status, Qt::UserRole); + } + else if ( item->column() == ADIF_FILE_COLUMN_PATH ) + { + updateAdifRecoveryPathItem(item); + } + else if ( item->column() == ADIF_FILE_COLUMN_STATION_PROFILE ) + { + validateAdifRecoveryStationProfiles(); + } + }); + + ui->adifFileTableView->setModel(adifRecoveryModel); + + ui->adifFileTableView->horizontalHeader()->setSectionResizeMode(ADIF_FILE_COLUMN_ENABLED, QHeaderView::ResizeToContents); + ui->adifFileTableView->horizontalHeader()->setSectionResizeMode(ADIF_FILE_COLUMN_PATH, QHeaderView::Stretch); + ui->adifFileTableView->horizontalHeader()->setSectionResizeMode(ADIF_FILE_COLUMN_STATION_PROFILE, QHeaderView::ResizeToContents); + ui->adifFileTableView->horizontalHeader()->setSectionResizeMode(ADIF_FILE_COLUMN_QSL_SENT, QHeaderView::ResizeToContents); + ui->adifFileTableView->horizontalHeader()->setSectionResizeMode(ADIF_FILE_COLUMN_LAST_RECOVERY, QHeaderView::ResizeToContents); + + refreshAdifRecoveryStationProfileDelegate(); + ui->adifFileTableView->setItemDelegateForColumn(ADIF_FILE_COLUMN_QSL_SENT, + new ComboFormatDelegate(QStringList() + << tr("Queued") + << tr("Ignored") + << tr("Requested") + << tr("No") + << tr("Yes") + << tr("Custom"), + ui->adifFileTableView)); + ui->adifFileTableView->setItemDelegateForColumn(ADIF_FILE_COLUMN_PATH, + new AdifRecoveryPathDelegate(ui->adifFileTableView)); + + setupAdifRecoveryQslSentComboData(); + loadAdifRecoveryQslSentCustomDefaults(); +} + +void SettingsDialog::refreshAdifRecoveryStationProfileDelegate() +{ + if ( !adifRecoveryModel ) + return; + + ui->adifFileTableView->setItemDelegateForColumn(ADIF_FILE_COLUMN_STATION_PROFILE, + new ComboFormatDelegate(stationProfManager->profileNameList(), + ui->adifFileTableView)); +} + +void SettingsDialog::validateAdifRecoveryStationProfiles() +{ + if ( !adifRecoveryModel ) + return; + + const QSignalBlocker blocker(adifRecoveryModel); + const QStringList stationProfiles = stationProfManager->profileNameList(); + + for ( int row = 0; row < adifRecoveryModel->rowCount(); ++row ) + { + QStandardItem *enabledItem = adifRecoveryModel->item(row, ADIF_FILE_COLUMN_ENABLED); + QStandardItem *stationProfileItem = adifRecoveryModel->item(row, ADIF_FILE_COLUMN_STATION_PROFILE); + if ( !enabledItem || !stationProfileItem ) + continue; + + const QString stationProfileName = stationProfileItem->text().trimmed(); + const bool valid = !stationProfileName.isEmpty() && stationProfiles.contains(stationProfileName); + + stationProfileItem->setForeground(valid ? QBrush() : QBrush(Qt::red)); + stationProfileItem->setToolTip(valid ? QString() + : tr("Station Profile does not exist. Select another profile and enable this row again.")); + + if ( !valid ) + enabledItem->setCheckState(Qt::Unchecked); + } + + ui->adifFileTableView->viewport()->update(); +} + +void SettingsDialog::updateAdifRecoveryPathItem(QStandardItem *item) const +{ + if ( !item ) + return; + + const QString path = item->text().trimmed(); + const bool exists = QFileInfo::exists(path); + const QString toolTip = exists ? tr("File exists") : tr("File does not exist"); + + if ( item->data(ADIF_FILE_EXISTS_ROLE).toBool() != exists ) + item->setData(exists, ADIF_FILE_EXISTS_ROLE); + + if ( item->toolTip() != toolTip ) + item->setToolTip(toolTip); +} + +void SettingsDialog::appendAdifRecoveryRow(const AdifRecoveryConfig &config) +{ + QStandardItem *enabledItem = new QStandardItem(); + + enabledItem->setCheckable(true); + enabledItem->setEditable(false); + enabledItem->setCheckState(config.enabled ? Qt::Checked : Qt::Unchecked); + + QStandardItem *stationProfileItem = new QStandardItem(config.stationProfileName); + stationProfileItem->setEditable(true); + + const QString qslSentStatus = config.qslSentStatusDefault.isEmpty() + ? QStringLiteral("Q") + : config.qslSentStatusDefault; + QStandardItem *qslSentItem = new QStandardItem(adifRecoveryQslSentStatusToText(qslSentStatus)); + qslSentItem->setEditable(true); + qslSentItem->setData(qslSentStatus, Qt::UserRole); + + const AdifRecoveryState state = LogParam::getAdifRecoveryState(AdifRecovery::fileKey(config.path, + config.stationProfileName)); + QStandardItem *lastRecoveryItem = new QStandardItem(state.lastRecoveryAt.isValid() + ? state.lastRecoveryAt.toLocalTime().toString(locale.formatDateShortWithYYYY() + + QStringLiteral(" ") + + locale.formatTimeLongWithoutTZ()) + : QString()); + lastRecoveryItem->setEditable(false); + + QStandardItem *pathItem = new QStandardItem(config.path); + pathItem->setEditable(true); + updateAdifRecoveryPathItem(pathItem); + + adifRecoveryModel->appendRow({enabledItem, + pathItem, + stationProfileItem, + qslSentItem, + lastRecoveryItem}); +} + +void SettingsDialog::loadAdifRecoveryTable() +{ + FCT_IDENTIFICATION; + + adifRecoveryModel->removeRows(0, adifRecoveryModel->rowCount()); + loadedAdifRecoveryKeys.clear(); + removedAdifRecoveryKeys.clear(); + + const QList files = LogParam::getAdifRecoveryFiles(); + + for ( const AdifRecoveryConfig &file : files ) + { + appendAdifRecoveryRow(file); + loadedAdifRecoveryKeys.insert(AdifRecovery::fileKey(file.path, file.stationProfileName)); + } + validateAdifRecoveryStationProfiles(); +} + +QList SettingsDialog::adifRecoveryFilesFromTable() const +{ + QList files; + QSet seenPaths; + const QStringList stationProfiles = stationProfManager->profileNameList(); + + for ( int row = 0; row < adifRecoveryModel->rowCount(); ++row ) + { + const QString path = adifRecoveryModel->item(row, ADIF_FILE_COLUMN_PATH)->text().trimmed(); + + if ( path.isEmpty() ) + continue; + + const QString normalizedPath = AdifRecovery::normalizePath(path); + const QStandardItem *stationProfileItem = adifRecoveryModel->item(row, ADIF_FILE_COLUMN_STATION_PROFILE); + const QString stationProfileName = stationProfileItem->text().trimmed(); + + if ( stationProfileName.isEmpty() ) + continue; + + const QString duplicateKey = normalizedPath + QChar(0x1f) + stationProfileName; + + if ( seenPaths.contains(duplicateKey) ) + continue; + + AdifRecoveryConfig config; + config.stationProfileName = stationProfileName; + config.enabled = adifRecoveryModel->item(row, ADIF_FILE_COLUMN_ENABLED)->checkState() == Qt::Checked + && stationProfiles.contains(stationProfileName); + config.qslSentStatusDefault = adifRecoveryQslSentStatusFromItem(adifRecoveryModel->item(row, ADIF_FILE_COLUMN_QSL_SENT)); + config.path = normalizedPath; + files.append(config); + seenPaths.insert(duplicateKey); + } + + return files; +} + +void SettingsDialog::saveAdifRecoveryTable() +{ + FCT_IDENTIFICATION; + + const QList files = adifRecoveryFilesFromTable(); + QSet currentKeys; + + for ( const AdifRecoveryConfig &file : files ) + currentKeys.insert(AdifRecovery::fileKey(file.path, file.stationProfileName)); + + for ( const QString &oldKey : static_cast&>(loadedAdifRecoveryKeys) ) + if ( !currentKeys.contains(oldKey) ) + LogParam::removeAdifRecoveryState(oldKey); + + for ( const QString &removedKey : static_cast&>(removedAdifRecoveryKeys) ) + LogParam::removeAdifRecoveryState(removedKey); + + for ( const AdifRecoveryConfig &file : files ) + { + const QString key = AdifRecovery::fileKey(file.path, file.stationProfileName); + AdifRecoveryState state = LogParam::getAdifRecoveryState(key); + if ( state.offset < 0 ) + { + state.path = file.path; + state.offset = QFileInfo::exists(file.path) ? QFileInfo(file.path).size() : -1; + state.lastRecoveryAt = QDateTime::currentDateTimeUtc(); + state.lastMessage = tr("Startup ADI initialized"); + LogParam::setAdifRecoveryState(key, state); + } + } + + LogParam::setAdifRecoveryFiles(files); + saveAdifRecoveryQslSentCustomDefaults(); +} + +void SettingsDialog::addAdifRecoveryFile() +{ + FCT_IDENTIFICATION; + + const QStringList files = QFileDialog::getOpenFileNames(this, + tr("Select ADIF File"), + QDir::homePath(), + tr("ADIF Files (*.adi *.adif);;All Files (*)")); + const QString defaultProfile = StationProfilesManager::instance()->getCurProfile1().profileName; + for ( const QString &file : files ) + { + AdifRecoveryConfig config; + config.enabled = true; + config.stationProfileName = defaultProfile; + config.qslSentStatusDefault = QStringLiteral("Q"); + config.path = AdifRecovery::normalizePath(file); + appendAdifRecoveryRow(config); + } +} + +void SettingsDialog::removeAdifRecoveryFile() +{ + FCT_IDENTIFICATION; + + const QModelIndexList selectedRows = ui->adifFileTableView->selectionModel()->selectedRows(); + QList rows; + for ( const QModelIndex &index : selectedRows ) + rows.append(index.row()); + + std::sort(rows.begin(), rows.end(), std::greater()); + for ( int row : static_cast&>(rows) ) + { + const QStandardItem *pathItem = adifRecoveryModel->item(row, ADIF_FILE_COLUMN_PATH); + if ( pathItem ) + { + const QString path = pathItem->text().trimmed(); + if ( !path.isEmpty() ) + { + const QStandardItem *stationProfileItem = adifRecoveryModel->item(row, ADIF_FILE_COLUMN_STATION_PROFILE); + const QString stationProfileName = stationProfileItem ? stationProfileItem->text().trimmed() : QString(); + removedAdifRecoveryKeys.insert(AdifRecovery::fileKey(path, stationProfileName)); + } + } + adifRecoveryModel->removeRow(row); + } +} + +void SettingsDialog::setupAdifRecoveryQslSentComboData() +{ + const QList combos = { + ui->adifRecoveryQslSentStatusPaperCombo, + ui->adifRecoveryQslSentStatusLotwCombo, + ui->adifRecoveryQslSentStatusEqslCombo, + ui->adifRecoveryQslSentStatusDclCombo + }; + + for ( QComboBox *combo : combos ) + { + combo->clear(); + combo->addItem(tr("Queued"), "Q"); + combo->addItem(tr("Requested"), "R"); + combo->addItem(tr("Ignored"), "I"); + combo->addItem(tr("No"), "N"); + combo->addItem(tr("Yes"), "Y"); + } +} + +void SettingsDialog::loadAdifRecoveryQslSentCustomDefaults() +{ + setComboByData(ui->adifRecoveryQslSentStatusPaperCombo, + LogParam::getAdifRecoveryQslSentStatusPaper(), + 0); + setComboByData(ui->adifRecoveryQslSentStatusLotwCombo, + LogParam::getAdifRecoveryQslSentStatusLoTW(), + 0); + setComboByData(ui->adifRecoveryQslSentStatusEqslCombo, + LogParam::getAdifRecoveryQslSentStatusEQSL(), + 0); + setComboByData(ui->adifRecoveryQslSentStatusDclCombo, + LogParam::getAdifRecoveryQslSentStatusDCL(), + 0); +} + +void SettingsDialog::saveAdifRecoveryQslSentCustomDefaults() const +{ + LogParam::setAdifRecoveryQslSentStatusPaper(ui->adifRecoveryQslSentStatusPaperCombo->currentData().toString()); + LogParam::setAdifRecoveryQslSentStatusLoTW(ui->adifRecoveryQslSentStatusLotwCombo->currentData().toString()); + LogParam::setAdifRecoveryQslSentStatusEQSL(ui->adifRecoveryQslSentStatusEqslCombo->currentData().toString()); + LogParam::setAdifRecoveryQslSentStatusDCL(ui->adifRecoveryQslSentStatusDclCombo->currentData().toString()); +} + +QString SettingsDialog::adifRecoveryQslSentStatusFromItem(const QStandardItem *item) const +{ + if ( !item ) + return QStringLiteral("Q"); + + const QString status = item->data(Qt::UserRole).toString(); + return status.isEmpty() ? adifRecoveryQslSentStatusFromText(item->text()) : status; +} + +QString SettingsDialog::adifRecoveryQslSentStatusFromText(const QString &text) const +{ + if ( text == tr("Ignored") ) + return QStringLiteral("I"); + if ( text == tr("Requested") ) + return QStringLiteral("R"); + if ( text == tr("No") ) + return QStringLiteral("N"); + if ( text == tr("Yes") ) + return QStringLiteral("Y"); + if ( text == tr("Custom") ) + return QStringLiteral("custom"); + return QStringLiteral("Q"); +} + +QString SettingsDialog::adifRecoveryQslSentStatusToText(const QString &status) const +{ + if ( status == QLatin1String("I") ) + return tr("Ignored"); + if ( status == QLatin1String("R") ) + return tr("Requested"); + if ( status == QLatin1String("N") ) + return tr("No"); + if ( status == QLatin1String("Y") ) + return tr("Yes"); + if ( status == QLatin1String("custom") ) + return tr("Custom"); + return tr("Queued"); } /* this function is called when user modify rig progile diff --git a/ui/SettingsDialog.h b/ui/SettingsDialog.h index 5729c133..c35f8646 100644 --- a/ui/SettingsDialog.h +++ b/ui/SettingsDialog.h @@ -12,7 +12,9 @@ #include #include #include +#include #include +#include #include "data/StationProfile.h" #include "data/RigProfile.h" @@ -22,6 +24,7 @@ #include "data/CWShortcutProfile.h" #include "data/RotUsrButtonsProfile.h" #include "core/LogLocale.h" +#include "core/AdifRecovery.h" #include "ui/MainWindow.h" #include "ui/component/MultiselectCompleter.h" #include "rig/RigCaps.h" @@ -31,6 +34,8 @@ class SettingsDialog; } class QSqlTableModel; +class QStandardItem; +class QStandardItemModel; class SettingsDialog : public QDialog { Q_OBJECT @@ -128,6 +133,8 @@ public slots: void rigFlowControlChanged(int); void showRigctldAdvanced(); void rigShareChanged(int); + void editBandmapGuide(); + void bandmapGuideChanged(int); void qrzAddCallsignAPIKey(); void qrzDelCallsignAPIKey(); @@ -135,12 +142,45 @@ public slots: void onDeleteAllPasswords(); void onDeleteAllQSOs(); + void addAdifRecoveryFile(); + void removeAdifRecoveryFile(); + void restoreDefaultQsoStatusColors(); + private: + enum QsoStatusColorColumn + { + QsoStatusColumn, + QsoStatusMeaningColumn, + QsoStatusColorColumn, + QsoStatusColorColumnCount + }; + + struct QsoStatusColorRow + { + QString key; + QString status; + QString meaning; + }; + void readSettings(); void writeSettings(); void setUIBasedOnRigCaps(const RigCaps&); void refreshRigAssignedCWKeyCombo(); + void refreshBandmapGuideCombo(); void updateRigShareEnabled(); + QList qsoStatusColorRows() const; + void setupQsoStatusColorsTable(); + void loadQsoStatusColors(); + void saveQsoStatusColors() const; + void chooseQsoStatusColor(QPushButton *button); + QColor qsoStatusNoColor() const; + void setQsoStatusColorButton(QPushButton *button, const QColor &color) const; + QColor qsoStatusColorFromButton(QPushButton *button) const; + QPushButton *qsoStatusColorButton(int row) const; + QString qsoStatusColorKey(QPushButton *button) const; + QColor qsoStatusColorFromSettings(const QString &key, const QVariantMap &colors) const; + bool qsoStatusColorsEqual(const QColor &left, const QColor &right) const; + QString qsoStatusColorStyleValue(const QColor &color) const; void setValidationResultColor(QLineEdit *); void generateMembershipCheckboxes(); static void refreshProfileView(QAbstractItemView *view, const QStringList &names); @@ -156,6 +196,20 @@ public slots: void generateQRZAPICallsignTable(); void saveQRZAPICallsignTable(); void updateCountyCompleter(int dxcc); + void setupAdifRecoveryTab(); + void loadAdifRecoveryTable(); + void saveAdifRecoveryTable(); + QList adifRecoveryFilesFromTable() const; + void appendAdifRecoveryRow(const AdifRecoveryConfig &config); + void refreshAdifRecoveryStationProfileDelegate(); + void validateAdifRecoveryStationProfiles(); + void updateAdifRecoveryPathItem(QStandardItem *item) const; + void setupAdifRecoveryQslSentComboData(); + void loadAdifRecoveryQslSentCustomDefaults(); + void saveAdifRecoveryQslSentCustomDefaults() const; + QString adifRecoveryQslSentStatusFromItem(const QStandardItem *item) const; + QString adifRecoveryQslSentStatusFromText(const QString &text) const; + QString adifRecoveryQslSentStatusToText(const QString &status) const; static constexpr int STACKED_WIDGET_SERIAL_SETTING = 0; static constexpr int STACKED_WIDGET_NETWORK_SETTING = 1; @@ -180,6 +234,12 @@ public slots: static constexpr int PTT_TYPE_CAT_INDEX = 1; static constexpr int CIVADDR_DISABLED_VALUE = -1; + static constexpr int ADIF_FILE_COLUMN_ENABLED = 0; + static constexpr int ADIF_FILE_COLUMN_PATH = 1; + static constexpr int ADIF_FILE_COLUMN_STATION_PROFILE = 2; + static constexpr int ADIF_FILE_COLUMN_QSL_SENT = 3; + static constexpr int ADIF_FILE_COLUMN_LAST_RECOVERY = 4; + static constexpr const char* EMPTY_CWKEY_PROFILE = " "; QSqlTableModel* modeTableModel; @@ -206,6 +266,9 @@ public slots: QString rigctldPath; QString rigctldArgs; QTimer *tqslVersionTimer; + QStandardItemModel *adifRecoveryModel; + QSet loadedAdifRecoveryKeys; + QSet removedAdifRecoveryKeys; }; #endif // QLOG_UI_SETTINGSDIALOG_H diff --git a/ui/SettingsDialog.ui b/ui/SettingsDialog.ui index 816f298e..ccb7c4a9 100644 --- a/ui/SettingsDialog.ui +++ b/ui/SettingsDialog.ui @@ -4001,6 +4001,147 @@ + + + Startup ADI + + + + + + Configured ADI/ADIF files are checked only at startup. A newly added file starts at its current end, so only later appended QSOs are loaded. This is not a live watcher; if many new QSOs are found, loading stops and the standard Import should be used. + + + true + + + + + + + true + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectRows + + + QAbstractItemView::ScrollPerPixel + + + QAbstractItemView::ScrollPerPixel + + + true + + + false + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Add + + + + + + + Removing a file also forgets its recovery position. + + + Remove + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Used when a file row has Missing QSL Sent set to Custom. Explicit ADIF values are kept. + + + Custom QSL Sent Defaults + + + + + + Paper QSL + + + + + + + eQSL + + + + + + + LoTW + + + + + + + DCL + + + + + + + + + + + + + + + + + + + + Others @@ -4196,6 +4337,95 @@ Bands + + + + 15 + + + + + Select the <b>Bandmap Guide</b> profile shown as visual frequency hints. It does not affect mode identification. + + + true + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + QComboBox::AdjustToContents + + + + + + + + 0 + 0 + + + + Manage + + + + .. + + + + + + + + + Qt::Vertical + + + QSizePolicy::Maximum + + + + 20 + 20 + + + + + + + + Double-click cells to edit start/end frequency, enabled state, or SAT mode. Band names are fixed; new bands cannot be added here. + + + true + + + @@ -4757,12 +4987,112 @@ - - - + + + + 0 + 0 + + + QSO DXCC Status Colors + + + + + + + 0 + 0 + + + + Used for DX spots, Bandmap, WSJT-X and QSO status hints. Confirmed has no highlight by default. Click a color cell to choose a color or set No color. + + + true + + + + + + + + 16777215 + 220 + + + + QAbstractItemView::NoEditTriggers + + + true + + + QAbstractItemView::NoSelection + + + 20 + + + 20 + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Restore Defaults + + + + + + + + + + 20 + + + + + + 0 + 0 + + + + Shortcuts + + + + + + + + + + + + @@ -4894,6 +5224,16 @@ + + + Email QSL + + + + + + + @@ -4909,6 +5249,11 @@ QLineEdit
ui/component/EditLine.h
+ + EmailQSLSettingsWidget + QWidget +
ui/EmailQSLSettingsWidget.h
+
stationProfileNameEdit @@ -5062,6 +5407,8 @@ dxccConfirmedByLotwCheckBox dxccConfirmedByPaperCheckBox dxccConfirmedByEqslCheckBox + bandmapGuideComboBox + editBandmapGuideButton bandTableView modeTableView wsjtForwardEdit @@ -5090,10 +5437,28 @@ timeFormat12RadioButton unitFormatMetricRadioButton unitFormatImperialRadioButton + qsoStatusColorsTable + qsoStatusColorsDefaultsButton shortcutsTableView + + qsoStatusColorsDefaultsButton + clicked() + SettingsDialog + restoreDefaultQsoStatusColors() + + + 607 + 430 + + + 405 + 430 + + + stationWWFFEdit editingFinished() @@ -6310,6 +6675,70 @@ + + adifFileAddButton + clicked() + SettingsDialog + addAdifRecoveryFile() + + + 99 + 407 + + + 399 + 417 + + + + + adifFileRemoveButton + clicked() + SettingsDialog + removeAdifRecoveryFile() + + + 178 + 407 + + + 399 + 417 + + + + + editBandmapGuideButton + clicked() + SettingsDialog + editBandmapGuide() + + + 600 + 420 + + + 399 + 417 + + + + + bandmapGuideComboBox + currentIndexChanged(int) + SettingsDialog + bandmapGuideChanged(int) + + + 520 + 420 + + + 399 + 417 + + + save() @@ -6383,10 +6812,15 @@ showRigctldAdvanced() onDeleteAllQSOs() onDeleteAllPasswords() + addAdifRecoveryFile() + removeAdifRecoveryFile() + restoreDefaultQsoStatusColors() + editBandmapGuide() + bandmapGuideChanged(int) + - diff --git a/ui/StatisticsWidget.cpp b/ui/StatisticsWidget.cpp index 2bd09ca5..8282e448 100644 --- a/ui/StatisticsWidget.cpp +++ b/ui/StatisticsWidget.cpp @@ -34,10 +34,14 @@ void StatisticsWidget::refreshWidget() { FCT_IDENTIFICATION; - if ( !isVisible() ) + pendingRefresh = true; + mapRenderDirty = true; + + if ( !isVisible() || !initialized ) return; refreshCombos(); + pendingRefresh = false; refreshGraph(); } @@ -308,6 +312,8 @@ void StatisticsWidget::refreshGraph() /***************/ else if ( ui->statTypeMainCombo->currentIndex() == 4 ) { + ui->stackedWidget->setCurrentIndex(1); + QStringList confirmed("1=2 "); if ( ui->eqslCheckBox->isChecked() ) @@ -319,6 +325,15 @@ void StatisticsWidget::refreshGraph() if ( ui->paperCheckBox->isChecked() ) confirmed << " qsl_rcvd = 'Y' "; + const QString currentRenderKey = currentMapRenderKey(genericFilter); + if ( !mapRenderKey.isEmpty() + && !mapRenderDirty + && currentRenderKey == mapRenderKey ) + { + mapController->invalidateSize(); + return; + } + QString innerCase = " CASE WHEN (" + confirmed.join("or") + ") THEN 1 ELSE 0 END "; QString stmtMyLocations = "SELECT DISTINCT my_gridsquare FROM contacts WHERE " + genericFilter.join(" AND "); QSqlQuery myLocations(stmtMyLocations); @@ -364,7 +379,9 @@ void StatisticsWidget::refreshGraph() break; } - ui->stackedWidget->setCurrentIndex(1); + mapRenderKey = currentRenderKey; + mapRenderDirty = false; + mapController->invalidateSize(); } } @@ -386,44 +403,19 @@ void StatisticsWidget::dateRangeCheckBoxChanged(int) refreshGraph(); } -void StatisticsWidget::mapLoaded(bool) -{ - FCT_IDENTIFICATION; - - isMainPageLoaded = true; - - /* which layers will be active */ - postponedScripts += layerControlHandler.generateMapMenuJS(true, false, false, false, false, false, false, false, true); - main_page->runJavaScript(postponedScripts); - - layerControlHandler.restoreLayerControlStates(main_page); -} - void StatisticsWidget::changeTheme(int theme, bool isDark) { FCT_IDENTIFICATION; qCDebug(function_parameters) << theme << isDark; - QString themeJavaScript; - - if (isDark) /* dark mode */ - themeJavaScript = "map.getPanes().tilePane.style.webkitFilter=\"brightness(0.6) invert(1) contrast(3) hue-rotate(200deg) saturate(0.3) brightness(0.9)\";"; - else - themeJavaScript = "map.getPanes().tilePane.style.webkitFilter=\"\";"; - - if ( !isMainPageLoaded ) - postponedScripts.append(themeJavaScript); - else - main_page->runJavaScript(themeJavaScript); + mapController->setDarkTheme(isDark); } StatisticsWidget::StatisticsWidget(QWidget *parent) : QWidget(parent), ui(new Ui::StatisticsWidget), - main_page(new WebEnginePage(this)), - isMainPageLoaded(false), - layerControlHandler("statistics", parent) + mapController(new MapPageController(QStringLiteral("statistics"), this)) { FCT_IDENTIFICATION; @@ -447,18 +439,14 @@ StatisticsWidget::StatisticsWidget(QWidget *parent) : ui->graphView->setRenderHint(QPainter::Antialiasing); ui->graphView->setChart(new QChart()); - main_page->setWebChannel(&channel); - ui->mapView->setPage(main_page); - connect(ui->mapView, &QWebEngineView::loadFinished, this, &StatisticsWidget::mapLoaded); - main_page->load(QUrl(QStringLiteral("qrc:/res/map/onlinemap.html"))); - ui->mapView->setFocusPolicy(Qt::ClickFocus); - channel.registerObject("layerControlHandler", &layerControlHandler); + mapController->attach(ui->mapView, + MapLayer::Grid + | MapLayer::Path); } StatisticsWidget::~StatisticsWidget() { FCT_IDENTIFICATION; - main_page->deleteLater(); delete ui; } @@ -466,25 +454,12 @@ bool StatisticsWidget::event(QEvent *event) { if (event->type() == QEvent::Show) { - // We will not use refreshWidget here, even though at first glance it appears - // to do the same thing. The difference is that we want class constructor to be as fast as possible. - // Therefore, in the constructor, we do not populate the combo boxes. As a result, they are empty - // when first displayed and need to be loaded and then combos for Rig, Ant, etc., can be set. - refreshCombos(); - ui->statTypeMainCombo->blockSignals(true); - ui->statTypeMainCombo->setCurrentIndex(0); - ui->statTypeMainCombo->blockSignals(false); - setSubTypesCombo(ui->statTypeMainCombo->currentIndex()); - ui->myRigCombo->blockSignals(true); - ui->myRigCombo->setCurrentIndex(0); - ui->myRigCombo->blockSignals(false); - ui->myAntennaCombo->blockSignals(true); - ui->myAntennaCombo->setCurrentIndex(0); - ui->myAntennaCombo->blockSignals(false); - ui->userFilterCombo->blockSignals(true); - ui->userFilterCombo->setCurrentIndex(0); - ui->userFilterCombo->blockSignals(false); - refreshGraph(); + if ( !initialized ) + initializeWidget(); + else if ( pendingRefresh ) + refreshWidget(); + else if ( ui->stackedWidget->currentWidget() == ui->mapPage ) + mapController->invalidateSize(); } return QWidget::event(event); // Propagate the event further } @@ -563,35 +538,27 @@ void StatisticsWidget::drawMyLocationsOnMap(QSqlQuery &query) if ( query.lastQuery().isEmpty() ) return; - QStringList locationIcons; - QStringList rawLocationsPoint; + QList locationIcons; + QList rawLocationsPoint; while ( query.next() ) { const QString &loc = query.value(0).toString(); - const Gridsquare stationGrid(loc); + const Gridsquare stationGrid = Gridsquare::mapDisplayGrid(loc); if ( stationGrid.isValid() ) { double lat = stationGrid.getLatitude(); double lon = stationGrid.getLongitude(); - locationIcons.append(QString("[\"%1\", %2, %3, homeIcon]").arg(loc).arg(lat).arg(lon)); - rawLocationsPoint.append(QString("[%1, %2]").arg(lat).arg(lon)); + locationIcons << MapPoint(stationGrid.getGrid(), lat, lon, QStringLiteral("homeIcon")); + rawLocationsPoint << MapCoordinate(lat, lon); } } - QString javaScript = QString("grids_confirmed = [];" - "grids_worked = [];" - "drawPointsGroup2([%1]);" - "maidenheadConfWorked.redraw();" - "map.panTo([0, L.latLngBounds([%2]).getCenter().lng]);").arg(locationIcons.join(","), rawLocationsPoint.join(",")); - - qCDebug(runtime) << javaScript; - - if ( !isMainPageLoaded ) - postponedScripts.append(javaScript); - else - main_page->runJavaScript(javaScript); + mapController->clearGridLayers(); + mapController->drawHomePoints(locationIcons); + mapController->redrawGridLayer(); + mapController->panToBoundsLongitudeCenter(rawLocationsPoint); } void StatisticsWidget::drawPointsOnMap(QSqlQuery &query) @@ -601,36 +568,40 @@ void StatisticsWidget::drawPointsOnMap(QSqlQuery &query) if ( query.lastQuery().isEmpty() ) return; - QList stations; - QList shortPaths; + QList stations; + QList shortPaths; qulonglong count = 0; while ( query.next() ) { - const Gridsquare stationGrid(query.value(1).toString()); - const Gridsquare myStationGrid(query.value(2).toString()); + const Gridsquare stationGrid = Gridsquare::mapDisplayGrid(query.value(1).toString()); + const Gridsquare myStationGrid = Gridsquare::mapDisplayGrid(query.value(2).toString()); if ( stationGrid.isValid() ) { count++; double lat = stationGrid.getLatitude(); double lon = stationGrid.getLongitude(); - // do not wrap the points - double delta = lon - myStationGrid.getLongitude(); - if ( delta > 180 ) - lon -= 360; - if ( delta < -180 ) - lon += 360; - stations.append(QString("[\"%1\", %2, %3, %4]").arg(query.value(0).toString()) - .arg(lat) - .arg(lon) - .arg((query.value(3).toInt()) > 0 ? "greenIconSmall" : "yellowIconSmall")); - shortPaths.append(QString("[%1, %2, %3, %4]") - .arg(myStationGrid.getLatitude()) - .arg(myStationGrid.getLongitude()) - .arg(lat) - .arg(lon)); + if ( myStationGrid.isValid() ) + { + // do not wrap the points + double delta = lon - myStationGrid.getLongitude(); + if ( delta > 180 ) + lon -= 360; + if ( delta < -180 ) + lon += 360; + + shortPaths << MapPath(MapCoordinate(myStationGrid.getLatitude(), + myStationGrid.getLongitude()), + MapCoordinate(lat, lon)); + } + + stations << MapPoint(query.value(0).toString(), + lat, + lon, + (query.value(3).toInt()) > 0 ? QStringLiteral("greenIconSmall") + : QStringLiteral("yellowIconSmall")); } } @@ -641,24 +612,15 @@ void StatisticsWidget::drawPointsOnMap(QSqlQuery &query) QMessageBox::Yes|QMessageBox::No); if ( reply != QMessageBox::Yes ) + { stations.clear(); + shortPaths.clear(); + } } - QString javaScript = QString("grids_confirmed = [];" - "grids_worked = [];" - "drawPointsBusy([%1], '%2');" - "drawShortPathsBusy([%3], '%4');" - "maidenheadConfWorked.redraw();").arg(stations.join(","), - tr("Rendering QSOs..."), - shortPaths.join(","), - tr("Rendering QSOs...")); - - qCDebug(runtime) << javaScript; - - if ( !isMainPageLoaded ) - postponedScripts.append(javaScript); - else - main_page->runJavaScript(javaScript); + mapController->clearGridLayers(); + mapController->drawPointsAndShortPathsBusy(stations, shortPaths, tr("Rendering QSOs...")); + mapController->redrawGridLayer(); } void StatisticsWidget::drawFilledGridsOnMap(QSqlQuery &query) @@ -668,30 +630,35 @@ void StatisticsWidget::drawFilledGridsOnMap(QSqlQuery &query) if ( query.lastQuery().isEmpty() ) return; - QList confirmedGrids; - QList workedGrids; + QStringList confirmedGrids; + QStringList workedGrids; while ( query.next() ) { - if ( query.value(3).toInt() > 0 && ! confirmedGrids.contains(query.value(1).toString()) ) - confirmedGrids << QString("\"" + query.value(1).toString() + "\""); - else - workedGrids << QString("\"" + query.value(1).toString() + "\""); - } + const Gridsquare grid = Gridsquare::mapDisplayGrid(query.value(1).toString()); - QString javaScript = QString("grids_confirmed = [ %1 ]; " - "grids_worked = [ %2 ];" - "mylocations = [];" - "drawPointsBusy([], '');" - "drawShortPathsBusy([], '');" - "maidenheadConfWorked.redraw();").arg(confirmedGrids.join(","), workedGrids.join(",")); + if ( !grid.isValid() ) + continue; - qCDebug(runtime) << javaScript; + const QString gridString = grid.getGrid(); - if ( !isMainPageLoaded ) - postponedScripts.append(javaScript); - else - main_page->runJavaScript(javaScript); + if ( query.value(3).toInt() > 0 ) + { + if ( !confirmedGrids.contains(gridString) ) + confirmedGrids << gridString; + workedGrids.removeAll(gridString); + } + else if ( !confirmedGrids.contains(gridString) + && !workedGrids.contains(gridString) ) + { + workedGrids << gridString; + } + } + + mapController->setGridLayers(confirmedGrids, workedGrids); + mapController->drawPointsBusy(QList(), QString()); + mapController->drawShortPathsBusy(QList(), QString()); + mapController->redrawGridLayer(); } void StatisticsWidget::refreshCombos() @@ -787,3 +754,43 @@ void StatisticsWidget::refreshCombo(QComboBox * combo, combo->setCurrentText(currSelection); combo->blockSignals(false); } + +void StatisticsWidget::initializeWidget() +{ + FCT_IDENTIFICATION; + + // We will not initialize the combos in the constructor. The constructor should + // stay fast, so the first real show populates them and sets default selections. + refreshCombos(); + ui->statTypeMainCombo->blockSignals(true); + ui->statTypeMainCombo->setCurrentIndex(0); + ui->statTypeMainCombo->blockSignals(false); + setSubTypesCombo(ui->statTypeMainCombo->currentIndex()); + ui->myRigCombo->blockSignals(true); + ui->myRigCombo->setCurrentIndex(0); + ui->myRigCombo->blockSignals(false); + ui->myAntennaCombo->blockSignals(true); + ui->myAntennaCombo->setCurrentIndex(0); + ui->myAntennaCombo->blockSignals(false); + ui->userFilterCombo->blockSignals(true); + ui->userFilterCombo->setCurrentIndex(0); + ui->userFilterCombo->blockSignals(false); + + initialized = true; + pendingRefresh = false; + refreshGraph(); +} + +QString StatisticsWidget::currentMapRenderKey(const QStringList &genericFilter) const +{ + QStringList key; + + key << QString::number(ui->statTypeMainCombo->currentIndex()) + << QString::number(ui->statTypeSecCombo->currentIndex()) + << genericFilter.join(QLatin1String(" AND ")) + << QString::number(ui->eqslCheckBox->isChecked()) + << QString::number(ui->lotwCheckBox->isChecked()) + << QString::number(ui->paperCheckBox->isChecked()); + + return key.join(QLatin1Char('\n')); +} diff --git a/ui/StatisticsWidget.h b/ui/StatisticsWidget.h index e12786d4..6f26c872 100644 --- a/ui/StatisticsWidget.h +++ b/ui/StatisticsWidget.h @@ -5,10 +5,9 @@ #include #include #include -#include +#include -#include "ui/MapWebChannelHandler.h" -#include "ui/WebEnginePage.h" +#include "ui/MapPageController.h" #include "core/LogLocale.h" namespace Ui { @@ -26,7 +25,6 @@ class StatisticsWidget : public QWidget public slots: void mainStatChanged(int); void dateRangeCheckBoxChanged(int); - void mapLoaded(bool); void changeTheme(int, bool isDark); void refreshWidget(); @@ -50,15 +48,17 @@ private slots: void refreshCombos(); void setSubTypesCombo(int mainTypeIdx); void refreshCombo(QComboBox * combo, const QString &sqlQeury); + void initializeWidget(); + QString currentMapRenderKey(const QStringList &genericFilter) const; private: Ui::StatisticsWidget *ui; - WebEnginePage *main_page; - bool isMainPageLoaded; - QString postponedScripts; - QWebChannel channel; - MapWebChannelHandler layerControlHandler; + QScopedPointer mapController; LogLocale locale; + bool initialized = false; + bool pendingRefresh = true; + bool mapRenderDirty = true; + QString mapRenderKey; // default statistics interval [in days] diff --git a/ui/WsjtxWidget.cpp b/ui/WsjtxWidget.cpp index ced19e8b..26802df2 100644 --- a/ui/WsjtxWidget.cpp +++ b/ui/WsjtxWidget.cpp @@ -348,6 +348,18 @@ void WsjtxWidget::clearTable() emit spotsCleared(); } +void WsjtxWidget::refreshStatusColors() +{ + FCT_IDENTIFICATION; + + wsjtxTableModel->refreshStatusColors(); + + emit spotsCleared(); + const QList entries = wsjtxTableModel->entries(); + for ( const WsjtxEntry &entry : entries ) + emit filteredCQSpot(entry); +} + void WsjtxWidget::saveTableHeaderState() { FCT_IDENTIFICATION; diff --git a/ui/WsjtxWidget.h b/ui/WsjtxWidget.h index 8cc5f001..9d1f2675 100644 --- a/ui/WsjtxWidget.h +++ b/ui/WsjtxWidget.h @@ -32,6 +32,7 @@ public slots: void callsignClicked(QString); void tableViewClicked(QModelIndex); void updateSpotsStatusWhenQSOAdded(const QSqlRecord &record); + void refreshStatusColors(); private slots: void displayedColumns(); diff --git a/ui/component/ButtonStyle.h b/ui/component/ButtonStyle.h index 786baf99..a5302b1d 100644 --- a/ui/component/ButtonStyle.h +++ b/ui/component/ButtonStyle.h @@ -25,28 +25,27 @@ #include - Q_DECL_IMPORT void qt_blurImage(QPainter *p, QImage &blurImage, qreal radius, bool quality, bool alphaOnly, int transposed = 0); // src/widgets/effects/qpixmapfilter.cpp -namespace Style { - -#define cyan500 QColor("#00bcd4") -#define gray50 QColor("#fafafa") -#define gray400 QColor("#bdbdbd") +namespace Style +{ using Type = QEasingCurve::Type; - struct Animation { - Animation() = default; - Animation(Type _easing, int _duration) :easing{ _easing }, duration{ _duration } { - - } + struct Animation + { + Animation(Type _easing, int _duration) :easing{ _easing }, duration{ _duration } {} Type easing; int duration; }; - struct Switch { + struct Switch + { + const QColor cyan500 = QColor(0x00bcd4); + const QColor gray50 = QColor(0xfafafa); + const QColor gray400 = QColor(0xbdbdbd); + Switch() : height{ 24 }, //font{ QFont("Roboto medium", 13) }, diff --git a/ui/component/CardEditorWidget.cpp b/ui/component/CardEditorWidget.cpp new file mode 100644 index 00000000..c6c04b53 --- /dev/null +++ b/ui/component/CardEditorWidget.cpp @@ -0,0 +1,380 @@ +#include +#include +#include +#include +#include + +#include "CardEditorWidget.h" + +CardEditorWidget::CardEditorWidget(QWidget *parent) + : QWidget(parent) +{ + setMouseTracking(true); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + setMinimumSize(300, 200); +} + +// --------------------------------------------------------------------------- +// Public interface +// --------------------------------------------------------------------------- + +void CardEditorWidget::setImage(const QPixmap &pm) +{ + m_image = pm; + update(); +} + +void CardEditorWidget::setOverlays(const QList &overlays) +{ + m_overlays = overlays; + if (m_selectedIndex >= m_overlays.size()) + m_selectedIndex = -1; + update(); +} + +void CardEditorWidget::updateOverlay(int index, const EmailQSLFieldOverlay &ov) +{ + if (index < 0 || index >= m_overlays.size()) + return; + m_overlays[index] = ov; + update(); +} + +void CardEditorWidget::setSelectedIndex(int index) +{ + if (m_selectedIndex == index) + return; + m_selectedIndex = index; + update(); +} + +QSize CardEditorWidget::sizeHint() const +{ + if (m_image.isNull()) + return QSize(500, 320); + return m_image.size().scaled(640, 420, Qt::KeepAspectRatio); +} + +QSize CardEditorWidget::minimumSizeHint() const { return QSize(300, 200); } + +// --------------------------------------------------------------------------- +// Coordinate helpers +// --------------------------------------------------------------------------- + +QRectF CardEditorWidget::imageRect() const +{ + if (m_image.isNull()) + return QRectF(rect()); + QSizeF sz = m_image.size(); + sz.scale(size(), Qt::KeepAspectRatio); + return QRectF(QPointF((width() - sz.width()) / 2.0, + (height() - sz.height()) / 2.0), sz); +} + +double CardEditorWidget::displayScale() const +{ + if (m_image.isNull() || m_image.width() == 0) return 1.0; + return imageRect().width() / m_image.width(); +} + +QPointF CardEditorWidget::imageToWidget(const QPointF &ip) const +{ + const QRectF r = imageRect(); + const double sx = r.width() / (m_image.isNull() ? 1 : m_image.width()); + const double sy = r.height() / (m_image.isNull() ? 1 : m_image.height()); + return QPointF(r.left() + ip.x() * sx, r.top() + ip.y() * sy); +} + +QPointF CardEditorWidget::widgetToImage(const QPointF &wp) const +{ + const QRectF r = imageRect(); + if (r.width() == 0 || r.height() == 0) return wp; + const double sx = (m_image.isNull() ? 1 : m_image.width()) / r.width(); + const double sy = (m_image.isNull() ? 1 : m_image.height()) / r.height(); + return QPointF((wp.x() - r.left()) * sx, (wp.y() - r.top()) * sy); +} + +QRectF CardEditorWidget::overlayWidgetRect(int idx) const +{ + if (idx < 0 || idx >= m_overlays.size()) return {}; + const EmailQSLFieldOverlay &ov = m_overlays.at(idx); + const QPointF tl = imageToWidget(QPointF(ov.x, ov.y)); + const double sc = displayScale(); + + if (ov.type == QLatin1String("BOX")) + { + return QRectF(tl, QSizeF(ov.width * sc, ov.height * sc)); + } + + // TEXT / LABEL: bounding box of the displayed string + QFont f(ov.fontFamily, qMax(8, qRound(ov.fontSize * sc))); + f.setBold(ov.bold); f.setItalic(ov.italic); + const QFontMetrics fm(f); + const QString lbl = (ov.type == QLatin1String("LABEL")) + ? ov.fieldName + : (QLatin1Char('{') + ov.fieldName + QLatin1Char('}')); + return QRectF(tl.x(), tl.y() - fm.ascent(), fm.horizontalAdvance(lbl), fm.height()); +} + +QRectF CardEditorWidget::resizeHandleRect(int idx) const +{ + const QRectF r = overlayWidgetRect(idx); + if (r.isNull()) return {}; + const double h = RESIZE_HANDLE_PX; + return QRectF(r.right() - h, r.bottom() - h, h * 2, h * 2); +} + +bool CardEditorWidget::isOnResizeHandle(int idx, const QPointF &wPt) const +{ + if (idx < 0 || idx >= m_overlays.size()) return false; + if (m_overlays.at(idx).type != QLatin1String("BOX")) return false; + return resizeHandleRect(idx).contains(wPt); +} + +int CardEditorWidget::overlayAt(const QPointF &wPt) const +{ + // Iterate in reverse so top-drawn items (higher index) are hit first + for (int i = m_overlays.size() - 1; i >= 0; --i) + { + const QRectF r = overlayWidgetRect(i).adjusted(-4, -4, 4, 4); + if (r.contains(wPt)) return i; + } + return -1; +} + +// --------------------------------------------------------------------------- +// Paint +// --------------------------------------------------------------------------- + +void CardEditorWidget::paintEvent(QPaintEvent *) +{ + QPainter p(this); + p.setRenderHint(QPainter::SmoothPixmapTransform); + p.setRenderHint(QPainter::Antialiasing); + p.setRenderHint(QPainter::TextAntialiasing); + + // Background + p.fillRect(rect(), palette().window()); + + if (m_image.isNull()) + { + p.setPen(QColor(0x88, 0x88, 0x88)); + p.drawText(rect(), Qt::AlignCenter, + tr("No card image selected.\nClick \"Browse…\" to choose a QSL card image.")); + p.setPen(QPen(QColor(0x88, 0x88, 0x88), 1, Qt::DashLine)); + p.drawRect(rect().adjusted(0, 0, -1, -1)); + return; + } + + // Card image + p.drawPixmap(imageRect().toRect(), m_image); + + const double sc = displayScale(); + + for (int i = 0; i < m_overlays.size(); ++i) + { + const EmailQSLFieldOverlay &ov = m_overlays.at(i); + const bool sel = (i == m_selectedIndex); + const QRectF wr = overlayWidgetRect(i); + + if (ov.type == QLatin1String("BOX")) + { + // --- Fill --- + QColor fill(ov.fillColor); + fill.setAlphaF(ov.opacity / 100.0); + const double cr = ov.cornerRadius * sc; + + p.setPen(QPen(QColor(ov.color), sel ? 2.0 : 1.5)); + p.setBrush(fill); + p.drawRoundedRect(wr, cr, cr); + + // --- Caption above box --- + if (!ov.fieldName.isEmpty()) + { + const int dispSz = qMax(8, qRound((ov.fontSize > 0 ? ov.fontSize : 11) * sc)); + QFont f(ov.fontFamily, dispSz); + f.setBold(ov.bold); + p.setFont(f); + p.setPen(QColor(ov.color)); + p.setBrush(Qt::NoBrush); + QFontMetrics fm(f); + p.drawText(QPointF(wr.left(), wr.top() - fm.descent() - 2), ov.fieldName); + } + + // --- Selection decoration --- + if (sel) + { + p.save(); + p.setPen(QPen(QColor(0, 120, 215), 1.5, Qt::DashLine)); + p.setBrush(Qt::NoBrush); + p.drawRoundedRect(wr.adjusted(-3, -3, 3, 3), cr + 3, cr + 3); + + // Resize handle (bottom-right corner) + const QRectF rh = resizeHandleRect(i); + p.setPen(Qt::NoPen); + p.setBrush(QColor(0, 120, 215)); + p.drawEllipse(rh.center(), RESIZE_HANDLE_PX * 0.5, RESIZE_HANDLE_PX * 0.5); + + // Size info label + p.setPen(QColor(0, 80, 180)); + p.setFont(QFont(QStringLiteral("Arial"), 8)); + p.drawText(QPointF(wr.left() + 3, wr.bottom() - 3), + QString("%1×%2").arg(ov.width).arg(ov.height)); + p.restore(); + } + } + else // TEXT or LABEL + { + const int dispSz = qMax(8, qRound(ov.fontSize * sc)); + QFont f(ov.fontFamily, dispSz); + f.setBold(ov.bold); f.setItalic(ov.italic); + p.setFont(f); + + // LABEL shows literal text; TEXT shows {FIELDNAME} placeholder + const QString lbl = (ov.type == QLatin1String("LABEL")) + ? ov.fieldName + : (QLatin1Char('{') + ov.fieldName + QLatin1Char('}')); + const QFontMetrics fm(f); + const QPointF anchor(wr.left(), wr.top() + fm.ascent()); + + // Selection box + if (sel) + { + p.save(); + p.setPen(QPen(QColor(0, 120, 215), 1.5, Qt::DashLine)); + p.setBrush(QColor(0, 120, 215, 25)); + p.drawRect(wr.adjusted(-3, -3, 3, 3)); + // Drag-handle dot + p.setPen(Qt::NoPen); + p.setBrush(QColor(0, 120, 215)); + p.drawEllipse(QPointF(wr.left() - 7, wr.center().y()), 4, 4); + p.restore(); + } + else + { + p.save(); + p.setPen(QPen(QColor(0, 0, 0, 35), 1, Qt::DotLine)); + p.setBrush(Qt::NoBrush); + p.drawRect(wr.adjusted(-2, -2, 2, 2)); + p.restore(); + } + + // Shadow + { + QColor shadow = QColor(ov.color).lightness() > 128 + ? QColor(0, 0, 0, 90) : QColor(255, 255, 255, 90); + p.save(); + p.setPen(shadow); + p.drawText(anchor + QPointF(1, 1), lbl); + p.restore(); + } + + p.setPen(QColor(ov.color)); + p.drawText(anchor, lbl); + } + } + + // Border + p.setPen(QPen(QColor(0x88, 0x88, 0x88), 1)); + p.setBrush(Qt::NoBrush); + p.drawRect(rect().adjusted(0, 0, -1, -1)); +} + +// --------------------------------------------------------------------------- +// Mouse events +// --------------------------------------------------------------------------- + +void CardEditorWidget::mousePressEvent(QMouseEvent *event) +{ + if (event->button() != Qt::LeftButton) { QWidget::mousePressEvent(event); return; } + + const QPointF wPos = event->pos(); + const int idx = overlayAt(wPos); + + m_selectedIndex = idx; + m_dragIndex = -1; + m_resizing = false; + + if (idx >= 0) + { + m_dragIndex = idx; + m_dragStartImg = widgetToImage(wPos); + m_dragAnchorImg = QPointF(m_overlays.at(idx).x, m_overlays.at(idx).y); + + if (isOnResizeHandle(idx, wPos)) + { + m_resizing = true; + m_dragSizeAtPress = QSizeF(m_overlays.at(idx).width, m_overlays.at(idx).height); + setCursor(Qt::SizeFDiagCursor); + } + else + { + setCursor(Qt::ClosedHandCursor); + } + } + + emit overlaySelected(m_selectedIndex); + update(); +} + +void CardEditorWidget::mouseMoveEvent(QMouseEvent *event) +{ + const QPointF wPos = event->pos(); + + if (m_dragIndex >= 0 && (event->buttons() & Qt::LeftButton)) + { + const QPointF curImg = widgetToImage(wPos); + const QPointF delta = curImg - m_dragStartImg; + + EmailQSLFieldOverlay &ov = m_overlays[m_dragIndex]; + const int imgW = m_image.isNull() ? 99999 : m_image.width(); + const int imgH = m_image.isNull() ? 99999 : m_image.height(); + + if (m_resizing) + { + const int newW = qMax(20, qRound(m_dragSizeAtPress.width() + delta.x())); + const int newH = qMax(10, qRound(m_dragSizeAtPress.height() + delta.y())); + ov.width = qMin(newW, imgW - ov.x); + ov.height = qMin(newH, imgH - ov.y); + } + else + { + ov.x = qBound(0, qRound(m_dragAnchorImg.x() + delta.x()), imgW - 1); + ov.y = qBound(0, qRound(m_dragAnchorImg.y() + delta.y()), imgH - 1); + } + update(); + } + else + { + // Cursor feedback + const int hov = overlayAt(wPos); + if (hov >= 0) + { + if (isOnResizeHandle(hov, wPos)) + setCursor(Qt::SizeFDiagCursor); + else + setCursor(Qt::OpenHandCursor); + } + else + { + setCursor(Qt::ArrowCursor); + } + } +} + +void CardEditorWidget::mouseReleaseEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton && m_dragIndex >= 0) + { + const EmailQSLFieldOverlay &ov = m_overlays.at(m_dragIndex); + if (m_resizing) + emit overlaySizeChanged(m_dragIndex, ov.width, ov.height); + else + emit overlayPositionChanged(m_dragIndex, ov.x, ov.y); + + m_dragIndex = -1; + m_resizing = false; + setCursor(Qt::ArrowCursor); + update(); + } +} diff --git a/ui/component/CardEditorWidget.h b/ui/component/CardEditorWidget.h new file mode 100644 index 00000000..c55d16d1 --- /dev/null +++ b/ui/component/CardEditorWidget.h @@ -0,0 +1,68 @@ +#ifndef QLOG_UI_COMPONENT_CARDEDITORWIDGET_H +#define QLOG_UI_COMPONENT_CARDEDITORWIDGET_H + +#include +#include + +#include "service/emailqsl/EmailQSLService.h" + +// Interactive QSL card editor widget. +// Supports two overlay types: +// TEXT — displays a {FIELD} placeholder; drag to reposition. +// BOX — filled rounded rectangle; drag to move, drag bottom-right handle to resize. +class CardEditorWidget : public QWidget +{ + Q_OBJECT + +public: + explicit CardEditorWidget(QWidget *parent = nullptr); + + void setImage(const QPixmap &pm); + const QPixmap &image() const { return m_image; } + + void setOverlays(const QList &overlays); + const QList &overlays() const { return m_overlays; } + + void updateOverlay(int index, const EmailQSLFieldOverlay &ov); + void setSelectedIndex(int index); + int selectedIndex() const { return m_selectedIndex; } + + QSize sizeHint() const override; + QSize minimumSizeHint() const override; + +signals: + void overlayPositionChanged(int index, int newX, int newY); + void overlaySizeChanged(int index, int newW, int newH); // BOX resize + void overlaySelected(int index); + +protected: + void paintEvent(QPaintEvent *) override; + void mousePressEvent(QMouseEvent *) override; + void mouseMoveEvent(QMouseEvent *) override; + void mouseReleaseEvent(QMouseEvent *) override; + +private: + static constexpr int RESIZE_HANDLE_PX = 10; // half-size of resize grip in widget px + + QRectF imageRect() const; + double displayScale() const; + QPointF imageToWidget(const QPointF &imgPt) const; + QPointF widgetToImage(const QPointF &wPt) const; + QRectF overlayWidgetRect(int index) const; // widget-coords bounding rect + QRectF resizeHandleRect(int index) const; // widget-coords resize grip + int overlayAt(const QPointF &wPt) const; // -1 = none + bool isOnResizeHandle(int index, const QPointF &wPt) const; + + QPixmap m_image; + QList m_overlays; + int m_selectedIndex = -1; + + // Drag state + int m_dragIndex = -1; + bool m_resizing = false; + QPointF m_dragStartImg; // image-coord of press point + QPointF m_dragAnchorImg; // image-coord anchor (overlay x,y at press) + QSizeF m_dragSizeAtPress; // overlay w,h at press (resize only) +}; + +#endif // QLOG_UI_COMPONENT_CARDEDITORWIDGET_H diff --git a/ui/component/LogbookFieldComboBox.cpp b/ui/component/LogbookFieldComboBox.cpp new file mode 100644 index 00000000..9922b4ec --- /dev/null +++ b/ui/component/LogbookFieldComboBox.cpp @@ -0,0 +1,106 @@ +#include "ui/component/LogbookFieldComboBox.h" + +#include +#include +#include + +LogbookFieldComboBox::LogbookFieldComboBox(QWidget *parent) : + QComboBox(parent) +{ +} + +void LogbookFieldComboBox::populate(ValueMode valueMode, + EmptyMode emptyMode, + const QString &emptyText) +{ + populateCombo(this, valueMode, emptyMode, emptyText); +} + +LogbookModel::ColumnID LogbookFieldComboBox::currentColumnId() const +{ + return static_cast(currentData().toInt()); +} + +QString LogbookFieldComboBox::currentDbFieldName() const +{ + return currentData().toString(); +} + +void LogbookFieldComboBox::setCurrentColumnId(LogbookModel::ColumnID columnId) +{ + const int index = findData(static_cast(columnId)); + + if ( index >= 0 ) + setCurrentIndex(index); +} + +void LogbookFieldComboBox::setCurrentDbFieldName(const QString &dbFieldName) +{ + const int index = findData(dbFieldName); + + if ( index >= 0 ) + setCurrentIndex(index); +} + +void LogbookFieldComboBox::populateCombo(QComboBox *combo, + ValueMode valueMode, + EmptyMode emptyMode, + const QString &emptyText) +{ + if ( !combo ) + return; + + combo->clear(); + + if ( emptyMode != EmptyMode::None ) + combo->addItem(emptyMode == EmptyMode::Blank ? QString() : emptyText, + emptyData(valueMode)); + + const QList items = fieldItems(); + for ( const FieldItem &item : items ) + combo->addItem(item.label, itemData(item, valueMode)); +} + +QList LogbookFieldComboBox::fieldItems() +{ + QList items; + const QSqlRecord contactsRecord = QSqlDatabase::database().record("contacts"); + + for ( int i = LogbookModel::ColumnID::COLUMN_ID; i < LogbookModel::ColumnID::COLUMN_LAST_ELEMENT; ++i ) + { + const LogbookModel::ColumnID columnId = static_cast(i); + const QString label = LogbookModel::getFieldNameTranslation(columnId); + + if ( label.isEmpty() ) + continue; + + const QString dbFieldName = contactsRecord.fieldName(i); + + if ( dbFieldName.isEmpty() ) + continue; + + items.append({columnId, label, dbFieldName}); + } + + std::sort(items.begin(), items.end(), + [](const FieldItem &a, const FieldItem &b) + { + return a.label.localeAwareCompare(b.label) < 0; + }); + + return items; +} + +QVariant LogbookFieldComboBox::itemData(const FieldItem &item, ValueMode valueMode) +{ + return valueMode == ValueMode::ColumnId + ? QVariant(static_cast(item.columnId)) + : QVariant(item.dbFieldName); +} + +QVariant LogbookFieldComboBox::emptyData(ValueMode valueMode) +{ + return valueMode == ValueMode::ColumnId + ? QVariant(static_cast(LogbookModel::COLUMN_INVALID)) + : QVariant(QString()); +} diff --git a/ui/component/LogbookFieldComboBox.h b/ui/component/LogbookFieldComboBox.h new file mode 100644 index 00000000..83ace01b --- /dev/null +++ b/ui/component/LogbookFieldComboBox.h @@ -0,0 +1,57 @@ +#ifndef QLOG_UI_COMPONENT_LOGBOOKFIELDCOMBOBOX_H +#define QLOG_UI_COMPONENT_LOGBOOKFIELDCOMBOBOX_H + +#include +#include +#include + +#include "models/LogbookModel.h" + +class LogbookFieldComboBox : public QComboBox +{ +public: + enum class ValueMode + { + ColumnId, + DbFieldName + }; + + // Existing users need three variants: no empty item (QSO filter), + // a blank empty item (Cabrillo), and a named empty item (QSL labels). + enum class EmptyMode + { + None, + Blank, + EmptyLabel + }; + + explicit LogbookFieldComboBox(QWidget *parent = nullptr); + + void populate(ValueMode valueMode, + EmptyMode emptyMode = EmptyMode::None, + const QString &emptyText = QString()); + + LogbookModel::ColumnID currentColumnId() const; + QString currentDbFieldName() const; + void setCurrentColumnId(LogbookModel::ColumnID columnId); + void setCurrentDbFieldName(const QString &dbFieldName); + + static void populateCombo(QComboBox *combo, + ValueMode valueMode, + EmptyMode emptyMode = EmptyMode::None, + const QString &emptyText = QString()); + +private: + struct FieldItem + { + LogbookModel::ColumnID columnId; + QString label; + QString dbFieldName; + }; + + static QList fieldItems(); + static QVariant itemData(const FieldItem &item, ValueMode valueMode); + static QVariant emptyData(ValueMode valueMode); +}; + +#endif // QLOG_UI_COMPONENT_LOGBOOKFIELDCOMBOBOX_H diff --git a/ui/component/ModeSubmodeDelegate.cpp b/ui/component/ModeSubmodeDelegate.cpp new file mode 100644 index 00000000..454cba19 --- /dev/null +++ b/ui/component/ModeSubmodeDelegate.cpp @@ -0,0 +1,132 @@ +#include "ModeSubmodeDelegate.h" + +#include +#include +#include +#include + +#include "models/LogbookModel.h" +#include "ui/ModeSelectionController.h" + +ModeSubmodeEditor::ModeSubmodeEditor(bool showMode, QWidget *parent) : + QWidget(parent), + modeCombo(new QComboBox(this)), + submodeCombo(new QComboBox(this)), + modeController(nullptr) +{ + QHBoxLayout *layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(2); + layout->addWidget(modeCombo); + layout->addWidget(submodeCombo); + + if ( !showMode ) + modeCombo->hide(); + + // Existing QSOs may contain modes disabled for new contacts. Both the + // merged editor and the submode-only editor must therefore offer all modes. + modeController = new ModeSelectionController(modeCombo, submodeCombo, + false, false, false, false, this); + connect(modeCombo, &QComboBox::currentTextChanged, + modeController, &ModeSelectionController::applyCurrentMode); +} + +void ModeSubmodeEditor::setModeSubmode(const QString &mode, const QString &submode) +{ + modeCombo->setCurrentText(mode); + modeController->applyCurrentMode(); + submodeCombo->setCurrentText(submode); +} + +QString ModeSubmodeEditor::mode() const +{ + return modeCombo->currentText(); +} + +QString ModeSubmodeEditor::submode() const +{ + return submodeCombo->currentText(); +} + +ModeSubmodeDelegate::ModeSubmodeDelegate(QObject *parent) : + QStyledItemDelegate(parent) +{ +} + +QWidget *ModeSubmodeDelegate::createEditor(QWidget *parent, + const QStyleOptionViewItem &, + const QModelIndex &) const +{ + return new ModeSubmodeEditor(true, parent); +} + +void ModeSubmodeDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + ModeSubmodeEditor *modeSubmodeEditor = static_cast(editor); + const QVariantMap value = index.model()->data(index, Qt::EditRole).toMap(); + + modeSubmodeEditor->setModeSubmode(value.value("mode").toString(), + value.value("submode").toString()); +} + +void ModeSubmodeDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const +{ + ModeSubmodeEditor *modeSubmodeEditor = static_cast(editor); + + QVariantMap value; + value.insert("mode", modeSubmodeEditor->mode()); + value.insert("submode", modeSubmodeEditor->submode()); + + model->setData(index, value, Qt::EditRole); +} + +void ModeSubmodeDelegate::updateEditorGeometry(QWidget *editor, + const QStyleOptionViewItem &option, + const QModelIndex &) const +{ + editor->setGeometry(option.rect); +} + +SubmodeDelegate::SubmodeDelegate(QObject *parent) : + QStyledItemDelegate(parent) +{ +} + +QWidget *SubmodeDelegate::createEditor(QWidget *parent, + const QStyleOptionViewItem &, + const QModelIndex &) const +{ + return new ModeSubmodeEditor(false, parent); +} + +void SubmodeDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + ModeSubmodeEditor *modeSubmodeEditor = static_cast(editor); + + const QAbstractItemModel *model = index.model(); + + if ( !model ) + return; + + const QString mode = model->data(index.sibling(index.row(), LogbookModel::COLUMN_MODE), + Qt::DisplayRole).toString(); + const QString submode = model->data(index, Qt::EditRole).toString(); + + modeSubmodeEditor->setModeSubmode(mode, submode); +} + +void SubmodeDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const +{ + ModeSubmodeEditor *modeSubmodeEditor = static_cast(editor); + const QString submode = modeSubmodeEditor->submode(); + model->setData(index, submode.isEmpty() ? QVariant() : QVariant(submode), Qt::EditRole); +} + +void SubmodeDelegate::updateEditorGeometry(QWidget *editor, + const QStyleOptionViewItem &option, + const QModelIndex &) const +{ + editor->setGeometry(option.rect); +} diff --git a/ui/component/ModeSubmodeDelegate.h b/ui/component/ModeSubmodeDelegate.h new file mode 100644 index 00000000..2edcee9f --- /dev/null +++ b/ui/component/ModeSubmodeDelegate.h @@ -0,0 +1,58 @@ +#ifndef QLOG_UI_MODESUBMODEDELEGATE_H +#define QLOG_UI_MODESUBMODEDELEGATE_H + +#include +#include +#include + +class QComboBox; +class ModeSelectionController; + +class ModeSubmodeEditor : public QWidget +{ +public: + explicit ModeSubmodeEditor(bool showMode, QWidget *parent = nullptr); + + void setModeSubmode(const QString &mode, const QString &submode); + QString mode() const; + QString submode() const; + +private: + QComboBox *modeCombo; + QComboBox *submodeCombo; + ModeSelectionController *modeController; +}; + +class ModeSubmodeDelegate : public QStyledItemDelegate +{ +public: + explicit ModeSubmodeDelegate(QObject *parent = nullptr); + + QWidget *createEditor(QWidget *parent, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const override; + void updateEditorGeometry(QWidget *editor, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override; +}; + +class SubmodeDelegate : public QStyledItemDelegate +{ +public: + explicit SubmodeDelegate(QObject *parent = nullptr); + + QWidget *createEditor(QWidget *parent, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const override; + void updateEditorGeometry(QWidget *editor, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override; +}; + +#endif // QLOG_UI_MODESUBMODEDELEGATE_H