diff --git a/API/CMakeLists.txt b/API/CMakeLists.txt new file mode 100644 index 0000000..1198a60 --- /dev/null +++ b/API/CMakeLists.txt @@ -0,0 +1,9 @@ +set(HEADERS + api.h +) + +set(SOURCES + api.cpp +) + +target_sources(megpie PRIVATE ${SOURCES}) diff --git a/API/api.cpp b/API/api.cpp new file mode 100644 index 0000000..3e57766 --- /dev/null +++ b/API/api.cpp @@ -0,0 +1,93 @@ +#include "api.h" +#include +#include +#include + +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 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() || + 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)}); + } +} + diff --git a/API/api.h b/API/api.h new file mode 100644 index 0000000..492d37c --- /dev/null +++ b/API/api.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +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; +}; diff --git a/CMakeLists.txt b/CMakeLists.txt index d1c3393..7be4480 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,7 @@ else() endif() add_subdirectory(qml) +add_subdirectory(API) target_link_libraries(megpie PRIVATE Qt6::Core diff --git a/main.cpp b/main.cpp index 4ed2fc8..19554f3 100644 --- a/main.cpp +++ b/main.cpp @@ -4,28 +4,6 @@ #include "root.h" int main(int argc, char *argv[]) { - Root app(argc, argv); - - 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; + Root app(u"qrc:qml/main.qml"_qs, argc, argv); + return app.exec(); } diff --git a/main_web.cpp b/main_web.cpp index a37688c..ba01824 100644 --- a/main_web.cpp +++ b/main_web.cpp @@ -4,22 +4,13 @@ #include "root.h" int main(int argc, char *argv[]) { - Root* app = new Root(argc, argv); + Root* app; - std::cout << "Starting" << std::endl; - QQmlApplicationEngine* engine = new QQmlApplicationEngine(); - 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; + try { + app = new Root(u"qrc:qml/main.qml"_qs, argc, argv); + } catch (const std::exception& exception) { + std::cerr << "Couldn't start megpie: " << exception.what() << std::endl; + } return 0; } diff --git a/qml/CMakeLists.txt b/qml/CMakeLists.txt index 450ac5d..e77993f 100644 --- a/qml/CMakeLists.txt +++ b/qml/CMakeLists.txt @@ -6,6 +6,8 @@ qt_add_qml_module(megpieQml NO_PLUGIN QML_FILES main.qml + ServerPick.qml + Welcome.qml ) target_link_libraries(megpie PRIVATE megpieQml) diff --git a/qml/ServerPick.qml b/qml/ServerPick.qml new file mode 100644 index 0000000..4146ef6 --- /dev/null +++ b/qml/ServerPick.qml @@ -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() + } + }); + } + } + } +} diff --git a/qml/Welcome.qml b/qml/Welcome.qml new file mode 100644 index 0000000..99091b5 --- /dev/null +++ b/qml/Welcome.qml @@ -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) + } + } + } + } +} diff --git a/qml/main.qml b/qml/main.qml index 080e1f2..43e5ea9 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -3,57 +3,43 @@ import QtQuick.Window import QtQuick.Controls import QtQuick.Layouts -Window { + + +ApplicationWindow { property int counter: 0 id: window width: 640 height: 480 visible: true - title: qsTr("Hello World") + title: "Megpie" - Rectangle { - id: page + header: Label { + text: stack.currentItem.title + horizontalAlignment: Text.AlignHCenter + } + + StackView { + id: stack + initialItem: welcome anchors.fill: parent - anchors.centerIn: parent - color: increment.down ? "blue" : decrement.down ? "red" : "lightgrey" + } - Behavior on color { - ColorAnimation { - duration: 100 - target: page - easing.type: Easing.InOutQuad - } + Welcome { + id: welcome + onPickServer: function (address) { + pick.address = address; + stack.push(pick) } + } - GridLayout { - anchors.centerIn: parent - columns: 2 - - Text { - Layout.columnSpan: 2 - Layout.alignment: Qt.AlignHCenter - 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 - } + ServerPick { + visible: false + id: pick + onBack: stack.pop() + onSuccess: function (address) { + welcome.serverAddress = address; + stack.pop(); } } } diff --git a/root.cpp b/root.cpp index 4e173f2..3058f0b 100644 --- a/root.cpp +++ b/root.cpp @@ -1,9 +1,24 @@ #include "root.h" -Root::Root(int& argc, char* argv[]) : - QGuiApplication(argc, argv) +Root::Root(const QUrl& root, int& argc, char* 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() { @@ -25,3 +40,8 @@ bool Root::notify(QObject* receiver, QEvent* e) { exit(1); return false; } + +void Root::onObjectCreated(QObject* obj, const QUrl& objUrl) { + if (!obj && objUrl == root) + exit(-2); +} diff --git a/root.h b/root.h index 27eafcc..a5ce5c0 100644 --- a/root.h +++ b/root.h @@ -1,13 +1,28 @@ #pragma once #include +#include +#include #include +#include +#include + +#include "API/api.h" class Root : public QGuiApplication { Q_OBJECT public: - Root(int& argc, char* argv[]); + Root(const QUrl& root, int& argc, char* argv[]); ~Root(); 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; };