mirror of
https://github.com/logos-storage/logos-storage-app-skeleton.git
synced 2026-06-16 13:29:32 +00:00
Update a lot of changes
This commit is contained in:
parent
9b85c68aaf
commit
b25f14210f
@ -80,7 +80,10 @@ LogosResult StorageBackend::init(const QString& configJson) {
|
||||
setStatus(Running);
|
||||
debug("Storage module started.");
|
||||
// QMetaObject::invokeMethod(this, &StorageBackend::downloadManifests, Qt::QueuedConnection);
|
||||
// QMetaObject::invokeMethod(this, &StorageBackend::space, Qt::QueuedConnection);
|
||||
|
||||
QMetaObject::invokeMethod(this, &StorageBackend::space, Qt::QueuedConnection);
|
||||
QMetaObject::invokeMethod(this, &StorageBackend::tryDebug, Qt::QueuedConnection);
|
||||
QMetaObject::invokeMethod(this, &StorageBackend::downloadManifests, Qt::QueuedConnection);
|
||||
emit startCompleted();
|
||||
}
|
||||
})) {
|
||||
@ -104,20 +107,6 @@ LogosResult StorageBackend::init(const QString& configJson) {
|
||||
qWarning() << "StorageWidget: failed to subscribe to storageStop events";
|
||||
}
|
||||
|
||||
if (!m_logos->storage_module.on("storageConnect", [this](const QVariantList& data) {
|
||||
bool success = data[0].toBool();
|
||||
|
||||
if (!success) {
|
||||
QString message = data[1].toString();
|
||||
debug("Failed to connect: " + message);
|
||||
} else {
|
||||
// TODO add the peer id
|
||||
debug("Successfully connected to peer.");
|
||||
}
|
||||
})) {
|
||||
qWarning() << "StorageWidget: failed to subscribe to storageConnect events";
|
||||
}
|
||||
|
||||
if (!m_logos->storage_module.on("storageUploadProgress", [this](const QVariantList& data) {
|
||||
bool success = data[0].toBool();
|
||||
|
||||
@ -161,17 +150,17 @@ LogosResult StorageBackend::init(const QString& configJson) {
|
||||
emit uploadStatusChanged();
|
||||
} else {
|
||||
QString sessionId = data[1].toString();
|
||||
m_cid = data[2].toString();
|
||||
emit cidChanged();
|
||||
debug("Upload completed for session " + sessionId + " with CID " + m_cid);
|
||||
QString cid = data[2].toString();
|
||||
debug("Upload completed for session " + sessionId + " with CID " + cid);
|
||||
|
||||
// Complète la progress bar
|
||||
m_uploadProgress = 100;
|
||||
m_uploadStatus = "Upload completed!";
|
||||
emit uploadProgressChanged();
|
||||
emit uploadStatusChanged();
|
||||
|
||||
emit uploadCompleted(cid);
|
||||
QMetaObject::invokeMethod(this, &StorageBackend::space, Qt::QueuedConnection);
|
||||
QMetaObject::invokeMethod(this, &StorageBackend::downloadManifests, Qt::QueuedConnection);
|
||||
}
|
||||
})) {
|
||||
qWarning() << "StorageWidget: failed to subscribe to storageUploadProgress events";
|
||||
@ -200,9 +189,11 @@ LogosResult StorageBackend::init(const QString& configJson) {
|
||||
debug("Failed to download: " + message);
|
||||
} else {
|
||||
QString sessionId = data[1].toString();
|
||||
m_cid = data[2].toString();
|
||||
emit cidChanged();
|
||||
debug("Download completed for session " + sessionId + " with CID " + m_cid);
|
||||
QString cid = data[2].toString();
|
||||
emit downloadCompleted(data[2].toString());
|
||||
|
||||
QMetaObject::invokeMethod(this, &StorageBackend::space, Qt::QueuedConnection);
|
||||
debug("Download completed for session " + sessionId + " with CID " + cid);
|
||||
}
|
||||
})) {
|
||||
qWarning() << "StorageWidget: failed to subscribe to storageDownloadProgress events";
|
||||
@ -321,8 +312,32 @@ void StorageBackend::debug(const QString& log) {
|
||||
void StorageBackend::tryDebug() {
|
||||
auto result = m_logos->storage_module.debug();
|
||||
|
||||
debug("Debug " + result.getString());
|
||||
debug("Peer ID: " + result.getString("id"));
|
||||
debug("SPR: " + result.getString("spr"));
|
||||
|
||||
QStringList addrs = result.getValue<QStringList>("addrs");
|
||||
for (const QString& addr : addrs) {
|
||||
debug("Listen address: " + addr);
|
||||
}
|
||||
|
||||
QStringList announceAddresses = result.getValue<QStringList>("announceAddresses");
|
||||
for (const QString& addr : announceAddresses) {
|
||||
debug("Announce address: " + addr);
|
||||
}
|
||||
|
||||
QVariantMap table = result.getValue<QVariantMap>("table");
|
||||
QVariantList nodes = table["nodes"].toList();
|
||||
|
||||
for (const QVariant& nodeVar : nodes) {
|
||||
QVariantMap node = nodeVar.toMap();
|
||||
QString peerId = node["peerId"].toString();
|
||||
bool seen = node["seen"].toBool();
|
||||
debug("Peer found, peerId=" + peerId + ", seen=" + (seen ? "true" : "false"));
|
||||
}
|
||||
|
||||
emit peersUpdated(nodes.size());
|
||||
}
|
||||
|
||||
void StorageBackend::tryPeerConnect(const QString& peerId) {
|
||||
qDebug().noquote() << "StorageBackend: tryPeerConnect called with peerId=" << peerId;
|
||||
|
||||
@ -565,26 +580,30 @@ void StorageBackend::exists(const QString& cid) {
|
||||
}
|
||||
|
||||
void StorageBackend::remove(const QString& cid) {
|
||||
qDebug() << "StorageBackend::remove called";
|
||||
qDebug() << "StorageBackend::remove called with cid=" << cid;
|
||||
|
||||
LogosResult result = m_logos->storage_module.remove(cid);
|
||||
LogosResult result = m_logos->storage_module.exists(cid);
|
||||
|
||||
if (!result.success) {
|
||||
// Log but continue — manifest might not have local data, remove it from the list anyway
|
||||
debug("StorageBackend::remove: storage returned error=" + result.getError() + " (removing from list regardless)");
|
||||
} else {
|
||||
debug("Cid " + cid + " removed from storage.");
|
||||
debug("StorageBackend::remove failed to check exists: " + result.getError());
|
||||
emit error("Failed to check exists " + cid + ": " + result.getError());
|
||||
return;
|
||||
}
|
||||
|
||||
// Always remove from manifests list
|
||||
for (int i = 0; i < m_manifests.size(); ++i) {
|
||||
if (m_manifests[i].toMap().value("cid").toString() == cid) {
|
||||
m_manifests.removeAt(i);
|
||||
emit manifestsChanged();
|
||||
break;
|
||||
}
|
||||
if (!result.getBool()) {
|
||||
debug("StorageBackend::remove blocks don't exist in store.");
|
||||
return;
|
||||
}
|
||||
|
||||
result = m_logos->storage_module.remove(cid);
|
||||
if (!result.success) {
|
||||
debug("StorageBackend::remove failed: " + result.getError());
|
||||
emit error("Failed to remove " + cid + ": " + result.getError());
|
||||
return;
|
||||
}
|
||||
|
||||
debug("Cid " + cid + " removed from local storage.");
|
||||
|
||||
QMetaObject::invokeMethod(this, &StorageBackend::space, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
@ -675,21 +694,15 @@ void StorageBackend::downloadManifest(const QString& cid) {
|
||||
debug("Manifest filename: " + filename);
|
||||
debug("Manifest mimetype: " + mimetype);
|
||||
|
||||
// Add to manifests list
|
||||
QVariantMap manifest;
|
||||
manifest["cid"] = cid;
|
||||
manifest["treeCid"] = treeCid;
|
||||
manifest["filename"] = filename;
|
||||
manifest["mimetype"] = mimetype;
|
||||
manifest["cid"] = cid;
|
||||
manifest["treeCid"] = treeCid;
|
||||
manifest["filename"] = filename;
|
||||
manifest["mimetype"] = mimetype;
|
||||
manifest["datasetSize"] = datasetSize;
|
||||
manifest["blockSize"] = blockSize;
|
||||
|
||||
m_manifests.append(manifest);
|
||||
emit manifestsChanged();
|
||||
manifest["blockSize"] = blockSize;
|
||||
}
|
||||
|
||||
QVariantList StorageBackend::manifests() const { return m_manifests; }
|
||||
|
||||
void StorageBackend::downloadManifests() {
|
||||
qDebug() << "StorageBackend::downloadManifests called";
|
||||
|
||||
@ -700,27 +713,9 @@ void StorageBackend::downloadManifests() {
|
||||
return;
|
||||
}
|
||||
|
||||
QVariantList manifestsList = result.getList();
|
||||
int count = manifestsList.size();
|
||||
debug(QString("Found %1 manifests").arg(count));
|
||||
qDebug() << "StorageBackend::downloadManifests called, size=" << result.getList().size();
|
||||
|
||||
m_manifests.clear();
|
||||
|
||||
for (const QVariant& manifestVariant : manifestsList) {
|
||||
QVariantMap src = manifestVariant.toMap();
|
||||
|
||||
QVariantMap manifest;
|
||||
manifest["cid"] = src.value("cid").toString();
|
||||
manifest["treeCid"] = src.value("treeCid").toString();
|
||||
manifest["filename"] = src.value("filename").toString();
|
||||
manifest["mimetype"] = src.value("mimetype").toString();
|
||||
manifest["datasetSize"] = src.value("datasetSize").toLongLong();
|
||||
manifest["blockSize"] = src.value("blockSize").toLongLong();
|
||||
|
||||
m_manifests.append(manifest);
|
||||
}
|
||||
|
||||
emit manifestsChanged();
|
||||
emit manifestsUpdated(result.getList());
|
||||
}
|
||||
|
||||
void StorageBackend::space() {
|
||||
@ -729,31 +724,18 @@ void StorageBackend::space() {
|
||||
LogosResult result = m_logos->storage_module.space();
|
||||
|
||||
if (!result.success) {
|
||||
debug("StorageBackend::space failed with error=" + result.getError());
|
||||
debug("Space failed with error=" + result.getError());
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "StorageBackend::space raw value:" << result.value;
|
||||
const qlonglong total = result.getValue<qlonglong>("quotaMaxBytes");
|
||||
const qlonglong used =
|
||||
result.getValue<qlonglong>("quotaUsedBytes") + result.getValue<qlonglong>("quotaReservedBytes");
|
||||
|
||||
static constexpr qint64 DEFAULT_QUOTA = 20LL * 1024 * 1024 * 1024; // 20 GB
|
||||
|
||||
// Check config for a quota-max-bytes override
|
||||
qint64 configQuota = m_config.object().value("quota-max-bytes").toVariant().toLongLong();
|
||||
qint64 apiQuota = result.getInt("quotaMaxBytes");
|
||||
m_quotaMaxBytes = apiQuota > 0 ? apiQuota : (configQuota > 0 ? configQuota : DEFAULT_QUOTA);
|
||||
m_quotaUsedBytes = result.getInt("quotaUsedBytes");
|
||||
m_quotaReservedBytes = result.getInt("quotaReservedBytes");
|
||||
emit quotaChanged();
|
||||
|
||||
debug(QString("Space totalBlocks %1").arg(result.getInt("totalBlocks")));
|
||||
debug(QString("Space quotaMaxBytes %1").arg(m_quotaMaxBytes));
|
||||
debug(QString("Space quotaUsedBytes %1").arg(m_quotaUsedBytes));
|
||||
debug(QString("Space quotaReservedBytes %1").arg(m_quotaReservedBytes));
|
||||
emit spaceUpdated(total, used);
|
||||
}
|
||||
|
||||
qint64 StorageBackend::quotaMaxBytes() const { return m_quotaMaxBytes; }
|
||||
qint64 StorageBackend::quotaUsedBytes() const { return m_quotaUsedBytes; }
|
||||
qint64 StorageBackend::quotaReservedBytes() const { return m_quotaReservedBytes; }
|
||||
QString StorageBackend::configJson() const { return QString::fromUtf8(m_config.toJson(QJsonDocument::Indented)); }
|
||||
|
||||
void StorageBackend::updateLogLevel(const QString& logLevel) {
|
||||
qDebug() << "StorageBackend::updateLogLevel called with logLevel=" << logLevel;
|
||||
@ -770,8 +752,6 @@ void StorageBackend::updateLogLevel(const QString& logLevel) {
|
||||
|
||||
StorageBackend::StorageStatus StorageBackend::status() const { return m_status; }
|
||||
|
||||
QString StorageBackend::cid() const { return m_cid; }
|
||||
|
||||
int StorageBackend::uploadProgress() const { return m_uploadProgress; }
|
||||
|
||||
QString StorageBackend::uploadStatus() const { return m_uploadStatus; }
|
||||
@ -822,7 +802,7 @@ void StorageBackend::reloadIfChanged(const QString& configJson) {
|
||||
|
||||
void StorageBackend::saveCurrentConfig() {
|
||||
qDebug() << "StorageBackend::saveUserConfig";
|
||||
saveUserConfig(QString::fromUtf8(m_config.toJson(QJsonDocument::Indented)));
|
||||
saveUserConfig(configJson());
|
||||
}
|
||||
|
||||
void StorageBackend::saveUserConfig(const QString& configJson) {
|
||||
@ -929,58 +909,56 @@ void StorageBackend::checkNodeIsUp() {
|
||||
|
||||
debug("DHT seems okay, found peers");
|
||||
|
||||
// Extract TCP ports from announceAddresses.
|
||||
// Extract IP+port pairs from announceAddresses.
|
||||
// Format: "/ip4/1.2.3.4/tcp/PORT"
|
||||
QVariantList announceAddresses = result.getValue<QVariantList>("announceAddresses");
|
||||
QList<int> ports;
|
||||
QList<QPair<QString, int>> endpoints;
|
||||
for (const QVariant& addr : announceAddresses) {
|
||||
QStringList parts = addr.toString().split("/");
|
||||
// "/ip4/1.2.3.4/tcp/8079" splits to ["", "ip4", "1.2.3.4", "tcp", "8079"]
|
||||
int tcpIndex = parts.indexOf("tcp");
|
||||
if (tcpIndex >= 0 && tcpIndex + 1 < parts.size()) {
|
||||
int port = parts[tcpIndex + 1].toInt();
|
||||
if (port > 0 && !ports.contains(port)) {
|
||||
ports.append(port);
|
||||
}
|
||||
const QStringList parts = addr.toString().split("/");
|
||||
// ["", "ip4", "1.2.3.4", "tcp", "8079"]
|
||||
const int tcpIndex = parts.indexOf("tcp");
|
||||
if (tcpIndex >= 1 && tcpIndex + 1 < parts.size()) {
|
||||
const QString ip = parts[tcpIndex - 1];
|
||||
const int port = parts[tcpIndex + 1].toInt();
|
||||
if (port > 0 && !ip.isEmpty())
|
||||
endpoints.append({ ip, port });
|
||||
}
|
||||
}
|
||||
|
||||
QString nat = m_config.object()["nat"].toString();
|
||||
|
||||
if (ports.isEmpty()) {
|
||||
debug("No TCP ports found in announce addresses, considering node as not up");
|
||||
if (endpoints.isEmpty()) {
|
||||
debug("No TCP endpoints found in announce addresses");
|
||||
if (nat == "upnp") {
|
||||
emit nodeIsntUp("UPnP is configured but there is no announced addresses. "
|
||||
"Try going back and configure port forwarding manually on your router.");
|
||||
} else {
|
||||
emit nodeIsntUp("No announced addresses found. Your TCP port is propably incorrect. "
|
||||
emit nodeIsntUp("No announced addresses found. Your TCP port is probably incorrect. "
|
||||
"Try going back and check your port forwarding configuration.");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
debug(QString("Checking reachability for %1 port(s)...").arg(ports.size()));
|
||||
debug(QString("Checking reachability for %1 endpoint(s)...").arg(endpoints.size()));
|
||||
|
||||
// Check each port via the echo service, one by one.
|
||||
bool foundReachable = false;
|
||||
for (int port : ports) {
|
||||
for (const auto& [ip, port] : endpoints) {
|
||||
QNetworkAccessManager manager;
|
||||
QNetworkRequest request(QUrl(QString("%1/port/%2").arg(ECHO_PROVIDER).arg(port)));
|
||||
QNetworkReply* reply = manager.get(request);
|
||||
const QUrl url(QString("%1/%2/%3").arg(PORT_CHECKER_PROVIDER).arg(ip).arg(port));
|
||||
QNetworkReply* reply = manager.get(QNetworkRequest(url));
|
||||
|
||||
QEventLoop loop;
|
||||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
bool reachable = QJsonDocument::fromJson(reply->readAll()).object()["reachable"].toBool();
|
||||
debug("Port " + QString::number(port) + (reachable ? " is reachable" : " is not reachable"));
|
||||
const bool reachable = reply->readAll() == "True";
|
||||
debug(QString("%1:%2 is %3").arg(ip).arg(port).arg(reachable ? "reachable" : "not reachable"));
|
||||
if (reachable) {
|
||||
foundReachable = true;
|
||||
}
|
||||
} else {
|
||||
debug("Port check failed for port " + QString::number(port) + ": " + reply->errorString());
|
||||
debug(QString("Port check failed for %1:%2 : %3").arg(ip).arg(port).arg(reply->errorString()));
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
static const int RET_OK = 0;
|
||||
static const int RET_PROGRESS = 3;
|
||||
static const QString ECHO_PROVIDER = "https://echo.codex.storage/";
|
||||
static const QString PORT_CHECKER_PROVIDER = "https://portchecker.io/api/";
|
||||
static const QString APP_HOME = QDir::homePath() + "/.logos_storage";
|
||||
static const QString DEFAULT_DATA_DIR = APP_HOME + "/data";
|
||||
static const QString USER_CONFIG_PATH = APP_HOME + "/config.json";
|
||||
@ -34,14 +35,8 @@ class StorageBackend : public QObject {
|
||||
QML_ELEMENT
|
||||
Q_PROPERTY(QString debugLogs READ debugLogs NOTIFY debugLogsChanged)
|
||||
Q_PROPERTY(StorageStatus status READ status WRITE status NOTIFY statusChanged)
|
||||
Q_PROPERTY(QString cid READ cid NOTIFY cidChanged)
|
||||
Q_PROPERTY(int uploadProgress READ uploadProgress NOTIFY uploadProgressChanged)
|
||||
Q_PROPERTY(QString uploadStatus READ uploadStatus NOTIFY uploadStatusChanged)
|
||||
Q_PROPERTY(QVariantList manifests READ manifests NOTIFY manifestsChanged)
|
||||
Q_PROPERTY(qint64 quotaMaxBytes READ quotaMaxBytes NOTIFY quotaChanged)
|
||||
Q_PROPERTY(qint64 quotaUsedBytes READ quotaUsedBytes NOTIFY quotaChanged)
|
||||
Q_PROPERTY(qint64 quotaReservedBytes READ quotaReservedBytes NOTIFY quotaChanged)
|
||||
|
||||
public:
|
||||
enum StorageStatus {
|
||||
// Stopped means that the context is created but the module is not started
|
||||
@ -59,15 +54,11 @@ class StorageBackend : public QObject {
|
||||
};
|
||||
Q_ENUM(StorageStatus)
|
||||
|
||||
QString cid() const;
|
||||
QString debugLogs() const;
|
||||
StorageStatus status() const;
|
||||
int uploadProgress() const;
|
||||
QString uploadStatus() const;
|
||||
QVariantList manifests() const;
|
||||
qint64 quotaMaxBytes() const;
|
||||
qint64 quotaUsedBytes() const;
|
||||
qint64 quotaReservedBytes() const;
|
||||
Q_INVOKABLE QString configJson() const;
|
||||
|
||||
static QJsonDocument defaultConfig();
|
||||
|
||||
@ -153,14 +144,16 @@ class StorageBackend : public QObject {
|
||||
void statusChanged();
|
||||
void debugLogsChanged();
|
||||
void stopCompleted();
|
||||
void cidChanged();
|
||||
void uploadProgressChanged();
|
||||
void uploadStatusChanged();
|
||||
void manifestsChanged();
|
||||
void manifestsUpdated(const QVariantList& manifests);
|
||||
void quotaChanged();
|
||||
void initCompleted();
|
||||
void natExtConfigCompleted();
|
||||
void uploadCompleted(const QString& cid);
|
||||
void downloadCompleted(const QString& cid);
|
||||
void error(const QString& message);
|
||||
void spaceUpdated(qlonglong total, qlonglong used);
|
||||
|
||||
// Emitted when the node port is reachable from the internet
|
||||
void nodeIsUp();
|
||||
@ -168,6 +161,9 @@ class StorageBackend : public QObject {
|
||||
// Emitted when the node port is not reachable, with a reason
|
||||
void nodeIsntUp(const QString& reason);
|
||||
|
||||
// Emitted when the peer count changes (from checkNodeIsUp)
|
||||
void peersUpdated(int count);
|
||||
|
||||
private slots:
|
||||
|
||||
private:
|
||||
@ -180,14 +176,9 @@ class StorageBackend : public QObject {
|
||||
LogosModules* m_logos;
|
||||
StorageStatus m_status;
|
||||
QString m_debugLogs;
|
||||
QString m_cid;
|
||||
int m_uploadProgress = 0;
|
||||
QString m_uploadStatus = "";
|
||||
qint64 m_uploadTotalBytes = 0;
|
||||
qint64 m_uploadedBytes = 0;
|
||||
QVariantList m_manifests;
|
||||
qint64 m_quotaMaxBytes = 0;
|
||||
qint64 m_quotaUsedBytes = 0;
|
||||
qint64 m_quotaReservedBytes = 0;
|
||||
QJsonDocument m_config;
|
||||
};
|
||||
|
||||
@ -32,50 +32,13 @@ LogosStorageLayout {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// ── JSON editor ──────────────────────────────────────────────────
|
||||
Rectangle {
|
||||
JsonEditor {
|
||||
id: jsonEditor
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Theme.palette.backgroundElevated
|
||||
radius: 8
|
||||
border.color: jsonArea.isValid ? Theme.palette.borderSecondary : Theme.palette.error
|
||||
border.width: 1
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
|
||||
TextArea {
|
||||
id: jsonArea
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 12
|
||||
color: Theme.palette.text
|
||||
wrapMode: Text.WrapAnywhere
|
||||
background: Item {}
|
||||
|
||||
property bool isValid: true
|
||||
|
||||
Component.onCompleted: {
|
||||
text = (root.backend
|
||||
&& root.backend.configJson) ? root.backend.configJson : "{}"
|
||||
validate()
|
||||
}
|
||||
|
||||
function validate() {
|
||||
try {
|
||||
JSON.parse(text)
|
||||
isValid = true
|
||||
} catch (e) {
|
||||
isValid = false
|
||||
}
|
||||
}
|
||||
|
||||
onTextChanged: validate()
|
||||
}
|
||||
}
|
||||
Component.onCompleted: load(root.backend.configJson() || "{}")
|
||||
}
|
||||
|
||||
// ── Buttons ──────────────────────────────────────────────────────
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Theme.spacing.medium
|
||||
@ -88,10 +51,9 @@ LogosStorageLayout {
|
||||
LogosStorageButton {
|
||||
text: "Validate"
|
||||
variant: "success"
|
||||
enabled: jsonArea.isValid
|
||||
enabled: jsonEditor.isValid
|
||||
onClicked: {
|
||||
root.backend.saveUserConfig(jsonArea.text)
|
||||
root.backend.reloadIfChanged(jsonArea.text)
|
||||
root.backend.saveUserConfig(jsonEditor.text)
|
||||
root.completed()
|
||||
}
|
||||
}
|
||||
|
||||
@ -138,12 +138,29 @@ qt_add_qml_module(appqml
|
||||
HealthIndicator.qml
|
||||
ModeSelector.qml
|
||||
AdvancedSetup.qml
|
||||
DotIcon.qml
|
||||
NodeStatusIcon.qml
|
||||
GuideIcon.qml
|
||||
AdvancedIcon.qml
|
||||
UpnpIcon.qml
|
||||
PortIcon.qml
|
||||
icons/DotIcon.qml
|
||||
icons/NodeStatusIcon.qml
|
||||
icons/GuideIcon.qml
|
||||
icons/AdvancedIcon.qml
|
||||
icons/UpnpIcon.qml
|
||||
icons/PortIcon.qml
|
||||
icons/StorageIcon.qml
|
||||
icons/PlayIcon.qml
|
||||
icons/StopIcon.qml
|
||||
icons/SettingsIcon.qml
|
||||
icons/UploadIcon.qml
|
||||
icons/DownloadIcon.qml
|
||||
icons/DeleteIcon.qml
|
||||
icons/ArcWidget.qml
|
||||
DiskWidget.qml
|
||||
UploadWidget.qml
|
||||
PeersWidget.qml
|
||||
JsonEditor.qml
|
||||
SettingsPopup.qml
|
||||
ManifestTable.qml
|
||||
NodeHeader.qml
|
||||
StatusWidgets.qml
|
||||
DebugPanel.qml
|
||||
)
|
||||
|
||||
# Set up QML module directory for runtime
|
||||
|
||||
@ -1,12 +1,6 @@
|
||||
import QtQuick
|
||||
|
||||
// X / delete icon
|
||||
// qmllint disable unqualified
|
||||
DotIcon {
|
||||
pattern: [
|
||||
1, 0, 0, 0, 1,
|
||||
0, 1, 0, 1, 0,
|
||||
0, 0, 1, 0, 0,
|
||||
0, 1, 0, 1, 0,
|
||||
1, 0, 0, 0, 1
|
||||
]
|
||||
pattern: [1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1]
|
||||
}
|
||||
|
||||
@ -3,66 +3,47 @@ import QtQuick.Layouts
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
Rectangle {
|
||||
ArcWidget {
|
||||
id: root
|
||||
|
||||
width: 140; height: 140
|
||||
radius: 14
|
||||
color: Theme.palette.backgroundSecondary
|
||||
border.color: Theme.palette.borderSecondary
|
||||
border.width: 1
|
||||
property var backend: mockBackend
|
||||
property double total: 0
|
||||
property double used: 0
|
||||
|
||||
property real total: 0
|
||||
property real used: 0
|
||||
fraction: root.total > 0 ? Math.min(root.used / root.total, 1.0) : 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"
|
||||
}
|
||||
|
||||
onTotalChanged: arc.requestPaint()
|
||||
onUsedChanged: arc.requestPaint()
|
||||
function refreshSpace() {
|
||||
let space = root.backend.space()
|
||||
root.total = space.total
|
||||
root.used = space.used
|
||||
}
|
||||
|
||||
Canvas {
|
||||
id: arc
|
||||
anchors.fill: parent
|
||||
Connections {
|
||||
target: root.backend
|
||||
|
||||
Component.onCompleted: requestPaint()
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d")
|
||||
ctx.reset()
|
||||
|
||||
var cx = width / 2
|
||||
var cy = height / 2
|
||||
var r = 46
|
||||
var lw = 8
|
||||
var startRad = 130 * Math.PI / 180
|
||||
var totalRad = 280 * Math.PI / 180
|
||||
|
||||
// ── Background track (available / grey) ───────────────────────────
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, r, startRad, startRad + totalRad)
|
||||
ctx.strokeStyle = Theme.palette.textMuted.toString()
|
||||
ctx.lineWidth = lw
|
||||
ctx.lineCap = "round"
|
||||
ctx.stroke()
|
||||
|
||||
// ── Fill (used / white) ───────────────────────────────────────────
|
||||
if (root.total > 0) {
|
||||
var fraction = Math.min(root.used / root.total, 1.0)
|
||||
if (fraction > 0) {
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, r, startRad, startRad + totalRad * fraction)
|
||||
ctx.strokeStyle = Theme.palette.text.toString()
|
||||
ctx.lineWidth = lw
|
||||
ctx.lineCap = "round"
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
function onSpaceUpdated(total, used) {
|
||||
root.total = total
|
||||
root.used = used
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,4 +66,19 @@ Rectangle {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: mockBackend
|
||||
|
||||
signal spaceUpdated(double total, double used)
|
||||
signal uploadCompleted
|
||||
signal downloadCompleted
|
||||
|
||||
function space() {
|
||||
return {
|
||||
"total": 0,
|
||||
"used": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import QtQuick
|
||||
|
||||
// Generic Nothing OS style dot grid icon.
|
||||
// Static mode : set `pattern` (flat array of 0/1) and leave `animated: false`
|
||||
// Animated mode: set `animated: true` — wave expands from center automatically
|
||||
// qmllint disable unqualified
|
||||
Item {
|
||||
id: root
|
||||
|
||||
@ -53,8 +51,8 @@ Item {
|
||||
|
||||
opacity: {
|
||||
if (!root.animated) {
|
||||
return (index < root.pattern.length && root.pattern[index])
|
||||
? root.activeOpacity : root.inactiveOpacity
|
||||
return (index < root.pattern.length
|
||||
&& root.pattern[index]) ? root.activeOpacity : root.inactiveOpacity
|
||||
}
|
||||
// Wave from center
|
||||
const cx = Math.floor(root.columns / 2)
|
||||
@ -64,8 +62,10 @@ Item {
|
||||
const d = Math.abs(col - cx) + Math.abs(row - cy)
|
||||
const wave = root.animPhase % root.columns
|
||||
const diff = Math.abs(d - wave)
|
||||
if (diff === 0) return root.activeOpacity
|
||||
if (diff === 1) return 0.35
|
||||
if (diff === 0)
|
||||
return root.activeOpacity
|
||||
if (diff === 1)
|
||||
return 0.35
|
||||
return root.inactiveOpacity
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,31 +1,45 @@
|
||||
import QtQuick
|
||||
import Logos.Theme
|
||||
|
||||
Item {
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property var backend
|
||||
property bool nodeIsUp: false
|
||||
property var backend: mockBackend
|
||||
property bool blinkOn: true
|
||||
readonly property int threeMinutes: 180000
|
||||
|
||||
// Backend status
|
||||
readonly property int running: 2
|
||||
|
||||
Timer {
|
||||
readonly property int threeMinutes: 180000
|
||||
|
||||
interval: threeMinutes
|
||||
// 600 ms blink toggle
|
||||
property Timer blinkTimer: Timer {
|
||||
interval: 600
|
||||
repeat: true
|
||||
running: root.backend.status == root.running
|
||||
triggeredOnStart: true
|
||||
onTriggered: root.backend.checkNodeIsUp()
|
||||
running: true
|
||||
onTriggered: root.blinkOn = !root.blinkOn
|
||||
}
|
||||
|
||||
Connections {
|
||||
// Reachability check every 3 minutes while running
|
||||
property Timer checkTimer: Timer {
|
||||
interval: root.threeMinutes
|
||||
repeat: true
|
||||
running: root.backend !== null && root.backend.status === root.running
|
||||
triggeredOnStart: true
|
||||
onTriggered: function () {
|
||||
if (root.backend) {
|
||||
root.backend.checkNodeIsUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property Connections connections: Connections {
|
||||
target: root.backend
|
||||
|
||||
function onNodeIsUp() {
|
||||
root.nodeIsUp = true
|
||||
}
|
||||
|
||||
function onNodeIsntUp(reason) {
|
||||
function onNodeIsntUp(r) {
|
||||
root.nodeIsUp = false
|
||||
}
|
||||
|
||||
@ -35,45 +49,4 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property bool blinkOn: true
|
||||
|
||||
Timer {
|
||||
interval: 600
|
||||
repeat: true
|
||||
running: true
|
||||
onTriggered: root.blinkOn = !root.blinkOn
|
||||
}
|
||||
|
||||
Row {
|
||||
id: nodeStatusBadge
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 18
|
||||
anchors.rightMargin: 20
|
||||
spacing: 7
|
||||
|
||||
Rectangle {
|
||||
width: 10
|
||||
height: 10
|
||||
radius: 5
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: root.nodeIsUp ? Theme.palette.success : Theme.palette.error
|
||||
opacity: root.blinkOn ? 1.0 : 0.15
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.nodeIsUp ? "Node reachable" : "Node unreachable"
|
||||
color: root.nodeIsUp ? Theme.palette.success : Theme.palette.error
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: mockBackend
|
||||
|
||||
signal nodeIsUp
|
||||
signal nodeIsntUp(string reason)
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,10 @@ Rectangle {
|
||||
border.color: root.isValid ? Theme.palette.borderSecondary : Theme.palette.error
|
||||
border.width: 1
|
||||
|
||||
function load(_text) {
|
||||
text = _text
|
||||
}
|
||||
|
||||
function validate() {
|
||||
try {
|
||||
JSON.parse(jsonArea.text)
|
||||
|
||||
@ -2,6 +2,7 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Logos.Theme
|
||||
|
||||
// qmllint disable unqualified
|
||||
Button {
|
||||
id: control
|
||||
padding: Theme.spacing.small
|
||||
@ -27,18 +28,22 @@ Button {
|
||||
|
||||
background: Rectangle {
|
||||
color: {
|
||||
if (!control.enabled)
|
||||
if (!control.enabled) {
|
||||
return Theme.palette.backgroundElevated
|
||||
if (control.isSuccess)
|
||||
}
|
||||
if (control.isSuccess) {
|
||||
return Theme.palette.success
|
||||
}
|
||||
return Theme.palette.backgroundSecondary
|
||||
}
|
||||
border.width: 1
|
||||
border.color: {
|
||||
if (!control.enabled)
|
||||
if (!control.enabled) {
|
||||
return Theme.palette.border
|
||||
if (control.isSuccess)
|
||||
}
|
||||
if (control.isSuccess) {
|
||||
return Theme.palette.success
|
||||
}
|
||||
return Theme.palette.border
|
||||
}
|
||||
radius: Theme.spacing.tiny
|
||||
|
||||
@ -2,15 +2,19 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Logos.Theme
|
||||
|
||||
// qmllint disable unqualified
|
||||
TextField {
|
||||
id: root
|
||||
|
||||
property bool isValid: acceptableInput && text.length > 0
|
||||
|
||||
height: 60
|
||||
placeholderTextColor: Theme.palette.textPlaceholder
|
||||
color: isValid ? Theme.palette.text : Theme.palette.error
|
||||
selectByMouse: true
|
||||
background: Rectangle {
|
||||
height: 60
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.palette.backgroundSecondary
|
||||
|
||||
@ -28,18 +28,6 @@ Item {
|
||||
Connections {
|
||||
target: root.backend
|
||||
|
||||
// The node is stopped during the onboarding
|
||||
// when the user try to change his settings
|
||||
// and click on "Back",
|
||||
// In that case, we pop the navigation after
|
||||
// the node is stopped.
|
||||
// function onStopCompleted() {
|
||||
// if (!settings.onboardingCompleted) {
|
||||
|
||||
// // stackView.pop()
|
||||
// }
|
||||
// }
|
||||
|
||||
// When the onboarding is completed,
|
||||
// the user should have a config save in his
|
||||
// home folder.
|
||||
|
||||
@ -1,28 +1,56 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Dialogs
|
||||
import QtQuick.Layouts
|
||||
import QtCore
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
// qmllint disable unqualified
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property var backend
|
||||
property bool running: false
|
||||
|
||||
signal downloadRequested(var manifest)
|
||||
property var manifests: []
|
||||
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
FileDialog {
|
||||
id: saveDialog
|
||||
|
||||
property var pendingManifest: null
|
||||
|
||||
fileMode: FileDialog.SaveFile
|
||||
onAccepted: {
|
||||
if (pendingManifest) {
|
||||
root.backend.tryDownloadFile(pendingManifest.cid, selectedFile)
|
||||
pendingManifest = null
|
||||
}
|
||||
}
|
||||
onRejected: pendingManifest = null
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
// ── Section title ─────────────────────────────────────────────────────────
|
||||
Connections {
|
||||
target: root.backend
|
||||
|
||||
onManifestsUpdated: function (manifests) {
|
||||
root.manifests = manifests
|
||||
}
|
||||
}
|
||||
|
||||
LogosText {
|
||||
text: "MANIFESTS"
|
||||
font.pixelSize: 11
|
||||
@ -30,7 +58,6 @@ ColumnLayout {
|
||||
font.letterSpacing: 1.5
|
||||
}
|
||||
|
||||
// ── CID input + fetch button ──────────────────────────────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
@ -38,11 +65,12 @@ ColumnLayout {
|
||||
LogosTextField {
|
||||
id: cidInput
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "Enter CID to fetch manifest…"
|
||||
placeholderText: "Enter CID to download manifest…"
|
||||
isValid: true
|
||||
}
|
||||
|
||||
LogosStorageButton {
|
||||
text: "↓ Fetch"
|
||||
text: "GET MANIFEST"
|
||||
enabled: root.running && cidInput.text.length > 0
|
||||
onClicked: {
|
||||
root.backend.downloadManifest(cidInput.text)
|
||||
@ -51,10 +79,9 @@ ColumnLayout {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Table header ──────────────────────────────────────────────────────────
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 30
|
||||
Layout.preferredHeight: 30
|
||||
color: Theme.palette.backgroundElevated
|
||||
radius: 4
|
||||
|
||||
@ -62,17 +89,48 @@ ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 10
|
||||
|
||||
Text { width: 160; text: "CID"; color: Theme.palette.textSecondary; font.pixelSize: 11; font.bold: true; elide: Text.ElideRight; anchors.verticalCenter: parent.verticalCenter }
|
||||
Text { width: 130; text: "Filename"; color: Theme.palette.textSecondary; font.pixelSize: 11; font.bold: true; elide: Text.ElideRight; anchors.verticalCenter: parent.verticalCenter }
|
||||
Text { width: 90; text: "MIME"; color: Theme.palette.textSecondary; font.pixelSize: 11; font.bold: true; elide: Text.ElideRight; anchors.verticalCenter: parent.verticalCenter }
|
||||
Text { width: 80; text: "Size"; color: Theme.palette.textSecondary; font.pixelSize: 11; font.bold: true; elide: Text.ElideRight; anchors.verticalCenter: parent.verticalCenter }
|
||||
Text {
|
||||
width: 160
|
||||
text: "CID"
|
||||
color: Theme.palette.textSecondary
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
width: 130
|
||||
text: "Filename"
|
||||
color: Theme.palette.textSecondary
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
width: 90
|
||||
text: "MIME"
|
||||
color: Theme.palette.textSecondary
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
width: 80
|
||||
text: "Size"
|
||||
color: Theme.palette.textSecondary
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Table body ────────────────────────────────────────────────────────────
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 240
|
||||
Layout.preferredHeight: 240
|
||||
color: Theme.palette.background
|
||||
border.color: Theme.palette.borderSecondary
|
||||
border.width: 1
|
||||
@ -82,13 +140,15 @@ ColumnLayout {
|
||||
ListView {
|
||||
id: manifestList
|
||||
anchors.fill: parent
|
||||
model: root.backend ? root.backend.manifests : []
|
||||
model: root.manifests
|
||||
clip: true
|
||||
|
||||
delegate: Rectangle {
|
||||
id: delegateItem
|
||||
width: manifestList.width
|
||||
height: 36
|
||||
color: index % 2 === 0 ? Theme.palette.background : Theme.palette.backgroundSecondary
|
||||
color: index % 2
|
||||
=== 0 ? Theme.palette.background : Theme.palette.backgroundSecondary
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
@ -97,19 +157,21 @@ ColumnLayout {
|
||||
|
||||
Text {
|
||||
width: 160
|
||||
text: modelData["cid"] ?? ""
|
||||
text: modelData.cid
|
||||
color: Theme.palette.text
|
||||
font.pixelSize: 11
|
||||
font.family: "monospace"
|
||||
elide: Text.ElideMiddle
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
ToolTip.visible: cidHover.hovered
|
||||
ToolTip.text: modelData["cid"] ?? ""
|
||||
HoverHandler { id: cidHover }
|
||||
ToolTip.text: modelData.cid
|
||||
HoverHandler {
|
||||
id: cidHover
|
||||
}
|
||||
}
|
||||
Text {
|
||||
width: 130
|
||||
text: modelData["filename"] ?? ""
|
||||
text: modelData.filename
|
||||
color: Theme.palette.textSecondary
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
@ -117,7 +179,7 @@ ColumnLayout {
|
||||
}
|
||||
Text {
|
||||
width: 90
|
||||
text: modelData["mimetype"] ?? ""
|
||||
text: modelData.mimetype
|
||||
color: Theme.palette.textSecondary
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
@ -125,82 +187,92 @@ ColumnLayout {
|
||||
}
|
||||
Text {
|
||||
width: 80
|
||||
text: root.formatBytes(parseInt(modelData["datasetSize"] ?? "0"))
|
||||
text: root.formatBytes(parseInt(modelData.datasetSize))
|
||||
color: Theme.palette.textSecondary
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
// ── Action buttons ────────────────────────────────────────
|
||||
Row {
|
||||
spacing: 6
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// Download
|
||||
Rectangle {
|
||||
width: 28; height: 28; radius: 4
|
||||
width: 28
|
||||
height: 28
|
||||
radius: 4
|
||||
color: dlHover.hovered ? Theme.palette.backgroundElevated : "transparent"
|
||||
border.color: Theme.palette.borderSecondary
|
||||
border.width: 1
|
||||
opacity: root.running ? 1.0 : 0.35
|
||||
|
||||
Text {
|
||||
DownloadIcon {
|
||||
anchors.centerIn: parent
|
||||
text: "↓"
|
||||
color: Theme.palette.text
|
||||
font.pixelSize: 14
|
||||
dotColor: Theme.palette.text
|
||||
dotSize: 3
|
||||
dotSpacing: 1
|
||||
}
|
||||
HoverHandler {
|
||||
id: dlHover
|
||||
}
|
||||
HoverHandler { id: dlHover }
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.running
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.downloadRequested(modelData)
|
||||
onClicked: {
|
||||
saveDialog.pendingManifest = modelData
|
||||
saveDialog.currentFile = StandardPaths.writableLocation(
|
||||
StandardPaths.HomeLocation)
|
||||
+ "/" + (modelData.filename
|
||||
|| modelData.cid
|
||||
|| "download")
|
||||
saveDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete
|
||||
Rectangle {
|
||||
width: 28; height: 28; radius: 4
|
||||
width: 28
|
||||
height: 28
|
||||
radius: 4
|
||||
color: rmHover.hovered ? Theme.palette.backgroundElevated : "transparent"
|
||||
border.color: Theme.palette.borderSecondary
|
||||
border.width: 1
|
||||
opacity: root.running ? 1.0 : 0.35
|
||||
|
||||
Text {
|
||||
DeleteIcon {
|
||||
anchors.centerIn: parent
|
||||
text: "×"
|
||||
color: Theme.palette.error
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
dotColor: Theme.palette.error
|
||||
dotSize: 3
|
||||
dotSpacing: 1
|
||||
}
|
||||
HoverHandler {
|
||||
id: rmHover
|
||||
}
|
||||
HoverHandler { id: rmHover }
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.running
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.backend.remove(modelData["cid"] ?? "")
|
||||
onClicked: {
|
||||
if (modelData.cid.length > 0) {
|
||||
root.backend.remove(modelData.cid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Empty state ───────────────────────────────────────────────────
|
||||
// Empty state
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 10
|
||||
visible: manifestList.count === 0
|
||||
|
||||
DotIcon {
|
||||
pattern: [
|
||||
0, 0, 1, 0, 0,
|
||||
0, 1, 0, 1, 0,
|
||||
1, 0, 0, 0, 1,
|
||||
0, 1, 0, 1, 0,
|
||||
0, 0, 1, 0, 0
|
||||
]
|
||||
pattern: [0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0]
|
||||
dotColor: Theme.palette.textMuted
|
||||
activeOpacity: 0.25
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
@ -30,19 +30,21 @@ LogosStorageLayout {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Item { height: Theme.spacing.medium }
|
||||
Item {
|
||||
Layout.preferredHeight: Theme.spacing.medium
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacing.medium
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
// ── Guide card ───────────────────────────────────────────────
|
||||
Rectangle {
|
||||
width: 190
|
||||
height: 230
|
||||
radius: 14
|
||||
color: root.selectedMode === 0 ? Theme.palette.overlayLight : "transparent"
|
||||
border.color: root.selectedMode === 0 ? Theme.palette.text : Theme.palette.borderTertiaryMuted
|
||||
border.color: root.selectedMode
|
||||
=== 0 ? Theme.palette.text : Theme.palette.borderTertiaryMuted
|
||||
border.width: root.selectedMode === 0 ? 2 : 1
|
||||
|
||||
ColumnLayout {
|
||||
@ -80,13 +82,13 @@ LogosStorageLayout {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Advanced card ────────────────────────────────────────────
|
||||
Rectangle {
|
||||
width: 190
|
||||
height: 230
|
||||
radius: 14
|
||||
color: root.selectedMode === 1 ? Theme.palette.overlayLight : "transparent"
|
||||
border.color: root.selectedMode === 1 ? Theme.palette.text : Theme.palette.borderTertiaryMuted
|
||||
border.color: root.selectedMode
|
||||
=== 1 ? Theme.palette.text : Theme.palette.borderTertiaryMuted
|
||||
border.width: root.selectedMode === 1 ? 2 : 1
|
||||
|
||||
ColumnLayout {
|
||||
@ -125,7 +127,9 @@ LogosStorageLayout {
|
||||
}
|
||||
}
|
||||
|
||||
Item { height: Theme.spacing.small }
|
||||
Item {
|
||||
Layout.preferredHeight: Theme.spacing.small
|
||||
}
|
||||
|
||||
LogosStorageButton {
|
||||
text: "Continue"
|
||||
|
||||
@ -1,11 +1,7 @@
|
||||
import QtQuick
|
||||
import Logos.Theme
|
||||
|
||||
// 7x7 animated dot grid for the StartNode screen.
|
||||
// States:
|
||||
// starting=true → white wave expanding from center
|
||||
// starting=false, success → all dots green (Theme.palette.success)
|
||||
// starting=false, !success → red X pattern (Theme.palette.error)
|
||||
// qmllint disable unqualified
|
||||
Item {
|
||||
id: root
|
||||
|
||||
@ -42,8 +38,10 @@ Item {
|
||||
radius: root.dotSize * 0.25
|
||||
|
||||
color: {
|
||||
if (root.success) return Theme.palette.success
|
||||
if (!root.starting) return Theme.palette.error
|
||||
if (root.success)
|
||||
return Theme.palette.success
|
||||
if (!root.starting)
|
||||
return Theme.palette.error
|
||||
return Theme.palette.text
|
||||
}
|
||||
|
||||
@ -55,12 +53,15 @@ Item {
|
||||
if (root.starting) {
|
||||
const wave = root.animPhase % root.columns
|
||||
const diff = Math.abs(d - wave)
|
||||
if (diff === 0) return 0.9
|
||||
if (diff === 1) return 0.35
|
||||
if (diff === 0)
|
||||
return 0.9
|
||||
if (diff === 1)
|
||||
return 0.35
|
||||
return 0.1
|
||||
}
|
||||
|
||||
if (root.success) return 0.85
|
||||
if (root.success)
|
||||
return 0.85
|
||||
|
||||
// Error — X pattern
|
||||
return (col === row || col + row === 6) ? 0.9 : 0.1
|
||||
|
||||
@ -33,19 +33,21 @@ LogosStorageLayout {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Item { height: Theme.spacing.medium }
|
||||
Item {
|
||||
Layout.preferredHeight: Theme.spacing.medium
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacing.medium
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
// ── UPnP card ────────────────────────────────────────────────
|
||||
Rectangle {
|
||||
width: 190
|
||||
height: 230
|
||||
radius: 14
|
||||
color: root.selectedMode === 0 ? Theme.palette.overlayLight : "transparent"
|
||||
border.color: root.selectedMode === 0 ? Theme.palette.text : Theme.palette.borderTertiaryMuted
|
||||
border.color: root.selectedMode
|
||||
=== 0 ? Theme.palette.text : Theme.palette.borderTertiaryMuted
|
||||
border.width: root.selectedMode === 0 ? 2 : 1
|
||||
|
||||
ColumnLayout {
|
||||
@ -83,13 +85,13 @@ LogosStorageLayout {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Port Forwarding card ─────────────────────────────────────
|
||||
Rectangle {
|
||||
width: 190
|
||||
height: 230
|
||||
radius: 14
|
||||
color: root.selectedMode === 1 ? Theme.palette.overlayLight : "transparent"
|
||||
border.color: root.selectedMode === 1 ? Theme.palette.text : Theme.palette.borderTertiaryMuted
|
||||
border.color: root.selectedMode
|
||||
=== 1 ? Theme.palette.text : Theme.palette.borderTertiaryMuted
|
||||
border.width: root.selectedMode === 1 ? 2 : 1
|
||||
|
||||
ColumnLayout {
|
||||
@ -128,7 +130,9 @@ LogosStorageLayout {
|
||||
}
|
||||
}
|
||||
|
||||
Item { height: Theme.spacing.small }
|
||||
Item {
|
||||
Layout.preferredHeight: Theme.spacing.small
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
@ -3,66 +3,21 @@ import QtQuick.Layouts
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
Rectangle {
|
||||
ArcWidget {
|
||||
id: root
|
||||
|
||||
width: 140; height: 140
|
||||
radius: 14
|
||||
color: Theme.palette.backgroundSecondary
|
||||
border.color: Theme.palette.borderSecondary
|
||||
border.width: 1
|
||||
|
||||
property int peerCount: 0
|
||||
// Soft ceiling: arc is full at maxPeers connected peers
|
||||
property var backend
|
||||
property int peers: 0
|
||||
property int maxPeers: 20
|
||||
|
||||
onPeerCountChanged: arc.requestPaint()
|
||||
|
||||
Canvas {
|
||||
id: arc
|
||||
anchors.fill: parent
|
||||
|
||||
Component.onCompleted: requestPaint()
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d")
|
||||
ctx.reset()
|
||||
|
||||
var cx = width / 2
|
||||
var cy = height / 2
|
||||
var r = 46
|
||||
var lw = 8
|
||||
var startRad = 130 * Math.PI / 180
|
||||
var totalRad = 280 * Math.PI / 180
|
||||
|
||||
// ── Background track ──────────────────────────────────────────────
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, r, startRad, startRad + totalRad)
|
||||
ctx.strokeStyle = Theme.palette.textMuted.toString()
|
||||
ctx.lineWidth = lw
|
||||
ctx.lineCap = "round"
|
||||
ctx.stroke()
|
||||
|
||||
// ── Fill (peers / white) ───────────────────────────────────────────
|
||||
var fraction = root.maxPeers > 0
|
||||
? Math.min(root.peerCount / root.maxPeers, 1.0) : 0
|
||||
if (fraction > 0) {
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, r, startRad, startRad + totalRad * fraction)
|
||||
ctx.strokeStyle = Theme.palette.text.toString()
|
||||
ctx.lineWidth = lw
|
||||
ctx.lineCap = "round"
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
}
|
||||
fraction: root.maxPeers > 0 ? Math.min(root.peers / root.maxPeers, 1.0) : 0
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 2
|
||||
|
||||
LogosText {
|
||||
text: root.peerCount
|
||||
text: root.peers
|
||||
font.pixelSize: 22
|
||||
font.bold: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
@ -76,4 +31,12 @@ Rectangle {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.backend
|
||||
|
||||
function onPeersUpdated(peers) {
|
||||
root.peers = peers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import QtQuick
|
||||
|
||||
// Settings / gear icon
|
||||
// Gear / cog icon — 4 cardinal teeth + ring with center hole
|
||||
// . . ●. .
|
||||
// . ● ● ● .
|
||||
// ● ● . ● ●
|
||||
// . ● ● ● .
|
||||
// . . ● . .
|
||||
DotIcon {
|
||||
pattern: [
|
||||
0, 1, 0, 1, 0,
|
||||
1, 1, 1, 1, 1,
|
||||
0, 1, 1, 1, 0,
|
||||
1, 1, 1, 1, 1,
|
||||
0, 1, 0, 1, 0
|
||||
]
|
||||
pattern: [0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0]
|
||||
}
|
||||
|
||||
@ -16,6 +16,9 @@ Popup {
|
||||
padding: 24
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
// Reload the live config every time the popup opens
|
||||
onOpened: jsonEditor.load(root.backend.configJson() || "{}")
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.palette.backgroundSecondary
|
||||
border.color: Theme.palette.borderSecondary
|
||||
@ -43,54 +46,12 @@ Popup {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// ── JSON editor ───────────────────────────────────────────────────────
|
||||
Rectangle {
|
||||
JsonEditor {
|
||||
id: jsonEditor
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Theme.palette.backgroundElevated
|
||||
radius: 8
|
||||
border.color: jsonArea.isValid
|
||||
? Theme.palette.borderSecondary : Theme.palette.error
|
||||
border.width: 1
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
|
||||
TextArea {
|
||||
id: jsonArea
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 12
|
||||
color: Theme.palette.text
|
||||
wrapMode: Text.WrapAnywhere
|
||||
background: Item {}
|
||||
|
||||
property bool isValid: true
|
||||
|
||||
function validate() {
|
||||
try { JSON.parse(text); isValid = true }
|
||||
catch (e) { isValid = false }
|
||||
}
|
||||
|
||||
onTextChanged: validate()
|
||||
|
||||
Component.onCompleted: {
|
||||
text = (root.backend && root.backend.configJson)
|
||||
? root.backend.configJson : "{}"
|
||||
validate()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.backend
|
||||
function onConfigJsonChanged() {
|
||||
jsonArea.text = root.backend.configJson
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Buttons ───────────────────────────────────────────────────────────
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Theme.spacing.medium
|
||||
@ -103,10 +64,10 @@ Popup {
|
||||
LogosStorageButton {
|
||||
text: "Save"
|
||||
variant: "success"
|
||||
enabled: jsonArea.isValid
|
||||
enabled: jsonEditor.isValid
|
||||
onClicked: {
|
||||
root.backend.saveUserConfig(jsonArea.text)
|
||||
root.backend.reloadIfChanged(jsonArea.text)
|
||||
root.backend.saveUserConfig(jsonEditor.text)
|
||||
root.backend.reloadIfChanged(jsonEditor.text)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Dialogs
|
||||
import QtQuick.Layouts
|
||||
import QtCore
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
@ -11,34 +9,10 @@ LogosStorageLayout {
|
||||
id: root
|
||||
|
||||
property var backend: mockBackend
|
||||
readonly property int stopped: 0
|
||||
readonly property int starting: 1
|
||||
readonly property int running: 2
|
||||
readonly property int stopping: 3
|
||||
readonly property int destroyed: 4
|
||||
property int peerCount: 0
|
||||
property var pendingDownloadManifest: null
|
||||
property bool showDebug: false
|
||||
|
||||
function isRunning() {
|
||||
return backend.status === running
|
||||
}
|
||||
|
||||
function getStatusLabel() {
|
||||
switch (backend.status) {
|
||||
case stopped:
|
||||
return "Stopped"
|
||||
case starting:
|
||||
return "Starting…"
|
||||
case running:
|
||||
return "Running"
|
||||
case stopping:
|
||||
return "Stopping…"
|
||||
case destroyed:
|
||||
return "Not initialised"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
return backend.status === 2 // StorageBackend.Running
|
||||
}
|
||||
|
||||
Component.onCompleted: root.backend.start()
|
||||
@ -48,53 +22,26 @@ LogosStorageLayout {
|
||||
backend: root.backend
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.backend
|
||||
function onPeersUpdated(count) {
|
||||
root._peerCount = count
|
||||
}
|
||||
}
|
||||
|
||||
// ── Clipboard helper (Qt6 has no Qt.copyToClipboard) ─────────────────────
|
||||
TextEdit {
|
||||
id: clipHelper
|
||||
visible: false
|
||||
function copyText(str) {
|
||||
clipHelper.text = str
|
||||
clipHelper.selectAll()
|
||||
clipHelper.copy()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mock backend ──────────────────────────────────────────────────────────
|
||||
QtObject {
|
||||
id: mockBackend
|
||||
property var status: root.stopped
|
||||
property var status: 0
|
||||
property var debugLogs: "Hello!"
|
||||
property var configJson: "{}"
|
||||
property string uploadStatus: ""
|
||||
property int uploadProgress: 0
|
||||
property var manifests: []
|
||||
property var quotaMaxBytes: 20 * 1024 * 1024 * 1024
|
||||
property var quotaUsedBytes: 0
|
||||
property string cid: ""
|
||||
|
||||
signal nodeIsUp
|
||||
signal nodeIsntUp(string reason)
|
||||
signal peersUpdated(int count)
|
||||
signal uploadCompleted(string cid)
|
||||
signal downloadCompleted(string cid)
|
||||
|
||||
function start() {
|
||||
status = root.running
|
||||
}
|
||||
function stop() {
|
||||
status = root.stopped
|
||||
}
|
||||
function start() { status = 2 }
|
||||
function stop() { status = 0 }
|
||||
function checkNodeIsUp() {}
|
||||
function tryUploadFile(f) {}
|
||||
function downloadManifest(c) {}
|
||||
function remove(c) {}
|
||||
function tryDownloadFile(c, d) {}
|
||||
function space() {}
|
||||
function tryDebug() {}
|
||||
function showPeerId() {}
|
||||
function dataDir() {}
|
||||
@ -102,47 +49,19 @@ LogosStorageLayout {
|
||||
function version() {}
|
||||
function saveUserConfig(j) {}
|
||||
function reloadIfChanged(j) {}
|
||||
function configJson() {
|
||||
return "{}"
|
||||
}
|
||||
function peerCount() {
|
||||
return 0
|
||||
}
|
||||
function configJson() { return "{}" }
|
||||
}
|
||||
|
||||
// ── File dialogs ──────────────────────────────────────────────────────────
|
||||
FileDialog {
|
||||
id: fileDialog
|
||||
onAccepted: root.backend.tryUploadFile(fileDialog.selectedFile)
|
||||
}
|
||||
|
||||
FileDialog {
|
||||
id: manifestSaveDialog
|
||||
fileMode: FileDialog.SaveFile
|
||||
onAccepted: {
|
||||
if (root.pendingDownloadManifest) {
|
||||
root.backend.tryDownloadFile(
|
||||
root.pendingDownloadManifest["cid"],
|
||||
manifestSaveDialog.selectedFile)
|
||||
root.pendingDownloadManifest = null
|
||||
}
|
||||
}
|
||||
onRejected: root.pendingDownloadManifest = null
|
||||
}
|
||||
|
||||
// ── Settings popup ────────────────────────────────────────────────────────
|
||||
SettingsPopup {
|
||||
id: settingsPopup
|
||||
backend: root.backend
|
||||
}
|
||||
|
||||
// ── Ctrl+D toggle ─────────────────────────────────────────────────────────
|
||||
Shortcut {
|
||||
sequence: "Ctrl+D"
|
||||
onActivated: root.showDebug = !root.showDebug
|
||||
}
|
||||
|
||||
// ── Main scrollable content ───────────────────────────────────────────────
|
||||
ScrollView {
|
||||
id: mainScroll
|
||||
anchors.fill: parent
|
||||
@ -154,132 +73,16 @@ LogosStorageLayout {
|
||||
width: mainScroll.availableWidth
|
||||
spacing: 0
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// Header — node identity + settings + start/stop
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
RowLayout {
|
||||
NodeHeader {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 24
|
||||
Layout.rightMargin: 24
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 20
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
StorageIcon {
|
||||
animated: root.backend.status === root.starting
|
||||
|| root.backend.status === root.stopping
|
||||
dotColor: {
|
||||
if (root.backend.status === root.starting)
|
||||
return Theme.palette.warning
|
||||
if (!root.isRunning())
|
||||
return Theme.palette.textMuted
|
||||
return health.nodeIsUp ? Theme.palette.success : Theme.palette.error
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 6
|
||||
|
||||
LogosText {
|
||||
text: "Logos Storage"
|
||||
font.pixelSize: Theme.typography.titleText
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 7
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 7
|
||||
Layout.preferredHeight: 7
|
||||
radius: 3.5
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
color: {
|
||||
if (root.backend.status === root.starting)
|
||||
return Theme.palette.warning
|
||||
if (!root.isRunning())
|
||||
return Theme.palette.textMuted
|
||||
return health.nodeIsUp ? Theme.palette.success : Theme.palette.error
|
||||
}
|
||||
opacity: root.isRunning(
|
||||
) ? (health.blinkOn ? 1.0 : 0.15) : 1.0
|
||||
}
|
||||
|
||||
LogosText {
|
||||
text: root.getStatusLabel()
|
||||
font.pixelSize: Theme.typography.primaryText
|
||||
color: Theme.palette.textSecondary
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 44
|
||||
Layout.preferredHeight: 44
|
||||
radius: 8
|
||||
color: settingsHover.hovered ? Theme.palette.backgroundElevated : "transparent"
|
||||
border.color: Theme.palette.borderSecondary
|
||||
border.width: 1
|
||||
|
||||
SettingsIcon {
|
||||
anchors.centerIn: parent
|
||||
dotColor: Theme.palette.text
|
||||
dotSize: 5
|
||||
dotSpacing: 2
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: settingsHover
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: settingsPopup.open()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 44
|
||||
Layout.preferredHeight: 44
|
||||
radius: 8
|
||||
color: startStopHover.hovered ? Theme.palette.backgroundElevated : "transparent"
|
||||
border.color: Theme.palette.borderSecondary
|
||||
border.width: 1
|
||||
opacity: (root.backend.status === root.running
|
||||
|| root.backend.status === root.stopped) ? 1.0 : 0.4
|
||||
|
||||
PlayIcon {
|
||||
anchors.centerIn: parent
|
||||
dotColor: Theme.palette.text
|
||||
dotSize: 5
|
||||
dotSpacing: 2
|
||||
visible: root.backend.status !== root.running
|
||||
}
|
||||
StopIcon {
|
||||
anchors.centerIn: parent
|
||||
dotColor: Theme.palette.text
|
||||
dotSize: 5
|
||||
dotSpacing: 2
|
||||
visible: root.backend.status === root.running
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: startStopHover
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.backend.status === root.running
|
||||
|| root.backend.status === root.stopped
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.backend.status
|
||||
=== root.running ? root.backend.stop(
|
||||
) : root.backend.start()
|
||||
}
|
||||
}
|
||||
backend: root.backend
|
||||
nodeIsUp: health.nodeIsUp
|
||||
blinkOn: health.blinkOn
|
||||
onSettingsRequested: settingsPopup.open()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@ -290,133 +93,13 @@ LogosStorageLayout {
|
||||
color: Theme.palette.borderSecondary
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
StatusWidgets {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 24
|
||||
Layout.rightMargin: 24
|
||||
Layout.topMargin: 20
|
||||
Layout.bottomMargin: 10
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
UploadWidget {
|
||||
uploadProgress: root.backend.uploadProgress
|
||||
running: root.isRunning()
|
||||
onUploadRequested: fileDialog.open()
|
||||
}
|
||||
|
||||
DiskWidget {
|
||||
total: root.backend.quotaMaxBytes
|
||||
used: root.backend.quotaUsedBytes
|
||||
}
|
||||
|
||||
PeersWidget {
|
||||
peerCount: root.peerCount
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 24
|
||||
Layout.rightMargin: 24
|
||||
Layout.bottomMargin: 20
|
||||
Layout.preferredHeight: 36
|
||||
|
||||
opacity: String(root.backend.cid).length > 0 ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: cidBadge
|
||||
height: 36
|
||||
width: cidBadgeRow.implicitWidth + 28
|
||||
radius: 6
|
||||
color: Theme.palette.backgroundSecondary
|
||||
border.color: Theme.palette.borderSecondary
|
||||
border.width: 1
|
||||
|
||||
RowLayout {
|
||||
id: cidBadgeRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 8
|
||||
|
||||
LogosText {
|
||||
text: "CID"
|
||||
font.pixelSize: 10
|
||||
color: Theme.palette.textTertiary
|
||||
}
|
||||
|
||||
LogosText {
|
||||
text: {
|
||||
var c = String(root.backend.cid)
|
||||
return c.length > 20 ? c.substring(
|
||||
0,
|
||||
8) + "…" + c.slice(
|
||||
-6) : c
|
||||
}
|
||||
font.pixelSize: 11
|
||||
font.family: "monospace"
|
||||
color: Theme.palette.text
|
||||
}
|
||||
|
||||
LogosText {
|
||||
text: "COPY"
|
||||
font.pixelSize: 9
|
||||
color: Theme.palette.textTertiary
|
||||
font.letterSpacing: 0.8
|
||||
}
|
||||
}
|
||||
|
||||
// ── Green flash on copy ───────────────────────────────────
|
||||
Rectangle {
|
||||
id: copyFlash
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: Theme.palette.success
|
||||
opacity: 0
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
id: copyFlashAnim
|
||||
running: false
|
||||
NumberAnimation {
|
||||
to: 0.18
|
||||
duration: 80
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 0
|
||||
duration: 500
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: cidBadgeHover
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: cidBadgeHover.hovered ? Qt.rgba(
|
||||
1, 1, 1,
|
||||
0.04) : "transparent"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
clipHelper.copyText(String(root.backend.cid))
|
||||
copyFlashAnim.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
backend: root.backend
|
||||
running: root.isRunning()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@ -435,14 +118,6 @@ LogosStorageLayout {
|
||||
Layout.bottomMargin: 20
|
||||
backend: root.backend
|
||||
running: root.isRunning()
|
||||
onDownloadRequested: function (manifest) {
|
||||
root.pendingDownloadManifest = manifest
|
||||
var filename = manifest["filename"] || manifest["cid"]
|
||||
|| "download"
|
||||
manifestSaveDialog.currentFile = StandardPaths.writableLocation(
|
||||
StandardPaths.HomeLocation) + "/" + filename
|
||||
manifestSaveDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
@ -451,97 +126,14 @@ LogosStorageLayout {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
DebugPanel {
|
||||
id: debugPanel
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: 220
|
||||
color: Theme.palette.backgroundElevated
|
||||
border.color: Theme.palette.borderSecondary
|
||||
border.width: 1
|
||||
visible: root.showDebug
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
// Dev action buttons
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 10
|
||||
Layout.topMargin: 6
|
||||
Layout.bottomMargin: 4
|
||||
spacing: 6
|
||||
|
||||
LogosStorageButton {
|
||||
text: "Space"
|
||||
enabled: root.isRunning()
|
||||
onClicked: root.backend.space()
|
||||
}
|
||||
LogosStorageButton {
|
||||
text: "Debug"
|
||||
enabled: root.isRunning()
|
||||
onClicked: root.backend.tryDebug()
|
||||
}
|
||||
LogosStorageButton {
|
||||
text: "Peer ID"
|
||||
enabled: root.isRunning()
|
||||
onClicked: root.backend.showPeerId()
|
||||
}
|
||||
LogosStorageButton {
|
||||
text: "Data dir"
|
||||
enabled: root.isRunning()
|
||||
onClicked: root.backend.dataDir()
|
||||
}
|
||||
LogosStorageButton {
|
||||
text: "SPR"
|
||||
enabled: root.isRunning()
|
||||
onClicked: root.backend.spr()
|
||||
}
|
||||
LogosStorageButton {
|
||||
text: "Version"
|
||||
enabled: root.isRunning()
|
||||
onClicked: root.backend.version()
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: Theme.palette.borderSecondary
|
||||
}
|
||||
|
||||
// Logs
|
||||
Flickable {
|
||||
id: logFlick
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
contentWidth: width
|
||||
contentHeight: debugText.paintedHeight
|
||||
|
||||
TextEdit {
|
||||
id: debugText
|
||||
width: logFlick.width
|
||||
text: root.backend.debugLogs
|
||||
color: Theme.palette.textSecondary
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 11
|
||||
wrapMode: Text.WrapAnywhere
|
||||
readOnly: true
|
||||
padding: 8
|
||||
bottomPadding: 20
|
||||
|
||||
onTextChanged: Qt.callLater(function () {
|
||||
logFlick.contentY = Math.max(
|
||||
0, logFlick.contentHeight - logFlick.height)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
backend: root.backend
|
||||
running: root.isRunning()
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,15 +6,16 @@ import Logos.Controls
|
||||
ArcWidget {
|
||||
id: root
|
||||
|
||||
property int uploadProgress: 0 // 0–100
|
||||
property int uploadProgress: 0 // 0–100
|
||||
property bool running: false
|
||||
|
||||
readonly property bool isUploading: uploadProgress > 0 && uploadProgress < 100
|
||||
readonly property bool isDone: uploadProgress >= 100
|
||||
readonly property bool isUploading: uploadProgress > 0
|
||||
&& uploadProgress < 100
|
||||
readonly property bool isDone: uploadProgress >= 100
|
||||
|
||||
signal uploadRequested
|
||||
|
||||
fraction: root.uploadProgress / 100.0
|
||||
fraction: root.uploadProgress / 100.0
|
||||
fillColor: root.isDone ? Theme.palette.success : Theme.palette.text
|
||||
|
||||
// ── Center content ────────────────────────────────────────────────────────
|
||||
@ -24,11 +25,11 @@ ArcWidget {
|
||||
|
||||
// Idle or done: upload icon
|
||||
UploadIcon {
|
||||
dotColor: Theme.palette.textSecondary
|
||||
dotSize: 4
|
||||
dotSpacing: 3
|
||||
dotColor: Theme.palette.textSecondary
|
||||
dotSize: 4
|
||||
dotSpacing: 3
|
||||
activeOpacity: 0.5
|
||||
visible: !root.isUploading
|
||||
visible: !root.isUploading
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
@ -50,19 +51,22 @@ ArcWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hover overlay ─────────────────────────────────────────────────────────
|
||||
HoverHandler { id: widgetHover }
|
||||
HoverHandler {
|
||||
id: widgetHover
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: root.radius
|
||||
color: widgetHover.hovered && root.running ? Qt.rgba(1, 1, 1, 0.04) : "transparent"
|
||||
color: widgetHover.hovered
|
||||
&& root.running ? Qt.rgba(1, 1, 1, 0.04) : "transparent"
|
||||
}
|
||||
|
||||
// ── Click → trigger upload when node is running ───────────────────────────
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: root.running ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: if (root.running) root.uploadRequested()
|
||||
onClicked: if (root.running) {
|
||||
root.uploadRequested()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,13 +13,14 @@
|
||||
<file alias="ModeSelector.qml">qml/ModeSelector.qml</file>
|
||||
<file alias="AdvancedSetup.qml">qml/AdvancedSetup.qml</file>
|
||||
<file alias="ManifestTable.qml">qml/ManifestTable.qml</file>
|
||||
<file alias="NodeHeader.qml">qml/NodeHeader.qml</file>
|
||||
<file alias="StatusWidgets.qml">qml/StatusWidgets.qml</file>
|
||||
<file alias="DebugPanel.qml">qml/DebugPanel.qml</file>
|
||||
<file alias="SettingsPopup.qml">qml/SettingsPopup.qml</file>
|
||||
<file alias="JsonEditor.qml">qml/JsonEditor.qml</file>
|
||||
<file alias="SpaceBar.qml">qml/SpaceBar.qml</file>
|
||||
<file alias="DiskWidget.qml">qml/DiskWidget.qml</file>
|
||||
<file alias="UploadWidget.qml">qml/UploadWidget.qml</file>
|
||||
<file alias="PeersWidget.qml">qml/PeersWidget.qml</file>
|
||||
<!-- Icons & arc -->
|
||||
<file alias="DotIcon.qml">qml/icons/DotIcon.qml</file>
|
||||
<file alias="NodeStatusIcon.qml">qml/icons/NodeStatusIcon.qml</file>
|
||||
<file alias="GuideIcon.qml">qml/icons/GuideIcon.qml</file>
|
||||
@ -34,7 +35,6 @@
|
||||
<file alias="DownloadIcon.qml">qml/icons/DownloadIcon.qml</file>
|
||||
<file alias="DeleteIcon.qml">qml/icons/DeleteIcon.qml</file>
|
||||
<file alias="ArcWidget.qml">qml/icons/ArcWidget.qml</file>
|
||||
<!-- Assets -->
|
||||
<file>icons/storage.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
2
vendor/logos-design-system
vendored
2
vendor/logos-design-system
vendored
@ -1 +1 @@
|
||||
Subproject commit 596811cbb0a0644322267368e87fab80e34203d8
|
||||
Subproject commit 063c4b46accc621bc85fa8baab46b31ef65f3957
|
||||
Loading…
x
Reference in New Issue
Block a user