First handshake with pica

This commit is contained in:
Blue 2023-11-24 20:48:01 -03:00
parent 4da9a275a9
commit f547170728
Signed by: blue
GPG Key ID: 9B203B252A63EE38
12 changed files with 391 additions and 82 deletions

9
API/CMakeLists.txt Normal file
View File

@ -0,0 +1,9 @@
set(HEADERS
api.h
)
set(SOURCES
api.cpp
)
target_sources(megpie PRIVATE ${SOURCES})

93
API/api.cpp Normal file
View File

@ -0,0 +1,93 @@
#include "api.h"
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
constexpr const char* json = "application/json";
struct NetworkReplyDeleter {
void operator () (QNetworkReply* reply) {
reply->deleteLater();
}
};
API::API(const QUrl& address, QObject* parent):
QObject(parent),
address(address),
network()
{
}
void API::test(const QString& path, const QJSValue& finished) {
qDebug() << "Testing" << path;
QNetworkRequest request(path + "/info");
request.setHeader(QNetworkRequest::ContentTypeHeader, json);
QNetworkReply* reply = network.get(request);
connect(reply, &QNetworkReply::finished,
std::bind(&API::onTestSuccess, this, reply, finished)
);
}
void API::onTestSuccess(QNetworkReply* reply, const QJSValue& finished) const {
std::unique_ptr<QNetworkReply, NetworkReplyDeleter> rpl(reply);
QNetworkReply::NetworkError error = reply->error();
if (error != QNetworkReply::NoError) {
QString err = reply->errorString();
qDebug() << "Test for" << reply->url() << "failed:" << err;
callCallback(finished, err);
return;
}
QVariant contentType = reply->header(QNetworkRequest::ContentTypeHeader);
if (!
contentType.isValid() ||
!contentType.canConvert<QString>() ||
contentType.toString() != json
) {
QString err("wrong response content type");
qDebug() << "Test for" << reply->url() << "failed:" << err;
callCallback(finished, err);
return;
}
QByteArray data = reply->readAll();
QJsonDocument document = QJsonDocument::fromJson(data);
QJsonObject rootObj = document.object();
QJsonValue type = rootObj.value("type");
QJsonValue version = rootObj.value("version");
if (!type.isString() || !version.isString()) {
QString err("malformed json");
qDebug() << "Test for" << reply->url() << "failed:" << err;
callCallback(finished, err);
return;
}
if (type.toString() != "Pica") {
QString err("server of this type (" + type.toString() + ") is not supported");
qDebug() << "Test for" << reply->url() << "failed:" << err;
callCallback(finished, err);
return;
}
if (version.toString() != "0.0.1") {
QString err("server of this version (" + version.toString() + ") is not supported");
qDebug() << "Test for" << reply->url() << "failed:" << err;
callCallback(finished, err);
return;
}
callCallback(finished, QString(), {QJSValue(true)});
}
void API::callCallback(const QJSValue& callback, const QString& error, const QJSValueList& arguments) const {
if (callback.isCallable()) {
if (error.isEmpty())
callback.call(QJSValueList({QJSValue(QJSValue::NullValue)}) + arguments);
else
callback.call({QJSValue(error)});
}
}

28
API/api.h Normal file
View File

@ -0,0 +1,28 @@
#pragma once
#include <memory>
#include <QObject>
#include <QString>
#include <QUrl>
#include <QJSValue>
#include <QNetworkAccessManager>
#include <QNetworkReply>
class API : public QObject {
Q_OBJECT
public:
explicit API(const QUrl& path = QString(), QObject* parent = nullptr);
Q_INVOKABLE void test(const QString& path, const QJSValue& finished = QJSValue());
private slots:
void onTestSuccess(QNetworkReply* reply, const QJSValue& finished) const;
private:
void callCallback(const QJSValue& callback, const QString& error = QString(), const QJSValueList& arguments = QJSValueList()) const;
private:
QUrl address;
QNetworkAccessManager network;
};

View File

