mirror of
https://github.com/logos-storage/logos-storage-app-skeleton.git
synced 2026-06-13 20:09:28 +00:00
Merge pull request #9 from logos-co/chore/better-config
feat: provide a better configuration management
This commit is contained in:
commit
2855adeb86
@ -72,11 +72,9 @@ After onboarding, settings are saved to a file whose location depends on the OS:
|
||||
| macOS | `~/Library/Preferences/Logos.LogosStorage.plist` |
|
||||
| Windows | `HKCU\Software\Logos\LogosStorage` (Registry) |
|
||||
|
||||
## Configuration Management
|
||||
The settings are saved to the preferences file to preserve the onboarding defaults, but the active configuration is stored in `${HOME}/.logos_storage/config.json`. You can tweak the values there directly. Note that running the onboarding again will override any onboarding-related values.
|
||||
|
||||
To restart the onboarding process, simply delete the configuration file and relaunch the application.
|
||||
|
||||
You can override the configuration by placing a `config.json` file in the app's startup folder. This file takes precedence over any existing configuration.
|
||||
To restart the onboarding process, simply delete the prefences file and relaunch the application.
|
||||
|
||||
The application also provides a JSON editor in the debug panel for runtime configuration tweaks. To apply changes, restart the Storage Module.
|
||||
|
||||
|
||||
@ -30,8 +30,10 @@ int main(int argc, char* argv[]) {
|
||||
QApplication app(argc, argv);
|
||||
|
||||
// Set application properties for Qt Settings
|
||||
// QCoreApplication::setOrganizationName("Logos");
|
||||
// QCoreApplication::setOrganizationDomain("logos.co");
|
||||
// QCoreApplication::setApplicationName("LogosStorage");
|
||||
QCoreApplication::setOrganizationName("Logos");
|
||||
QCoreApplication::setOrganizationDomain("logos.co");
|
||||
QCoreApplication::setApplicationName("LogosStorage");
|
||||
|
||||
// Set the plugins directory
|
||||
|
||||
@ -201,6 +201,8 @@ LogosResult StorageBackend::init(const QString& configJson = "{}") {
|
||||
debug("new config is: " + m_configJson);
|
||||
}
|
||||
|
||||
emit initCompleted();
|
||||
|
||||
return {true, ""};
|
||||
}
|
||||
|
||||
@ -827,9 +829,20 @@ void StorageBackend::reloadIfChanged(const QString& configJson) {
|
||||
emit configJsonChanged();
|
||||
}
|
||||
|
||||
bool StorageBackend::validateDataDir(const QString& path) {
|
||||
QFileInfo info(path);
|
||||
return info.exists() && info.isDir() && info.isReadable() && info.isWritable();
|
||||
void StorageBackend::saveUserConfig(const QString& configJson) {
|
||||
qDebug() << "StorageBackend::saveUserConfig";
|
||||
|
||||
QString configPath = getUserConfigPath();
|
||||
QString folderPath = QFileInfo(configPath).absolutePath();
|
||||
QDir().mkpath(folderPath);
|
||||
QFile file(configPath);
|
||||
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
file.write(configJson.toUtf8());
|
||||
file.close();
|
||||
debug("Config saved to " + configPath);
|
||||
} else {
|
||||
debug("Failed to save config to " + configPath);
|
||||
}
|
||||
}
|
||||
|
||||
QString StorageBackend::buildConfig(const QString& dataDir, int discPort, int tcpPort) {
|
||||
@ -871,6 +884,17 @@ QString StorageBackend::buildConfigFromFile(const QString& path) {
|
||||
|
||||
void StorageBackend::status(StorageStatus status) { m_status = status; }
|
||||
|
||||
QString StorageBackend::getUserConfigPath() { return QDir::homePath() + "/.logos_storage/config.json"; }
|
||||
|
||||
QString StorageBackend::getUserConfig() {
|
||||
QFile file(getUserConfigPath());
|
||||
if (file.exists() && file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return QString::fromUtf8(file.readAll());
|
||||
}
|
||||
|
||||
return "{}";
|
||||
}
|
||||
|
||||
QString StorageBackend::defaultDataDir() {
|
||||
QString home = QDir::homePath();
|
||||
#ifdef Q_OS_WIN
|
||||
|
||||
@ -54,6 +54,8 @@ class StorageBackend : public QObject {
|
||||
qint64 quotaReservedBytes() const;
|
||||
|
||||
Q_INVOKABLE static QString defaultDataDir();
|
||||
static QString getUserConfig();
|
||||
static QString getUserConfigPath();
|
||||
|
||||
explicit StorageBackend(LogosAPI* logosAPI = nullptr, QObject* parent = nullptr);
|
||||
~StorageBackend();
|
||||
@ -78,13 +80,13 @@ class StorageBackend : public QObject {
|
||||
void downloadManifest(const QString& cid);
|
||||
void downloadManifests();
|
||||
void space();
|
||||
bool validateDataDir(const QString& path);
|
||||
LogosResult init(const QString& configJson);
|
||||
void updateLogLevel(const QString& logLevel);
|
||||
void reloadIfChanged(const QString& configJson);
|
||||
void status(StorageStatus status);
|
||||
QString buildConfig(const QString& dataDir, int discPort, int tcpPort);
|
||||
QString buildConfigFromFile(const QString& path);
|
||||
void saveUserConfig(const QString& configJson);
|
||||
|
||||
signals:
|
||||
void startCompleted();
|
||||
@ -98,6 +100,7 @@ class StorageBackend : public QObject {
|
||||
void uploadStatusChanged();
|
||||
void manifestsChanged();
|
||||
void quotaChanged();
|
||||
void initCompleted();
|
||||
|
||||
private slots:
|
||||
|
||||
|
||||
@ -14,7 +14,6 @@ QWidget* StorageUIPlugin::createWidget(LogosAPI* logosAPI) {
|
||||
qDebug() << "StorageUIPlugin::createWidget called";
|
||||
|
||||
QCoreApplication::setOrganizationName("Logos");
|
||||
QCoreApplication::setOrganizationDomain("logos.co");
|
||||
QCoreApplication::setApplicationName("LogosStorage");
|
||||
|
||||
QQuickWidget* quickWidget = new QQuickWidget();
|
||||
@ -27,29 +26,23 @@ QWidget* StorageUIPlugin::createWidget(LogosAPI* logosAPI) {
|
||||
|
||||
qDebug() << "StorageUIPlugin: Loading settings...";
|
||||
|
||||
QSettings settings("Logos", "LogosStorage");
|
||||
int discoveryPort = settings.value("Storage/discoveryPort", 0).toInt();
|
||||
int tcpPort = settings.value("Storage/tcpPort", 0).toInt();
|
||||
QString dataDir = settings.value("Storage/dataDir", "").toString();
|
||||
bool onboardingCompleted = settings.value("Storage/onboardingCompleted", false).toBool();
|
||||
// Default constructor uses QCoreApplication org/domain/app — same path as QML QtCore.Settings
|
||||
// QSettings settings;
|
||||
// int discoveryPort = settings.value("Storage/discoveryPort", 8090).toInt();
|
||||
// int tcpPort = settings.value("Storage/tcpPort", 0).toInt();
|
||||
// QString dataDir = settings.value("Storage/dataDir", "").toString();
|
||||
// bool onboardingCompleted = settings.value("Storage/onboardingCompleted", false).toBool();
|
||||
|
||||
qDebug() << "StorageUIPlugin: Settings Loaded onboardingCompleted=" << onboardingCompleted;
|
||||
qDebug() << "StorageUIPlugin: Settings Loaded dataDir=" << dataDir;
|
||||
qDebug() << "StorageUIPlugin: Settings Loaded discoveryPort=" << discoveryPort;
|
||||
qDebug() << "StorageUIPlugin: Settings Loaded tcpPort=" << tcpPort;
|
||||
// qDebug() << "StorageUIPlugin: Settings file:" << settings.fileName();
|
||||
// qDebug() << "StorageUIPlugin: onboardingCompleted=" << onboardingCompleted;
|
||||
// qDebug() << "StorageUIPlugin: dataDir=" << dataDir;
|
||||
// qDebug() << "StorageUIPlugin: discoveryPort=" << discoveryPort;
|
||||
// qDebug() << "StorageUIPlugin: tcpPort=" << tcpPort;
|
||||
|
||||
QString qmlPath = "qrc:/Main.qml";
|
||||
|
||||
// Create backend instance
|
||||
// Always load Main.qml — QML handles navigation (onboarding vs startNode)
|
||||
StorageBackend* backend = new StorageBackend(logosAPI, quickWidget);
|
||||
|
||||
if (onboardingCompleted) {
|
||||
qmlPath = "qrc:/StorageView.qml";
|
||||
}
|
||||
|
||||
qDebug() << "StorageUIPlugin: qmlPath=" << qmlPath;
|
||||
|
||||
quickWidget->setSource(QUrl(qmlPath));
|
||||
quickWidget->setSource(QUrl("qrc:/Main.qml"));
|
||||
|
||||
if (quickWidget->status() == QQuickWidget::Error) {
|
||||
qWarning() << "StorageUIPlugin: Failed to load QML:" << quickWidget->errors();
|
||||
@ -61,33 +54,27 @@ QWidget* StorageUIPlugin::createWidget(LogosAPI* logosAPI) {
|
||||
|
||||
root->setProperty("backend", QVariant::fromValue(static_cast<QObject*>(backend)));
|
||||
|
||||
QString configJson = "{}";
|
||||
// Build config from settings if onboarding was done, otherwise use empty config
|
||||
QString configJson = StorageBackend::getUserConfig();
|
||||
qDebug() << "UserConfig" << StorageBackend::getUserConfigPath();
|
||||
qDebug() << "configJson" << configJson;
|
||||
// if (onboardingCompleted && !dataDir.isEmpty()) {
|
||||
// configJson = backend->buildConfig(dataDir, discoveryPort, tcpPort);
|
||||
// }
|
||||
|
||||
if (onboardingCompleted) {
|
||||
configJson = backend->buildConfig(dataDir, discoveryPort, tcpPort);
|
||||
}
|
||||
// config.json overrides everything (dev/debug use)
|
||||
// QFileInfo info("config.json");
|
||||
// if (info.exists() && info.isFile()) {
|
||||
// qWarning() << "StorageUIPlugin: config.json found — overriding settings config";
|
||||
// configJson = backend->buildConfigFromFile("config.json");
|
||||
// }
|
||||
|
||||
QFileInfo info("config.json");
|
||||
|
||||
if (info.exists() && info.isFile()) {
|
||||
qWarning()
|
||||
<< "StorageUIPlugin: config.json is found ! It will override the configuration loaded by the onboarding !";
|
||||
configJson = backend->buildConfigFromFile("config.json");
|
||||
}
|
||||
|
||||
qDebug() << "StorageUIPlugin: configuration loaded configLoaded=" << configJson;
|
||||
// qDebug() << "StorageUIPlugin: configJson=" << configJson;
|
||||
|
||||
LogosResult result = backend->init(configJson);
|
||||
|
||||
if (!result.success) {
|
||||
QString error = result.getError();
|
||||
qWarning() << "StorageUIPlugin: Failed to init backend, will use mock version:" << error;
|
||||
} else if (onboardingCompleted) {
|
||||
LogosResult result = backend->start();
|
||||
|
||||
if (!result.success) {
|
||||
qWarning() << "StorageUIPlugin: Failed to start the Storage Module.";
|
||||
}
|
||||
qWarning() << "StorageUIPlugin: Failed to init backend:" << result.getError();
|
||||
}
|
||||
|
||||
return quickWidget;
|
||||
|
||||
@ -18,8 +18,6 @@ Button {
|
||||
color: {
|
||||
if (!control.enabled)
|
||||
return Theme.palette.backgroundElevated
|
||||
if (control.hovered)
|
||||
return Theme.palette.backgroundTertiary
|
||||
return Theme.palette.backgroundSecondary
|
||||
}
|
||||
border.width: 1
|
||||
|
||||
@ -12,20 +12,19 @@ Item {
|
||||
|
||||
property var backend: mockBackend
|
||||
|
||||
Timer {
|
||||
readonly property int running: 2
|
||||
|
||||
id: timer
|
||||
interval: 2000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
console.log("timer triggered")
|
||||
root.backend.status = running
|
||||
root.backend.startCompleted()
|
||||
console.info(root.backend.status)
|
||||
}
|
||||
}
|
||||
// Timer {
|
||||
// readonly property int running: 2
|
||||
|
||||
// id: timer
|
||||
// interval: 2000
|
||||
// repeat: false
|
||||
// onTriggered: {
|
||||
// console.log("timer triggered")
|
||||
// // root.backend.status = running
|
||||
// // root.backend.startCompleted()
|
||||
// // console.info(root.backend.status)
|
||||
// }
|
||||
// }
|
||||
QtObject {
|
||||
id: mockBackend
|
||||
|
||||
@ -40,7 +39,8 @@ Item {
|
||||
}
|
||||
|
||||
function start() {
|
||||
timer.start()
|
||||
// timer.start()
|
||||
console.log("mock start callde")
|
||||
}
|
||||
|
||||
function stop() {
|
||||
@ -62,18 +62,10 @@ Item {
|
||||
id: settings
|
||||
category: "Storage"
|
||||
|
||||
property int discoveryPort: 0
|
||||
property int discoveryPort: 8090
|
||||
property int tcpPort: 0
|
||||
property string dataDir: ""
|
||||
property bool onboardingCompleted: false
|
||||
|
||||
Component.onCompleted: {
|
||||
if (onboardingCompleted) {
|
||||
|
||||
// stackView.replace(storageView)
|
||||
// root.backend.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StackView {
|
||||
@ -87,6 +79,10 @@ Item {
|
||||
|
||||
OnBoarding {
|
||||
id: onboardingInstance
|
||||
backend: root.backend
|
||||
discoveryPort: settings.discoveryPort
|
||||
tcpPort: settings.tcpPort
|
||||
dataDir: settings.dataDir.length > 0 ? settings.dataDir : root.backend.defaultDataDir()
|
||||
|
||||
onCompleted: {
|
||||
settings.discoveryPort = discoveryPort
|
||||
@ -96,6 +92,7 @@ Item {
|
||||
|
||||
let config = root.backend.buildConfig(dataDir,
|
||||
discoveryPort, tcpPort)
|
||||
root.backend.saveUserConfig(config)
|
||||
root.backend.reloadIfChanged(config)
|
||||
root.backend.start()
|
||||
|
||||
@ -107,7 +104,7 @@ Item {
|
||||
Component {
|
||||
id: storageView
|
||||
StorageView {
|
||||
backend: root.backend // @disable-check M228
|
||||
backend: root.backend
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,5 +129,12 @@ Item {
|
||||
function onStopCompleted() {
|
||||
stackView.pop()
|
||||
}
|
||||
|
||||
function onInitCompleted() {
|
||||
if (settings.onboardingCompleted) {
|
||||
root.backend.start()
|
||||
stackView.replace(storageView, StackView.Immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,10 +22,6 @@ Rectangle {
|
||||
QtObject {
|
||||
id: mockBackend
|
||||
|
||||
function validateDataDir(path) {
|
||||
return path != "error"
|
||||
}
|
||||
|
||||
function defaultDataDir() {
|
||||
return ".cache/storage"
|
||||
}
|
||||
@ -111,17 +107,12 @@ Rectangle {
|
||||
spacing: Theme.spacing.tiny
|
||||
|
||||
LogosTextField {
|
||||
isValid: text.trim().length > 0
|
||||
id: dataDirTextField
|
||||
placeholderText: "Enter the data dir"
|
||||
text: root.dataDir
|
||||
Layout.fillWidth: true
|
||||
onTextChanged: {
|
||||
if (text.length > 0) {
|
||||
isValid = root.backend.validateDataDir(text)
|
||||
} else {
|
||||
isValid = false
|
||||
}
|
||||
|
||||
root.dataDir = text
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,16 +128,20 @@ Rectangle {
|
||||
function updateLogLevel(logLevel) {}
|
||||
|
||||
property var manifests: []
|
||||
property var quotaMaxBytes: 20 * 1024 * 1024 * 1024 // 20 GB default
|
||||
property var quotaMaxBytes: 20 * 1024 * 1024 * 1024 // 20 GB default
|
||||
property var quotaUsedBytes: 0
|
||||
property var quotaReservedBytes: 0
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes <= 0) return "0 B"
|
||||
if (bytes < 1024) return bytes + " B"
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"
|
||||
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + " MB"
|
||||
if (bytes <= 0)
|
||||
return "0 B"
|
||||
if (bytes < 1024)
|
||||
return bytes + " B"
|
||||
if (bytes < 1024 * 1024)
|
||||
return (bytes / 1024).toFixed(1) + " KB"
|
||||
if (bytes < 1024 * 1024 * 1024)
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + " MB"
|
||||
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB"
|
||||
}
|
||||
|
||||
@ -417,8 +421,8 @@ Rectangle {
|
||||
width: parent.width - 40
|
||||
height: root.backend.quotaMaxBytes > 0 ? 36 : 20
|
||||
|
||||
readonly property real total: root.backend.quotaMaxBytes
|
||||
readonly property real used: root.backend.quotaUsedBytes
|
||||
readonly property real total: root.backend.quotaMaxBytes
|
||||
readonly property real used: root.backend.quotaUsedBytes
|
||||
readonly property real reserved: root.backend.quotaReservedBytes
|
||||
|
||||
// No quota configured
|
||||
@ -446,7 +450,9 @@ Rectangle {
|
||||
|
||||
// Used (green)
|
||||
Rectangle {
|
||||
width: Math.min(parent.width * (spaceBarSection.used / spaceBarSection.total), parent.width)
|
||||
width: Math.min(
|
||||
parent.width * (spaceBarSection.used / spaceBarSection.total),
|
||||
parent.width)
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: "#4CAF50"
|
||||
@ -455,8 +461,9 @@ Rectangle {
|
||||
// Reserved (orange), stacked after used
|
||||
Rectangle {
|
||||
x: parent.width * (spaceBarSection.used / spaceBarSection.total)
|
||||
width: Math.min(parent.width * (spaceBarSection.reserved / spaceBarSection.total),
|
||||
parent.width - x)
|
||||
width: Math.min(
|
||||
parent.width * (spaceBarSection.reserved / spaceBarSection.total),
|
||||
parent.width - x)
|
||||
height: parent.height
|
||||
color: "#FF9800"
|
||||
}
|
||||
@ -481,7 +488,8 @@ Rectangle {
|
||||
font.pixelSize: 10
|
||||
}
|
||||
Text {
|
||||
text: "Free: " + root.formatBytes(spaceBarSection.total - spaceBarSection.used - spaceBarSection.reserved)
|
||||
text: "Free: " + root.formatBytes(
|
||||
spaceBarSection.total - spaceBarSection.used - spaceBarSection.reserved)
|
||||
color: "#888888"
|
||||
font.pixelSize: 10
|
||||
}
|
||||
@ -618,7 +626,7 @@ Rectangle {
|
||||
font.family: "monospace"
|
||||
elide: Text.ElideMiddle
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
ToolTip.visible: hovered
|
||||
// ToolTip.visible: hovered
|
||||
ToolTip.text: modelData["cid"] ?? ""
|
||||
HoverHandler {}
|
||||
}
|
||||
@ -899,8 +907,19 @@ Rectangle {
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
|
||||
// try {
|
||||
// const jsonData = JSON.parse(text)
|
||||
// isValid = true
|
||||
// } catch (e) {
|
||||
// isValid = false
|
||||
// }
|
||||
}
|
||||
|
||||
onEditingFinished: {
|
||||
try {
|
||||
const jsonData = JSON.parse(text)
|
||||
root.backend.saveUserConfig(text)
|
||||
isValid = true
|
||||
} catch (e) {
|
||||
isValid = false
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user