summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJuha Vuolle <juha.vuolle@qt.io>2023-01-22 13:13:25 +0200
committerQt Cherry-pick Bot <cherrypick_bot@qt-project.org>2023-02-14 14:13:34 +0000
commitf84a161802cc9808750ca7549b901e617dbf1b00 (patch)
tree161fb6439ec5c32b4703c198fb9d53563302130e
parente84448b301320752f1b9712f2ae26eb67388eb1a (diff)
downloadqtdoc-f84a161802cc9808750ca7549b901e617dbf1b00.tar.gz
Colorpalette HTTP REST client example
Replace the addressbook client example with a colorpalette client. The corresponding addressbook server has been superseded by a colorpalette server. The client can interface the local QHttpServer colorpaletteserver example as well as the publicly accessible reqres.in. Please note that the reqres.in is stateless service, and eg. adding/updating colors will have no actual effect. Task-number: QTBUG-106041 Change-Id: I4ea06bbcd9b32900a661f19e0abe0629275aaef1 Reviewed-by: Mitch Curtis <mitch.curtis@qt.io> Reviewed-by: Konrad Kujawa <konrad.kujawa@qt.io> Reviewed-by: Ivan Solovev <ivan.solovev@qt.io> (cherry picked from commit 80f9a83d9091bdca548b576b073d20496c37a1c0) Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
-rw-r--r--LICENSES/Apache-2.0.txt61
-rw-r--r--examples/demos/CMakeLists.txt1
-rw-r--r--examples/demos/addressbook/CMakeLists.txt58
-rw-r--r--examples/demos/addressbook/addressbookmodel.cpp127
-rw-r--r--examples/demos/addressbook/addressbookmodel.h50
-rw-r--r--examples/demos/addressbook/contactentry.h29
-rw-r--r--examples/demos/addressbook/doc/images/addressbookclient.pngbin39190 -> 0 bytes
-rw-r--r--examples/demos/addressbook/doc/images/authorize.pngbin33509 -> 0 bytes
-rw-r--r--examples/demos/addressbook/doc/images/newcontact.pngbin46340 -> 0 bytes
-rw-r--r--examples/demos/addressbook/doc/src/addressbook-client-example.qdoc70
-rw-r--r--examples/demos/addressbook/main.cpp38
-rw-r--r--examples/demos/addressbook/qml/main.qml175
-rw-r--r--examples/demos/addressbook/restaccessmanager.cpp114
-rw-r--r--examples/demos/addressbook/restaccessmanager.h56
-rw-r--r--examples/demos/colorpaletteclient/CMakeLists.txt64
-rw-r--r--examples/demos/colorpaletteclient/MainWindow.qml455
-rw-r--r--examples/demos/colorpaletteclient/abstractresource.h29
-rw-r--r--examples/demos/colorpaletteclient/basiclogin.cpp69
-rw-r--r--examples/demos/colorpaletteclient/basiclogin.h46
-rw-r--r--examples/demos/colorpaletteclient/colorpaletteclient.pro69
-rw-r--r--examples/demos/colorpaletteclient/doc/images/colorpalette_editing.pngbin0 -> 35435 bytes
-rw-r--r--examples/demos/colorpaletteclient/doc/images/colorpalette_listing.pngbin0 -> 43020 bytes
-rw-r--r--examples/demos/colorpaletteclient/doc/images/colorpalette_urlselection.pngbin0 -> 28973 bytes
-rw-r--r--examples/demos/colorpaletteclient/doc/src/colorpaletteclient.qdoc92
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/add.pngbin0 -> 99 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/delete.pngbin0 -> 153 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/edit.pngbin0 -> 174 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/file_upload.pngbin0 -> 141 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/login.pngbin0 -> 123 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/logout.pngbin0 -> 133 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/refresh.pngbin0 -> 230 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/add.pngbin0 -> 133 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/delete.pngbin0 -> 180 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/edit.pngbin0 -> 215 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/file_upload.pngbin0 -> 197 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/login.pngbin0 -> 169 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/logout.pngbin0 -> 181 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/refresh.pngbin0 -> 412 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/add.pngbin0 -> 148 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/delete.pngbin0 -> 201 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/edit.pngbin0 -> 272 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/file_upload.pngbin0 -> 266 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/login.pngbin0 -> 229 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/logout.pngbin0 -> 246 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/refresh.pngbin0 -> 549 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/add.pngbin0 -> 167 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/delete.pngbin0 -> 254 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/edit.pngbin0 -> 307 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/file_upload.pngbin0 -> 304 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/login.pngbin0 -> 270 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/logout.pngbin0 -> 277 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/refresh.pngbin0 -> 745 bytes
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/index.theme22
-rw-r--r--examples/demos/colorpaletteclient/icons/colorpaletteclient/qt_attribution.json11
-rw-r--r--examples/demos/colorpaletteclient/main.cpp20
-rw-r--r--examples/demos/colorpaletteclient/paginatedresource.cpp106
-rw-r--r--examples/demos/colorpaletteclient/paginatedresource.h58
-rw-r--r--examples/demos/colorpaletteclient/qmldir4
-rw-r--r--examples/demos/colorpaletteclient/restaccessmanager.cpp99
-rw-r--r--examples/demos/colorpaletteclient/restaccessmanager.h39
-rw-r--r--examples/demos/colorpaletteclient/restservice.cpp45
-rw-r--r--examples/demos/colorpaletteclient/restservice.h52
-rw-r--r--examples/demos/colorpaletteclient/util.cpp17
-rw-r--r--examples/demos/colorpaletteclient/util.h11
-rw-r--r--examples/demos/demos.pro3
65 files changed, 1372 insertions, 718 deletions
diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt
new file mode 100644
index 00000000..136d9004
--- /dev/null
+++ b/LICENSES/Apache-2.0.txt
@@ -0,0 +1,61 @@
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
+ 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
+ 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
+ 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
+ (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
+ (b) You must cause any modified files to carry prominent notices stating that You changed the files; and
+ (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
+ (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
+ 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
+ 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
+ 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
+ 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
+ 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/examples/demos/CMakeLists.txt b/examples/demos/CMakeLists.txt
index 6a9b7120..20cab504 100644
--- a/examples/demos/CMakeLists.txt
+++ b/examples/demos/CMakeLists.txt
@@ -11,6 +11,7 @@ if(TARGET Qt::Quick)
qt_internal_add_example(stocqt)
endif()
if(TARGET Qt::Quick AND TARGET Qt::QuickControls2)
+ qt_internal_add_example(colorpaletteclient)
qt_internal_add_example(coffee)
if(ANDROID OR IOS)
qt_internal_add_example(hangman)
diff --git a/examples/demos/addressbook/CMakeLists.txt b/examples/demos/addressbook/CMakeLists.txt
deleted file mode 100644
index 7572f4dc..00000000
--- a/examples/demos/addressbook/CMakeLists.txt
+++ /dev/null
@@ -1,58 +0,0 @@
-# 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
deleted file mode 100644
index 97f61d1e..00000000
--- a/examples/demos/addressbook/addressbookmodel.cpp
+++ /dev/null
@@ -1,127 +0,0 @@
-// Copyright (C) 2022 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
-
-#include "addressbookmodel.h"
-#include "contactentry.h"
-
-#include <QMutexLocker>
-
-AddressBookModel::AddressBookModel(QSharedPointer<RestAccessManager> 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<int, QByteArray> AddressBookModel::roleNames() const
-{
- QHash<int, QByteArray> 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<ContactEntry> tmpContacts;
- const RestAccessManager::ContactsMap contactsMap = accessManager->getContacts();
- std::transform(contactsMap.constKeyValueBegin(), contactsMap.constKeyValueEnd(),
- std::back_inserter(tmpContacts), 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
deleted file mode 100644
index e7b8d602..00000000
--- a/examples/demos/addressbook/addressbookmodel.h
+++ /dev/null
@@ -1,50 +0,0 @@
-// 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 <QAbstractItemModel>
-#include <QMutex>
-#include <QSharedPointer>
-#include <QtQml/qqml.h>
-
-class AddressBookModel : public QAbstractTableModel
-{
- Q_OBJECT
- QML_ELEMENT
-public:
- enum AddressBookRoles { IdRole = Qt::UserRole + 1, NameRole, AddressRole };
- Q_ENUM(AddressBookRoles)
-
- explicit AddressBookModel(QSharedPointer<RestAccessManager> 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<int, QByteArray> 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<RestAccessManager> accessManager;
- mutable QMutex contactsMtx;
- QList<ContactEntry> contacts;
-};
-
-#endif // ADDRESSBOOKMODEL_H
diff --git a/examples/demos/addressbook/contactentry.h b/examples/demos/addressbook/contactentry.h
deleted file mode 100644
index e70e629f..00000000
--- a/examples/demos/addressbook/contactentry.h
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (C) 2022 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
-
-#ifndef TYPES_H
-#define TYPES_H
-
-#include <QJsonObject>
-#include <QString>
-
-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
deleted file mode 100644
index 9ed28446..00000000
--- a/examples/demos/addressbook/doc/images/addressbookclient.png
+++ /dev/null
Binary files differ
diff --git a/examples/demos/addressbook/doc/images/authorize.png b/examples/demos/addressbook/doc/images/authorize.png
deleted file mode 100644
index 16ad5e70..00000000
--- a/examples/demos/addressbook/doc/images/authorize.png
+++ /dev/null
Binary files differ
diff --git a/examples/demos/addressbook/doc/images/newcontact.png b/examples/demos/addressbook/doc/images/newcontact.png
deleted file mode 100644
index df351c37..00000000
--- a/examples/demos/addressbook/doc/images/newcontact.png
+++ /dev/null
Binary files differ
diff --git a/examples/demos/addressbook/doc/src/addressbook-client-example.qdoc b/examples/demos/addressbook/doc/src/addressbook-client-example.qdoc
deleted file mode 100644
index 23bcfe37..00000000
--- a/examples/demos/addressbook/doc/src/addressbook-client-example.qdoc
+++ /dev/null
@@ -1,70 +0,0 @@
-// 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
deleted file mode 100644
index f419e5b3..00000000
--- a/examples/demos/addressbook/main.cpp
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (C) 2022 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
-
-#include "addressbookmodel.h"
-#include "restaccessmanager.h"
-
-#include <QCommandLineParser>
-#include <QGuiApplication>
-#include <QQmlApplicationEngine>
-#include <QQmlContext>
-
-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<RestAccessManager>::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
deleted file mode 100644
index 76cfec37..00000000
--- a/examples/demos/addressbook/qml/main.qml
+++ /dev/null
@@ -1,175 +0,0 @@
-// 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 {
- SystemPalette { id: activePalette }
- color: activePalette.alternateBase
- 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
deleted file mode 100644
index 7b746a08..00000000
--- a/examples/demos/addressbook/restaccessmanager.cpp
+++ /dev/null
@@ -1,114 +0,0 @@
-// Copyright (C) 2022 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
-
-#include "restaccessmanager.h"
-
-#include <QJsonArray>
-#include <QJsonDocument>
-#include <QNetworkReply>
-
-static std::optional<QJsonArray> 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<QJsonArray> 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
deleted file mode 100644
index 19d417ed..00000000
--- a/examples/demos/addressbook/restaccessmanager.h
+++ /dev/null
@@ -1,56 +0,0 @@
-// 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 <QMap>
-#include <QMutex>
-#include <QNetworkAccessManager>
-#include <QObject>
-#include <QString>
-
-#include <optional>
-
-class RestAccessManager : public QObject
-{
- Q_OBJECT
- struct AuthHeader
- {
- QString key;
- QString value;
- };
-
-public:
- using ContactsMap = QMap<qint64, ContactEntry>;
-
- 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> authHeader;
- QNetworkAccessManager manager;
- mutable QMutex contactsMtx;
- ContactsMap contacts;
-};
-
-#endif // RESTACCESSMANAGER_H
diff --git a/examples/demos/colorpaletteclient/CMakeLists.txt b/examples/demos/colorpaletteclient/CMakeLists.txt
new file mode 100644
index 00000000..47aeaf00
--- /dev/null
+++ b/examples/demos/colorpaletteclient/CMakeLists.txt
@@ -0,0 +1,64 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+cmake_minimum_required(VERSION 3.16)
+project(colorpaletteclient LANGUAGES CXX)
+
+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_standard_project_setup()
+
+qt_add_executable(colorpaletteclient
+ main.cpp
+)
+
+set(icon_files)
+foreach(icon IN ITEMS add delete edit file_upload login logout refresh)
+ foreach(scale IN ITEMS "" "@2" "@3" "@4")
+ message("icons/colorpaletteclient/20x20${scale}/${icon}.png")
+ list(APPEND icon_files "icons/colorpaletteclient/20x20${scale}/${icon}.png")
+ endforeach()
+endforeach()
+
+set_target_properties(colorpaletteclient PROPERTIES
+ WIN32_EXECUTABLE TRUE
+ MACOSX_BUNDLE TRUE
+)
+
+qt_add_resources(colorpaletteclient "theme" FILES
+ ${icon_files}
+ icons/colorpaletteclient/index.theme
+)
+
+qt_add_qml_module(colorpaletteclient
+ URI ColorPalette
+ VERSION 1.0
+ AUTO_RESOURCE_PREFIX
+ QML_FILES
+ MainWindow.qml
+ SOURCES
+ abstractresource.h
+ util.h util.cpp
+ basiclogin.h basiclogin.cpp
+ restservice.h restservice.cpp
+ paginatedresource.h paginatedresource.cpp
+ restaccessmanager.h restaccessmanager.cpp
+)
+
+target_link_libraries(colorpaletteclient PRIVATE
+ Qt::Core
+ Qt::Gui
+ Qt::Qml
+ Qt::Quick
+)
+
+install(TARGETS colorpaletteclient
+ RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}"
+ BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"
+ LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"
+)
diff --git a/examples/demos/colorpaletteclient/MainWindow.qml b/examples/demos/colorpaletteclient/MainWindow.qml
new file mode 100644
index 00000000..7b219104
--- /dev/null
+++ b/examples/demos/colorpaletteclient/MainWindow.qml
@@ -0,0 +1,455 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+import ColorPalette
+
+ApplicationWindow {
+ id: window
+ width: 480
+ height: 400
+ visible: true
+ title: qsTr("Color Palette Client")
+
+ enum DataView {
+ UserView = 0,
+ ColorView = 1
+ }
+
+ // When the application starts, prompt the user to select the server URL
+ Component.onCompleted: urlSelectionPopup.open()
+
+ //! [RestService QML element]
+ RestService {
+ id: paletteService
+
+ PaginatedResource {
+ id: users
+ path: "/api/users"
+ }
+
+ PaginatedResource {
+ id: colors
+ path: "/api/unknown"
+ }
+
+ BasicLogin {
+ id: loginService
+ loginPath: "/api/login"
+ logoutPath: "/api/logout"
+ }
+ }
+ //! [RestService QML element]
+
+ Popup {
+ // A popup for selecting the server URL
+ id: urlSelectionPopup
+ anchors.centerIn: parent
+ padding: 10
+ modal: true
+ focus: true
+ closePolicy: Popup.NoAutoClose
+
+ Connections {
+ target: colors
+ // Closes the URL selection popup once we have received data successfully
+ function onDataUpdated() {
+ fetchTester.stop()
+ urlSelectionPopup.close()
+ }
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 5
+
+ Label {
+ text: qsTr("Select server URL")
+ Layout.alignment: Qt.AlignHCenter
+ }
+ GridLayout {
+ id: urlSelectionLayout
+ columns: 2
+ columnSpacing: 5
+ rowSpacing: 5
+ enabled: !fetchTester.running
+
+ TextArea {
+ id: url1TextArea
+ // The default URL of the QtHttpServer colorpaletteserver example
+ text: "http://127.0.0.1:49425"
+ }
+ Button {
+ text: qsTr("Use")
+
+ onClicked: fetchTester.test(url1TextArea.text)
+ }
+ Label {
+ id: url2Label
+ // Well-known REST API test service
+ text: "https://reqres.in"
+ leftPadding: url1TextArea.leftPadding
+ }
+ Button {
+ enabled: paletteService.network.sslSupported
+ text: paletteService.network.sslSupported ? qsTr("Use") : qsTr("No SSL")
+
+ onClicked: fetchTester.test(url2Label.text)
+ }
+ }
+
+ Timer {
+ id: fetchTester
+ interval: 2000
+
+ function test(url) {
+ paletteService.url = url
+ colors.refreshCurrentPage()
+ users.refreshCurrentPage()
+ start()
+ }
+ }
+
+ RowLayout {
+ id: fetchIndicator
+ visible: fetchTester.running
+ Layout.fillWidth: true
+
+ Label {
+ text: qsTr("Testing URL")
+ }
+ BusyIndicator {
+ running: visible
+ Layout.fillWidth: true
+ }
+ }
+ }
+ }
+
+ Popup {
+ // Popup for adding or updating a color
+ id: colorPopup
+ padding: 10
+ modal: true
+ focus: true
+ anchors.centerIn: parent
+ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
+
+ property bool newColor: true
+ property int colorId: -1
+
+ function createNewColor() {
+ newColor = true
+ colorNameField.text = "cute green"
+ colorRGBField.text = "#41cd52"
+ colorPantoneField.text = "PMS 802C"
+ open()
+ }
+
+ function updateColor(data) {
+ newColor = false
+ colorNameField.text = data.name
+ colorRGBField.text = data.color
+ colorPantoneField.text = data.pantone_value
+ colorId = data.id
+ open()
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+
+ Label {
+ visible: !loginService.loggedIn
+ text: qsTr("Login to make any changes")
+ Layout.alignment: Qt.AlignVCenter
+ }
+ GridLayout {
+ columns: 3
+
+ Label {
+ text: qsTr("Color name")
+ }
+ Label {
+ text: qsTr("RGB value")
+ }
+ Label {
+ text: qsTr("Pantone value")
+ }
+
+ TextField {
+ id: colorNameField
+ padding: 10
+ }
+ TextField {
+ id: colorRGBField
+ padding: 10
+ }
+ TextField {
+ id: colorPantoneField
+ padding: 10
+ }
+ }
+
+ Rectangle {
+ color: colorRGBField.text
+ Layout.fillWidth: true
+ Layout.preferredHeight: 30
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ Button {
+ text: qsTr("Cancel")
+ onClicked: colorPopup.close()
+ }
+ Item { Layout.fillWidth: true /* spacer */ }
+ Button {
+ enabled: loginService.loggedIn
+ icon.name: "file_upload"
+ text: colorPopup.newColor ? qsTr("Add") : qsTr("Update")
+
+ onClicked: {
+ if (colorPopup.newColor) {
+ colors.add({"name" : colorNameField.text,
+ "color" : colorRGBField.text,
+ "pantone_value" : colorPantoneField.text})
+ } else {
+ colors.update({"name" : colorNameField.text,
+ "color" : colorRGBField.text,
+ "pantone_value" : colorPantoneField.text},
+ colorPopup.colorId)
+ }
+ colorPopup.close()
+ }
+ }
+ }
+ }
+ }
+
+ ColumnLayout {
+ // The main application layout
+ anchors.fill :parent
+
+ ToolBar {
+ // Main toolbar
+ Layout.fillWidth: true
+
+ RowLayout {
+ anchors.fill: parent
+ ToolButton {
+ text: qsTr("Users")
+ font.bold: dataView.currentIndex === MainWindow.UserView
+
+ onClicked: dataView.currentIndex = MainWindow.UserView
+ }
+ ToolButton {
+ text: qsTr("Colors")
+ font.bold: dataView.currentIndex === MainWindow.ColorView
+
+ onClicked: dataView.currentIndex = MainWindow.ColorView
+ }
+ Item { Layout.fillWidth: true /* spacer */ }
+
+ ToolButton {
+ visible: dataView.currentIndex === MainWindow.ColorView
+ icon.name: loginService.loggedIn
+ ? "logout" : "login"
+
+ onClicked: {
+ if (loginService.loggedIn)
+ loginService.logout()
+ else
+ dataView.currentIndex = MainWindow.UserView
+ }
+ }
+ ToolButton {
+ visible: dataView.currentIndex === MainWindow.ColorView
+ icon.name: "add"
+
+ onClicked: colorPopup.createNewColor()
+ }
+ ToolButton {
+ icon.name: "refresh"
+
+ onClicked: {
+ if (dataView.currentIndex === MainWindow.ColorView)
+ colors.refreshCurrentPage()
+ else
+ users.refreshCurrentPage()
+ }
+ }
+ }
+ }
+
+ SwipeView {
+ id: dataView
+
+ // Controls which view to show
+ currentIndex: MainWindow.UserView
+
+ // The area for the actual user and color data views
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ Pane {
+ // The user data
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ ColumnLayout {
+ anchors.fill: parent
+
+ ListView {
+ model: users.data
+ spacing: 5
+ footerPositioning: ListView.OverlayFooter
+
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+
+ delegate: RowLayout {
+ id: userInfo
+
+ required property var modelData
+ readonly property bool logged: (modelData.email === loginService.user)
+
+ Image {
+ source: userInfo.modelData.avatar
+
+ Layout.preferredHeight: 30
+ Layout.preferredWidth: 30
+ }
+ ToolButton {
+ icon.name: userInfo.logged
+ ? "logout" : "login"
+ enabled: userInfo.logged || !loginService.loggedIn
+
+ onClicked: {
+ if (userInfo.logged) {
+ loginService.logout()
+ } else {
+ //! [Login]
+ loginService.login({"email" : userInfo.modelData.email,
+ "password" : "apassword",
+ "id" : userInfo.modelData.id})
+ //! [Login]
+ }
+ }
+ }
+ Label {
+ text: userInfo.modelData.email
+ font.bold: userInfo.logged
+ }
+ }
+ footer: ToolBar {
+ // Paginate buttons if more than one page
+ visible: users.pages > 1
+ implicitWidth: parent.width
+
+ RowLayout {
+ anchors.fill: parent
+
+ Item { Layout.fillWidth: true /* spacer */ }
+ Repeater {
+ model: users.pages
+
+ ToolButton {
+ text: qsTr("Page") + " " + page
+ font.bold: users.page === page
+
+ required property int index
+ readonly property int page: (index + 1)
+
+ onClicked: users.page = page
+ }
+ }
+ Item { Layout.fillWidth: true /* spacer */ }
+ }
+ }
+ }
+ }
+ }
+
+ Pane {
+ // The color data
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ ColumnLayout {
+ anchors.fill: parent
+
+ //! [View and model]
+ ListView {
+ model: colors.data
+ //! [View and model]
+ footerPositioning: ListView.OverlayFooter
+ spacing: 1
+
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+
+ delegate: RowLayout {
+ id: colorInfo
+
+ required property var modelData
+
+ Rectangle {
+ implicitWidth: 50
+ implicitHeight: 20
+ color: colorInfo.modelData.color
+ }
+ ToolButton {
+ icon.name: "delete"
+ enabled: loginService.loggedIn
+
+ onClicked: colors.remove(colorInfo.modelData.id)
+ }
+ ToolButton {
+ icon.name: "edit"
+
+ onClicked: colorPopup.updateColor(colorInfo.modelData)
+ }
+ Label {
+ text: colorInfo.modelData.name
+ }
+ Label {
+ text: colorInfo.modelData.pantone_value
+ }
+ }
+ footer: ToolBar {
+ // Paginate buttons if more than one page
+ visible: colors.pages > 1
+ implicitWidth: parent.width
+
+ RowLayout {
+ anchors.fill: parent
+
+ Item { Layout.fillWidth: true /* spacer */ }
+ Repeater {
+ model: colors.pages
+
+ ToolButton {
+ text: qsTr("Page") + " " + page
+ font.bold: colors.page === page
+
+ required property int index
+ readonly property int page: (index + 1)
+
+ onClicked: colors.page = page
+ }
+ }
+ Item { Layout.fillWidth: true /* spacer */ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/examples/demos/colorpaletteclient/abstractresource.h b/examples/demos/colorpaletteclient/abstractresource.h
new file mode 100644
index 00000000..4ae718e9
--- /dev/null
+++ b/examples/demos/colorpaletteclient/abstractresource.h
@@ -0,0 +1,29 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+#ifndef ABSTRACTRESOURCE_H
+#define ABSTRACTRESOURCE_H
+
+#include "restaccessmanager.h"
+#include <QtQml/qqml.h>
+#include <QtCore/qobject.h>
+
+class AbstractResource : public QObject
+{
+ Q_OBJECT
+ QML_ANONYMOUS
+
+public:
+ explicit AbstractResource(QObject* parent = nullptr) : QObject (parent)
+ {}
+
+ virtual ~AbstractResource() = default;
+ void setAccessManager(std::shared_ptr<RestAccessManager> manager)
+ {
+ m_manager = manager;
+ }
+
+protected:
+ std::shared_ptr<RestAccessManager> m_manager;
+};
+
+#endif // ABSTRACTRESOURCE_H
diff --git a/examples/demos/colorpaletteclient/basiclogin.cpp b/examples/demos/colorpaletteclient/basiclogin.cpp
new file mode 100644
index 00000000..4a0bbd27
--- /dev/null
+++ b/examples/demos/colorpaletteclient/basiclogin.cpp
@@ -0,0 +1,69 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#include "util.h"
+#include "basiclogin.h"
+#include "restaccessmanager.h"
+#include <QtNetwork/qnetworkreply.h>
+#include <QtCore/qjsonobject.h>
+
+using namespace Qt::StringLiterals;
+
+static constexpr auto tokenField = "token"_L1;
+static constexpr auto emailField = "email"_L1;
+static constexpr auto idField = "id"_L1;
+
+BasicLogin::BasicLogin(QObject* parent)
+ : AbstractResource(parent)
+{
+}
+
+QString BasicLogin::user() const
+{
+ return m_user ? m_user->email : QString{};
+}
+
+bool BasicLogin::loggedIn() const
+{
+ return m_user.has_value();
+}
+
+void BasicLogin::login(const QVariantMap& data)
+{
+ RestAccessManager::ResponseCallback callback =
+ [this, data](QNetworkReply* reply, bool success) {
+ if (success)
+ loginRequestFinished(reply, data);
+ };
+ m_manager->post(m_loginPath, data, callback);
+}
+
+void BasicLogin::loginRequestFinished(QNetworkReply* reply, const QVariantMap& data)
+{
+ std::optional<QJsonObject> json = byteArrayToJsonObject(reply->readAll());
+ if (json && json->contains(tokenField)) {
+ m_user = User{data.value(emailField).toString(),
+ json->value(tokenField).toVariant().toByteArray(),
+ data.value(idField).toInt()};
+ } else {
+ m_user.reset();
+ }
+ m_manager->setAuthorizationToken(m_user ? m_user->token : ""_ba);
+ emit userChanged();
+}
+
+void BasicLogin::logout()
+{
+ RestAccessManager::ResponseCallback callback = [this](QNetworkReply* reply, bool success) {
+ if (success)
+ logoutRequestFinished(reply);
+ };
+ m_manager->post(m_logoutPath, {}, callback);
+}
+
+void BasicLogin::logoutRequestFinished(QNetworkReply* reply)
+{
+ Q_UNUSED(reply);
+ m_user.reset();
+ emit userChanged();
+}
diff --git a/examples/demos/colorpaletteclient/basiclogin.h b/examples/demos/colorpaletteclient/basiclogin.h
new file mode 100644
index 00000000..f7e2b1d4
--- /dev/null
+++ b/examples/demos/colorpaletteclient/basiclogin.h
@@ -0,0 +1,46 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#ifndef LOGINSERVICE_H
+#define LOGINSERVICE_H
+
+#include "abstractresource.h"
+
+#include <QtQml/qqml.h>
+#include <QtCore/qjsonobject.h>
+#include <QtNetwork/qnetworkreply.h>
+
+class BasicLogin: public AbstractResource
+{
+ Q_OBJECT
+ Q_PROPERTY(QString user READ user NOTIFY userChanged)
+ Q_PROPERTY(bool loggedIn READ loggedIn NOTIFY userChanged)
+ Q_PROPERTY(QString loginPath MEMBER m_loginPath REQUIRED)
+ Q_PROPERTY(QString logoutPath MEMBER m_logoutPath REQUIRED)
+ QML_ELEMENT
+
+public:
+ explicit BasicLogin(QObject* parent = nullptr);
+
+ QString user() const;
+ bool loggedIn() const;
+ Q_INVOKABLE void login(const QVariantMap& data);
+ Q_INVOKABLE void logout();
+
+signals:
+ void userChanged();
+
+private:
+ void loginRequestFinished(QNetworkReply* reply, const QVariantMap& data);
+ void logoutRequestFinished(QNetworkReply* reply);
+ struct User {
+ QString email;
+ QByteArray token;
+ int id;
+ };
+ QString m_loginPath;
+ QString m_logoutPath;
+ std::optional<User> m_user;
+};
+
+#endif // LOGINSERVICE_H
diff --git a/examples/demos/colorpaletteclient/colorpaletteclient.pro b/examples/demos/colorpaletteclient/colorpaletteclient.pro
new file mode 100644
index 00000000..b55716aa
--- /dev/null
+++ b/examples/demos/colorpaletteclient/colorpaletteclient.pro
@@ -0,0 +1,69 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+TEMPLATE = app
+TARGET = colorpaletteclient
+
+QT += core network qml quick
+
+CONFIG += qmltypes
+QML_IMPORT_NAME = ColorPalette
+QML_IMPORT_MAJOR_VERSION = 1
+
+SOURCES += main.cpp \
+ util.cpp \
+ restservice.cpp \
+ paginatedresource.cpp \
+ basiclogin.cpp \
+ restaccessmanager.cpp \
+
+HEADERS += abstractresource.h \
+ util.h \
+ restservice.h \
+ paginatedresource.h \
+ basiclogin.h \
+ restaccessmanager.h \
+
+qml_resources.files = \
+ qmldir \
+ MainWindow.qml \
+
+qml_resources.prefix = /qt/qml/ColorPalette
+
+theme_resources.files = \
+ icons/colorpaletteclient/index.theme \
+ icons/colorpaletteclient/20x20@2/add.png \
+ icons/colorpaletteclient/20x20@2/login.png \
+ icons/colorpaletteclient/20x20@2/logout.png \
+ icons/colorpaletteclient/20x20@2/file_upload.png \
+ icons/colorpaletteclient/20x20@2/delete.png \
+ icons/colorpaletteclient/20x20@2/edit.png \
+ icons/colorpaletteclient/20x20@2/refresh.png \
+ icons/colorpaletteclient/20x20@4/add.png \
+ icons/colorpaletteclient/20x20@4/login.png \
+ icons/colorpaletteclient/20x20@4/logout.png \
+ icons/colorpaletteclient/20x20@4/file_upload.png \
+ icons/colorpaletteclient/20x20@4/delete.png \
+ icons/colorpaletteclient/20x20@4/edit.png \
+ icons/colorpaletteclient/20x20@4/refresh.png \
+ icons/colorpaletteclient/20x20@3/add.png \
+ icons/colorpaletteclient/20x20@3/login.png \
+ icons/colorpaletteclient/20x20@3/logout.png \
+ icons/colorpaletteclient/20x20@3/file_upload.png \
+ icons/colorpaletteclient/20x20@3/delete.png \
+ icons/colorpaletteclient/20x20@3/edit.png \
+ icons/colorpaletteclient/20x20@3/refresh.png \
+ icons/colorpaletteclient/20x20/add.png \
+ icons/colorpaletteclient/20x20/login.png \
+ icons/colorpaletteclient/20x20/logout.png \
+ icons/colorpaletteclient/20x20/file_upload.png \
+ icons/colorpaletteclient/20x20/delete.png \
+ icons/colorpaletteclient/20x20/edit.png \
+ icons/colorpaletteclient/20x20/refresh.png \
+
+theme_resources.prefix = /
+
+RESOURCES += qml_resources theme_resources
+
+target.path = $$[QT_INSTALL_EXAMPLES]/demos/colorpaletteclient
+INSTALLS += target
diff --git a/examples/demos/colorpaletteclient/doc/images/colorpalette_editing.png b/examples/demos/colorpaletteclient/doc/images/colorpalette_editing.png
new file mode 100644
index 00000000..b2a29b78
--- /dev/null
+++ b/examples/demos/colorpaletteclient/doc/images/colorpalette_editing.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/doc/images/colorpalette_listing.png b/examples/demos/colorpaletteclient/doc/images/colorpalette_listing.png
new file mode 100644
index 00000000..19b50aa4
--- /dev/null
+++ b/examples/demos/colorpaletteclient/doc/images/colorpalette_listing.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/doc/images/colorpalette_urlselection.png b/examples/demos/colorpaletteclient/doc/images/colorpalette_urlselection.png
new file mode 100644
index 00000000..ed020da5
--- /dev/null
+++ b/examples/demos/colorpaletteclient/doc/images/colorpalette_urlselection.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/doc/src/colorpaletteclient.qdoc b/examples/demos/colorpaletteclient/doc/src/colorpaletteclient.qdoc
new file mode 100644
index 00000000..fe4674f3
--- /dev/null
+++ b/examples/demos/colorpaletteclient/doc/src/colorpaletteclient.qdoc
@@ -0,0 +1,92 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
+
+/*!
+\ingroup qtquickdemos
+\example demos/colorpaletteclient
+\meta category {Networking}
+\meta tags {quick, network, http}
+\title Qt Quick Demo - RESTful Color Palette API client
+\brief Example of how to create a RESTful API QML client.
+\image colorpalette_listing.png
+
+This example shows how to create a basic QML RESTful application with an imaginary color palette
+service. The application uses RESTful communication with the selected server to request and send
+data. The REST service is provided as a QML element whose child elements wrap the individual
+JSON data APIs provided by the server.
+
+\section1 Application functionality
+
+The example provides the following basic functionalities:
+\list
+ \li Select the server to communicate with
+ \li List users and colors
+ \li Login and logout users
+ \li Modify and create new colors
+\endlist
+
+\section2 Server selection
+
+At start the application presents the options for the color palette server to communicate
+with. The predefined options are:
+\list
+ \li \l {https://reqres.in}{reqres.in}, a publicly available REST API test service
+ \li \l {RESTful server Color Palette Example}{Qt-based REST API server example} in QtHttpServer
+\endlist
+Once selected, the application issues a test HTTP GET to the color API to check if the service is
+accessible.
+
+\image colorpalette_urlselection.png
+
+One major difference between the two predefined API options is that the
+\l {RESTful server Color Palette Example}{Qt-based REST API server example} is a stateful
+application which allows modifying colors, whereas the \e reqres.in is a stateless API testing
+service. In other words, when using the \e reqres.in backend, modifying the colors has no impact on
+the UI.
+
+\section2 List users and colors
+
+The users and colors are paginated resources on the server-side. This means that the server
+provides the data in chunks called \e pages. The UI listing reflects this pagination and views the
+data on pages.
+
+Viewing the data on UI is done with standard QML views:
+\snippet demos/colorpaletteclient/MainWindow.qml View and model
+Where the model is a list of JSON data received from the server.
+
+\section2 Logging in
+
+Logging in happens via the login function provided by the login QML element:
+\snippet demos/colorpaletteclient/MainWindow.qml Login
+Under the hood the login sends a HTTP POST request. Upon receiving a successful
+response the authorization token is extracted from the response, which in turn is then used in
+subsequent HTTP requests which require the token.
+
+\section2 Editing colors
+
+Editing and adding new colors is done in a popup:
+\image colorpalette_editing.png
+Note that uploading the color changes to the server requires that a user has logged in.
+
+\section1 REST implementation
+
+The example illustrates one way to compose a REST service from individual resource elements. In
+this example the resources are the paginated \e user and \e color resources plus the login service.
+The resource elements are bound together by the base URL (server URL) and the shared network access
+manager.
+
+The basis of the REST service is the \e RestService element whose children items compose the actual
+service:
+\snippet demos/colorpaletteclient/MainWindow.qml RestService QML element
+
+Upon instantiation the \e RestService element loops its children elements and sets them up
+to use the same network access manager. This way the individual resources share the same access
+details such as the server URL and authorization token.
+
+The actual communication is done with a rest access manager which implements some
+convenience functionality to deal specifically with HTTP REST APIs and effectively deals
+with sending and receiving the QNetworkRequest and QNetworkReply as needed.
+
+\include examples-run.qdocinc
+
+*/
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/add.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/add.png
new file mode 100644
index 00000000..49659bf3
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/add.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/delete.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/delete.png
new file mode 100644
index 00000000..cda9773e
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/delete.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/edit.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/edit.png
new file mode 100644
index 00000000..cf581444
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/edit.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/file_upload.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/file_upload.png
new file mode 100644
index 00000000..7a13c507
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/file_upload.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/login.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/login.png
new file mode 100644
index 00000000..01b6be1a
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/login.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/logout.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/logout.png
new file mode 100644
index 00000000..9b25d9e8
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/logout.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/refresh.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/refresh.png
new file mode 100644
index 00000000..a22d22b7
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20/refresh.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/add.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/add.png
new file mode 100644
index 00000000..8114d615
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/add.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/delete.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/delete.png
new file mode 100644
index 00000000..0aa222a0
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/delete.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/edit.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/edit.png
new file mode 100644
index 00000000..251e5919
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/edit.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/file_upload.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/file_upload.png
new file mode 100644
index 00000000..22364309
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/file_upload.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/login.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/login.png
new file mode 100644
index 00000000..0c1550d7
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/login.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/logout.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/logout.png
new file mode 100644
index 00000000..fbe4185c
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/logout.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/refresh.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/refresh.png
new file mode 100644
index 00000000..7739d6b3
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@2/refresh.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/add.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/add.png
new file mode 100644
index 00000000..c55d4144
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/add.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/delete.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/delete.png
new file mode 100644
index 00000000..e7973061
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/delete.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/edit.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/edit.png
new file mode 100644
index 00000000..0fb8f912
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/edit.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/file_upload.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/file_upload.png
new file mode 100644
index 00000000..1b240284
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/file_upload.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/login.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/login.png
new file mode 100644
index 00000000..2b73a0cb
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/login.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/logout.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/logout.png
new file mode 100644
index 00000000..eacae42a
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/logout.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/refresh.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/refresh.png
new file mode 100644
index 00000000..97bb4243
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@3/refresh.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/add.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/add.png
new file mode 100644
index 00000000..4813a26f
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/add.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/delete.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/delete.png
new file mode 100644
index 00000000..21d7de67
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/delete.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/edit.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/edit.png
new file mode 100644
index 00000000..3e73da3d
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/edit.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/file_upload.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/file_upload.png
new file mode 100644
index 00000000..22a98cc2
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/file_upload.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/login.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/login.png
new file mode 100644
index 00000000..95777832
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/login.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/logout.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/logout.png
new file mode 100644
index 00000000..c03ad2ab
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/logout.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/refresh.png b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/refresh.png
new file mode 100644
index 00000000..74015517
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/20x20@4/refresh.png
Binary files differ
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/index.theme b/examples/demos/colorpaletteclient/icons/colorpaletteclient/index.theme
new file mode 100644
index 00000000..bd8cf98a
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/index.theme
@@ -0,0 +1,22 @@
+[Icon Theme]
+Name=colorpaletteclient
+Directories=20x20,20x20@2,20x20@3,20x20@4
+
+[20x20]
+Size=20
+Type=Fixed
+
+[20x20@2]
+Size=20
+Scale=2
+Type=Fixed
+
+[20x20@3]
+Size=20
+Scale=3
+Type=Fixed
+
+[20x20@4]
+Size=20
+Scale=4
+Type=Fixed
diff --git a/examples/demos/colorpaletteclient/icons/colorpaletteclient/qt_attribution.json b/examples/demos/colorpaletteclient/icons/colorpaletteclient/qt_attribution.json
new file mode 100644
index 00000000..d3c7afa8
--- /dev/null
+++ b/examples/demos/colorpaletteclient/icons/colorpaletteclient/qt_attribution.json
@@ -0,0 +1,11 @@
+{
+ "Id": "colorpaletteclient",
+ "Name": "Selected Material Icons",
+ "QDocModule": "qtdoc",
+ "QtUsage": "Used in Color Palette Client example in QtDoc",
+ "Files": "20x20 20x20@2 20x20@3 20x20@4",
+ "Homepage": "https://fonts.google.com/icons",
+ "License": "Apache License Version 2.0",
+ "LicenseId": "Apache-2.0",
+ "Copyright": "Copyright 2018 Google, Inc. All Rights Reserved."
+}
diff --git a/examples/demos/colorpaletteclient/main.cpp b/examples/demos/colorpaletteclient/main.cpp
new file mode 100644
index 00000000..2cf1b2f8
--- /dev/null
+++ b/examples/demos/colorpaletteclient/main.cpp
@@ -0,0 +1,20 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#include <QtQml/qqmlapplicationengine.h>
+#include <QtQml/qqmlcontext.h>
+#include <QtGui/qguiapplication.h>
+#include <QtGui/qicon.h>
+
+int main(int argc, char *argv[])
+{
+ QGuiApplication app(argc, argv);
+ QIcon::setThemeName("colorpaletteclient");
+
+ QQmlApplicationEngine engine;
+ QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, &app,
+ [](){ QCoreApplication::exit(EXIT_FAILURE);}, Qt::QueuedConnection);
+ engine.loadFromModule("ColorPalette", "MainWindow");
+
+ return QGuiApplication::exec();
+}
diff --git a/examples/demos/colorpaletteclient/paginatedresource.cpp b/examples/demos/colorpaletteclient/paginatedresource.cpp
new file mode 100644
index 00000000..e03b6edd
--- /dev/null
+++ b/examples/demos/colorpaletteclient/paginatedresource.cpp
@@ -0,0 +1,106 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#include "util.h"
+#include "paginatedresource.h"
+#include "restaccessmanager.h"
+#include <QtNetwork/qnetworkreply.h>
+#include <QtCore/qurlquery.h>
+#include <QtCore/qjsonobject.h>
+#include <QtCore/qjsonarray.h>
+
+using namespace Qt::StringLiterals;
+
+static constexpr auto totalPagesField = "total_pages"_L1;
+static constexpr auto currentPageField = "page"_L1;
+static constexpr auto resourceIdentification = "/%1"_L1;
+
+PaginatedResource::PaginatedResource(QObject* parent)
+ : AbstractResource(parent)
+{
+}
+
+QList<QJsonObject> PaginatedResource::data() const
+{
+ return m_data;
+}
+
+int PaginatedResource::pages() const
+{
+ return m_pages;
+}
+
+int PaginatedResource::page() const
+{
+ return m_currentPage;
+}
+
+void PaginatedResource::setPage(int page)
+{
+ if (m_currentPage == page || page < 1)
+ return;
+ m_currentPage = page;
+ emit pageUpdated();
+ refreshCurrentPage();
+}
+
+void PaginatedResource::refreshCurrentPage()
+{
+ RestAccessManager::ResponseCallback callback = [this](QNetworkReply* reply, bool success) {
+ if (success)
+ refreshRequestFinished(reply);
+ };
+ QUrlQuery query;
+ query.addQueryItem("page"_L1, QString::number(m_currentPage));
+ m_manager->get(m_path, query, callback);
+}
+
+void PaginatedResource::refreshRequestFinished(QNetworkReply* reply)
+{
+ m_data.clear();
+ std::optional<QJsonObject> json = byteArrayToJsonObject(reply->readAll());
+ if (json) {
+ QJsonArray data = json->value("data"_L1).toArray();
+ for (const auto& entry : std::as_const(data))
+ m_data.append(entry.toObject());
+ m_pages = json->value(totalPagesField).toInt();
+ m_currentPage = json->value(currentPageField).toInt();
+ emit pageUpdated();
+ } else if (m_currentPage != 1) {
+ // An unexpected response. If we weren't on page 1, try that.
+ // Last resource on currentPage might have been deleted, causing a failure
+ m_pages = 0;
+ setPage(1);
+ }
+ emit dataUpdated();
+}
+
+void PaginatedResource::update(const QVariantMap& data, int id)
+{
+ RestAccessManager::ResponseCallback callback = [this](QNetworkReply* reply, bool success) {
+ Q_UNUSED(reply);
+ if (success)
+ refreshCurrentPage();
+ };
+ m_manager->put(m_path + resourceIdentification.arg(QString::number(id)), data, callback);
+}
+
+void PaginatedResource::add(const QVariantMap& data)
+{
+ RestAccessManager::ResponseCallback callback = [this](QNetworkReply* reply, bool success) {
+ Q_UNUSED(reply);
+ if (success)
+ refreshCurrentPage();
+ };
+ m_manager->post(m_path, data, callback);
+}
+
+void PaginatedResource::remove(int id)
+{
+ RestAccessManager::ResponseCallback callback = [this](QNetworkReply* reply, bool success) {
+ Q_UNUSED(reply);
+ if (success)
+ refreshCurrentPage();
+ };
+ m_manager->deleteResource(m_path + resourceIdentification.arg(QString::number(id)), callback);
+}
diff --git a/examples/demos/colorpaletteclient/paginatedresource.h b/examples/demos/colorpaletteclient/paginatedresource.h
new file mode 100644
index 00000000..2ec08520
--- /dev/null
+++ b/examples/demos/colorpaletteclient/paginatedresource.h
@@ -0,0 +1,58 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+#ifndef PAGINATEDRESOURCE_H
+#define PAGINATEDRESOURCE_H
+
+#include "abstractresource.h"
+
+#include <QtQml/qqml.h>
+#include <QtNetwork/qnetworkreply.h>
+#include <QtCore/qjsonobject.h>
+
+class RestAccessManager;
+
+// This class manages a simple paginated Crud resource,
+// where the resource is a paginated list of JSON items
+class PaginatedResource : public AbstractResource
+{
+ Q_OBJECT
+ Q_PROPERTY(QList<QJsonObject> data READ data NOTIFY dataUpdated)
+ Q_PROPERTY(int page READ page WRITE setPage NOTIFY pageUpdated)
+ Q_PROPERTY(int pages READ pages NOTIFY pageUpdated)
+ Q_PROPERTY(QString path MEMBER m_path REQUIRED)
+ QML_ELEMENT
+
+public:
+ explicit PaginatedResource(QObject* parent = nullptr);
+ ~PaginatedResource() override = default;
+
+ QList<QJsonObject> data() const;
+
+ // Total number of pages according to the server
+ int pages() const;
+
+ // Current page this resource is on. Changing the page will initiate a refresh.
+ // The default page is 1
+ int page() const;
+ void setPage(int page);
+
+ Q_INVOKABLE void refreshCurrentPage();
+ Q_INVOKABLE void update(const QVariantMap& data, int id);
+ Q_INVOKABLE void add(const QVariantMap& data);
+ Q_INVOKABLE void remove(int id);
+
+signals:
+ void dataUpdated();
+ void pageUpdated();
+
+private:
+ void refreshRequestFinished(QNetworkReply* reply);
+ QList<QJsonObject> m_data;
+ // The total number of pages as reported by the server responses
+ int m_pages = 0;
+ // The default page we request if the user hasn't set otherwise
+ int m_currentPage = 1;
+ QString m_path;
+};
+
+#endif // PAGINATEDRESOURCE_H
diff --git a/examples/demos/colorpaletteclient/qmldir b/examples/demos/colorpaletteclient/qmldir
new file mode 100644
index 00000000..c6772edd
--- /dev/null
+++ b/examples/demos/colorpaletteclient/qmldir
@@ -0,0 +1,4 @@
+module ColorPalette
+prefer :/qt/qml/ColorPalette/
+MainWindow 1.0 MainWindow.qml
+
diff --git a/examples/demos/colorpaletteclient/restaccessmanager.cpp b/examples/demos/colorpaletteclient/restaccessmanager.cpp
new file mode 100644
index 00000000..39418f7a
--- /dev/null
+++ b/examples/demos/colorpaletteclient/restaccessmanager.cpp
@@ -0,0 +1,99 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#include "restaccessmanager.h"
+
+using namespace Qt::StringLiterals;
+
+static constexpr auto contentTypeJson = "application/json"_L1;
+// A custom authorization scheme implemented by the Qt example server
+static const auto authorizationToken = "TOKEN"_ba;
+
+static bool httpResponseSuccess(QNetworkReply* reply)
+{
+ const int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ const bool isReplyError = (reply->error() != QNetworkReply::NoError);
+
+ qDebug() << "Request to path" << reply->request().url().path() << "finished";
+ if (isReplyError)
+ qDebug() << "Error" << reply->error();
+ else
+ qDebug() << "HTTP:" << httpStatusCode;
+
+ return (!isReplyError && (httpStatusCode >= 200 && httpStatusCode < 300));
+}
+
+RestAccessManager::RestAccessManager(QObject *parent)
+ : QNetworkAccessManager{parent}
+{
+}
+
+void RestAccessManager::setUrl(const QUrl& url)
+{
+ m_url = url;
+}
+
+bool RestAccessManager::sslSupported() const
+{
+#if QT_CONFIG(ssl)
+ return QSslSocket::supportsSsl();
+#else
+ return false;
+#endif
+}
+
+void RestAccessManager::setAuthorizationToken(const QByteArray& token)
+{
+ m_authorizationToken = token;
+}
+
+void RestAccessManager::post(const QString& api, const QVariantMap& value,
+ ResponseCallback callback)
+{
+ m_url.setPath(api);
+ auto request = QNetworkRequest(m_url);
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, contentTypeJson);
+ request.setRawHeader(authorizationToken, m_authorizationToken);
+ QNetworkReply* reply = QNetworkAccessManager::post(request,
+ QJsonDocument::fromVariant(value).toJson(QJsonDocument::Compact));
+ QObject::connect(reply, &QNetworkReply::finished, reply, [reply, callback](){
+ callback(reply, httpResponseSuccess(reply));
+ });
+}
+
+void RestAccessManager::get(const QString& api, const QUrlQuery& parameters,
+ ResponseCallback callback)
+{
+ m_url.setPath(api);
+ m_url.setQuery(parameters);
+ auto request = QNetworkRequest(m_url);
+ QNetworkReply* reply = QNetworkAccessManager::get(request);
+ QObject::connect(reply, &QNetworkReply::finished, reply, [reply, callback](){
+ callback(reply, httpResponseSuccess(reply));
+ });
+}
+
+void RestAccessManager::put(const QString& api, const QVariantMap& value,
+ ResponseCallback callback)
+{
+ m_url.setPath(api);
+ auto request = QNetworkRequest(m_url);
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, contentTypeJson);
+ request.setRawHeader(authorizationToken, m_authorizationToken);
+ QNetworkReply* reply = QNetworkAccessManager::put(request,
+ QJsonDocument::fromVariant(value).toJson(QJsonDocument::Compact));
+ QObject::connect(reply, &QNetworkReply::finished, reply, [reply, callback](){
+ callback(reply, httpResponseSuccess(reply));
+ });
+}
+
+void RestAccessManager::deleteResource(const QString& api, ResponseCallback callback)
+{
+ m_url.setPath(api);
+ auto request = QNetworkRequest(m_url);
+ request.setRawHeader(authorizationToken, m_authorizationToken);
+ QNetworkReply* reply = QNetworkAccessManager::deleteResource(request);
+ QObject::connect(reply, &QNetworkReply::finished, reply, [reply, callback](){
+ callback(reply, httpResponseSuccess(reply));
+ });
+}
diff --git a/examples/demos/colorpaletteclient/restaccessmanager.h b/examples/demos/colorpaletteclient/restaccessmanager.h
new file mode 100644
index 00000000..7e56ec01
--- /dev/null
+++ b/examples/demos/colorpaletteclient/restaccessmanager.h
@@ -0,0 +1,39 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#ifndef RESTACCESSMANAGER_H
+#define RESTACCESSMANAGER_H
+
+#include <functional>
+#include <QtCore/qurlquery.h>
+#include <QtCore/qjsondocument.h>
+#include <QtNetwork/qnetworkreply.h>
+#include <QtNetwork/qnetworkaccessmanager.h>
+#include <QtQml/qqml.h>
+
+class RestAccessManager : public QNetworkAccessManager
+{
+ Q_OBJECT
+ Q_PROPERTY(bool sslSupported READ sslSupported CONSTANT)
+ QML_ANONYMOUS
+
+public:
+ explicit RestAccessManager(QObject *parent = nullptr);
+
+ void setUrl(const QUrl& url);
+ void setAuthorizationToken(const QByteArray& token);
+
+ bool sslSupported() const;
+
+ using ResponseCallback = std::function<void(QNetworkReply*, bool)>;
+ void post(const QString& api, const QVariantMap& value, ResponseCallback callback);
+ void get(const QString& api, const QUrlQuery& parameters, ResponseCallback callback);
+ void put(const QString& api, const QVariantMap& value, ResponseCallback callback);
+ void deleteResource(const QString& api, ResponseCallback callback);
+
+private:
+ QUrl m_url;
+ QByteArray m_authorizationToken;
+};
+
+#endif // RESTACCESSMANAGER_H
diff --git a/examples/demos/colorpaletteclient/restservice.cpp b/examples/demos/colorpaletteclient/restservice.cpp
new file mode 100644
index 00000000..e38e3bc8
--- /dev/null
+++ b/examples/demos/colorpaletteclient/restservice.cpp
@@ -0,0 +1,45 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#include "restservice.h"
+#include "abstractresource.h"
+#include "restaccessmanager.h"
+
+RestService::RestService(QObject* parent) : QObject(parent)
+{
+ m_manager = std::make_shared<RestAccessManager>();
+}
+
+RestAccessManager* RestService::network() const
+{
+ return m_manager.get();
+}
+
+void RestService::setUrl(const QUrl& url)
+{
+ if (m_url == url)
+ return;
+ m_url = url;
+ m_manager->setUrl(url);
+ emit urlChanged();
+}
+
+QUrl RestService::url() const
+{
+ return m_url;
+}
+
+QQmlListProperty<AbstractResource> RestService::resources()
+{
+ return {this, &m_resources};
+}
+
+void RestService::classBegin()
+{
+}
+
+void RestService::componentComplete()
+{
+ for (const auto resource : std::as_const(m_resources))
+ resource->setAccessManager(m_manager);
+}
diff --git a/examples/demos/colorpaletteclient/restservice.h b/examples/demos/colorpaletteclient/restservice.h
new file mode 100644
index 00000000..ccddfa63
--- /dev/null
+++ b/examples/demos/colorpaletteclient/restservice.h
@@ -0,0 +1,52 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#ifndef RESTSERVICE_H
+#define RESTSERVICE_H
+
+#include "abstractresource.h"
+
+#include <QtQml/qqml.h>
+#include <QtQml/qqmlparserstatus.h>
+#include <QtNetwork/qnetworkaccessmanager.h>
+#include <QtNetwork/qnetworkreply.h>
+#include <QtCore/qobject.h>
+#include <QtCore/qstring.h>
+#include <QtCore/qjsonobject.h>
+
+class RestAccessManager;
+
+class RestService : public QObject, public QQmlParserStatus
+{
+ Q_OBJECT
+ Q_PROPERTY(RestAccessManager* network READ network CONSTANT)
+ Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged)
+ Q_PROPERTY(QQmlListProperty<AbstractResource> resources READ resources)
+ Q_CLASSINFO("DefaultProperty", "resources")
+ Q_INTERFACES(QQmlParserStatus)
+ QML_ELEMENT
+
+public:
+ explicit RestService(QObject* parent = nullptr);
+ ~RestService() override = default;
+
+ RestAccessManager* network() const;
+
+ QUrl url() const;
+ void setUrl(const QUrl& url);
+
+ void classBegin() override;
+ void componentComplete() override;
+
+ QQmlListProperty<AbstractResource> resources();
+
+signals:
+ void urlChanged();
+
+private:
+ QUrl m_url;
+ QList<AbstractResource*> m_resources;
+ std::shared_ptr<RestAccessManager> m_manager;
+};
+
+#endif // RESTSERVICE_H
diff --git a/examples/demos/colorpaletteclient/util.cpp b/examples/demos/colorpaletteclient/util.cpp
new file mode 100644
index 00000000..df33dd53
--- /dev/null
+++ b/examples/demos/colorpaletteclient/util.cpp
@@ -0,0 +1,17 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#include "util.h"
+#include <QtCore/qjsondocument.h>
+
+std::optional<QJsonObject> byteArrayToJsonObject(const QByteArray& data)
+{
+ QJsonParseError parseError;
+ const auto json = QJsonDocument::fromJson(data, &parseError);
+
+ if (parseError.error) {
+ qDebug() << "Response data not JSON:" << parseError.errorString()
+ << "at" << parseError.offset << data;
+ }
+ return json.isObject() ? json.object() : std::optional<QJsonObject>(std::nullopt);
+}
diff --git a/examples/demos/colorpaletteclient/util.h b/examples/demos/colorpaletteclient/util.h
new file mode 100644
index 00000000..6a10497e
--- /dev/null
+++ b/examples/demos/colorpaletteclient/util.h
@@ -0,0 +1,11 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#ifndef UTIL_H
+#define UTIL_H
+
+#include <QtCore/qjsonobject.h>
+
+std::optional<QJsonObject> byteArrayToJsonObject(const QByteArray& data);
+
+#endif // UTIL_H
diff --git a/examples/demos/demos.pro b/examples/demos/demos.pro
index 9a1487c2..62046935 100644
--- a/examples/demos/demos.pro
+++ b/examples/demos/demos.pro
@@ -11,7 +11,8 @@ qtHaveModule(quick) {
stocqt
qtHaveModule(quickcontrols2) {
- SUBDIRS += coffee
+ SUBDIRS += coffee \
+ colorpaletteclient
android|ios: SUBDIRS += hangman
}