@ -54,6 +54,7 @@ else()
endif() endif()
add_subdirectory(qml) add_subdirectory(qml)
add_subdirectory(API)
target_link_libraries(megpie PRIVATE target_link_libraries(megpie PRIVATE
Qt6::Core Qt6::Core

View File

@ -4,28 +4,6 @@
#include "root.h" #include "root.h"
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
Root app(argc, argv); Root app(u"qrc:qml/main.qml"_qs, argc, argv);
return app.exec();
std::cout << "Starting" << std::endl;
QQmlApplicationEngine engine;
const QUrl url(u"qrc:qml/main.qml"_qs);
QObject::connect(
&engine, &QQmlApplicationEngine::objectCreated, &app,
[&url](QObject* obj, const QUrl& objUrl) {
if (!obj && url == objUrl)
QCoreApplication::exit(-2);
},
Qt::QueuedConnection);
engine.load(url);
if (engine.rootObjects().isEmpty())
return -1;
int result;
try {
result = app.exec();
} catch (...) {
}
return result;
} }

View File

@ -4,22 +4,13 @@
#include "root.h" #include "root.h"
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
Root* app = new Root(argc, argv); Root* app;
std::cout << "Starting" << std::endl; try {
QQmlApplicationEngine* engine = new QQmlApplicationEngine(); app = new Root(u"qrc:qml/main.qml"_qs, argc, argv);
const QUrl url(u"qrc:qml/main.qml"_qs); } catch (const std::exception& exception) {
QObject::connect( std::cerr << "Couldn't start megpie: " << exception.what() << std::endl;
engine, &QQmlApplicationEngine::objectCreated, app, }
[url](QObject* obj, const QUrl& objUrl) {
if (!obj && url == objUrl)
QCoreApplication::exit(-2);
},
Qt::QueuedConnection);
engine->load(url);
if (engine->rootObjects().isEmpty())
return -1;
return 0; return 0;
} }

View File

@ -6,6 +6,8 @@ qt_add_qml_module(megpieQml
NO_PLUGIN NO_PLUGIN
QML_FILES QML_FILES
main.qml main.qml
ServerPick.qml
Welcome.qml
) )
target_link_libraries(megpie PRIVATE megpieQml) target_link_libraries(megpie PRIVATE megpieQml)

138
qml/ServerPick.qml Normal file
View File

@ -0,0 +1,138 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Page {
property string address
property bool valid
signal back()
signal success(address: string)
title: qsTr("Chosing a server")
Column {
anchors.centerIn: parent
spacing: 10
Label {
anchors.horizontalCenter: parent.horizontalCenter
id: label
text: qsTr("Type server address in the field below")
}
TextField {
width: parent.width
anchors.horizontalCenter: parent.horizontalCenter
id: input
placeholderText: "https://example.org"
text: address
onAccepted: modal.check()
validator: RegularExpressionValidator {
regularExpression: /^(?:http|https)?:\/\/(?:\w*\.)*(?:\w)+(?:\:\d+)?(?:(?:\/\w+)\/?)*$/
}
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 10
Button {
text: qsTr("Cancel")
onClicked: back()
}
Button {
text: qsTr("Confirm")
enabled: input.acceptableInput
onClicked: modal.check()
}
}
Popup {
property bool inProgress: false
id: modal
anchors.centerIn: parent
modal: true
focus: true
width: column.width + 60
height: column.height + 60
closePolicy: Popup.CloseOnEscape
onClosed: function () {
modal.inProgress = false;
if (valid)
success(address);
}
Column {
id: column
anchors.centerIn: parent
spacing: 10
Label {
id: status
width: 300
wrapMode: Label.WordWrap
horizontalAlignment: Label.AlignHCenter
}
BusyIndicator {
id: indicator
anchors.horizontalCenter: parent.horizontalCenter
visible: modal.inProgress
running: modal.inProgress
}
Button {
id: button
text: qsTr("Close")
anchors.horizontalCenter: parent.horizontalCenter
visible: !modal.inProgress
onClicked: modal.close()
focus: true
Keys.onReturnPressed: {
if (!modal.inProgress)
modal.close()
}
}
}
enter: Transition {
NumberAnimation {
property: "opacity"
from: 0.0
to: 1.0
duration: 200
}
}
exit: Transition {
NumberAnimation {
property: "opacity"
from: 1.0
to: 0.0
duration: 200
}
}
function check () {
valid = false;
modal.inProgress = true;
status.text = qsTr("Checking") + " " + address + "...";
modal.open()
API.test(input.text, function (err, success) {
if (!modal.inProgress)
return;
modal.inProgress = false;
if (err)
status.text = err;
else
status.text = qsTr("Success");
valid = !!success;
if (valid) {
address = input.text;
modal.close()
}
});
}
}
}
}

