From 2083d39eda91000e072b41778ccf5a7b2e9538c1 Mon Sep 17 00:00:00 2001 From: Juny Date: Tue, 13 Feb 2024 19:26:15 -0300 Subject: [PATCH] Load configuration from disk - Add FileIO backend - Fill login form with saved configuration --- CMakeLists.txt | 1 + src/FileIO.cpp | 159 ++++++++++++++++++++++++++++++++++ src/FileIO.hpp | 41 +++++++++ src/Gui/App.qml | 25 ++++++ src/Gui/Login/BrokerInput.qml | 95 ++++++++++++++++++++ src/Gui/Login/LoadingInfo.qml | 20 +++++ src/Gui/Login/Login.qml | 39 +++++++++ src/Gui/Settings/Settings.qml | 39 +++++++++ src/Gui/Settings/qmldir | 1 + src/main.cpp | 16 ++++ src/resources.qrc | 6 +- 11 files changed, 441 insertions(+), 1 deletion(-) create mode 100644 src/FileIO.cpp create mode 100644 src/FileIO.hpp create mode 100644 src/Gui/Login/BrokerInput.qml create mode 100644 src/Gui/Login/LoadingInfo.qml create mode 100644 src/Gui/Login/Login.qml create mode 100644 src/Gui/Settings/Settings.qml create mode 100644 src/Gui/Settings/qmldir diff --git a/CMakeLists.txt b/CMakeLists.txt index c53172b..27d6f33 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,7 @@ find_package(Qt6 COMPONENTS Widgets Qml QuickControls2 REQUIRED) add_executable(${PROJECT} src/main.cpp + src/FileIO.cpp src/resources.qrc src/Gui/Style/resources.qrc) diff --git a/src/FileIO.cpp b/src/FileIO.cpp new file mode 100644 index 0000000..bac7e94 --- /dev/null +++ b/src/FileIO.cpp @@ -0,0 +1,159 @@ +/** + * @file FileIO.cpp + * @author Juny (marisa@fwmari.net) + * @brief File I/O backend implementation + * @version 0.1 + * @date 2023-12-27 + * + * @copyright Copyright (c) 2023-2024 + * + */ + +#include "FileIO.hpp" +#include +#include +#include + +static FileIO *m_fileIO = nullptr; + +QObject *FileIO::singletonProvider(QQmlEngine *engine, QJSEngine *) { + if (!m_fileIO) + m_fileIO = new FileIO((QObject *)engine); + + return (QObject *)m_fileIO; +} + +void FileIO::registerTypes(const char *uri) { + qmlRegisterSingletonType(uri, 0, 1, "FileIO", + FileIO::singletonProvider); +} + +QString FileIO::read(const QUrl &_path) { + QString path(_path.toLocalFile()); + + if (path.isEmpty()) { + emit this->error("Invalid path"); + return QString(); + } + + QFile file(path); + + if (!file.open(QIODevice::ReadOnly)) { + emit this->error("Could not open file"); + return QString(); + } + + QString line, content; + QTextStream stream((QIODevice *)&file); + + do { + line = stream.readLine(); + content += line; + } while (!line.isNull()); + + ((QFileDevice *)&file)->close(); + return content; +} + +bool FileIO::write(const QUrl &_path, const QString &data) { + QString path(_path.toLocalFile()); + QString tempPath(_path.toLocalFile() + ".temp"); + + if (path.isEmpty()) { + emit this->error("Invalid path: " + path); + return false; + } + + QFile file(tempPath); + + if (!file.open(QIODevice::WriteOnly)) { + emit this->error("Could not open file: " + tempPath); + return false; + } + + qint64 result = file.write(data.toUtf8()); + file.close(); + + if (result < 0) { + emit this->error("Failed to write data to file: " + tempPath); + return false; + } + + return this->move(tempPath, path); +} + +bool FileIO::move(const QUrl &_src, const QUrl &_dst) { + QString src(_src.toLocalFile()); + QString dst(_dst.toLocalFile()); + + return this->move(src, dst); +} + +bool FileIO::move(const QString &src, const QString &dst) { + if (src.isEmpty() || dst.isEmpty()) { + emit this->error("Invalid path: " + src); + return false; + } + + if (QFile::exists(dst)) + if (!QFile::remove(dst)) { + emit this->error("Could not remove: " + dst); + return false; + } + + if (!QFile::rename(src, dst)) { + emit this->error("Could not move to: " + dst); + return false; + } + + return true; +} + +bool FileIO::copy(const QUrl &_src, const QUrl &_dst) { + QString src(_src.toLocalFile()); + QString dst(_dst.toLocalFile()); + + return this->copy(src, dst); +} + +bool FileIO::copy(const QString &src, const QString &dst) { + if (src.isEmpty() || dst.isEmpty()) { + emit this->error("Invalid path: " + src); + return false; + } + + if (QFile::exists(dst)) + if (!QFile::remove(dst)) { + emit this->error("Could not remove: " + dst); + return false; + } + + if (!QFile::copy(src, dst)) { + emit this->error("Could not copy to: " + dst); + return false; + } + + return true; +} + +bool FileIO::mkpath(const QUrl &_path) { + QString path(_path.toLocalFile()); + + if (path.isEmpty()) { + emit this->error("Invalid path"); + return false; + } + + if (!QDir().mkpath(path)) { + emit this->error("Failed to make path"); + return false; + }; + + return true; +} + +bool FileIO::exists(const QUrl &_path) { + QString path(_path.toLocalFile()); + + return QDir(path).exists() || QFile(path).exists(); +} \ No newline at end of file diff --git a/src/FileIO.hpp b/src/FileIO.hpp new file mode 100644 index 0000000..150ece9 --- /dev/null +++ b/src/FileIO.hpp @@ -0,0 +1,41 @@ +/** + * @file FileIO.hpp + * @author Juny (marisa@fwmari.net) + * @brief File I/O backend + * @version 0.1 + * @date 2023-12-27 + * + * @copyright Copyright (c) 2023-2024 + * + */ + +#pragma once + +#include +#include +#include + +class FileIO : public QObject { + Q_OBJECT + +public: + explicit FileIO(QObject *parent = nullptr) : QObject(parent){}; + + static void registerTypes(const char *uri); + static QObject *singletonProvider(QQmlEngine *engine, QJSEngine *); + + Q_INVOKABLE QString read(const QUrl &path); + Q_INVOKABLE bool write(const QUrl &path, const QString &data); + Q_INVOKABLE bool move(const QUrl &src, const QUrl &dst); + Q_INVOKABLE bool copy(const QUrl &src, const QUrl &dst); + + Q_INVOKABLE bool mkpath(const QUrl &path); + Q_INVOKABLE bool exists(const QUrl &path); + +signals: + void error(const QString &msg); + +private: + bool move(const QString &src, const QString &dst); + bool copy(const QString &src, const QString &dst); +}; \ No newline at end of file diff --git a/src/Gui/App.qml b/src/Gui/App.qml index 2f7465a..d7e615d 100644 --- a/src/Gui/App.qml +++ b/src/Gui/App.qml @@ -2,6 +2,31 @@ pragma Singleton import QtQuick +import "Settings" + QtObject { id: app + + property string brokerURL: Settings._json.brokerURL || "" + property string brokerHost + property string brokerPort + + property string brokerUsername: Settings._json.brokerUsername || "" + property string brokerPassword: Settings._json.brokerPassword || "" + + onBrokerURLChanged: { + brokerHost = brokerURL.split(":")[0] + brokerPort = brokerURL.split(":")[1] || 1883 + } + + function saveBroker(url, user, pass) { + Settings._json.brokerURL = url + Settings._json.brokerUsername = user + Settings._json.brokerPassword = pass + Settings.save() + } + + function setup() { + Settings.load() + } } \ No newline at end of file diff --git a/src/Gui/Login/BrokerInput.qml b/src/Gui/Login/BrokerInput.qml new file mode 100644 index 0000000..bcb89ff --- /dev/null +++ b/src/Gui/Login/BrokerInput.qml @@ -0,0 +1,95 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +import "../PStyle" +import ".." + +ColumnLayout { + id: root + spacing: 8 + + signal tryConnect(url: string, username: string, password: string) + + Timer { + id: focusTimer + running: true + onTriggered: brokerURL.pTf.forceActiveFocus() + repeat: false + interval: 250 + } + + PText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("QuteSpectrometer") + font: PStyle.getFont(36) + } + + RowLayout { + PText { + Layout.alignment: Qt.AlignBottom + Layout.bottomMargin: 8 + text: "mqtt://" + } + + PInputField { + id: brokerURL + Layout.preferredWidth: 200 + label: qsTr("URL") + placeholderText: "example.com:1883" + tfText: App.brokerURL || "" + + onTfAccepted: _tryConnect() + + onTfChanged: (text) => { + let url; + + try { + url = new URL(`https://${text}`) + } catch (_) { + btn.enabled = false + return + } + + btn.enabled = true + } + } + } + + PGroupBox { + Layout.fillWidth: true + + title: qsTr("Authentication (optional)") + + ColumnLayout { + anchors.fill: parent + + PInputField { + id: brokerUsername + Layout.preferredWidth: 100 + label: qsTr("Username") + tfText: App.brokerUsername || "" + } + + PInputField { + id: brokerPassword + Layout.preferredWidth: 100 + label: qsTr("Password") + tfText: App.brokerPassword || "" + } + } + } + + PTextButton { + id: btn + text: qsTr("Connect") + Layout.fillWidth: true + // enabled: false + + onClicked: _tryConnect() + } + + function _tryConnect() { + tryConnect(brokerURL.tfText, brokerUsername.tfText, brokerPassword.tfText) + } +} \ No newline at end of file diff --git a/src/Gui/Login/LoadingInfo.qml b/src/Gui/Login/LoadingInfo.qml new file mode 100644 index 0000000..11e7488 --- /dev/null +++ b/src/Gui/Login/LoadingInfo.qml @@ -0,0 +1,20 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +import "../PStyle" + +ColumnLayout { + spacing: 24 + + PBusyIndicator { + Layout.alignment: Qt.AlignHCenter + Layout.preferredHeight: 48 + running: true + } + + PText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Trying to connect...") + } +} \ No newline at end of file diff --git a/src/Gui/Login/Login.qml b/src/Gui/Login/Login.qml new file mode 100644 index 0000000..b9568fe --- /dev/null +++ b/src/Gui/Login/Login.qml @@ -0,0 +1,39 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQml + +import "../PStyle" +import ".." + +PStackView { + id: login + initialItem: brokerInput + + property var instanceInfo + + Component { + id: brokerInput + + PBackground { + BrokerInput { + anchors.centerIn: parent + + onTryConnect: (url, user, pass) => { + App.saveBroker(url, user, pass) + login.push(loadInstance) + } + } + } + } + + Component { + id: loadInstance + + PBackground { + LoadingInfo { + anchors.centerIn: parent + } + } + } +} \ No newline at end of file diff --git a/src/Gui/Settings/Settings.qml b/src/Gui/Settings/Settings.qml new file mode 100644 index 0000000..200171d --- /dev/null +++ b/src/Gui/Settings/Settings.qml @@ -0,0 +1,39 @@ +pragma Singleton + +import QtQuick +import QtCore + +import QSpec + +QtObject { + property string configDir: StandardPaths.writableLocation(StandardPaths.AppConfigLocation) + property string dataDir: StandardPaths.writableLocation(StandardPaths.AppDataLocation) + + property var _json + + function load() { + if (FileIO.exists(configDir) === false) + FileIO.mkpath(configDir) + + if (FileIO.exists(dataDir) === false) + FileIO.mkpath(dataDir) + + try { + _json = JSON.parse(FileIO.read(`${configDir}/config.json`)) + } catch (_) { + print(`Failed to read ${configDir}/config.json`) + return false + } + + if (_json) + return true + + print(`Empty or invalid configuration at ${configDir}/config.json`) + return false + } + + function save() { + print(`Saving settings to ${configDir}/config.json`) + FileIO.write(`${configDir}/config.json`, JSON.stringify(_json)) + } +} \ No newline at end of file diff --git a/src/Gui/Settings/qmldir b/src/Gui/Settings/qmldir new file mode 100644 index 0000000..446d3fd --- /dev/null +++ b/src/Gui/Settings/qmldir @@ -0,0 +1 @@ +singleton Settings 1.0 Settings.qml \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index be83a8c..fcc6cfd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,14 @@ +/** + * @file main.cpp + * @author Juny + * @brief Main program entry + * @version 0.1 + * @date 2024-02-13 + * + * @copyright Copyright (c) 2024 + * + */ + #include #include @@ -8,6 +19,8 @@ #include #include +#include "FileIO.hpp" + int main(int argc, char *argv[]) { QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGLRhi); @@ -21,6 +34,9 @@ int main(int argc, char *argv[]) { QQmlEngine *engine = new QQmlEngine(); QObject::connect(engine, &QQmlEngine::quit, &QApplication::quit); + + FileIO::registerTypes("QSpec"); + engine->addImportPath("qrc:/"); QQmlContext *ctx = new QQmlContext(engine->rootContext()); diff --git a/src/resources.qrc b/src/resources.qrc index ca7e6ec..1e8eca0 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -6,7 +6,11 @@ Gui/Login/Login.qml - Gui/Login/InstanceInput.qml + Gui/Login/BrokerInput.qml Gui/Login/LoadingInfo.qml + + + Gui/Settings/Settings.qml + Gui/Settings/qmldir \ No newline at end of file