RESTful API client¶

Example of how to create a RESTful API QML client.

This example shows how to create a basic QML RESTful API client 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.

Application functionality¶

The example provides the following basic functionalities:

  • Select the server to communicate with

  • List users and colors

  • Login and logout users

  • Modify and create new colors

Server selection¶

At start the application presents the options for the color palette server to communicate with. The predefined options are:

Once selected, the RESTful API client issues a test HTTP GET to the color API to check if the service is accessible.

One major difference between the two predefined API options is that the Qt-based REST API server example is a stateful application which allows modifying colors, whereas the reqres.in is a stateless API testing service. In other words, when using the reqres.in backend, modifying the colors has no lasting impact.

The users and colors are paginated resources on the server-side. This means that the server provides the data in chunks called pages. The UI listing reflects this pagination and views the data on pages.

Viewing the data on UI is done with standard QML views populated by JSON data received from the server via the data property of the class PaginatedResource. For C++ compatibility, it is declared to be of type QList<QJsonObject>. It can be passed a list of dicts as obtained from parsing using QJsonDocument.

Logging in happens via the login function provided by the login popup. 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.

Editing and adding new colors is done in a popup. Note that uploading the color changes to the server requires that a user has logged in.

REST implementation¶

The example illustrates one way to compose a REST service from individual resource elements. In this example the resources are the paginated user and 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 RestService QML element whose children items compose the actual service.

Upon instantiation the 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.

RESTful API client

Download this example

# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from PySide6.QtCore import QObject
from PySide6.QtQml import QmlAnonymous


QML_IMPORT_NAME = "ColorPalette"
QML_IMPORT_MAJOR_VERSION = 1


@QmlAnonymous
class AbstractResource(QObject):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.m_manager = None  # QRestAccessManager
        self.m_api = None  # QNetworkRequestFactory

    def setAccessManager(self, manager):
        self.m_manager = manager

    def setServiceApi(self, serviceApi):
        self.m_api = serviceApi
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

import sys
from functools import partial
from dataclasses import dataclass

from PySide6.QtCore import Property, Signal, Slot
from PySide6.QtNetwork import QHttpHeaders
from PySide6.QtQml import QmlElement

from abstractresource import AbstractResource


tokenField = "token"
emailField = "email"
idField = "id"


QML_IMPORT_NAME = "ColorPalette"
QML_IMPORT_MAJOR_VERSION = 1


@QmlElement
class BasicLogin(AbstractResource):
    @dataclass
    class User:
        email: str
        token: bytes
        id: int

    userChanged = Signal()

    def __init__(self, parent=None):
        super().__init__(parent)
        self.m_user = None
        self.m_loginPath = ""
        self.m_logoutPath = ""
        self.m_user = None

    @Property(str, notify=userChanged)
    def user(self):
        return self.m_user.email if self.m_user else ""

    @Property(bool, notify=userChanged)
    def loggedIn(self):
        return bool(self.m_user)

    @Property(str)
    def loginPath(self):
        return self.m_loginPath

    @loginPath.setter
    def loginPath(self, p):
        self.m_loginPath = p

    @Property(str)
    def logoutPath(self):
        return self.m_logoutPath

    @logoutPath.setter
    def logoutPath(self, p):
        self.m_logoutPath = p

    @Slot("QVariantMap")
    def login(self, data):
        request = self.m_api.createRequest(self.m_loginPath)
        self.m_manager.post(request, data, self, partial(self.loginReply, data))

    def loginReply(self, data, reply):
        self.m_user = None
        if not reply.isSuccess():
            print("login: ", reply.errorString(), file=sys.stderr)
        (json, error) = reply.readJson()
        if json and json.isObject():
            json_object = json.object()
            if token := json_object.get(tokenField):
                email = data[emailField]
                token = json_object[tokenField]
                id = data[idField]
                self.m_user = BasicLogin.User(email, token, id)

        headers = QHttpHeaders()
        headers.append("token", self.m_user.token if self.m_user else "")
        self.m_api.setCommonHeaders(headers)
        self.userChanged.emit()

    @Slot()
    def logout(self):
        request = self.m_api.createRequest(self.m_logoutPath)
        self.m_manager.post(request, b"", self, self.logoutReply)

    def logoutReply(self, reply):
        if reply.isSuccess():
            self.m_user = None
            self.m_api.clearCommonHeaders()  # clears 'token' header
            self.userChanged.emit()
        else:
            print("logout: ", reply.errorString(), file=sys.stderr)
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

"""PySide6 port of the Qt RESTful API client demo from Qt v6.x"""

import os
import sys
from pathlib import Path

from PySide6.QtCore import QUrl
from PySide6.QtGui import QIcon, QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine

from basiclogin import BasicLogin  # noqa: F401
from paginatedresource import PaginatedResource  # noqa: F401
from restservice import RestService  # noqa: F401
import rc_colorpaletteclient  # noqa: F401

if __name__ == "__main__":
    app = QGuiApplication(sys.argv)
    QIcon.setThemeName("colorpaletteclient")

    engine = QQmlApplicationEngine()
    app_dir = Path(__file__).parent
    app_dir_url = QUrl.fromLocalFile(os.fspath(app_dir))
    engine.addImportPath(os.fspath(app_dir))
    engine.loadFromModule("ColorPalette", "Main")
    if not engine.rootObjects():
        sys.exit(-1)

    exit_code = app.exec()
    del engine
    sys.exit(exit_code)
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

