harbour-aira 0.1.0: client Sailfish per pompa di calore Aira (bridge HTTP + stato e comandi ACS)
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
#include "airajson.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QUrl>
|
||||
|
||||
namespace AiraJson {
|
||||
|
||||
QString normalizeBaseUrl(const QString &raw)
|
||||
{
|
||||
QString s = raw.trimmed();
|
||||
if (s.isEmpty())
|
||||
return QString();
|
||||
|
||||
if (!s.startsWith(QLatin1String("http://")) && !s.startsWith(QLatin1String("https://")))
|
||||
s.prepend(QLatin1String("http://"));
|
||||
|
||||
while (s.endsWith(QLatin1Char('/')))
|
||||
s.chop(1);
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
QVariantMap parseSummary(const QByteArray &body, bool *ok)
|
||||
{
|
||||
if (ok)
|
||||
*ok = false;
|
||||
|
||||
QJsonParseError err;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(body, &err);
|
||||
if (err.error != QJsonParseError::NoError || !doc.isObject())
|
||||
return QVariantMap();
|
||||
|
||||
const QJsonObject root = doc.object();
|
||||
if (root.value(QStringLiteral("ok")).toBool() == false)
|
||||
return QVariantMap();
|
||||
|
||||
const QJsonObject summary = root.value(QStringLiteral("summary")).toObject();
|
||||
if (summary.isEmpty())
|
||||
return QVariantMap();
|
||||
|
||||
if (ok)
|
||||
*ok = true;
|
||||
return summary.toVariantMap();
|
||||
}
|
||||
|
||||
QString modeLabel(const QString &operatingStatus)
|
||||
{
|
||||
const QString s = operatingStatus;
|
||||
if (s == QLatin1String("OPERATING_STATUS_AUTO"))
|
||||
return QStringLiteral("Automatico");
|
||||
if (s == QLatin1String("OPERATING_STATUS_HEATING"))
|
||||
return QStringLiteral("Riscaldamento");
|
||||
if (s == QLatin1String("OPERATING_STATUS_COOLING"))
|
||||
return QStringLiteral("Raffrescamento");
|
||||
if (s == QLatin1String("OPERATING_STATUS_HOT_WATER"))
|
||||
return QStringLiteral("Acqua calda");
|
||||
if (s == QLatin1String("OPERATING_STATUS_STANDBY"))
|
||||
return QStringLiteral("Stand-by");
|
||||
if (s == QLatin1String("OPERATING_STATUS_OFF"))
|
||||
return QStringLiteral("Spento");
|
||||
if (s.isEmpty())
|
||||
return QStringLiteral("—");
|
||||
// Sconosciuto: togliamo il prefisso e mostriamo il resto, minuscolo.
|
||||
QString rest = s;
|
||||
rest.remove(QLatin1String("OPERATING_STATUS_"));
|
||||
return rest.toLower();
|
||||
}
|
||||
|
||||
QString formatValue(const QVariant &value)
|
||||
{
|
||||
if (!value.isValid() || value.isNull())
|
||||
return QStringLiteral("—");
|
||||
|
||||
bool isNumber = false;
|
||||
const double d = value.toDouble(&isNumber);
|
||||
if (isNumber)
|
||||
return QString::number(d, 'f', 1);
|
||||
|
||||
const QString s = value.toString();
|
||||
return s.isEmpty() ? QStringLiteral("—") : s;
|
||||
}
|
||||
|
||||
QByteArray commandPayload(const QString &id, const QVariant &value)
|
||||
{
|
||||
QJsonObject o;
|
||||
o.insert(QStringLiteral("id"), id);
|
||||
if (value.isValid() && !value.isNull())
|
||||
o.insert(QStringLiteral("value"), QJsonValue::fromVariant(value));
|
||||
return QJsonDocument(o).toJson(QJsonDocument::Compact);
|
||||
}
|
||||
|
||||
QString errorMessage(const QByteArray &body, int httpStatus)
|
||||
{
|
||||
QJsonParseError err;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(body, &err);
|
||||
if (err.error == QJsonParseError::NoError && doc.isObject()) {
|
||||
const QString msg = doc.object().value(QStringLiteral("error")).toString();
|
||||
if (!msg.isEmpty())
|
||||
return msg;
|
||||
}
|
||||
if (httpStatus == 0)
|
||||
return QStringLiteral("server non raggiungibile");
|
||||
return QStringLiteral("errore HTTP %1").arg(httpStatus);
|
||||
}
|
||||
|
||||
} // namespace AiraJson
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef AIRAJSON_H
|
||||
#define AIRAJSON_H
|
||||
|
||||
// Funzioni PURE (solo QtCore, nessun QObject): sono la parte dell'app
|
||||
// verificabile senza SDK, con l'harness locale in tests/core_test.cpp.
|
||||
// Il bridge HTTP parla JSON; qui stanno normalizzazione dell'indirizzo,
|
||||
// lettura del riassunto e costruzione del corpo dei comandi.
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
#include <QVariantMap>
|
||||
|
||||
namespace AiraJson {
|
||||
|
||||
// "192.168.2.15:8790" -> "http://192.168.2.15:8790"
|
||||
// "" -> "" (nessun indirizzo: il client non fa richieste)
|
||||
// toglie gli slash finali e gli spazi; preserva schema e porta espliciti.
|
||||
QString normalizeBaseUrl(const QString &raw);
|
||||
|
||||
// Legge il corpo di GET /summary nel dizionario usato dalla UI.
|
||||
// In caso di JSON non valido o di risposta di errore ritorna un dizionario
|
||||
// vuoto e mette *ok a false.
|
||||
QVariantMap parseSummary(const QByteArray &body, bool *ok);
|
||||
|
||||
// Codice di stato Aira ("OPERATING_STATUS_AUTO") in etichetta italiana.
|
||||
QString modeLabel(const QString &operatingStatus);
|
||||
|
||||
// Temperatura leggibile: "51,0" oppure "—" se il valore manca.
|
||||
QString formatValue(const QVariant &value);
|
||||
|
||||
// Corpo JSON per POST /command. value viene omesso se nullo (i comandi
|
||||
// semplici non hanno parametri).
|
||||
QByteArray commandPayload(const QString &id, const QVariant &value);
|
||||
|
||||
// Messaggio d'errore leggibile da una risposta del bridge
|
||||
// ({"ok":false,"error":"..."}), con fallback sullo stato HTTP.
|
||||
QString errorMessage(const QByteArray &body, int httpStatus);
|
||||
|
||||
} // namespace AiraJson
|
||||
|
||||
#endif // AIRAJSON_H
|
||||
@@ -0,0 +1,196 @@
|
||||
#include "apiclient.h"
|
||||
#include "airajson.h"
|
||||
#include "settings.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
ApiClient::ApiClient(Settings *settings, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_settings(settings)
|
||||
, m_busy(false)
|
||||
{
|
||||
if (m_settings) {
|
||||
// La UI deve riflettere subito il passaggio da "non configurato" a
|
||||
// "configurato" quando si salvano indirizzo o token.
|
||||
connect(m_settings, &Settings::baseUrlChanged, this, &ApiClient::configuredChanged);
|
||||
connect(m_settings, &Settings::tokenChanged, this, &ApiClient::configuredChanged);
|
||||
}
|
||||
}
|
||||
|
||||
bool ApiClient::configured() const
|
||||
{
|
||||
if (!m_settings)
|
||||
return false;
|
||||
return !AiraJson::normalizeBaseUrl(m_settings->baseUrl()).isEmpty()
|
||||
&& !m_settings->token().trimmed().isEmpty();
|
||||
}
|
||||
|
||||
void ApiClient::setBusy(bool value)
|
||||
{
|
||||
if (value == m_busy)
|
||||
return;
|
||||
m_busy = value;
|
||||
emit busyChanged();
|
||||
}
|
||||
|
||||
void ApiClient::armTimeout(QNetworkReply *reply, int seconds)
|
||||
{
|
||||
// QNetworkAccessManager non applica NESSUN timeout: senza watchdog una
|
||||
// richiesta verso un host irraggiungibile resta appesa e la UI sembra morta.
|
||||
QTimer *t = new QTimer(reply); // figlio della reply: si pulisce da solo
|
||||
t->setSingleShot(true);
|
||||
t->setInterval(seconds * 1000);
|
||||
connect(t, &QTimer::timeout, reply, [reply]() {
|
||||
if (reply->isRunning())
|
||||
reply->abort();
|
||||
});
|
||||
connect(reply, &QNetworkReply::finished, t, &QTimer::stop);
|
||||
t->start();
|
||||
}
|
||||
|
||||
QNetworkReply *ApiClient::send(const QByteArray &verb, const QString &path,
|
||||
const QByteArray &body, int timeoutSec)
|
||||
{
|
||||
const QString base = AiraJson::normalizeBaseUrl(m_settings ? m_settings->baseUrl() : QString());
|
||||
if (base.isEmpty()) {
|
||||
emit errorOccurred(QStringLiteral("Indirizzo del bridge non configurato"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
const QUrl url(base + path);
|
||||
QNetworkRequest request(url); // variabile intermedia: evita il most-vexing-parse
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
||||
request.setRawHeader("Authorization", "Bearer " + m_settings->token().toUtf8());
|
||||
|
||||
QNetworkReply *reply = 0;
|
||||
if (verb == "GET")
|
||||
reply = m_nam.get(request);
|
||||
else
|
||||
reply = m_nam.post(request, body);
|
||||
|
||||
armTimeout(reply, timeoutSec);
|
||||
return reply;
|
||||
}
|
||||
|
||||
void ApiClient::refresh()
|
||||
{
|
||||
if (!configured()) {
|
||||
emit errorOccurred(QStringLiteral("Configura indirizzo e token del bridge"));
|
||||
return;
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
QNetworkReply *reply = send("GET", QStringLiteral("/summary"), QByteArray(), 15);
|
||||
if (!reply) {
|
||||
setBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
reply->deleteLater();
|
||||
setBusy(false);
|
||||
|
||||
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
const QByteArray body = reply->readAll();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError || status != 200) {
|
||||
const QString msg = (reply->error() == QNetworkReply::OperationCanceledError)
|
||||
? QStringLiteral("Server non raggiungibile (timeout)")
|
||||
: AiraJson::errorMessage(body, status);
|
||||
qWarning() << "[api] summary non riuscito:" << status << msg;
|
||||
emit errorOccurred(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
const QVariantMap summary = AiraJson::parseSummary(body, &ok);
|
||||
if (!ok) {
|
||||
qWarning() << "[api] riassunto non interpretabile";
|
||||
emit errorOccurred(QStringLiteral("Risposta del bridge non interpretabile"));
|
||||
return;
|
||||
}
|
||||
m_summary = summary;
|
||||
emit summaryChanged(summary);
|
||||
});
|
||||
}
|
||||
|
||||
void ApiClient::testConnection()
|
||||
{
|
||||
const QString base = AiraJson::normalizeBaseUrl(m_settings ? m_settings->baseUrl() : QString());
|
||||
if (base.isEmpty()) {
|
||||
emit connectionTested(false, QStringLiteral("Inserisci l'indirizzo del bridge"));
|
||||
return;
|
||||
}
|
||||
|
||||
// /health non richiede token: cosi' si distingue "server irraggiungibile"
|
||||
// da "token sbagliato".
|
||||
const QUrl url(base + QStringLiteral("/health"));
|
||||
QNetworkRequest request(url); // variabile intermedia (most vexing parse)
|
||||
QNetworkReply *reply = m_nam.get(request);
|
||||
armTimeout(reply, 10);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
reply->deleteLater();
|
||||
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (reply->error() != QNetworkReply::NoError || status != 200) {
|
||||
const QString msg = (reply->error() == QNetworkReply::OperationCanceledError)
|
||||
? QStringLiteral("Timeout: nessuna risposta")
|
||||
: QStringLiteral("Irraggiungibile (HTTP %1)").arg(status);
|
||||
emit connectionTested(false, msg);
|
||||
return;
|
||||
}
|
||||
emit connectionTested(true, QStringLiteral("Bridge raggiungibile"));
|
||||
});
|
||||
}
|
||||
|
||||
QString ApiClient::modeLabel(const QString &operatingStatus) const
|
||||
{
|
||||
return AiraJson::modeLabel(operatingStatus);
|
||||
}
|
||||
|
||||
QString ApiClient::formatValue(const QVariant &value) const
|
||||
{
|
||||
return AiraJson::formatValue(value);
|
||||
}
|
||||
|
||||
void ApiClient::sendCommand(const QString &id, const QVariant &value)
|
||||
{
|
||||
if (!configured()) {
|
||||
emit commandFinished(false, QStringLiteral("Configura indirizzo e token del bridge"));
|
||||
return;
|
||||
}
|
||||
|
||||
const QByteArray payload = AiraJson::commandPayload(id, value);
|
||||
qDebug() << "[api] comando:" << id; // mai i valori: qui potrebbe passare altro
|
||||
|
||||
QNetworkReply *reply = send("POST", QStringLiteral("/command"), payload, 30);
|
||||
if (!reply) {
|
||||
emit commandFinished(false, QStringLiteral("Bridge non configurato"));
|
||||
return;
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, id]() {
|
||||
reply->deleteLater();
|
||||
setBusy(false);
|
||||
|
||||
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
const QByteArray body = reply->readAll();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError || status != 200) {
|
||||
const QString msg = (reply->error() == QNetworkReply::OperationCanceledError)
|
||||
? QStringLiteral("Timeout: comando non inviato")
|
||||
: AiraJson::errorMessage(body, status);
|
||||
qWarning() << "[api] comando" << id << "fallito:" << status << msg;
|
||||
emit commandFinished(false, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
emit commandFinished(true, QStringLiteral("Comando inviato"));
|
||||
refresh(); // i valori letti cambiano: rileggo
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
#ifndef APICLIENT_H
|
||||
#define APICLIENT_H
|
||||
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
#include <QVariantMap>
|
||||
|
||||
class QNetworkReply;
|
||||
class Settings;
|
||||
|
||||
// Client HTTP verso il bridge sul server (~/workspace/Carlo/Aira/aira_bridge.py).
|
||||
// Non parla con il cloud Aira: Qt 5.6 non ha HTTP/2, quindi il gRPC resta sul
|
||||
// server e qui arrivano solo JSON.
|
||||
class ApiClient : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
|
||||
Q_PROPERTY(bool configured READ configured NOTIFY configuredChanged)
|
||||
// Ultimo riassunto letto. Sta in C++ (context property) e non nel QML:
|
||||
// le property dell'ApplicationWindow NON sono visibili alle pagine dello
|
||||
// pageStack, quindi la dashboard e la copertina non potrebbero condividerle.
|
||||
Q_PROPERTY(QVariantMap summary READ summary NOTIFY summaryChanged)
|
||||
|
||||
public:
|
||||
explicit ApiClient(Settings *settings, QObject *parent = 0);
|
||||
|
||||
bool busy() const { return m_busy; }
|
||||
bool configured() const;
|
||||
QVariantMap summary() const { return m_summary; }
|
||||
|
||||
// Scarica il riassunto e lo emette con summaryChanged().
|
||||
Q_INVOKABLE void refresh();
|
||||
|
||||
// Prova GET /health (non richiede token) e risponde con connectionTested().
|
||||
Q_INVOKABLE void testConnection();
|
||||
|
||||
// Invia un comando: sendCommand("set_hot_water_target", 55).
|
||||
Q_INVOKABLE void sendCommand(const QString &id, const QVariant &value = QVariant());
|
||||
|
||||
// Formattazione per la UI: unica fonte di verita' in C++ (testata in
|
||||
// tests/core_test.cpp), niente duplicazione di logica nel QML.
|
||||
Q_INVOKABLE QString modeLabel(const QString &operatingStatus) const;
|
||||
Q_INVOKABLE QString formatValue(const QVariant &value) const;
|
||||
|
||||
signals:
|
||||
void busyChanged();
|
||||
void configuredChanged();
|
||||
void summaryChanged(const QVariantMap &summary);
|
||||
void commandFinished(bool ok, const QString &message);
|
||||
void connectionTested(bool ok, const QString &message);
|
||||
void errorOccurred(const QString &message);
|
||||
|
||||
private:
|
||||
QNetworkReply *send(const QByteArray &verb, const QString &path,
|
||||
const QByteArray &body, int timeoutSec);
|
||||
void armTimeout(QNetworkReply *reply, int seconds);
|
||||
void setBusy(bool value);
|
||||
|
||||
Settings *m_settings;
|
||||
QNetworkAccessManager m_nam;
|
||||
bool m_busy;
|
||||
QVariantMap m_summary;
|
||||
};
|
||||
|
||||
#endif // APICLIENT_H
|
||||
@@ -0,0 +1,41 @@
|
||||
// main.cpp — punti che NON vanno cambiati:
|
||||
// * pathTo() per il QML: un QUrl relativo si risolve contro la CWD e dal
|
||||
// launcher la prima schermata resta BIANCA senza errori;
|
||||
// * org/app allineati alla sezione [X-Sailjail] del desktop file, altrimenti
|
||||
// la sandbox nasconde ~/.config/harbour/aira e le impostazioni non persistono;
|
||||
// * context property (appSettings, api) invece di singleton.
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QGuiApplication>
|
||||
#include <QQmlContext>
|
||||
#include <QQuickView>
|
||||
#include <sailfishapp.h>
|
||||
|
||||
#include "apiclient.h"
|
||||
#include "settings.h"
|
||||
|
||||
Q_DECL_EXPORT int main(int argc, char *argv[])
|
||||
{
|
||||
QGuiApplication *app = SailfishApp::application(argc, argv);
|
||||
|
||||
app->setOrganizationName(QStringLiteral("harbour"));
|
||||
app->setApplicationName(QStringLiteral("aira"));
|
||||
app->setApplicationVersion(QStringLiteral("0.1.0"));
|
||||
|
||||
Settings settings;
|
||||
ApiClient api(&settings);
|
||||
|
||||
QQuickView *view = SailfishApp::createView();
|
||||
|
||||
// Prima riga del log: dice subito se il device sta girando la build nuova
|
||||
// o una stale (trappola nota del build engine).
|
||||
qDebug() << "harbour-aira v0.1.0 build" << __DATE__ << __TIME__;
|
||||
|
||||
view->rootContext()->setContextProperty(QStringLiteral("appSettings"), &settings);
|
||||
view->rootContext()->setContextProperty(QStringLiteral("api"), &api);
|
||||
|
||||
view->setSource(SailfishApp::pathTo(QStringLiteral("qml/harbour-aira.qml")));
|
||||
view->show();
|
||||
|
||||
return app->exec();
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#include "settings.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QStandardPaths>
|
||||
|
||||
QString Settings::configPath()
|
||||
{
|
||||
// Path canonico per la sandbox SailJail: con org/app impostati in main.cpp
|
||||
// (harbour/aira) AppConfigLocation = ~/.config/harbour/aira, l'unica zona
|
||||
// config persistente tra gli avvii.
|
||||
const QString dir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
|
||||
QDir().mkpath(dir);
|
||||
return dir + QStringLiteral("/aira.conf");
|
||||
}
|
||||
|
||||
Settings::Settings(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_settings(configPath(), QSettings::IniFormat)
|
||||
{
|
||||
// Diagnostica: senza questa riga un problema di persistenza resta invisibile.
|
||||
qDebug() << "Settings file:" << m_settings.fileName();
|
||||
if (m_settings.status() != QSettings::NoError)
|
||||
qDebug() << "Settings WARNING: file di configurazione non accessibile";
|
||||
qDebug() << "Settings read: baseUrl=[" << baseUrl() << "] token impostato:" << !token().isEmpty();
|
||||
}
|
||||
|
||||
QString Settings::baseUrl() const
|
||||
{
|
||||
return m_settings.value(QStringLiteral("baseUrl"), QString()).toString();
|
||||
}
|
||||
|
||||
void Settings::setBaseUrl(const QString &value)
|
||||
{
|
||||
if (value == baseUrl())
|
||||
return;
|
||||
m_settings.setValue(QStringLiteral("baseUrl"), value);
|
||||
m_settings.sync(); // le chiusure brusche perdono il flush del distruttore
|
||||
emit baseUrlChanged();
|
||||
}
|
||||
|
||||
QString Settings::token() const
|
||||
{
|
||||
return m_settings.value(QStringLiteral("token"), QString()).toString();
|
||||
}
|
||||
|
||||
void Settings::setToken(const QString &value)
|
||||
{
|
||||
if (value == token())
|
||||
return;
|
||||
m_settings.setValue(QStringLiteral("token"), value);
|
||||
m_settings.sync();
|
||||
emit tokenChanged();
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#ifndef SETTINGS_H
|
||||
#define SETTINGS_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QSettings>
|
||||
|
||||
// Preferenze di harbour-aira, persistite in AppConfigLocation
|
||||
// (~/.config/harbour/aira/aira.conf) perche' la sandbox SailJail rende
|
||||
// persistente SOLO quella zona: con QSettings org/app le impostazioni
|
||||
// sparivano quando l'app veniva avviata dal launcher.
|
||||
//
|
||||
// baseUrl indirizzo del bridge, es. "192.168.2.50:8790" (schema opzionale)
|
||||
// token token del bridge (Authorization: Bearer ...)
|
||||
class Settings : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QString baseUrl READ baseUrl WRITE setBaseUrl NOTIFY baseUrlChanged)
|
||||
Q_PROPERTY(QString token READ token WRITE setToken NOTIFY tokenChanged)
|
||||
|
||||
public:
|
||||
explicit Settings(QObject *parent = 0);
|
||||
|
||||
QString baseUrl() const;
|
||||
void setBaseUrl(const QString &value);
|
||||
|
||||
QString token() const;
|
||||
void setToken(const QString &value);
|
||||
|
||||
signals:
|
||||
void baseUrlChanged();
|
||||
void tokenChanged();
|
||||
|
||||
private:
|
||||
static QString configPath();
|
||||
QSettings m_settings;
|
||||
};
|
||||
|
||||
#endif // SETTINGS_H
|
||||
Reference in New Issue
Block a user