From dac404a0c8949304c86e817160ce007eccec1c6a Mon Sep 17 00:00:00 2001 From: Michael Morgan <84428382+aa5sh@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:40:24 -0500 Subject: [PATCH] QSL Manager Added an option to use qslinfo.de for qsl manager info. If it is available it will prioritize it over callbook lookup. --- QLog.pro | 2 + core/CallbookManager.cpp | 195 +++++++++++++++++++++++++++++++++--- core/CallbookManager.h | 12 +++ core/LogParam.cpp | 10 ++ core/LogParam.h | 2 + service/GenericCallbook.h | 3 + service/qslinfo/QSLInfo.cpp | 185 ++++++++++++++++++++++++++++++++++ service/qslinfo/QSLInfo.h | 38 +++++++ ui/LogbookWidget.cpp | 3 +- ui/NewContactWidget.cpp | 5 +- ui/SettingsDialog.cpp | 2 + ui/SettingsDialog.ui | 29 ++++++ 12 files changed, 468 insertions(+), 18 deletions(-) create mode 100644 service/qslinfo/QSLInfo.cpp create mode 100644 service/qslinfo/QSLInfo.h diff --git a/QLog.pro b/QLog.pro index d52c63d2..35332082 100644 --- a/QLog.pro +++ b/QLog.pro @@ -172,6 +172,7 @@ SOURCES += \ service/lotw/Lotw.cpp \ service/potaapp/PotaApp.cpp \ service/qrzcom/QRZ.cpp \ + service/qslinfo/QSLInfo.cpp \ ui/ActivityEditor.cpp \ ui/AdifRecoveryManager.cpp \ ui/AlertRuleDetail.cpp \ @@ -375,6 +376,7 @@ HEADERS += \ service/lotw/Lotw.h \ service/potaapp/PotaApp.h \ service/qrzcom/QRZ.h \ + service/qslinfo/QSLInfo.h \ ui/ActivityEditor.h \ ui/AdifRecoveryManager.h \ ui/AlertRuleDetail.h \ diff --git a/core/CallbookManager.cpp b/core/CallbookManager.cpp index 14e30aee..3b735f95 100644 --- a/core/CallbookManager.cpp +++ b/core/CallbookManager.cpp @@ -3,6 +3,7 @@ #include "core/debug.h" #include "service/hamqth/HamQTH.h" #include "service/qrzcom/QRZ.h" +#include "service/qslinfo/QSLInfo.h" #include "data/Callsign.h" #include "LogParam.h" @@ -11,7 +12,9 @@ MODULE_IDENTIFICATION("qlog.ui.callbookmanager"); CallbookManager::CallbookManager(QObject *parent) : QObject{parent}, primaryCallbookAuthSuccess(false), - secondaryCallbookAuthSuccess(false) + secondaryCallbookAuthSuccess(false), + pendingCallbookResultAvailable(false), + waitingForQSLInfo(false) { FCT_IDENTIFICATION; @@ -34,12 +37,18 @@ void CallbookManager::queryCallsign(const QString &callsign) // if there is the second query for the same call immediately after // the first query, then it returns a result of empty object queryCache.insert(callsign, new CallbookResponseData); + currentQueryCallsign = callsign; if ( !primaryCallbook.isNull() ) { - currentQueryCallsign = callsign; primaryCallbook->queryCallsign(currentQueryCallsign); } + else if ( !qslInfoCallbook.isNull() ) + { + CallbookResponseData data; + data.call = callsign; + requestQSLInfo(data, false); + } else { queryCache.remove(callsign); @@ -51,7 +60,9 @@ bool CallbookManager::isActive() { FCT_IDENTIFICATION; - bool ret = primaryCallbookAuthSuccess || secondaryCallbookAuthSuccess; + bool ret = primaryCallbookAuthSuccess + || secondaryCallbookAuthSuccess + || !qslInfoCallbook.isNull(); qCDebug(runtime) << ret; return ret; } @@ -81,16 +92,30 @@ GenericCallbook *CallbookManager::createCallbook(const QString &callbookID) void CallbookManager::initCallbooks() { - primaryCallbook.clear(); - secondaryCallbook.clear(); + FCT_IDENTIFICATION; + + disposeCallbook(primaryCallbook); + disposeCallbook(secondaryCallbook); + disposeCallbook(qslInfoCallbook); + primaryCallbookAuthSuccess = false; secondaryCallbookAuthSuccess = false; + pendingCallbookData = CallbookResponseData(); + pendingCallbookResultAvailable = false; + waitingForQSLInfo = false; queryCache.clear(); currentQueryCallsign = QString(); primaryCallbook = createCallbook(LogParam::getPrimaryCallbook(GenericCallbook::CALLBOOK_NAME)); secondaryCallbook = createCallbook(LogParam::getSecondaryCallbook(GenericCallbook::CALLBOOK_NAME)); + if ( LogParam::getQSLInfoCallbookEnabled() ) + qslInfoCallbook = new QSLInfoCallbook(this); + + qCDebug(runtime) << "Initializing callbooks" + << "primary" << (primaryCallbook ? primaryCallbook->getDisplayName() : QStringLiteral("disabled")) + << "secondary" << (secondaryCallbook ? secondaryCallbook->getDisplayName() : QStringLiteral("disabled")) + << "QSLInfo.de" << !qslInfoCallbook.isNull(); if ( !primaryCallbook.isNull() ) { @@ -125,6 +150,16 @@ void CallbookManager::initCallbooks() }); secondaryCallbookAuthSuccess = true; } + + if ( !qslInfoCallbook.isNull() ) + { + connect(qslInfoCallbook, &GenericCallbook::callsignResult, + this, &CallbookManager::processQSLInfoResult); + connect(qslInfoCallbook, &GenericCallbook::callsignNotFound, + this, &CallbookManager::qslInfoCallsignNotFound); + connect(qslInfoCallbook, &GenericCallbook::lookupError, + this, &CallbookManager::qslInfoLookupError); + } } void CallbookManager::abortQuery() @@ -134,6 +169,10 @@ void CallbookManager::abortQuery() queryCache.remove(currentQueryCallsign); if ( ! primaryCallbook.isNull() ) primaryCallbook->abortQuery(); if ( ! secondaryCallbook.isNull() ) secondaryCallbook->abortQuery(); + if ( ! qslInfoCallbook.isNull() ) qslInfoCallbook->abortQuery(); + pendingCallbookData = CallbookResponseData(); + pendingCallbookResultAvailable = false; + waitingForQSLInfo = false; } void CallbookManager::primaryCallbookCallsignNotFound(const QString ¬FoundCallsign) @@ -148,7 +187,7 @@ void CallbookManager::primaryCallbookCallsignNotFound(const QString ¬FoundCal if ( secondaryCallbook.isNull() ) { - emit callsignNotFound(notFoundCallsign); + completeCallsignNotFound(notFoundCallsign); return; } @@ -169,21 +208,17 @@ void CallbookManager::secondaryCallbookCallsignNotFound(const QString ¬FoundC qCDebug(runtime) << "Callsign not found - secondary "; - emit callsignNotFound(notFoundCallsign); + completeCallsignNotFound(notFoundCallsign); } void CallbookManager::processCallsignResult(const CallbookResponseData &data) { FCT_IDENTIFICATION; - CallbookResponseData *cacheData = new CallbookResponseData; - *cacheData = data; - queryCache.insert(data.call, cacheData); - // Callbook returned queried callsign if ( data.call == currentQueryCallsign ) { - emit callsignResult(data); + requestQSLInfo(data, true); return; } @@ -217,12 +252,140 @@ void CallbookManager::processCallsignResult(const CallbookResponseData &data) newData.nick = data.nick; newData.image_url = data.image_url; - CallbookResponseData *cdata = new CallbookResponseData; - *cdata = newData; + requestQSLInfo(newData, true); + } +} + +void CallbookManager::requestQSLInfo(const CallbookResponseData &data, + bool baseResultAvailable) +{ + FCT_IDENTIFICATION; + + qCDebug(function_parameters) << data.call << baseResultAvailable; + + if ( qslInfoCallbook.isNull() ) + { + if ( baseResultAvailable ) + completeCallsignResult(data); + else + completeCallsignNotFound(data.call); + return; + } + + pendingCallbookData = data; + pendingCallbookResultAvailable = baseResultAvailable; + waitingForQSLInfo = true; - queryCache.insert(currentQueryCallsign, cdata); - emit callsignResult(newData); + qCDebug(runtime) << "Querying QSLInfo.de supplemental QSL manager for" + << currentQueryCallsign; + qslInfoCallbook->queryCallsign(currentQueryCallsign); +} + +void CallbookManager::processQSLInfoResult(const CallbookResponseData &data) +{ + FCT_IDENTIFICATION; + + qCDebug(function_parameters) << data.call << data.qsl_via; + + if ( !waitingForQSLInfo || data.call != currentQueryCallsign ) + { + qCDebug(runtime) << "Ignoring stale QSLInfo.de result for" << data.call; + return; } + + if ( !data.qsl_via.isEmpty() ) + { + qCDebug(runtime) << "QSLInfo.de overrides QSL Via for" + << currentQueryCallsign << data.qsl_via; + pendingCallbookData.qsl_via = data.qsl_via; + pendingCallbookData.qsl_via_override = true; + } + + pendingCallbookData.call = currentQueryCallsign; + const CallbookResponseData result = pendingCallbookData; + waitingForQSLInfo = false; + pendingCallbookResultAvailable = false; + pendingCallbookData = CallbookResponseData(); + completeCallsignResult(result); +} + +void CallbookManager::qslInfoCallsignNotFound(const QString &callsign) +{ + FCT_IDENTIFICATION; + + qCDebug(function_parameters) << callsign; + + if ( !waitingForQSLInfo || callsign != currentQueryCallsign ) + return; + + const bool baseResultAvailable = pendingCallbookResultAvailable; + const CallbookResponseData result = pendingCallbookData; + waitingForQSLInfo = false; + pendingCallbookResultAvailable = false; + pendingCallbookData = CallbookResponseData(); + + if ( baseResultAvailable ) + completeCallsignResult(result); + else + { + queryCache.remove(callsign); + emit callsignNotFound(callsign); + } +} + +void CallbookManager::qslInfoLookupError(const QString &error) +{ + FCT_IDENTIFICATION; + + qCWarning(runtime) << "QSLInfo.de supplemental lookup failed" << error; + + // A supplemental service outage must not discard or interrupt a valid + // result from the primary/secondary callbook. Surface the error only when + // QSLInfo.de is the sole provider for this lookup. + if ( !pendingCallbookResultAvailable ) + emit lookupError(QStringLiteral("QSLInfo.de - ") + error); + + qslInfoCallsignNotFound(currentQueryCallsign); +} + +void CallbookManager::completeCallsignResult(const CallbookResponseData &data) +{ + FCT_IDENTIFICATION; + + CallbookResponseData *cacheData = new CallbookResponseData; + *cacheData = data; + queryCache.insert(data.call, cacheData); + emit callsignResult(data); +} + +void CallbookManager::completeCallsignNotFound(const QString &callsign) +{ + FCT_IDENTIFICATION; + + queryCache.remove(callsign); + + if ( !qslInfoCallbook.isNull() ) + { + CallbookResponseData data; + data.call = callsign; + requestQSLInfo(data, false); + return; + } + + emit callsignNotFound(callsign); +} + +void CallbookManager::disposeCallbook(QPointer &callbook) +{ + FCT_IDENTIFICATION; + + if ( callbook.isNull() ) + return; + + disconnect(callbook, nullptr, this, nullptr); + callbook->abortQuery(); + callbook->deleteLater(); + callbook.clear(); } QCache CallbookManager::queryCache(100); diff --git a/core/CallbookManager.h b/core/CallbookManager.h index cbfd12ea..c3d28b16 100644 --- a/core/CallbookManager.h +++ b/core/CallbookManager.h @@ -28,15 +28,27 @@ private slots: void primaryCallbookCallsignNotFound(const QString&); void secondaryCallbookCallsignNotFound(const QString&); void processCallsignResult(const CallbookResponseData &data); + void processQSLInfoResult(const CallbookResponseData &data); + void qslInfoCallsignNotFound(const QString &callsign); + void qslInfoLookupError(const QString &error); private: GenericCallbook *createCallbook(const QString&); + void requestQSLInfo(const CallbookResponseData &data, + bool baseResultAvailable); + void completeCallsignResult(const CallbookResponseData &data); + void completeCallsignNotFound(const QString &callsign); + void disposeCallbook(QPointer &callbook); private: QPointer primaryCallbook; bool primaryCallbookAuthSuccess; QPointer secondaryCallbook; bool secondaryCallbookAuthSuccess; + QPointer qslInfoCallbook; + CallbookResponseData pendingCallbookData; + bool pendingCallbookResultAvailable; + bool waitingForQSLInfo; QString currentQueryCallsign; static QCache queryCache; diff --git a/core/LogParam.cpp b/core/LogParam.cpp index 2e4421f9..94e35c71 100644 --- a/core/LogParam.cpp +++ b/core/LogParam.cpp @@ -667,6 +667,16 @@ void LogParam::setSecondaryCallbook(const QString &callbookName) setParam("callbook/secondary", callbookName); } +bool LogParam::getQSLInfoCallbookEnabled() +{ + return getParam("callbook/qslinfo/enabled", false).toBool(); +} + +void LogParam::setQSLInfoCallbookEnabled(bool enabled) +{ + setParam("callbook/qslinfo/enabled", enabled); +} + QString LogParam::getCallbookWebLookupURL(const QString &defaultURL) { return getParam("callbook/weblookupurl", defaultURL).toString(); diff --git a/core/LogParam.h b/core/LogParam.h index a1f5bacc..b47ce680 100644 --- a/core/LogParam.h +++ b/core/LogParam.h @@ -217,6 +217,8 @@ class LogParam : public QObject static void setPrimaryCallbook(const QString& callbookName); static QString getSecondaryCallbook(const QString &defaultValue); static void setSecondaryCallbook(const QString& callbookName); + static bool getQSLInfoCallbookEnabled(); + static void setQSLInfoCallbookEnabled(bool enabled); static QString getCallbookWebLookupURL(const QString &defaultURL); static void setCallbookWebLookupURL(const QString& url); diff --git a/service/GenericCallbook.h b/service/GenericCallbook.h index d93bbc01..319b735e 100644 --- a/service/GenericCallbook.h +++ b/service/GenericCallbook.h @@ -11,6 +11,9 @@ struct CallbookResponseData QString qth; QString gridsquare; QString qsl_via; + // True when a supplemental service explicitly requested replacement of + // the existing QSL Via value instead of the usual fill-if-empty behavior. + bool qsl_via_override = false; QString cqz; QString ituz; QString dok; diff --git a/service/qslinfo/QSLInfo.cpp b/service/qslinfo/QSLInfo.cpp new file mode 100644 index 00000000..35ed3d60 --- /dev/null +++ b/service/qslinfo/QSLInfo.cpp @@ -0,0 +1,185 @@ +#include "QSLInfo.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/debug.h" + +MODULE_IDENTIFICATION("qlog.service.qslinfo"); + +const QString QSLInfoCallbook::CALLBOOK_NAME = QStringLiteral("qslinfo"); + +QSLInfoCallbook::QSLInfoCallbook(QObject *parent) + : GenericCallbook(parent), + currentReply(nullptr) +{ + FCT_IDENTIFICATION; +} + +QSLInfoCallbook::~QSLInfoCallbook() +{ + FCT_IDENTIFICATION; + + if ( currentReply ) + currentReply->abort(); +} + +QString QSLInfoCallbook::getDisplayName() +{ + FCT_IDENTIFICATION; + + return tr("QSLInfo.de"); +} + +void QSLInfoCallbook::queryCallsign(const QString &callsign) +{ + FCT_IDENTIFICATION; + + qCDebug(function_parameters) << callsign; + + if ( currentReply ) + { + qCWarning(runtime) << "processing a new request but the previous one hasn't been completed yet"; + currentReply->abort(); + currentReply = nullptr; + } + + const QString normalizedCallsign = callsign.trimmed().toUpper(); + if ( normalizedCallsign.isEmpty() ) + { + qCWarning(runtime) << "Cannot query QSLInfo.de with an empty callsign"; + emit callsignNotFound(callsign); + return; + } + + // Encode portable callsigns as one path component instead of treating '/' + // as an API path separator. + const QString encodedCallsign = QString::fromLatin1( + QUrl::toPercentEncoding(normalizedCallsign)); + const QUrl url(API_URL + encodedCallsign, QUrl::StrictMode); + + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("QLog")); + request.setRawHeader(QByteArrayLiteral("Accept"), QByteArrayLiteral("application/json")); + + qCDebug(runtime) << "Sending QSLInfo.de callsign request" << url; + + currentReply = getNetworkAccessManager()->get(request); + // Preserve the caller's spelling for result correlation while using the + // normalized value only in the HTTP request. + currentReply->setProperty("queryCallsign", callsign.trimmed()); +} + +void QSLInfoCallbook::abortQuery() +{ + FCT_IDENTIFICATION; + + if ( currentReply ) + { + qCDebug(runtime) << "Aborting QSLInfo.de request"; + currentReply->abort(); + currentReply = nullptr; + } +} + +void QSLInfoCallbook::processReply(QNetworkReply *reply) +{ + FCT_IDENTIFICATION; + + if ( reply == currentReply ) + currentReply = nullptr; + + if ( !reply ) + { + qCWarning(runtime) << "QSLInfo.de returned no network reply"; + return; + } + + const QString callsign = reply->property("queryCallsign").toString(); + const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + qCDebug(runtime) << "QSLInfo.de response" + << reply->request().url() << "HTTP status" << status; + + if ( status == 404 ) + { + qCDebug(runtime) << "QSLInfo.de has no entry for" << callsign; + emit callsignNotFound(callsign); + reply->deleteLater(); + return; + } + + if ( reply->error() != QNetworkReply::NoError + || status < 200 + || status >= 300 ) + { + qCWarning(runtime) << "QSLInfo.de request failed" + << reply->errorString() << "HTTP status" << status; + + if ( reply->error() != QNetworkReply::OperationCanceledError ) + emit lookupError(reply->errorString()); + + reply->deleteLater(); + return; + } + + const QByteArray response = reply->readAll(); + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(response, &parseError); + + if ( parseError.error != QJsonParseError::NoError || !document.isArray() ) + { + qCWarning(runtime) << "QSLInfo.de JSON parse error" + << parseError.errorString(); + emit lookupError(tr("Invalid JSON response")); + reply->deleteLater(); + return; + } + + const QJsonArray entries = document.array(); + QString qslInformation; + + // Use the first usable information entry returned by the API as the + // QSL Via override. + for ( const QJsonValue &value : entries ) + { + if ( !value.isObject() ) + continue; + + const QJsonObject object = value.toObject(); + const QString info = decodeHtmlEntities( + object.value(QStringLiteral("info")).toString()); + + if ( !info.isEmpty() ) + { + qslInformation = info; + break; + } + } + + if ( qslInformation.isEmpty() ) + { + qCDebug(runtime) << "QSLInfo.de returned no usable QSL information for" + << callsign << "entries" << entries.size(); + emit callsignNotFound(callsign); + reply->deleteLater(); + return; + } + + CallbookResponseData responseData; + responseData.call = callsign; + responseData.qsl_via = qslInformation; + responseData.qsl_via_override = true; + + qCDebug(runtime) << "QSLInfo.de QSL manager result" + << responseData.call << responseData.qsl_via; + + emit callsignResult(responseData); + reply->deleteLater(); +} diff --git a/service/qslinfo/QSLInfo.h b/service/qslinfo/QSLInfo.h new file mode 100644 index 00000000..e183e614 --- /dev/null +++ b/service/qslinfo/QSLInfo.h @@ -0,0 +1,38 @@ +#ifndef QLOG_SERVICE_QSLINFO_QSLINFO_H +#define QLOG_SERVICE_QSLINFO_QSLINFO_H + +#include "service/GenericCallbook.h" + +class QNetworkReply; + +/** + * Supplemental callbook provider for the public QSLInfo.de REST API. + * + * QSLInfo supplies QSL routing information only. CallbookManager combines + * this response with the selected primary/secondary callbook response. + */ +class QSLInfoCallbook : public GenericCallbook +{ + Q_OBJECT + +public: + const static QString CALLBOOK_NAME; + + explicit QSLInfoCallbook(QObject *parent = nullptr); + ~QSLInfoCallbook() override; + + QString getDisplayName() override; + +public slots: + void queryCallsign(const QString &callsign) override; + void abortQuery() override; + +protected: + void processReply(QNetworkReply *reply) override; + +private: + QNetworkReply *currentReply; + const QString API_URL = QStringLiteral("https://www.qslinfo.de/api/v1/call/"); +}; + +#endif // QLOG_SERVICE_QSLINFO_QSLINFO_H diff --git a/ui/LogbookWidget.cpp b/ui/LogbookWidget.cpp index 2e11b35c..1154821b 100644 --- a/ui/LogbookWidget.cpp +++ b/ui/LogbookWidget.cpp @@ -490,7 +490,8 @@ void LogbookWidget::updateQSORecordFromCallbook(const CallbookResponseData& data setIfEmpty(LogbookModel::COLUMN_IOTA, data.iota); setIfEmpty(LogbookModel::COLUMN_EMAIL, data.email); setIfEmpty(LogbookModel::COLUMN_COUNTY, data.county); - setIfEmpty(LogbookModel::COLUMN_QSL_VIA, data.qsl_via); + setIfEmpty(LogbookModel::COLUMN_QSL_VIA, data.qsl_via, + false, data.qsl_via_override); setIfEmpty(LogbookModel::COLUMN_WEB, data.url); setIfEmpty(LogbookModel::COLUMN_STATE, data.us_state); setIfEmpty(LogbookModel::COLUMN_ITUZ, data.ituz, false, true); // always replace if different diff --git a/ui/NewContactWidget.cpp b/ui/NewContactWidget.cpp index 004079de..1593ed07 100644 --- a/ui/NewContactWidget.cpp +++ b/ui/NewContactWidget.cpp @@ -669,7 +669,10 @@ void NewContactWidget::setCallbookFields(const CallbookResponseData& data) if ( uiDynamic->iotaEdit->text().isEmpty() ) uiDynamic->iotaEdit->setText(data.iota); if ( uiDynamic->emailEdit->text().isEmpty() ) uiDynamic->emailEdit->setText(data.email); if ( uiDynamic->countyEdit->text().isEmpty() ) uiDynamic->countyEdit->setText(data.county); - if ( ui->qslViaEdit->text().isEmpty() ) ui->qslViaEdit->setText(data.qsl_via); + if ( data.qsl_via_override && !data.qsl_via.isEmpty() ) + ui->qslViaEdit->setText(data.qsl_via); + else if ( ui->qslViaEdit->text().isEmpty() ) + ui->qslViaEdit->setText(data.qsl_via); if ( uiDynamic->urlEdit->text().isEmpty() ) uiDynamic->urlEdit->setText(data.url); if ( uiDynamic->stateEdit->text().isEmpty() ) uiDynamic->stateEdit->setText(data.us_state); if ( data.eqsl == "Y" ) ui->eqslLabel->setText("eQSL"); diff --git a/ui/SettingsDialog.cpp b/ui/SettingsDialog.cpp index 9d2cf0b2..91339d66 100644 --- a/ui/SettingsDialog.cpp +++ b/ui/SettingsDialog.cpp @@ -2678,6 +2678,7 @@ void SettingsDialog::readSettings() ui->qrzUsernameEdit->setText(QRZBase::getUsername()); ui->qrzPasswordEdit->setText(QRZBase::getPasswd(QRZBase::getUsername())); + ui->qslInfoEnableCheckBox->setChecked(LogParam::getQSLInfoCallbookEnabled()); ui->webLookupURLEdit->setText(GenericCallbook::getWebLookupURL("", QString(), false)); @@ -2807,6 +2808,7 @@ void SettingsDialog::writeSettings() LogParam::setPrimaryCallbook(ui->primaryCallbookCombo->itemData(ui->primaryCallbookCombo->currentIndex()).toString()); LogParam::setSecondaryCallbook(ui->secondaryCallbookCombo->itemData(ui->secondaryCallbookCombo->currentIndex()).toString()); + LogParam::setQSLInfoCallbookEnabled(ui->qslInfoEnableCheckBox->isChecked()); LogParam::setCallbookWebLookupURL(ui->webLookupURLEdit->text()); /********/ diff --git a/ui/SettingsDialog.ui b/ui/SettingsDialog.ui index a5486d68..3b6324c0 100644 --- a/ui/SettingsDialog.ui +++ b/ui/SettingsDialog.ui @@ -3399,6 +3399,35 @@ + + + + + + QSLInfo.de + + + + + + Look up DX QSL routing information and replace the QSL Via value returned by the primary or secondary callbook. + + + Use QSLInfo.de for QSL manager lookup + + + + + + + When enabled, QSLInfo.de is queried after the selected callbook and its current information replaces the QSL Via field. + + + true + + + +