import sys
from PySide6.QtCore import (QUrlQuery, Property, Signal, Slot)
from PySide6.QtQml import QmlElement

from abstractresource import AbstractResource


QML_IMPORT_NAME = "ColorPalette"
QML_IMPORT_MAJOR_VERSION = 1


totalPagesField = "total_pages"
currentPageField = "page"


@QmlElement
class PaginatedResource(AbstractResource):
    """This class manages a simple paginated Crud resource,
       where the resource is a paginated list of JSON items."""

    dataUpdated = Signal()
    pageUpdated = Signal()
    pagesUpdated = Signal()

    def __init__(self, parent=None):
        super().__init__(parent)
        # The total number of pages as reported by the server responses
        self.m_pages = 0
        # The default page we request if the user hasn't set otherwise
        self.m_currentPage = 1
        self.m_path = ""
        self._data = []

    @Property(str)
    def path(self):
        return self.m_path

    @path.setter
    def path(self, p):
        self.m_path = p

    @Property(int, notify=pagesUpdated)
    def pages(self):
        return self.m_pages

    @Property(int, notify=pageUpdated)
    def page(self):
        return self.m_currentPage

    @page.setter
    def page(self, page):
        if self.m_currentPage == page or page < 1:
            return
        self.m_currentPage = page
        self.pageUpdated.emit()
        self.refreshCurrentPage()

    @Slot()
    def refreshCurrentPage(self):
        query = QUrlQuery()
        query.addQueryItem("page", str(self.m_currentPage))
        request = self.m_api.createRequest(self.m_path, query)
        self.m_manager.get(request, self, self.refreshCurrentPageReply)

    def refreshCurrentPageReply(self, reply):
        error = ""
        if reply.isSuccess():
            (json, jsonError) = reply.readJson()
            if json:
                self.refreshRequestFinished(json)
            else:
                error = jsonError.errorString()
        else:
            reply_error = reply.errorString()
            error = reply_error if reply_error else "Network error"

        if error:
            url = reply.networkReply().url().toString()
            print(f'PaginatedResource: request "{url}" failed: "{error}"', file=sys.stderr)
            self.refreshRequestFailed()

    def refreshRequestFinished(self, json):
        json_object = json.object()
        data = json_object.get("data")
        totalPages = json_object.get(totalPagesField)
        currentPage = json_object.get(currentPageField)
        self._data = data if data else []
        self.m_pages = int(totalPages) if totalPages else 1
        self.m_currentPage = int(currentPage) if currentPage else 1
        self.pageUpdated.emit()
        self.pagesUpdated.emit()
        self.dataUpdated.emit()

    def refreshRequestFailed(self):
        if self.m_currentPage != 1:
            # A failed refresh. If we weren't on page 1, try that.
            # Last resource on currentPage might have been deleted, causing a failure
            self.setPage(1)
        else:
            # Refresh failed and we we're already on page 1 => clear data
            self.m_pages = 0
            self.pagesUpdated.emit()
            self._data = []
            self.dataUpdated.emit()

    @Slot("QVariantMap", int)
    def update(self, data, id):
        request = self.m_api.createRequest(f"{self.m_path}/{id}")
        self.m_manager.put(request, data, self, self.updateReply)

    def updateReply(self, reply):
        if reply.isSuccess():
            self.refreshCurrentPage()

    @Slot("QVariantMap")
    def add(self, data):
        request = self.m_api.createRequest(self.m_path)
        self.m_manager.post(request, data, self, self.updateReply)

    @Slot(int)
    def remove(self, id):
        request = self.m_api.createRequest(f"{self.m_path}/{id}")
        self.m_manager.deleteResource(request, self, self.updateReply)

    @Property("QList<QJsonObject>", notify=dataUpdated, final=True)
    def data(self):
        return self._data
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from PySide6.QtCore import Property, Signal, ClassInfo
from PySide6.QtNetwork import (QNetworkAccessManager, QRestAccessManager,
                               QNetworkRequestFactory, QSslSocket)
from PySide6.QtQml import QmlElement, QPyQmlParserStatus, ListProperty
from abstractresource import AbstractResource

QML_IMPORT_NAME = "ColorPalette"
QML_IMPORT_MAJOR_VERSION = 1


class ApiKeyRequestFactory(QNetworkRequestFactory):
    """Custom request factory that adds the reqres.in API key to all requests"""

    def createRequest(self, path, query=None):
        """Override to add API key header to every request"""
        if query is None:
            request = super().createRequest(path)
        else:
            request = super().createRequest(path, query)
        request.setRawHeader(b"x-api-key", b"reqres-free-v1")
        return request


@QmlElement
@ClassInfo(DefaultProperty="resources")
class RestService(QPyQmlParserStatus):

    urlChanged = Signal()

    def __init__(self, parent=None):
        super().__init__(parent)
        self.m_resources = []
        self.m_qnam = QNetworkAccessManager()
        self.m_qnam.setAutoDeleteReplies(True)
        self.m_manager = QRestAccessManager(self.m_qnam)
        self.m_serviceApi = ApiKeyRequestFactory()

    @Property(str, notify=urlChanged)
    def url(self):
        return self.m_serviceApi.baseUrl()

    @url.setter
    def url(self, url):
        if self.m_serviceApi.baseUrl() != url:
            self.m_serviceApi.setBaseUrl(url)
            self.urlChanged.emit()

    @Property(bool, constant=True)
    def sslSupported(self):
        return QSslSocket.supportsSsl()

    def classBegin(self):
        pass

    def componentComplete(self):
        for resource in self.m_resources:
            resource.setAccessManager(self.m_manager)
            resource.setServiceApi(self.m_serviceApi)

    def appendResource(self, r):
        self.m_resources.append(r)

    resources = ListProperty(AbstractResource, appendResource)
