Load configuration from disk

- Add FileIO backend
- Fill login form with saved configuration
This commit is contained in:
2024-02-13 19:26:15 -03:00
parent bd93be6424
commit 2083d39eda
11 changed files with 441 additions and 1 deletions

View File

@@ -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)

159
src/FileIO.cpp Normal file
View File

@@ -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 <QDir>
#include <QFile>
#include <QTextStream>
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<FileIO>(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();
}

41
src/FileIO.hpp Normal file
View File

@@ -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 <QJSEngine>
#include <QObject>
#include <QQmlEngine>
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);
};

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View File

@@ -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...")
}
}

39
src/Gui/Login/Login.qml Normal file
View File

@@ -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
}
}
}
}

View File

@@ -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))
}
}

1
src/Gui/Settings/qmldir Normal file
View File

@@ -0,0 +1 @@
singleton Settings 1.0 Settings.qml

View File

@@ -1,3 +1,14 @@
/**
* @file main.cpp
* @author Juny <marisa@fwmari.net>
* @brief Main program entry
* @version 0.1
* @date 2024-02-13
*
* @copyright Copyright (c) 2024
*
*/
#include <iostream>
#include <QApplication>
@@ -8,6 +19,8 @@
#include <QQuickStyle>
#include <QQuickWindow>
#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());

View File

@@ -6,7 +6,11 @@
<!-- login -->
<file alias="Login/Login.qml">Gui/Login/Login.qml</file>
<file alias="Login/InstanceInput.qml">Gui/Login/InstanceInput.qml</file>
<file alias="Login/BrokerInput.qml">Gui/Login/BrokerInput.qml</file>
<file alias="Login/LoadingInfo.qml">Gui/Login/LoadingInfo.qml</file>
<!-- settings -->
<file alias="Settings/Settings.qml">Gui/Settings/Settings.qml</file>
<file alias="Settings/qmldir">Gui/Settings/qmldir</file>
</qresource>
</RCC>