From ce3ba5df2a011033f366fcc7279198f732ae311a Mon Sep 17 00:00:00 2001 From: Konrad Kujawa Date: Wed, 24 Aug 2022 16:00:11 +0200 Subject: Qt Quick UI for the RESTful Address Book MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add UI and client side extension for the RESTful Address Book example Fixes: QTBUG-105199 Change-Id: I01c5e2ac44838f01ccbb73d5b6e1d1e578c67618 Reviewed-by: Qt CI Bot Reviewed-by: Alex Blasche (cherry picked from commit 91e567ba53bf8c7be9aff8feb00e46f978937290) Reviewed-by: MÃ¥rten Nordheim --- examples/demos/addressbook/CMakeLists.txt | 58 +++++++ examples/demos/addressbook/addressbookmodel.cpp | 127 +++++++++++++++ examples/demos/addressbook/addressbookmodel.h | 50 ++++++ examples/demos/addressbook/contactentry.h | 29 ++++ .../addressbook/doc/images/addressbookclient.png | Bin 0 -> 39190 bytes .../demos/addressbook/doc/images/authorize.png | Bin 0 -> 33509 bytes .../demos/addressbook/doc/images/newcontact.png | Bin 0 -> 46340 bytes .../doc/src/addressbook-client-example.qdoc | 70 +++++++++ examples/demos/addressbook/main.cpp | 38 +++++ examples/demos/addressbook/qml/main.qml | 173 +++++++++++++++++++++ examples/demos/addressbook/restaccessmanager.cpp | 114 ++++++++++++++ examples/demos/addressbook/restaccessmanager.h | 56 +++++++ 12 files changed, 715 insertions(+) create mode 100644 examples/demos/addressbook/CMakeLists.txt create mode 100644 examples/demos/addressbook/addressbookmodel.cpp create mode 100644 examples/demos/addressbook/addressbookmodel.h create mode 100644 examples/demos/addressbook/contactentry.h create mode 100644 examples/demos/addressbook/doc/images/addressbookclient.png create mode 100644 examples/demos/addressbook/doc/images/authorize.png create mode 100644 examples/demos/addressbook/doc/images/newcontact.png create mode 100644 examples/demos/addressbook/doc/src/addressbook-client-example.qdoc create mode 100644 examples/demos/addressbook/main.cpp create mode 100644 examples/demos/addressbook/qml/main.qml create mode 100644 examples/demos/addressbook/restaccessmanager.cpp create mode 100644 examples/demos/addressbook/restaccessmanager.h (limited to 'examples') diff --git a/examples/demos/addressbook/CMakeLists.txt b/examples/demos/addressbook/CMakeLists.txt new file mode 100644 index 000000000..7572f4dc8 --- /dev/null +++ b/examples/demos/addressbook/CMakeLists.txt @@ -0,0 +1,58 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) +project(addressbookclient LANGUAGES CXX) + +set(CMAKE_AUTOMOC ON) + +if(NOT DEFINED INSTALL_EXAMPLESDIR) + set(INSTALL_EXAMPLESDIR "examples") +endif() + +set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/demos/${PROJECT_NAME}") + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Qml Quick) + +qt_add_executable(addressbookclient + addressbookmodel.h addressbookmodel.cpp + restaccessmanager.h restaccessmanager.cpp + main.cpp +) + +set(qml_files + "qml/main.qml" +) + +qt_add_resources(addressbookclient "qml" + PREFIX + "/" + FILES + ${qml_files} +) + +set_target_properties(addressbookclient PROPERTIES + WIN32_EXECUTABLE TRUE + MACOSX_BUNDLE TRUE +) + +qt_add_qml_module(addressbookclient + URI AddressBookModel + VERSION 1.0 + QML_FILES + "qml/main.qml" + NO_RESOURCE_TARGET_PATH +) + +target_link_libraries(addressbookclient PRIVATE + Qt::Core + Qt::Gui + Qt::Qml + Qt::Quick +) + +install(TARGETS addressbookclient + RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" +) diff --git a/examples/demos/addressbook/addressbookmodel.cpp b/examples/demos/addressbook/addressbookmodel.cpp new file mode 100644 index 000000000..83422964f --- /dev/null +++ b/examples/demos/addressbook/addressbookmodel.cpp @@ -0,0 +1,127 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "addressbookmodel.h" +#include "contactentry.h" + +#include + +AddressBookModel::AddressBookModel(QSharedPointer manager, QObject *parent) + : QAbstractTableModel(parent), accessManager(std::move(manager)) +{ + QObject::connect(accessManager.get(), &RestAccessManager::contactsChanged, this, + &AddressBookModel::updateContacts); +} + +AddressBookModel::~AddressBookModel() = default; + +int AddressBookModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + QMutexLocker lock(&contactsMtx); + return contacts.size(); +} + +int AddressBookModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return ContactEntry::size(); +} + +QVariant AddressBookModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Orientation::Horizontal) { + switch (section) { + case 0: + return ""; // ID is not shown + case 1: + return "Name"; + case 2: + return "Address"; + } + } + return QVariant(); +} + +QVariant AddressBookModel::data(const QModelIndex &index, int role) const +{ + QMutexLocker lock(&contactsMtx); + + if (index.row() < 0 || index.row() >= contacts.count()) + return QVariant(); + + const ContactEntry &contact = contacts.at(index.row()); + QVariant ret; + + switch (role) { + case AddressBookRoles::IdRole: + ret = contact.id; + break; + + case AddressBookRoles::NameRole: + ret = contact.name; + break; + + case AddressBookRoles::AddressRole: + ret = contact.address; + break; + } + return ret; +} + +QHash AddressBookModel::roleNames() const +{ + QHash roles; + roles[AddressBookRoles::IdRole] = "id"; + roles[AddressBookRoles::NameRole] = "name"; + roles[AddressBookRoles::AddressRole] = "address"; + return roles; +} + +Q_INVOKABLE void AddressBookModel::setAuthorizationHeader(const QString &key, const QString &value) +{ + accessManager->setAuthorizationHeader(key, value); +} + +Q_INVOKABLE void AddressBookModel::addContact(const QString &name, const QString &contact) +{ + accessManager->addContact(ContactEntry{ 0, name, contact }); +} + +Q_INVOKABLE void AddressBookModel::updateContact(qint64 id, const QString &name, + const QString &contact) +{ + accessManager->updateContact(ContactEntry{ id, name, contact }); +} + +Q_INVOKABLE void AddressBookModel::removeContact(const qint64 id) +{ + accessManager->deleteContact(id); +} + +Q_INVOKABLE void AddressBookModel::refresh() +{ + this->updateContacts(); +} + +void AddressBookModel::updateContacts() +{ + auto toContactEntry = [](const auto &it) { + return ContactEntry{ it.first, it.second.name, it.second.address }; + }; + QList tmpContacts; + const RestAccessManager::ContactsMap contactsMap = accessManager->getContacts(); + std::transform(contactsMap.constKeyValueBegin(), contactsMap.constKeyValueEnd(), + std::back_inserter(contacts), toContactEntry); + + this->beginInsertRows(this->index(-1, -1), 0, this->rowCount()); + { + QMutexLocker lock(&contactsMtx); + contacts.swap(tmpContacts); + } + this->endInsertRows(); +} diff --git a/examples/demos/addressbook/addressbookmodel.h b/examples/demos/addressbook/addressbookmodel.h new file mode 100644 index 000000000..e7b8d6027 --- /dev/null +++ b/examples/demos/addressbook/addressbookmodel.h @@ -0,0 +1,50 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef ADDRESSBOOKMODEL_H +#define ADDRESSBOOKMODEL_H + +#include "contactentry.h" +#include "restaccessmanager.h" + +#include +#include +#include +#include + +class AddressBookModel : public QAbstractTableModel +{ + Q_OBJECT + QML_ELEMENT +public: + enum AddressBookRoles { IdRole = Qt::UserRole + 1, NameRole, AddressRole }; + Q_ENUM(AddressBookRoles) + + explicit AddressBookModel(QSharedPointer manager, QObject *parent = nullptr); + ~AddressBookModel(); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + Q_INVOKABLE void setAuthorizationHeader(const QString &key, const QString &value); + Q_INVOKABLE void addContact(const QString &name, const QString &contact); + Q_INVOKABLE void updateContact(qint64 id, const QString &name, const QString &contact); + Q_INVOKABLE void removeContact(qint64 id); + Q_INVOKABLE void refresh(); + +signals: + void contactsChanged(); + +private slots: + void updateContacts(); + +private: + QSharedPointer accessManager; + mutable QMutex contactsMtx; + QList contacts; +}; + +#endif // ADDRESSBOOKMODEL_H diff --git a/examples/demos/addressbook/contactentry.h b/examples/demos/addressbook/contactentry.h new file mode 100644 index 000000000..e70e629fa --- /dev/null +++ b/examples/demos/addressbook/contactentry.h @@ -0,0 +1,29 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef TYPES_H +#define TYPES_H + +#include +#include + +struct ContactEntry +{ + qint64 id; + QString name; + QString address; + + QJsonObject toJson() const + { + return QJsonObject{ { "id", id }, { "name", name }, { "address", address } }; + } + + static constexpr qsizetype size() { return 3; } + + bool operator==(const ContactEntry &other) const + { + return name == other.name && address == other.address; + } +}; + +#endif // TYPES_H diff --git a/examples/demos/addressbook/doc/images/addressbookclient.png b/examples/demos/addressbook/doc/images/addressbookclient.png new file mode 100644 index 000000000..9ed284464 Binary files /dev/null and b/examples/demos/addressbook/doc/images/addressbookclient.png differ diff --git a/examples/demos/addressbook/doc/images/authorize.png b/examples/demos/addressbook/doc/images/authorize.png new file mode 100644 index 000000000..16ad5e709 Binary files /dev/null and b/examples/demos/addressbook/doc/images/authorize.png differ diff --git a/examples/demos/addressbook/doc/images/newcontact.png b/examples/demos/addressbook/doc/images/newcontact.png new file mode 100644 index 000000000..df351c371 Binary files /dev/null and b/examples/demos/addressbook/doc/images/newcontact.png differ diff --git a/examples/demos/addressbook/doc/src/addressbook-client-example.qdoc b/examples/demos/addressbook/doc/src/addressbook-client-example.qdoc new file mode 100644 index 000000000..23bcfe37f --- /dev/null +++ b/examples/demos/addressbook/doc/src/addressbook-client-example.qdoc @@ -0,0 +1,70 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +/*! +\ingroup qtquickdemos +\example demos/addressbook +\title Qt Quick Demo - RESTful API client Address Book +\brief Example of how to create a RESTful API client. +\image addressbookclient.png + +This example shows how to create a basic QML application with address book +functionality. +The application uses RESTful communication with a given server to send +requests and retrieve data. + +The application allows users to add new contacts by clicking the +'Add Contact' button and then entering the data for the record and +clicking the 'Add' button (see image below). +\image newcontact.png + +The Address Book application gives you the ability to delete an entry, +by clicking the 'Delete' button next to the entry, and update by updating +the data in the table. + +In order to use the modification features, users must authorize themselves +by providing a key and value, which will be used in communication +with the RESTful API. +\image authorize.png + +To run the client application, first run the +\l {RESTful server Address Book Example} {Address Book server example} +in the background or use an already running server that provides used API. +Then run the client application, to run it host and port arguments must be provided, +for example: +\code +./addressbookclient --host http://127.0.0.1 --port 62122 +\endcode + +This example application uses QNetworkAccessManager +which is wrapped in the \c RestAccessManager class. + +\snippet demos/addressbook/restaccessmanager.cpp Connect QNetworkAccessManager example +The code snippet above shows how to connect QNetworkAccessManager to this wrapper. +First, a connection to the server is established +and the QNetworkAccessManager::setAutoDeleteReplies method is called to simplify +the QNetworkReply deletion. +Then QObject::connect is used to call the internal \c RestAccessManager::readContacts +after the QNetworkReply is ready to be processed. + +\snippet demos/addressbook/restaccessmanager.cpp Update contacts signal example +This method asynchronously processes each QNetworkReply +for each request sent via QNetworkAccessManager. +When the response is an array, that practically means that RestAccessManage +got a new list of the contacts, so it has to update it. +When the response is different, it means that the corresponding request +has changed the list of contacts and it needs to be retrieved from the server. + +\snippet demos/addressbook/restaccessmanager.cpp GET contacts example +To send a \c GET request, the QNetworkAccessManager::get method is used +with the prepared QNetworkRequest. +QNetworkRequest::setHeader is used to ensure correct encoding of the content. + +\snippet demos/addressbook/restaccessmanager.cpp POST contacts example +To send the \c POST request, a similar approach can be used. +In addition, to set the authorization header +QNetworkRequest::setRawHeader was used. + + +\sa {RESTful server Address Book Example} +*/ diff --git a/examples/demos/addressbook/main.cpp b/examples/demos/addressbook/main.cpp new file mode 100644 index 000000000..f419e5b36 --- /dev/null +++ b/examples/demos/addressbook/main.cpp @@ -0,0 +1,38 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "addressbookmodel.h" +#include "restaccessmanager.h" + +#include +#include +#include +#include + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + + QCommandLineParser parser; + parser.addOptions({ + { "host", QCoreApplication::translate("main", "Hostname of the server."), "host" }, + { "port", QCoreApplication::translate("main", "Port of the server."), "port" }, + }); + parser.addHelpOption(); + parser.process(app); + + if (parser.value("host").isEmpty() || parser.value("port").isEmpty()) + parser.showHelp(-1); + + QQmlApplicationEngine engine; + auto manager = QSharedPointer::create(parser.value("host"), + parser.value("port").toInt()); + AddressBookModel model(manager); + engine.setInitialProperties({ { "addressBookModel", QVariant::fromValue(&model) } }); + + engine.load(QUrl("qrc:/qml/main.qml")); + if (engine.rootObjects().isEmpty()) + return -1; + + return QGuiApplication::exec(); +} diff --git a/examples/demos/addressbook/qml/main.qml b/examples/demos/addressbook/qml/main.qml new file mode 100644 index 000000000..2505cd40b --- /dev/null +++ b/examples/demos/addressbook/qml/main.qml @@ -0,0 +1,173 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Window +import Qt.labs.qmlmodels + +import AddressBookModel + +ApplicationWindow { + id: root + width: 850 + height: 350 + visible: true + title: qsTr("Address Book") + required property AddressBookModel addressBookModel + + Button { + id: newContactButton + text: qsTr("Add new contact") + onClicked: newContactPopup.open() + } + Popup { + id: newContactPopup + padding: 10 + anchors.centerIn: parent + modal: true + focus: true + + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent + + GridLayout { + columns: 2 + TextField { + id: newNameField + placeholderText: qsTr("Enter name") + padding: 10 + } + TextField { + id: newAddressField + placeholderText: qsTr("Enter address") + padding: 10 + } + Button { + text: qsTr("Cancel") + onClicked: { + newContactPopup.close(); + } + } + Button { + text: qsTr("Add") + onClicked: { + addressBookModel.addContact(newNameField.displayText, + newAddressField.displayText); + newContactPopup.close(); + } + } + } + } + + Button { + id: authorizeButton + text: qsTr("Authorize") + onClicked: authorizePopup.open() + anchors { + top: newContactButton.bottom + left: newContactButton.left + } + } + Popup { + id: authorizePopup + padding: 10 + anchors.centerIn: parent + modal: true + focus: true + + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent + + GridLayout { + columns: 2 + TextField { + id: apiKeyField + text: "api_key" + padding: 10 + readOnly: true + } + TextField { + id: apiKeyValueField + placeholderText: qsTr("Enter API key value") + padding: 10 + } + Button { + text: qsTr("Cancel") + onClicked: { + authorizePopup.close(); + } + } + Button { + text: qsTr("Set") + onClicked: { + addressBookModel.setAuthorizationHeader(apiKeyField.displayText, + apiKeyValueField.displayText); + authorizePopup.close(); + } + } + } + } + + HorizontalHeaderView { + id: horizontalHeader + model: addressBookModel + syncView: tableView + anchors { + top: newContactButton.top + left: newContactButton.right + } + } + + TableView { + id: tableView + model: addressBookModel + anchors { + top: horizontalHeader.bottom + left: horizontalHeader.left + } + width: 700 + height: 350 + columnSpacing: 1 + rowSpacing: 1 + clip: true + property var columnWidthsFactor: [0.15, 0.35, 0.5] + columnWidthProvider: function (column) { + return tableView.model ? tableView.width * columnWidthsFactor[column] : 0; + } + rowHeightProvider: function (row) { + return 50; + } + + delegate: DelegateChooser { + DelegateChoice { + column: 0 + delegate: Rectangle { + Button { + id: deleteButton + text: qsTr("Delete") + onClicked: addressBookModel.removeContact(id) + } + } + } + DelegateChoice { + column: 1 + delegate: Rectangle { + TextInput { + text: name + padding: 10 + onEditingFinished: addressBookModel.updateContact(id, displayText, address) + } + } + } + DelegateChoice { + column: 2 + delegate: Rectangle { + TextInput { + text: address + padding: 10 + onEditingFinished: addressBookModel.updateContact(id, name, displayText) + } + } + } + } + } +} diff --git a/examples/demos/addressbook/restaccessmanager.cpp b/examples/demos/addressbook/restaccessmanager.cpp new file mode 100644 index 000000000..7b746a085 --- /dev/null +++ b/examples/demos/addressbook/restaccessmanager.cpp @@ -0,0 +1,114 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "restaccessmanager.h" + +#include +#include +#include + +static std::optional byteArrayToJsonArray(const QByteArray &arr) +{ + QJsonParseError err; + const auto json = QJsonDocument::fromJson(arr, &err); + if (err.error || !json.isArray()) + return std::nullopt; + return json.array(); +} + +RestAccessManager::RestAccessManager(const QString &host, quint16 port) : host(host), port(port) +{ + //! [Connect QNetworkAccessManager example] + manager.connectToHost(host, port); + manager.setAutoDeleteReplies(true); + QObject::connect(&manager, &QNetworkAccessManager::finished, this, + &RestAccessManager::readContacts); + //! [Connect QNetworkAccessManager example] + this->updateContacts(); +} + +RestAccessManager::~RestAccessManager() = default; + +void RestAccessManager::setAuthorizationHeader(const QString &key, const QString &value) +{ + authHeader = AuthHeader{ key, value }; +} + +RestAccessManager::ContactsMap RestAccessManager::getContacts() const +{ + QMutexLocker lock(&contactsMtx); + return contacts; +} + +//! [POST contacts example] +void RestAccessManager::addContact(const ContactEntry &entry) +{ + auto request = QNetworkRequest(QUrl(QString("%1:%2/v2/contact").arg(host).arg(port))); + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + if (authHeader) { + request.setRawHeader(authHeader->key.toLatin1(), authHeader->value.toLatin1()); + } + manager.post(request, QJsonDocument(entry.toJson()).toJson(QJsonDocument::Compact)); +} +//! [POST contacts example] + +void RestAccessManager::updateContact(const ContactEntry &entry) +{ + auto request = + QNetworkRequest(QUrl(QString("%1:%2/v2/contact/%3").arg(host).arg(port).arg(entry.id))); + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + if (authHeader) { + request.setRawHeader(authHeader->key.toLatin1(), authHeader->value.toLatin1()); + } + manager.put(request, QJsonDocument(entry.toJson()).toJson(QJsonDocument::Compact)); +} + +void RestAccessManager::deleteContact(qint64 id) +{ + auto request = + QNetworkRequest(QUrl(QString("%1:%2/v2/contact/%3").arg(host).arg(port).arg(id))); + if (authHeader) { + request.setRawHeader(authHeader->key.toLatin1(), authHeader->value.toLatin1()); + } + manager.deleteResource(request); +} + +//! [Update contacts signal example] +void RestAccessManager::readContacts(QNetworkReply *reply) +{ + if (reply->error()) { + return; + } + const std::optional array = byteArrayToJsonArray(reply->readAll()); + if (array) { + ContactsMap tmpContacts; + for (const auto &jsonValue : *array) { + if (jsonValue.isObject()) { + const QJsonObject obj = jsonValue.toObject(); + if (obj.contains("id") && obj.contains("name") && obj.contains("address")) { + tmpContacts.insert(obj.value("id").toInt(), + ContactEntry{ obj.value("id").toInt(), + obj.value("name").toString(), + obj.value("address").toString() }); + } + } + } + { + QMutexLocker lock(&contactsMtx); + contacts.swap(tmpContacts); + } + emit contactsChanged(); + } else { + this->updateContacts(); + } +} +//! [Update contacts signal example] + +//! [GET contacts example] +void RestAccessManager::updateContacts() +{ + auto request = QNetworkRequest(QUrl(QString("%1:%2/v2/contact").arg(host).arg(port))); + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + manager.get(request); +} +//! [GET contacts example] diff --git a/examples/demos/addressbook/restaccessmanager.h b/examples/demos/addressbook/restaccessmanager.h new file mode 100644 index 000000000..19d417ede --- /dev/null +++ b/examples/demos/addressbook/restaccessmanager.h @@ -0,0 +1,56 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef RESTACCESSMANAGER_H +#define RESTACCESSMANAGER_H + +#include "contactentry.h" + +#include +#include +#include +#include +#include + +#include + +class RestAccessManager : public QObject +{ + Q_OBJECT + struct AuthHeader + { + QString key; + QString value; + }; + +public: + using ContactsMap = QMap; + + explicit RestAccessManager(const QString &host, quint16 port); + ~RestAccessManager(); + + void setAuthorizationHeader(const QString &key, const QString &value); + ContactsMap getContacts() const; + + void addContact(const ContactEntry &entry); + void updateContact(const ContactEntry &entry); + void deleteContact(qint64 id); + +signals: + void contactsChanged(); + +private slots: + void readContacts(QNetworkReply *reply); + +private: + void updateContacts(); + + const QString host; + const quint16 port; + std::optional authHeader; + QNetworkAccessManager manager; + mutable QMutex contactsMtx; + ContactsMap contacts; +}; + +#endif // RESTACCESSMANAGER_H -- cgit v1.2.3