<RCC>
    <qresource prefix="/qt/qml/ColorPalette">
        <file>icons/close.svg</file>
        <file>icons/close_dark.svg</file>
        <file>icons/delete.svg</file>
        <file>icons/delete_dark.svg</file>
        <file>icons/dots.svg</file>
        <file>icons/edit.svg</file>
        <file>icons/edit_dark.svg</file>
        <file>icons/login.svg</file>
        <file>icons/login_dark.svg</file>
        <file>icons/logout.svg</file>
        <file>icons/logout_dark.svg</file>
        <file>icons/ok.svg</file>
        <file>icons/ok_dark.svg</file>
        <file>icons/plus.svg</file>
        <file>icons/plus_dark.svg</file>
        <file>icons/qt.png</file>
        <file>icons/testserver.png</file>
        <file>icons/update.svg</file>
        <file>icons/update_dark.svg</file>
        <file>icons/user.svg</file>
        <file>icons/userMask.svg</file>
        <file>icons/user_dark.svg</file>
    </qresource>
</RCC>
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

import QtExampleStyle

Popup {
    id: colorDeleter
    padding: 10
    modal: true
    focus: true
    anchors.centerIn: parent
    closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
    signal deleteClicked(int cid)

    property int colorId: -1

    property string colorName: ""

    function maybeDelete(data) {
        colorName = data.name
        colorId = data.id
        open()
    }


    ColumnLayout {
        anchors.fill: parent
        spacing: 10

        Text {
            color: UIStyle.titletextColor
            text: qsTr("Delete Color?")
            font.pixelSize: UIStyle.fontSizeL
            font.bold: true
        }

        Text {
            color: UIStyle.textColor
            text: qsTr("Are you sure, you want to delete color") + " \"" + colorDeleter.colorName + "\"?"
            font.pixelSize: UIStyle.fontSizeM
        }

        RowLayout {
            Layout.fillWidth: true
            spacing: 10

            Button {
                Layout.fillWidth: true
                text: qsTr("Cancel")
                onClicked: colorDeleter.close()
            }

            Button {
                Layout.fillWidth: true
                text: qsTr("Delete")

                buttonColor: UIStyle.colorRed
                textColor: UIStyle.textOnLightBackground

                onClicked: {
                    colorDeleter.deleteClicked(colorDeleter.colorId)
                    colorDeleter.close()
                }
            }
       }
    }
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs

import QtExampleStyle

Popup {
    id: colorEditor
    // Popup for adding or updating a color
    padding: 10
    modal: true
    focus: true
    anchors.centerIn: parent
    closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
    signal colorAdded(string name, string color, string pantone_value)
    signal colorUpdated(string name, string color, string pantone_value, int cid)

    property bool newColor: true
    property int colorId: -1
    property alias currentColor: colordialogButton.buttonColor

    function createNewColor() {
        newColor = true
        colorNameField.text = "cute green"
        colorRGBField.text = "#41cd52"
        colorPantoneField.text = "PMS 802C"
        currentColor = colorRGBField.text
        colorDialog.selectedColor = currentColor
        open()
    }

    function updateColor(data) {
        newColor = false
        colorNameField.text = data.name
        currentColor = data.color
        colorPantoneField.text = data.pantone_value
        colorId = data.id
        open()
    }

    ColorDialog {
        id: colorDialog
        title: qsTr("Choose a color")
        onAccepted: {
            colorEditor.currentColor = Qt.color(colorDialog.selectedColor)
            colorDialog.close()
        }
        onRejected: {
            colorDialog.close()
        }
    }

    ColumnLayout {
        anchors.fill: parent
        spacing: 10

        GridLayout {
            columns: 2
            rowSpacing: 10
            columnSpacing: 10

            Label {
                text: qsTr("Color Name")
            }
            TextField {
                id: colorNameField
                padding: 10
            }

            Label {
                text: qsTr("Pantone Value")
            }
            TextField {
                id: colorPantoneField
                padding: 10
            }

            Label {
                text: qsTr("Rgb Value")
            }

            TextField {
                id: colorRGBField
                text: colorEditor.currentColor.toString()
                readOnly: true
                padding: 10
            }
        }

        Button {
            id: colordialogButton
            Layout.fillWidth: true
            Layout.preferredHeight: 30
            text: qsTr("Change Color")
            textColor: isColorDark(buttonColor) ?
                           UIStyle.textOnDarkBackground :
                           UIStyle.textOnLightBackground

            onClicked: colorDialog.open()

            function isColorDark(color) {
                return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) < 0.5;
            }
        }

        RowLayout {
            Layout.fillWidth: true
            spacing: 10

            Button {
                text: qsTr("Cancel")
                onClicked: colorEditor.close()
                Layout.fillWidth: true
            }

            Button {
                Layout.fillWidth: true
                text: colorEditor.newColor ? qsTr("Add") : qsTr("Update")

                buttonColor: UIStyle.highlightColor
                buttonBorderColor: UIStyle.highlightBorderColor
                textColor: UIStyle.textColor

                onClicked: {
                    if (colorEditor.newColor) {
                        colorEditor.colorAdded(colorNameField.text,
                                               colorRGBField.text,
                                               colorPantoneField.text)
                    } else {
                        colorEditor.colorUpdated(colorNameField.text,
                                                 colorRGBField.text,
                                                 colorPantoneField.text,
                                                 colorEditor.colorId)
                    }
                    colorEditor.close()
                }
            }
        }
    }
}
// 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 QtQuick.Effects
import QtQuick.Shapes

