Client OPDS per Calibre Web (Sailfish OS): core C++ testato, UI Silica, packaging RPM
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
#include "apiclient.h"
|
||||
|
||||
#include "opdsparser.h"
|
||||
#include "settings.h"
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QSslError>
|
||||
#include <QStandardPaths>
|
||||
#include <QUrl>
|
||||
|
||||
ApiClient::ApiClient(Settings *settings, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_settings(settings)
|
||||
{
|
||||
m_cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
|
||||
+ QStringLiteral("/covers");
|
||||
QDir().mkpath(m_cacheDir);
|
||||
}
|
||||
|
||||
QString ApiClient::resolveUrl(const QString &relative) const
|
||||
{
|
||||
if (relative.isEmpty())
|
||||
return QString();
|
||||
if (relative.startsWith(QLatin1String("http://"))
|
||||
|| relative.startsWith(QLatin1String("https://")))
|
||||
return relative;
|
||||
const QString base = m_settings->baseUrl();
|
||||
if (relative.startsWith(QLatin1Char('/')))
|
||||
return base + relative;
|
||||
return base + QLatin1Char('/') + relative;
|
||||
}
|
||||
|
||||
void ApiClient::applyAuth(QNetworkRequest &request)
|
||||
{
|
||||
const QString auth = m_settings->authHeader();
|
||||
if (!auth.isEmpty())
|
||||
request.setRawHeader("Authorization", auth.toUtf8());
|
||||
}
|
||||
|
||||
QNetworkReply *ApiClient::startGet(const QUrl &url)
|
||||
{
|
||||
QNetworkRequest request(url);
|
||||
request.setRawHeader("Accept",
|
||||
"application/atom+xml, application/xml, text/xml;q=0.9, */*;q=0.8");
|
||||
request.setRawHeader("User-Agent", "harbour-calibreweb/0.1");
|
||||
applyAuth(request);
|
||||
|
||||
QNetworkReply *reply = m_nam.get(request);
|
||||
if (m_settings->ignoreSslErrors()) {
|
||||
connect(reply, &QNetworkReply::sslErrors, reply,
|
||||
static_cast<void (QNetworkReply::*)(const QList<QSslError> &)>(
|
||||
&QNetworkReply::ignoreSslErrors));
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
|
||||
void ApiClient::getFeed(const QString &url)
|
||||
{
|
||||
QNetworkReply *reply = startGet(QUrl(url));
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
reply->deleteLater();
|
||||
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (reply->error() != QNetworkReply::NoError || status >= 400) {
|
||||
QString message;
|
||||
if (status == 401)
|
||||
message = QStringLiteral(
|
||||
"Autenticazione richiesta (401). Controlla utente e password.");
|
||||
else if (status == 404)
|
||||
message = QStringLiteral("Endpoint non trovato (404). Controlla l'indirizzo.");
|
||||
else
|
||||
message = reply->errorString();
|
||||
emit feedError(message);
|
||||
return;
|
||||
}
|
||||
|
||||
const QByteArray body = reply->readAll();
|
||||
QVariantList entries;
|
||||
QString nextUrl;
|
||||
QString feedTitle;
|
||||
QString parseError;
|
||||
if (OpdsParser::parse(body, &entries, &nextUrl, &feedTitle, &parseError)) {
|
||||
emit feedReady(entries, resolveUrl(nextUrl), feedTitle);
|
||||
} else {
|
||||
emit feedError(parseError.isEmpty()
|
||||
? QStringLiteral("Risposta non valida dal server")
|
||||
: parseError);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ApiClient::fetchImage(const QString &url, const QString &id)
|
||||
{
|
||||
if (url.isEmpty()) {
|
||||
emit imageReady(id, QString());
|
||||
return;
|
||||
}
|
||||
|
||||
const QString resolved = resolveUrl(url);
|
||||
const QUrl imageUrl(resolved);
|
||||
|
||||
QString fileName = QString::fromLatin1(
|
||||
QCryptographicHash::hash(imageUrl.path().toUtf8(), QCryptographicHash::Md5).toHex());
|
||||
const QString suffix = QFileInfo(imageUrl.path()).suffix();
|
||||
if (!suffix.isEmpty() && suffix.length() <= 5)
|
||||
fileName += QLatin1Char('.') + suffix;
|
||||
const QString localPath = m_cacheDir + QLatin1Char('/') + fileName;
|
||||
|
||||
if (QFile::exists(localPath)) {
|
||||
emit imageReady(id, localPath);
|
||||
return;
|
||||
}
|
||||
|
||||
QNetworkReply *reply = startGet(imageUrl);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, id, localPath]() {
|
||||
reply->deleteLater();
|
||||
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (reply->error() != QNetworkReply::NoError || status >= 400) {
|
||||
emit imageReady(id, QString());
|
||||
return;
|
||||
}
|
||||
QFile file(localPath);
|
||||
if (file.open(QIODevice::WriteOnly)) {
|
||||
file.write(reply->readAll());
|
||||
file.close();
|
||||
emit imageReady(id, localPath);
|
||||
} else {
|
||||
emit imageReady(id, QString());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#ifndef APICLIENT_H
|
||||
#define APICLIENT_H
|
||||
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
#include <QVariantList>
|
||||
|
||||
class QNetworkReply;
|
||||
class QNetworkRequest;
|
||||
class Settings;
|
||||
|
||||
// Cliente OPDS: scarica i feed (getFeed), li parsa e li espone come QVariantList
|
||||
// di entry (title, authors, summary, coverUrl, formats, isNavigation, linkUrl).
|
||||
// Gestisce anche il fetch delle copertine in una cache locale su disco.
|
||||
class ApiClient : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ApiClient(Settings *settings, QObject *parent = 0);
|
||||
|
||||
Q_INVOKABLE void getFeed(const QString &url);
|
||||
Q_INVOKABLE void fetchImage(const QString &url, const QString &id);
|
||||
|
||||
signals:
|
||||
void feedReady(const QVariantList &entries, const QString &nextUrl, const QString &feedTitle);
|
||||
void feedError(const QString &message);
|
||||
void imageReady(const QString &id, const QString &localPath);
|
||||
|
||||
private:
|
||||
QNetworkReply *startGet(const QUrl &url);
|
||||
void applyAuth(QNetworkRequest &request);
|
||||
QString resolveUrl(const QString &relative) const;
|
||||
|
||||
Settings *m_settings;
|
||||
QNetworkAccessManager m_nam;
|
||||
QString m_cacheDir;
|
||||
};
|
||||
|
||||
#endif // APICLIENT_H
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "downloader.h"
|
||||
|
||||
#include "settings.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QSslError>
|
||||
#include <QUrl>
|
||||
|
||||
Downloader::Downloader(Settings *settings, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_settings(settings)
|
||||
{
|
||||
}
|
||||
|
||||
void Downloader::download(const QString &url, const QString &destPath)
|
||||
{
|
||||
const QUrl requestUrl(url);
|
||||
QNetworkRequest request(requestUrl);
|
||||
request.setRawHeader("User-Agent", "harbour-calibreweb/0.1");
|
||||
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
|
||||
const QString auth = m_settings->authHeader();
|
||||
if (!auth.isEmpty())
|
||||
request.setRawHeader("Authorization", auth.toUtf8());
|
||||
|
||||
QNetworkReply *reply = m_nam.get(request);
|
||||
if (m_settings->ignoreSslErrors()) {
|
||||
connect(reply, &QNetworkReply::sslErrors, reply,
|
||||
static_cast<void (QNetworkReply::*)(const QList<QSslError> &)>(
|
||||
&QNetworkReply::ignoreSslErrors));
|
||||
}
|
||||
|
||||
QFile *file = new QFile(destPath);
|
||||
if (!file->open(QIODevice::WriteOnly)) {
|
||||
delete file;
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
emit finished(destPath, false,
|
||||
QStringLiteral("Impossibile creare il file di destinazione"));
|
||||
return;
|
||||
}
|
||||
m_files.insert(reply, file);
|
||||
m_paths.insert(reply, destPath);
|
||||
|
||||
connect(reply, &QNetworkReply::downloadProgress, this, [this, reply](qint64 received, qint64 total) {
|
||||
emit progress(received, total, m_paths.value(reply));
|
||||
});
|
||||
connect(reply, &QNetworkReply::readyRead, this, [this, reply]() {
|
||||
QFile *file = m_files.value(reply);
|
||||
if (file)
|
||||
file->write(reply->readAll());
|
||||
});
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
QFile *file = m_files.take(reply);
|
||||
const QString path = m_paths.take(reply);
|
||||
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
const bool ok = reply->error() == QNetworkReply::NoError && status >= 200 && status < 300;
|
||||
if (file) {
|
||||
file->flush();
|
||||
file->close();
|
||||
delete file;
|
||||
}
|
||||
if (!ok && QFile::exists(path))
|
||||
QFile::remove(path);
|
||||
reply->deleteLater();
|
||||
|
||||
QString error;
|
||||
if (!ok) {
|
||||
if (status == 401)
|
||||
error = QStringLiteral("Autenticazione richiesta (401)");
|
||||
else
|
||||
error = reply->errorString();
|
||||
}
|
||||
emit finished(path, ok, error);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#ifndef DOWNLOADER_H
|
||||
#define DOWNLOADER_H
|
||||
|
||||
#include <QHash>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
|
||||
class QFile;
|
||||
class QNetworkReply;
|
||||
class Settings;
|
||||
|
||||
// Download di file (libri) con segnali di progresso.
|
||||
// Supporta più download concorrenti.
|
||||
class Downloader : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Downloader(Settings *settings, QObject *parent = 0);
|
||||
|
||||
Q_INVOKABLE void download(const QString &url, const QString &destPath);
|
||||
|
||||
signals:
|
||||
void progress(qint64 received, qint64 total, const QString &path);
|
||||
void finished(const QString &path, bool ok, const QString &error);
|
||||
|
||||
private:
|
||||
Settings *m_settings;
|
||||
QNetworkAccessManager m_nam;
|
||||
QHash<QNetworkReply *, QFile *> m_files;
|
||||
QHash<QNetworkReply *, QString> m_paths;
|
||||
};
|
||||
|
||||
#endif // DOWNLOADER_H
|
||||
@@ -0,0 +1,27 @@
|
||||
#include <QGuiApplication>
|
||||
#include <QQuickView>
|
||||
#include <QQmlContext>
|
||||
#include <sailfishapp.h>
|
||||
|
||||
#include "apiclient.h"
|
||||
#include "downloader.h"
|
||||
#include "settings.h"
|
||||
|
||||
Q_DECL_EXPORT int main(int argc, char *argv[])
|
||||
{
|
||||
QGuiApplication *app = SailfishApp::application(argc, argv);
|
||||
QQuickView *view = SailfishApp::createView();
|
||||
|
||||
Settings settings;
|
||||
ApiClient apiClient(&settings);
|
||||
Downloader downloader(&settings);
|
||||
|
||||
view->rootContext()->setContextProperty(QStringLiteral("appSettings"), &settings);
|
||||
view->rootContext()->setContextProperty(QStringLiteral("apiClient"), &apiClient);
|
||||
view->rootContext()->setContextProperty(QStringLiteral("downloader"), &downloader);
|
||||
|
||||
view->setSource(SailfishApp::pathTo(QStringLiteral("qml/harbour-calibreweb.qml")));
|
||||
view->show();
|
||||
|
||||
return app->exec();
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "opdsparser.h"
|
||||
|
||||
#include <QStringList>
|
||||
#include <QVariantMap>
|
||||
#include <QXmlStreamReader>
|
||||
|
||||
static QString elementText(QXmlStreamReader &reader)
|
||||
{
|
||||
return reader.readElementText(QXmlStreamReader::IncludeChildElements).trimmed();
|
||||
}
|
||||
|
||||
static QVariantMap parseEntry(QXmlStreamReader &reader)
|
||||
{
|
||||
QVariantMap entry;
|
||||
QStringList authors;
|
||||
QVariantList formats;
|
||||
QString title;
|
||||
QString summary;
|
||||
QString id;
|
||||
QString coverHref;
|
||||
QString thumbHref;
|
||||
QString navHref;
|
||||
|
||||
while (!reader.atEnd()) {
|
||||
reader.readNext();
|
||||
if (reader.tokenType() == QXmlStreamReader::EndElement
|
||||
&& reader.name() == QLatin1String("entry"))
|
||||
break;
|
||||
if (reader.tokenType() != QXmlStreamReader::StartElement)
|
||||
continue;
|
||||
|
||||
const QStringRef name = reader.name();
|
||||
if (name == QLatin1String("title")) {
|
||||
title = elementText(reader);
|
||||
} else if (name == QLatin1String("id")) {
|
||||
id = elementText(reader);
|
||||
} else if (name == QLatin1String("name")) {
|
||||
const QString author = elementText(reader);
|
||||
if (!author.isEmpty())
|
||||
authors.append(author);
|
||||
} else if (name == QLatin1String("content") || name == QLatin1String("summary")) {
|
||||
if (summary.isEmpty())
|
||||
summary = elementText(reader);
|
||||
} else if (name == QLatin1String("link")) {
|
||||
const QXmlStreamAttributes attrs = reader.attributes();
|
||||
const QString rel = attrs.value(QLatin1String("rel")).toString();
|
||||
const QString href = attrs.value(QLatin1String("href")).toString();
|
||||
const QString type = attrs.value(QLatin1String("type")).toString();
|
||||
|
||||
if (rel == QLatin1String("http://opds-spec.org/image")) {
|
||||
coverHref = href;
|
||||
} else if (rel == QLatin1String("http://opds-spec.org/image/thumbnail")) {
|
||||
thumbHref = href;
|
||||
} else if (rel == QLatin1String("http://opds-spec.org/acquisition")) {
|
||||
QVariantMap format;
|
||||
format.insert(QStringLiteral("format"), attrs.value(QLatin1String("title")).toString());
|
||||
format.insert(QStringLiteral("mime"), type);
|
||||
format.insert(QStringLiteral("size"), attrs.value(QLatin1String("length")).toString().toLongLong());
|
||||
format.insert(QStringLiteral("url"), href);
|
||||
formats.append(format);
|
||||
} else if (rel == QLatin1String("subsection")) {
|
||||
if (navHref.isEmpty())
|
||||
navHref = href;
|
||||
} else if (navHref.isEmpty() && type.contains(QLatin1String("opds-catalog"))) {
|
||||
// calibre-web: i link di navigazione dell'indice non hanno rel="subsection"
|
||||
navHref = href;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entry.insert(QStringLiteral("title"), title);
|
||||
entry.insert(QStringLiteral("authors"), authors.join(QStringLiteral(", ")));
|
||||
entry.insert(QStringLiteral("summary"), summary);
|
||||
entry.insert(QStringLiteral("coverUrl"), !coverHref.isEmpty() ? coverHref : thumbHref);
|
||||
entry.insert(QStringLiteral("formats"), formats);
|
||||
entry.insert(QStringLiteral("id"), id);
|
||||
entry.insert(QStringLiteral("isNavigation"), !navHref.isEmpty());
|
||||
entry.insert(QStringLiteral("linkUrl"), navHref);
|
||||
return entry;
|
||||
}
|
||||
|
||||
bool OpdsParser::parse(const QByteArray &xml,
|
||||
QVariantList *entries,
|
||||
QString *nextUrl,
|
||||
QString *feedTitle,
|
||||
QString *errorMessage)
|
||||
{
|
||||
QXmlStreamReader reader(xml);
|
||||
QVariantList result;
|
||||
QString next;
|
||||
QString title;
|
||||
|
||||
while (!reader.atEnd()) {
|
||||
reader.readNext();
|
||||
if (reader.tokenType() != QXmlStreamReader::StartElement)
|
||||
continue;
|
||||
|
||||
const QStringRef name = reader.name();
|
||||
if (name == QLatin1String("entry")) {
|
||||
result.append(parseEntry(reader));
|
||||
} else if (name == QLatin1String("link")) {
|
||||
const QXmlStreamAttributes attrs = reader.attributes();
|
||||
if (attrs.value(QLatin1String("rel")).toString() == QLatin1String("next"))
|
||||
next = attrs.value(QLatin1String("href")).toString();
|
||||
} else if (name == QLatin1String("title") && title.isEmpty()) {
|
||||
title = elementText(reader);
|
||||
}
|
||||
}
|
||||
|
||||
if (reader.hasError() && result.isEmpty()) {
|
||||
if (errorMessage)
|
||||
*errorMessage = reader.errorString();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entries)
|
||||
*entries = result;
|
||||
if (nextUrl)
|
||||
*nextUrl = next;
|
||||
if (feedTitle)
|
||||
*feedTitle = title;
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef OPDSPARSER_H
|
||||
#define OPDSPARSER_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QVariantList>
|
||||
#include <QString>
|
||||
|
||||
// Parser minimalista dei feed Atom/OPDS.
|
||||
// Ogni <entry> diventa un QVariantMap con:
|
||||
// title, authors, summary, coverUrl, formats (lista di {format, mime, size, url}),
|
||||
// id, isNavigation, linkUrl
|
||||
class OpdsParser
|
||||
{
|
||||
public:
|
||||
static bool parse(const QByteArray &xml,
|
||||
QVariantList *entries,
|
||||
QString *nextUrl,
|
||||
QString *feedTitle,
|
||||
QString *errorMessage);
|
||||
};
|
||||
|
||||
#endif // OPDSPARSER_H
|
||||
@@ -0,0 +1,88 @@
|
||||
#include "settings.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QStandardPaths>
|
||||
|
||||
Settings::Settings(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_settings(QStringLiteral("harbour"), QStringLiteral("calibreweb"))
|
||||
{
|
||||
}
|
||||
|
||||
QString Settings::serverUrl() const
|
||||
{
|
||||
return m_settings.value(QStringLiteral("serverUrl")).toString();
|
||||
}
|
||||
|
||||
void Settings::setServerUrl(const QString &value)
|
||||
{
|
||||
if (value == serverUrl())
|
||||
return;
|
||||
m_settings.setValue(QStringLiteral("serverUrl"), value);
|
||||
emit serverUrlChanged();
|
||||
}
|
||||
|
||||
QString Settings::username() const
|
||||
{
|
||||
return m_settings.value(QStringLiteral("username")).toString();
|
||||
}
|
||||
|
||||
void Settings::setUsername(const QString &value)
|
||||
{
|
||||
if (value == username())
|
||||
return;
|
||||
m_settings.setValue(QStringLiteral("username"), value);
|
||||
emit usernameChanged();
|
||||
}
|
||||
|
||||
QString Settings::password() const
|
||||
{
|
||||
return m_settings.value(QStringLiteral("password")).toString();
|
||||
}
|
||||
|
||||
void Settings::setPassword(const QString &value)
|
||||
{
|
||||
if (value == password())
|
||||
return;
|
||||
m_settings.setValue(QStringLiteral("password"), value);
|
||||
emit passwordChanged();
|
||||
}
|
||||
|
||||
bool Settings::ignoreSslErrors() const
|
||||
{
|
||||
return m_settings.value(QStringLiteral("ignoreSslErrors"), false).toBool();
|
||||
}
|
||||
|
||||
void Settings::setIgnoreSslErrors(bool value)
|
||||
{
|
||||
if (value == ignoreSslErrors())
|
||||
return;
|
||||
m_settings.setValue(QStringLiteral("ignoreSslErrors"), value);
|
||||
emit ignoreSslErrorsChanged();
|
||||
}
|
||||
|
||||
QString Settings::downloadDir() const
|
||||
{
|
||||
QString dir = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
|
||||
if (dir.isEmpty())
|
||||
dir = QStringLiteral("/home/nemo/Downloads");
|
||||
QDir().mkpath(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
QString Settings::authHeader() const
|
||||
{
|
||||
const QString user = username();
|
||||
if (user.isEmpty())
|
||||
return QString();
|
||||
const QString credentials = user + QLatin1Char(':') + password();
|
||||
return QStringLiteral("Basic ") + QString::fromLatin1(credentials.toUtf8().toBase64());
|
||||
}
|
||||
|
||||
QString Settings::baseUrl() const
|
||||
{
|
||||
QString url = serverUrl().trimmed();
|
||||
while (url.endsWith(QLatin1Char('/')))
|
||||
url.chop(1);
|
||||
return url;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#ifndef SETTINGS_H
|
||||
#define SETTINGS_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QSettings>
|
||||
|
||||
class Settings : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QString serverUrl READ serverUrl WRITE setServerUrl NOTIFY serverUrlChanged)
|
||||
Q_PROPERTY(QString username READ username WRITE setUsername NOTIFY usernameChanged)
|
||||
Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged)
|
||||
Q_PROPERTY(bool ignoreSslErrors READ ignoreSslErrors WRITE setIgnoreSslErrors NOTIFY ignoreSslErrorsChanged)
|
||||
Q_PROPERTY(QString downloadDir READ downloadDir CONSTANT)
|
||||
|
||||
public:
|
||||
explicit Settings(QObject *parent = 0);
|
||||
|
||||
QString serverUrl() const;
|
||||
void setServerUrl(const QString &value);
|
||||
|
||||
QString username() const;
|
||||
void setUsername(const QString &value);
|
||||
|
||||
QString password() const;
|
||||
void setPassword(const QString &value);
|
||||
|
||||
bool ignoreSslErrors() const;
|
||||
void setIgnoreSslErrors(bool value);
|
||||
|
||||
QString downloadDir() const;
|
||||
|
||||
// Header "Authorization: Basic ..." pronto da usare, vuoto se utente non impostato
|
||||
QString authHeader() const;
|
||||
|
||||
// URL del server senza slash finale
|
||||
QString baseUrl() const;
|
||||
|
||||
signals:
|
||||
void serverUrlChanged();
|
||||
void usernameChanged();
|
||||
void passwordChanged();
|
||||
void ignoreSslErrorsChanged();
|
||||
|
||||
private:
|
||||
QSettings m_settings;
|
||||
};
|
||||
|
||||
#endif // SETTINGS_H
|
||||
Reference in New Issue
Block a user