48
qml/Welcome.qml Normal file
View File

@ -0,0 +1,48 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Page {
property string serverAddress
signal pickServer(address: string)
title: qsTr("Welcome")
Column {
anchors.centerIn: parent
spacing: 10
Label {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Welcome to Megpie!")
font {
pixelSize: 22
bold: true
}
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 5
Label {
horizontalAlignment: Label.AlignRight
text: qsTr("Current server:")
}
Label {
horizontalAlignment: Label.AlignLeft
text: serverAddress || qsTr("choose")
font {
italic: true
underline: true
}
MouseArea {
anchors.fill: parent
onClicked: pickServer(serverAddress)
}
}
}
}
}

View File

@ -3,57 +3,43 @@ import QtQuick.Window
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
Window {
ApplicationWindow {
property int counter: 0 property int counter: 0
id: window id: window
width: 640 width: 640
height: 480 height: 480
visible: true visible: true
title: qsTr("Hello World") title: "Megpie"
Rectangle { header: Label {
id: page text: stack.currentItem.title
horizontalAlignment: Text.AlignHCenter
}
StackView {
id: stack
initialItem: welcome
anchors.fill: parent anchors.fill: parent
anchors.centerIn: parent }
color: increment.down ? "blue" : decrement.down ? "red" : "lightgrey"
Behavior on color { Welcome {
ColorAnimation { id: welcome
duration: 100 onPickServer: function (address) {
target: page pick.address = address;
easing.type: Easing.InOutQuad stack.push(pick)
} }
} }
GridLayout { ServerPick {
anchors.centerIn: parent visible: false
columns: 2 id: pick
onBack: stack.pop()
Text { onSuccess: function (address) {
Layout.columnSpan: 2 welcome.serverAddress = address;
Layout.alignment: Qt.AlignHCenter stack.pop();
text: qsTr("Hello World")
font.pointSize: 24; font.bold: true
}
Button {
id: increment
text: "Increment"
onClicked: window.counter++
}
Button {
id: decrement
text: "Decrement"
onClicked: window.counter--
}
Text {
Layout.columnSpan: 2
Layout.alignment: Qt.AlignHCenter
text: "The value is " + window.counter
}
} }
} }
} }

View File

@ -1,9 +1,24 @@
#include "root.h" #include "root.h"
Root::Root(int& argc, char* argv[]) : Root::Root(const QUrl& root, int& argc, char* argv[]) :
QGuiApplication(argc, argv) QGuiApplication(argc, argv),
root(root),
engine(),
context(engine.rootContext()),
api()
{ {
std::cout << "Starting megpie..." << std::endl;
connect(&engine, &QQmlApplicationEngine::objectCreated,
this, &Root::onObjectCreated,
Qt::QueuedConnection
);
engine.load(root);
if (engine.rootObjects().isEmpty())
throw std::runtime_error("Couldn't looad root qml object");
context->setContextProperty("API", &api);
} }
Root::~Root() { Root::~Root() {
@ -25,3 +40,8 @@ bool Root::notify(QObject* receiver, QEvent* e) {
exit(1); exit(1);
return false; return false;
} }
void Root::onObjectCreated(QObject* obj, const QUrl& objUrl) {
if (!obj && objUrl == root)
exit(-2);
}

17
root.h
View File

@ -1,13 +1,28 @@
#pragma once #pragma once
#include <iostream> #include <iostream>
#include <stdexcept>
#include <QString>
#include <QGuiApplication> #include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "API/api.h"
class Root : public QGuiApplication { class Root : public QGuiApplication {
Q_OBJECT Q_OBJECT
public: public:
Root(int& argc, char* argv[]); Root(const QUrl& root, int& argc, char* argv[]);
~Root(); ~Root();
bool notify(QObject* receiver, QEvent* e) override; bool notify(QObject* receiver, QEvent* e) override;
private slots:
void onObjectCreated(QObject* obj, const QUrl& objUrl);
private:
QUrl root;
QQmlApplicationEngine engine;
QQmlContext* context;
API api;
}; };