import QtExampleStyle
import ColorPalette

Rectangle {
    id: root
    required property BasicLogin loginService
    required property PaginatedResource colors
    required property PaginatedResource colorViewUsers

    color: UIStyle.background

    ColorDialogEditor {
        id: colorPopup
        onColorAdded: (colorNameField, colorRGBField, colorPantoneField) => {
            root.colors.add({"name" : colorNameField,
                        "color" : colorRGBField,
                        "pantone_value" : colorPantoneField})
        }

        onColorUpdated: (colorNameField, colorRGBField, colorPantoneField, cid) => {
            root.colors.update({"name" : colorNameField,
                        "color" : colorRGBField,
                        "pantone_value" : colorPantoneField},
                        cid)
        }
    }

    ColorDialogDelete {
        id: colorDeletePopup
        onDeleteClicked: (cid) => {
            root.colors.remove(cid)
        }
    }

    ColumnLayout {
        // The main application layout
        anchors.fill :parent
        spacing: 0
        ToolBar {
            Layout.fillWidth: true
            Layout.minimumHeight: 35

            UserMenu {
                id: userMenu

                userMenuUsers: root.colorViewUsers
                userLoginService: root.loginService
            }

            RowLayout {
                anchors.fill: parent
                anchors.leftMargin: 5
                anchors.rightMargin: 5

                AbstractButton {
                    Layout.preferredWidth: 25
                    Layout.preferredHeight: 25
                    Layout.alignment: Qt.AlignVCenter

                    Rectangle {
                        anchors.fill: parent
                        radius: 4
                        color: UIStyle.buttonBackground
                        border.color: UIStyle.buttonOutline
                        border.width: 1
                    }

                    Image {
                        source: UIStyle.iconPath("plus")
                        fillMode: Image.PreserveAspectFit
                        anchors.fill: parent
                        sourceSize.width: width
                        sourceSize.height: height

                    }
                    visible: root.loginService.loggedIn
                    onClicked: colorPopup.createNewColor()
                }

                AbstractButton {
                    Layout.preferredWidth: 25
                    Layout.preferredHeight: 25
                    Layout.alignment: Qt.AlignVCenter

                    Rectangle {
                        anchors.fill: parent
                        radius: 4
                        color: UIStyle.buttonBackground
                        border.color: UIStyle.buttonOutline
                        border.width: 1
                    }

                    Image {
                        source: UIStyle.iconPath("update")
                        fillMode: Image.PreserveAspectFit
                        anchors.fill: parent
                        sourceSize.width: width
                        sourceSize.height: height
                    }

                    onClicked: {
                        root.colors.refreshCurrentPage()
                        root.colorViewUsers.refreshCurrentPage()
                    }
                }

                Item { Layout.fillWidth: true }

                Image {
                    Layout.preferredWidth: 25
                    Layout.preferredHeight: 25
                    Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft

                    source: "qrc:/qt/qml/ColorPalette/icons/qt.png"
                    fillMode: Image.PreserveAspectFit
                }

                Text {
                    Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft

                    text: qsTr("Color Palette")
                    font.pixelSize: UIStyle.fontSizeM
                    font.bold: true
                    color: UIStyle.titletextColor
                }

                Item { Layout.fillWidth: true }

                AbstractButton {
                    id: loginButton
                    Layout.preferredWidth: 25
                    Layout.preferredHeight: 25
                    Item {
                        id: userImageCliped
                        anchors.left: parent.left
                        anchors.verticalCenter: parent.verticalCenter
                        width: 25
                        height: 25

                        Image {
                            id: userImage
                            anchors.fill: parent
                            source: getCurrentUserImage()
                            visible: false

                            function getCurrentUserImage() {
                                if (!root.loginService.loggedIn)
                                    return UIStyle.iconPath("user");
                                let users = root.colorViewUsers
                                for (let i = 0; i < users.data.length; i++) {
                                    if (users.data[i].email === root.loginService.user)
                                        return users.data[i].avatar;
                                }
                            }
                        }

                        Image {
                            id: userMask
                            source: "qrc:/qt/qml/ColorPalette/icons/userMask.svg"
                            anchors.fill: userImage
                            anchors.margins: 4
                            visible: false
                        }

                        MultiEffect {
                            source: userImage
                            anchors.fill: userImage
                            maskSource: userMask
                            maskEnabled: true
                        }
                    }

                    onClicked: {
                        userMenu.open()
                        var pos = mapToGlobal(Qt.point(x, y))
                        pos = userMenu.parent.mapFromGlobal(pos)
                        userMenu.x = x - userMenu.width + 50
                        userMenu.y = y + 15
                    }

                    Shape {
                       id: bubble
                       x: -text.width - 25
                       y: -3
                       anchors.margins: 3

                       preferredRendererType: Shape.CurveRenderer

                        visible: !root.loginService.loggedIn

                       ShapePath {
                           strokeWidth: 0
                           fillColor: UIStyle.highlightColor
                           strokeColor: UIStyle.highlightBorderColor
                           startX: 5; startY: 0
                           PathLine { x: 5 + text.width + 6; y: 0 }
                           PathArc { x: 10 + text.width + 6; y: 5; radiusX: 5; radiusY: 5}
                           // arrow
                           PathLine { x: 10 + text.width + 6; y: 8 + text.height / 2 - 6 }
                           PathLine { x: 10 + text.width + 6 + 6; y: 8 + text.height / 2 }
                           PathLine { x: 10 + text.width + 6; y: 8 + text.height / 2 + 6}
                           PathLine { x: 10 + text.width + 6; y: 5 + text.height + 6 }
                           // end arrow
                           PathArc { x: 5 + text.width + 6; y: 10 + text.height + 6 ; radiusX: 5; radiusY: 5}
                           PathLine { x: 5; y: 10 + text.height + 6 }
                           PathArc { x: 0; y: 5 + text.height + 6 ; radiusX: 5; radiusY: 5}
                           PathLine { x: 0; y: 5 }
                           PathArc { x: 5; y: 0 ; radiusX: 5; radiusY: 5}
                       }
                       Text {
                           x: 8
                           y: 8
                           id: text
                           color: UIStyle.textColor
                           text: qsTr("Log in to edit")
                           font.bold: true
                           horizontalAlignment: Qt.AlignHCenter
                           verticalAlignment: Qt.AlignVCenter
                       }
                   }
                }

            }
        }



        //! [View and model]
        ListView {
            id: colorListView

            model: root.colors.data
        //! [View and model]
            footerPositioning: ListView.OverlayFooter
            spacing: 15
            clip: true

            Layout.fillHeight: true
            Layout.fillWidth: true

            header:  Rectangle {
                height: 32
                width: parent.width
                color: UIStyle.background

                RowLayout {
                    anchors.fill: parent

                    component HeaderText : Text {
                        Layout.alignment: Qt.AlignVCenter
                        horizontalAlignment: Qt.AlignHCenter

                        font.pixelSize: UIStyle.fontSizeS
                        color: UIStyle.titletextColor
                    }
                    HeaderText {
                        id: headerName
                        text: qsTr("Color Name")
                        Layout.fillWidth: true
                        Layout.horizontalStretchFactor: 30
                    }
                    HeaderText {
                        id: headerRgb
                        text: qsTr("Rgb Value")
                        Layout.fillWidth: true
                        Layout.horizontalStretchFactor: 25
                    }
                    HeaderText {
                        id: headerPantone
                        text: qsTr("Pantone Value")
                        Layout.fillWidth: true
                        Layout.horizontalStretchFactor: 25
                        font.pixelSize: UIStyle.fontSizeS
                    }
                    HeaderText {
                        id: headerAction
                        text: qsTr("Action")
                        Layout.fillWidth: true
                        Layout.horizontalStretchFactor: 20
                    }
                }
            }

            delegate: Item {
                id: colorInfo

                required property var modelData

                width: colorListView.width
                height: (colorListView.height - 55) / 6 - colorListView.spacing
                // Header: 35, Footer 20, 55 together
                RowLayout {
                    anchors.fill: parent
                    anchors.leftMargin: 5
                    anchors.rightMargin: 5

                    Rectangle {
                        id: colorSample
                        Layout.alignment: Qt.AlignVCenter
                        implicitWidth: 36
                        implicitHeight: 36
                        radius: 6
                        color: colorInfo.modelData.color
                    }

                    Text {
                        Layout.preferredWidth: colorInfo.width * 0.3 - colorSample.width
                        horizontalAlignment: Qt.AlignLeft
                        leftPadding: 5
                        text: colorInfo.modelData.name
                        color: UIStyle.textColor
                        font.pixelSize: UIStyle.fontSizeS
                    }

                    Text {
                        Layout.preferredWidth: colorInfo.width * 0.25
                        horizontalAlignment: Qt.AlignHCenter
                        text: colorInfo.modelData.color
                        color: UIStyle.textColor
                        font.pixelSize: UIStyle.fontSizeS
                    }

                    Text {
                        Layout.preferredWidth: colorInfo.width * 0.25
                        horizontalAlignment: Qt.AlignHCenter
                        text: colorInfo.modelData.pantone_value
                        color: UIStyle.textColor
                        font.pixelSize: UIStyle.fontSizeS
                    }

                    Item {
                        Layout.maximumHeight: 28
                        implicitHeight: buttonBox.implicitHeight
                        implicitWidth: buttonBox.implicitWidth

                        RowLayout {
                            id: buttonBox
                            anchors.fill: parent
                            ToolButton {
                                icon.source: UIStyle.iconPath("delete")
                                enabled: root.loginService.loggedIn
                                onClicked: colorDeletePopup.maybeDelete(colorInfo.modelData)
                            }
                            ToolButton {
                                icon.source: UIStyle.iconPath("edit")
                                enabled: root.loginService.loggedIn
                                onClicked: colorPopup.updateColor(colorInfo.modelData)
                            }
                        }
                    }
                }
            }

            footer: ToolBar {
                // Paginate buttons if more than one page
                visible: root.colors.pages > 1
                implicitWidth: parent.width

                RowLayout {
                    anchors.fill: parent

                    Item { Layout.fillWidth: true /* spacer */ }

                    Repeater {
                        model: root.colors.pages

                        ToolButton {
                            text: page
                            font.bold: root.colors.page === page

                            required property int index
                            readonly property int page: (index + 1)

                            onClicked: root.colors.page = page
                        }
                    }
                }
            }
        }
    }
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

pragma ComponentBehavior: Bound

import QtQuick

import ColorPalette

Window {
    id: window
    width: 500
    height: 400
    visible: true
    title: qsTr("Color Palette Client")

    enum DataView {
        UserView = 0,
        ColorView = 1
    }

    ServerSelection {
        id: serverview
        anchors.fill: parent
        onServerSelected: {colorview.visible = true; serverview.visible = false}
        colorResources: colors
        restPalette: paletteService
        colorUsers: users
    }

    ColorView {
        id: colorview
        anchors.fill: parent
        visible: false
        loginService: colorLogin
        colors: colors
        colorViewUsers: users
    }

    //! [RestService QML element]
    RestService {
        id: paletteService

        PaginatedResource {
            id: users
            path: "users"
        }

        PaginatedResource {
            id: colors
            path: "unknown"
        }

        BasicLogin {
            id: colorLogin
            loginPath: "login"
            logoutPath: "logout"
        }
    }
    //! [RestService QML element]

}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

import ColorPalette
import QtExampleStyle

pragma ComponentBehavior: Bound

Rectangle {
    id: root
    // A popup for selecting the server URL

    signal serverSelected()

    required property PaginatedResource colorResources
    required property PaginatedResource colorUsers
    required property RestService restPalette

    Connections {
        target: root.colorResources
        // Closes the URL selection popup once we have received data successfully
        function onDataUpdated() {
            fetchTester.stop()
            root.serverSelected()
        }
    }

    color: UIStyle.background

    ListModel {
        id: server
        ListElement {
            title: qsTr("Public REST API Test Server")
            url: "https://reqres.in/api"
            icon: "qrc:/qt/qml/ColorPalette/icons/testserver.png"
        }
        ListElement {
            title: qsTr("Qt-based REST API server")
            url: "http://127.0.0.1:49425/api"
            icon: "qrc:/qt/qml/ColorPalette/icons/qt.png"
        }
    }

    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 10

        Image {
            Layout.alignment: Qt.AlignHCenter
            source: "qrc:/qt/qml/ColorPalette/icons/qt.png"
            fillMode: Image.PreserveAspectFit
            Layout.preferredWidth: 40
        }

        Label {
            text: qsTr("Choose a server")
            Layout.alignment: Qt.AlignHCenter
            font.pixelSize: UIStyle.fontSizeXL
            color: UIStyle.titletextColor
        }

        component ServerListDelegate: Rectangle {
            id: serverListDelegate
            required property string title
            required property string url
            required property string icon
            required property int index

            radius: 10
            color: UIStyle.background1

            border.color: ListView.view.currentIndex === index ?
                              UIStyle.highlightColor :
                              UIStyle.buttonGrayOutline
            border.width: ListView.view.currentIndex === index ? 3 : 1

            implicitWidth: 210
            implicitHeight: 100

            Rectangle {
                id: img
                anchors.left: parent.left
                anchors.top: parent.top
                anchors.topMargin: 10
                anchors.leftMargin: 20

                width: 30
                height: 30
                radius: 15

                color: UIStyle.background
                border.color: parent.border.color
                border.width: 2

                Image {
                    anchors.centerIn: parent
                    source: serverListDelegate.icon
                    width: UIStyle.fontSizeM
                    height: UIStyle.fontSizeM
                    fillMode: Image.PreserveAspectFit
                    smooth: true
                }
            }

                Text {
                    text: parent.url

                    anchors.left: parent.left
                    anchors.top: img.bottom
                    anchors.topMargin: 10
                    anchors.leftMargin: 20
                    color: UIStyle.textColor
                    font.pixelSize: UIStyle.fontSizeS
                }
                Text {
                    text: parent.title

                    anchors.horizontalCenter: parent.horizontalCenter
                    anchors.bottom: parent.bottom
                    anchors.bottomMargin: 10
                    color: UIStyle.textColor
                    font.pixelSize: UIStyle.fontSizeS
                    font.bold: true
                }

                MouseArea {
                anchors.fill: parent
                onClicked: serverList.currentIndex = serverListDelegate.index;
            }
        }

        ListView {
            id: serverList
            Layout.alignment: Qt.AlignHCenter
            Layout.minimumWidth: 210 * server.count + 20
            Layout.minimumHeight: 100
            orientation: ListView.Horizontal

            model: server
            spacing: 20

            delegate: ServerListDelegate {}
        }

        Button {
            Layout.alignment: Qt.AlignHCenter
            text: root.restPalette.sslSupported ? qsTr("Connect (SSL)") : qsTr("Connect")

            buttonColor: UIStyle.highlightColor
            buttonBorderColor: UIStyle.highlightBorderColor
            textColor: UIStyle.textColor

            onClicked: {
                busyIndicatorPopup.title = (serverList.currentItem as ServerListDelegate).title
                busyIndicatorPopup.icon = (serverList.currentItem as ServerListDelegate).icon
                busyIndicatorPopup.open()

                fetchTester.test((serverList.currentItem  as ServerListDelegate).url)
            }
        }

        Timer {
            id: fetchTester
            interval: 2000

            function test(url) {
                root.restPalette.url = url
                root.colorResources.refreshCurrentPage()
                root.colorUsers.refreshCurrentPage()
                start()
            }
            onTriggered: busyIndicatorPopup.close()
        }
    }

    onVisibleChanged: {if (!visible) busyIndicatorPopup.close();}

    Popup {
        id: busyIndicatorPopup
        padding: 10
        modal: true
        focus: true
        anchors.centerIn: parent
        closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent

        property alias title: titleText.text
        property alias icon: titleImg.source

        ColumnLayout {
            id: fetchIndicator
            anchors.fill: parent

            RowLayout {
                Rectangle {
                    Layout.preferredWidth: 50
                    Layout.preferredHeight: 50
                    radius: 200
                    border.color: UIStyle.buttonOutline
                    border.width: 5

                    Image {
                        id: titleImg
                        anchors.centerIn: parent
                        width: 25
                        height: 25
                        fillMode: Image.PreserveAspectFit
                    }
                }

                Label {
                    id: titleText
                    text:""
                    font.pixelSize: UIStyle.fontSizeM
                    color: UIStyle.titletextColor
                }
            }

            RowLayout {
                Layout.fillWidth: false
                Layout.alignment: Qt.AlignHCenter
                BusyIndicator {
                    running: visible
                    Layout.fillWidth: true
                }

                Label {
                    text: qsTr("Testing URL")
                    font.pixelSize: UIStyle.fontSizeS
                    color: UIStyle.textColor
                }
            }

            Button {
                Layout.alignment: Qt.AlignHCenter
                text: qsTr("Cancel")
                onClicked: {
                    busyIndicatorPopup.close()
                }
            }

        }

    }
}
// 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 QtQuick.Effects

