Load configuration from disk
- Add FileIO backend - Fill login form with saved configuration
This commit is contained in:
@@ -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
159
src/FileIO.cpp
Normal 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
41
src/FileIO.hpp
Normal 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);
|
||||
};
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
95
src/Gui/Login/BrokerInput.qml
Normal file
95
src/Gui/Login/BrokerInput.qml
Normal 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)
|
||||
}
|
||||
}
|
||||
20
src/Gui/Login/LoadingInfo.qml
Normal file
20
src/Gui/Login/LoadingInfo.qml
Normal 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
39
src/Gui/Login/Login.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/Gui/Settings/Settings.qml
Normal file
39
src/Gui/Settings/Settings.qml
Normal 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
1
src/Gui/Settings/qmldir
Normal file
@@ -0,0 +1 @@
|
||||
singleton Settings 1.0 Settings.qml
|
||||
16
src/main.cpp
16
src/main.cpp
@@ -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());
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user