import QtExampleStyle
import ColorPalette

Popup {
    id: userMenu

    required property BasicLogin userLoginService
    required property PaginatedResource userMenuUsers

    width: 280
    height: 270

    background: Item {}

    Rectangle {
        radius: 8
        border.width: 0
        color: UIStyle.background

        anchors.fill: parent

        ListView {
            id: userListView
            anchors.fill: parent
            anchors.leftMargin: 10
            anchors.rightMargin: 5
            anchors.topMargin: 5
            anchors.bottomMargin: 2

            model: userMenu.userMenuUsers.data
            spacing: 7
            footerPositioning: ListView.PullBackFooter
            clip: true

            Layout.fillHeight: true
            Layout.fillWidth: true

            delegate: Item {
                id: userInfo

                height: 30
                width: userListView.width

                required property var modelData
                readonly property bool logged: (modelData.email === userMenu.userLoginService.user)

                Item {
                    id: userImageCliped
                    anchors.left: parent.left
                    anchors.verticalCenter: parent.verticalCenter
                    width: 30
                    height: 30

                    Image {
                        id: userImage
                        anchors.fill: parent
                        source: userInfo.modelData.avatar
                        visible: false
                    }

                    Image {
                        id: userMask
                        source: "qrc:/qt/qml/ColorPalette/icons/userMask.svg"
                        anchors.fill: userImage
                        anchors.margins: 4
                        visible: false
                    }

                    MultiEffect {
                        source: userImage
                        anchors.fill: userImage
                        maskSource: userMask
                        maskEnabled: true
                    }
                }

                Text {
                    id: userMailLabel
                    anchors.left: userImageCliped.right
                    anchors.verticalCenter: parent.verticalCenter
                    anchors.margins: 5
                    text: userInfo.modelData.email
                    color: UIStyle.textColor
                    font.bold: userInfo.logged
                }

                ToolButton {
                    anchors.right: parent.right
                    anchors.verticalCenter: parent.verticalCenter
                    anchors.margins: 5

                    icon.source: UIStyle.iconPath(userInfo.logged
                                 ? "logout" : "login")
                    enabled: userInfo.logged || !userMenu.userLoginService.loggedIn

                    onClicked: {
                        if (userInfo.logged) {
                            userMenu.userLoginService.logout()
                        } else {
                            //! [Login]
                            userMenu.userLoginService.login({"email" : userInfo.modelData.email,
                                                "password" : "apassword",
                                                "id" : userInfo.modelData.id})
                            //! [Login]
                            userMenu.close()
                        }
                    }
                }

            }
            footer: ToolBar {
                // Paginate buttons if more than one page
                visible: userMenu.userMenuUsers.pages > 1
                implicitWidth: parent.width

                RowLayout {
                    anchors.fill: parent

                    Item { Layout.fillWidth: true /* spacer */ }

                    Repeater {
                        model: userMenu.userMenuUsers.pages

                        ToolButton {
                            text: page
                            font.bold: userMenu.userMenuUsers.page === page

                            required property int index
                            readonly property int page: (index + 1)

                            onClicked: userMenu.userMenuUsers.page = page
                        }
                    }
                }
            }
        }
    }

    Rectangle {
        radius: 8
        border.color: UIStyle.buttonOutline
        border.width: 2
        color: "transparent"

        anchors.fill: parent
    }
}
module ColorPalette
Main 1.0 Main.qml
ColorDialogDelete 1.0 ColorDialogDelete.qml
ColorDialogEditor 1.0 ColorDialogEditor.qml
ColorView 1.0 ColorView.qml
ServerSelection 1.0 ServerSelection.qml
UserMenu 1.0 UserMenu.qml
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Controls.impl
import QtQuick.Templates as T

T.Button {
    id: control

    property alias buttonColor: rect.color
    property alias buttonBorderColor: rect.border.color
    property alias textColor: label.color

    implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
                            implicitContentWidth + leftPadding + rightPadding)
    implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
                             implicitContentHeight + topPadding + bottomPadding)

    leftPadding: 15
    rightPadding: 15
    topPadding: 10
    bottomPadding: 10

    background: Rectangle {
        id: rect
        radius: 8
        border.color: UIStyle.buttonOutline
        border.width: 1
        color: UIStyle.buttonBackground
    }

    icon.width: 24
    icon.height: 24
    icon.color: UIStyle.textColor

    contentItem: IconLabel {
        id: label
        spacing: control.spacing
        mirrored: control.mirrored
        display: control.display

        icon: control.icon
        text: control.text
        font.pixelSize: UIStyle.fontSizeS
        color: UIStyle.textColor
    }
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Templates as T

T.Popup {
    id: control

    implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
                            implicitContentWidth + leftPadding + rightPadding)
    implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
                             implicitContentHeight + topPadding + bottomPadding)

    leftPadding: 15
    rightPadding: 15
    topPadding: 10
    bottomPadding: 10

    background: Rectangle {
        id: bg
        radius: 8
        border.color: UIStyle.buttonOutline
        border.width: 2
        color: UIStyle.background
    }
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Templates as T

T.TextField {
    id: control
    placeholderText: ""

    implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding)
    implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
                             contentHeight + topPadding + bottomPadding)

    background: Rectangle {
        implicitWidth: 200
        radius: 5

        color: control.readOnly
               ? UIStyle.buttonGray
               : UIStyle.background

        border.color: UIStyle.buttonOutline
    }

    color: control.readOnly
              ? Qt.rgba(UIStyle.textColor.r,
                        UIStyle.textColor.g,
                        UIStyle.textColor.b,
                        0.6)
              : UIStyle.textColor
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

pragma Singleton

import QtQuick

QtObject {
    id: uiStyle

    property bool darkMode: (Application.styleHints.colorScheme === Qt.ColorScheme.Dark)

    // Font Sizes
    readonly property int fontSizeXXS: 8
    readonly property int fontSizeXS: 10
    readonly property int fontSizeS: 12
    readonly property int fontSizeM: 16
    readonly property int fontSizeL: 20
    readonly property int fontSizeXL: 24

    // Color Scheme
    readonly property color colorRed: "#E91E63"

    readonly property color buttonGray: darkMode ? "#808080" : "#f3f3f4"
    readonly property color buttonGrayPressed: darkMode ? "#707070" : "#cecfd5"
    readonly property color buttonGrayOutline: darkMode ? "#0D0D0D" : "#999999"

    readonly property color buttonBackground: darkMode ? "#262626" : "#CCCCCC"
    readonly property color buttonPressed: darkMode ? "#1E1E1E" : "#BEBEC4"
    readonly property color buttonOutline: darkMode ? "#0D0D0D" : "#999999"

    readonly property color background: darkMode ? "#262626" : "#E6E6E6"
    readonly property color background1: darkMode ? "#00414A" : "#ceded6"

    readonly property color textOnLightBackground: "#191919"
    readonly property color textOnDarkBackground: "#E6E6E6"

    readonly property color textColor: darkMode ? "#E6E6E6" : "#191919"
    readonly property color titletextColor: darkMode ? "#2CDE85" : "#191919"

    readonly property color highlightColor: darkMode ? "#33676E" : "#28C878"
    readonly property color highlightBorderColor: darkMode ? "#4F8C95" : "#1FA05E"

    function iconPath(baseImagePath) {
        if (darkMode)
            return `qrc:/qt/qml/ColorPalette/icons/${baseImagePath}_dark.svg`
        else
            return `qrc:/qt/qml/ColorPalette/icons/${baseImagePath}.svg`

    }
}
module QtExampleStyle
Button 1.0 Button.qml
Popup 1.0 Popup.qml
TextField 1.0 TextField.qml
singleton UIStyle 1.0 UIStyle.qml
<RCC>
    <qresource prefix="/qt/qml/ColorPalette">
        <file>icons/close.svg</file>
        <file>icons/close_dark.svg</file>
        <file>icons/delete.svg</file>
        <file>icons/delete_dark.svg</file>
        <file>icons/dots.svg</file>
        <file>icons/edit.svg</file>
        <file>icons/edit_dark.svg</file>
        <file>icons/login.svg</file>
        <file>icons/login_dark.svg</file>
        <file>icons/logout.svg</file>
        <file>icons/logout_dark.svg</file>
        <file>icons/ok.svg</file>
        <file>icons/ok_dark.svg</file>
        <file>icons/plus.svg</file>
        <file>icons/plus_dark.svg</file>
        <file>icons/qt.png</file>
        <file>icons/testserver.png</file>
        <file>icons/update.svg</file>
        <file>icons/update_dark.svg</file>
        <file>icons/user.svg</file>
        <file>icons/userMask.svg</file>
        <file>icons/user_dark.svg</file>
    </qresource>
</RCC>