mirror of
https://github.com/logos-storage/logos-storage-app-skeleton.git
synced 2026-06-13 20:09:28 +00:00
Merge pull request #12 from logos-co/feat/improve-dashboard
feat: improve dashboard
This commit is contained in:
commit
006d0b6a68
4
.gitmodules
vendored
4
.gitmodules
vendored
@ -10,6 +10,6 @@
|
||||
[submodule "vendor/logos-storage-nim"]
|
||||
path = vendor/logos-storage-nim
|
||||
url = https://github.com/logos-storage/logos-storage-nim
|
||||
[submodule "logos-design-system"]
|
||||
path = logos-design-system
|
||||
[submodule "vendor/logos-design-system"]
|
||||
path = vendor/logos-design-system
|
||||
url = https://github.com/logos-co/logos-design-system
|
||||
|
||||
@ -5,6 +5,7 @@ set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(LOGOS_STORAGE_UI_USE_VENDOR OFF)
|
||||
|
||||
########### DEPENDENCIES SECTION ###########
|
||||
|
||||
@ -176,6 +177,8 @@ if(_cpp_sdk_is_source)
|
||||
${LOGOS_CPP_SDK_ROOT}/cpp/token_manager.h
|
||||
${LOGOS_CPP_SDK_ROOT}/cpp/module_proxy.cpp
|
||||
${LOGOS_CPP_SDK_ROOT}/cpp/module_proxy.h
|
||||
${LOGOS_CPP_SDK_ROOT}/cpp/logos_types.cpp
|
||||
${LOGOS_CPP_SDK_ROOT}/cpp/logos_types.h
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
@ -1 +0,0 @@
|
||||
Subproject commit 596811cbb0a0644322267368e87fab80e34203d8
|
||||
File diff suppressed because it is too large
Load Diff
@ -11,7 +11,9 @@
|
||||
|
||||
static const int RET_OK = 0;
|
||||
static const int RET_PROGRESS = 3;
|
||||
static const QString ECHO_PROVIDER = "https://echo.codex.storage/";
|
||||
// static const QString ECHO_PROVIDER = "https://echo.codex.storage/";
|
||||
static const QString ECHO_PROVIDER = "https://ipv4.icanhazip.com";
|
||||
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";
|
||||
@ -33,15 +35,7 @@ class StorageBackend : public QObject {
|
||||
Q_OBJECT
|
||||
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)
|
||||
|
||||
Q_PROPERTY(StorageStatus status READ status NOTIFY statusChanged)
|
||||
public:
|
||||
enum StorageStatus {
|
||||
// Stopped means that the context is created but the module is not started
|
||||
@ -59,44 +53,76 @@ 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;
|
||||
|
||||
// Provide a default config for onboarding
|
||||
static QJsonDocument defaultConfig();
|
||||
|
||||
explicit StorageBackend(LogosAPI* logosAPI = nullptr, QObject* parent = nullptr);
|
||||
~StorageBackend();
|
||||
|
||||
public slots:
|
||||
// Init the Storage Module using the config json
|
||||
// passed in parameter.
|
||||
// It subscribes to events:
|
||||
// 1- storageStart
|
||||
// 2- storageStop
|
||||
// 3- storageUploadProgress
|
||||
// 4- storageUploadDone
|
||||
// 5- storageDownloadProgress
|
||||
// 6- storageDownloadProgress
|
||||
LogosResult init(const QString& configJson);
|
||||
|
||||
// Start the node
|
||||
// If a configuration is passed (not empty string),
|
||||
// the configuration will be reloaded before trying
|
||||
// to start.
|
||||
LogosResult start(const QString& configJson = "");
|
||||
|
||||
// Destroy the Storage Module
|
||||
void destroy();
|
||||
|
||||
// Emit stopCompleted() on completion of it the module is not started
|
||||
void stop();
|
||||
void tryPeerConnect(const QString& peerId);
|
||||
void tryDebug();
|
||||
void tryUpload();
|
||||
void tryUploadFinalize();
|
||||
|
||||
// Log debug info
|
||||
// Emit peersUpdated(int peers)
|
||||
void logDebugInfo();
|
||||
|
||||
// Other log methods for debug
|
||||
void logDataDir();
|
||||
void logVersion();
|
||||
void logSpr();
|
||||
void logPeerId();
|
||||
|
||||
void exists(const QString& cid);
|
||||
void remove(const QString& cid);
|
||||
|
||||
// Fetch a cid in background
|
||||
void fetch(const QString& cid);
|
||||
void tryUploadFile(const QUrl& url);
|
||||
void tryDownloadFile(const QString& cid, const QUrl& url);
|
||||
void dataDir();
|
||||
void version();
|
||||
void spr();
|
||||
void showPeerId();
|
||||
|
||||
// Upload a file from the url
|
||||
// Emit uploadStarted(totalBytes) when the upload begins
|
||||
// Emit uploadChunk(len) on each storageUploadProgress event
|
||||
// Emit uploadCompleted(cid) on storageUploadDone
|
||||
void uploadFile(const QUrl& url);
|
||||
|
||||
// Upload a file from the url
|
||||
// Emit downloadCompleted(cid) on storageDownloadDone
|
||||
void downloadFile(const QString& cid, const QUrl& url);
|
||||
|
||||
// Emit manifestsUpdated
|
||||
void downloadManifest(const QString& cid);
|
||||
|
||||
// Download all the manifests and notify
|
||||
// Emit manifestsUpdated
|
||||
void downloadManifests();
|
||||
void space();
|
||||
LogosResult init(const QString& configJson);
|
||||
void updateLogLevel(const QString& logLevel);
|
||||
void status(StorageStatus status);
|
||||
|
||||
// Call space from the Storage Module
|
||||
// Emit spaceUpdated to refresh the widget
|
||||
void refreshSpace();
|
||||
|
||||
// Save the user config passed in parameter
|
||||
// into the user config json.
|
||||
@ -120,8 +146,6 @@ class StorageBackend : public QObject {
|
||||
//
|
||||
// On success, the status will be set to Stopped.
|
||||
//
|
||||
// Emit initCompleted on success.
|
||||
// Emit initFailed on failure.
|
||||
void reloadIfChanged(const QString& configJson);
|
||||
|
||||
// Enables the upnp in the config
|
||||
@ -130,6 +154,7 @@ class StorageBackend : public QObject {
|
||||
|
||||
// Enables the net external in the config
|
||||
// and re-create a context with the new configuration
|
||||
// Emit natExtConfigCompleted
|
||||
void enableNatExtConfig(int tcpPort);
|
||||
|
||||
// This method will ensure that the node is ready to be used.
|
||||
@ -146,20 +171,50 @@ class StorageBackend : public QObject {
|
||||
// Emit nodeIsntUp(error) on failure
|
||||
void checkNodeIsUp();
|
||||
|
||||
// Fetch multiple data for the widgets: manifests, debug..
|
||||
void fetchWidgetsData();
|
||||
|
||||
signals:
|
||||
// Used to start the Storage Module
|
||||
// if the onboarding is already done
|
||||
void ready();
|
||||
|
||||
// Used in StartNode component to detect
|
||||
// success in the onboarding.
|
||||
void startCompleted();
|
||||
|
||||
// Used in StartNode component to detect
|
||||
// failure in the onboarding.
|
||||
void startFailed(const QString& error);
|
||||
|
||||
// Refresh the node state indicator
|
||||
void statusChanged();
|
||||
|
||||
// Refresh the debug logs panel.
|
||||
void debugLogsChanged();
|
||||
|
||||
// Used in the shutdown process
|
||||
void stopCompleted();
|
||||
void cidChanged();
|
||||
void uploadProgressChanged();
|
||||
void uploadStatusChanged();
|
||||
void manifestsChanged();
|
||||
void quotaChanged();
|
||||
void initCompleted();
|
||||
|
||||
// Used to refresh the disk widgets
|
||||
void spaceUpdated(qlonglong total, qlonglong used);
|
||||
|
||||
// Emitted when an upload starts, with the total file size
|
||||
void uploadStarted(qint64 totalBytes);
|
||||
|
||||
// Emitted for each chunk received during upload
|
||||
void uploadChunk(qint64 len);
|
||||
|
||||
// Used to refresh the Manifests table
|
||||
void manifestsUpdated(const QVariantList& manifests);
|
||||
|
||||
// Used in the on boarding to detect success
|
||||
void natExtConfigCompleted();
|
||||
|
||||
void uploadCompleted(const QString& cid);
|
||||
void downloadCompleted(const QString& cid);
|
||||
|
||||
// Display a toast message on error
|
||||
void error(const QString& message);
|
||||
|
||||
// Emitted when the node port is reachable from the internet
|
||||
@ -168,26 +223,38 @@ 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:
|
||||
// Update the status
|
||||
// Emit statusUpdated if the status was different from the previous status
|
||||
void setStatus(StorageStatus newStatus);
|
||||
void peerConnect(const QString& peerId);
|
||||
void debug(const QString& log);
|
||||
|
||||
// Display debug (or message) in the terminal and
|
||||
// add it to the debugLogs to make it accessible
|
||||
// from the debug panel.
|
||||
// Default level is debug, can be "warning" to display warning
|
||||
// messages.
|
||||
void debug(const QString& log, const QString& level = "debug");
|
||||
|
||||
// Display log and add it to debugLogs
|
||||
// Emit error(message)
|
||||
void reportError(const QString& message);
|
||||
|
||||
// Logos related variables
|
||||
LogosAPI* m_logosAPI;
|
||||
LogosModules* m_logos;
|
||||
|
||||
// Status of the Storage Module
|
||||
StorageStatus m_status;
|
||||
|
||||
// List of debug logs displayed to the application.
|
||||
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;
|
||||
|
||||
// Internal configuration object. It can be updated by
|
||||
// upnp or port forwarning methods.
|
||||
QJsonDocument m_config;
|
||||
};
|
||||
|
||||
@ -26,19 +26,6 @@ QWidget* StorageUIPlugin::createWidget(LogosAPI* logosAPI) {
|
||||
|
||||
qDebug() << "StorageUIPlugin: Loading settings...";
|
||||
|
||||
// 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 file:" << settings.fileName();
|
||||
// qDebug() << "StorageUIPlugin: onboardingCompleted=" << onboardingCompleted;
|
||||
// qDebug() << "StorageUIPlugin: dataDir=" << dataDir;
|
||||
// qDebug() << "StorageUIPlugin: discoveryPort=" << discoveryPort;
|
||||
// qDebug() << "StorageUIPlugin: tcpPort=" << tcpPort;
|
||||
|
||||
// Always load Main.qml — QML handles navigation (onboarding vs startNode)
|
||||
StorageBackend* backend = new StorageBackend(logosAPI, quickWidget);
|
||||
|
||||
@ -54,18 +41,10 @@ QWidget* StorageUIPlugin::createWidget(LogosAPI* logosAPI) {
|
||||
|
||||
root->setProperty("backend", QVariant::fromValue(static_cast<QObject*>(backend)));
|
||||
|
||||
// Here we emit an event ready because
|
||||
// if the onboarding is already done,
|
||||
// the backend can be init using onReady subscription
|
||||
backend->ready();
|
||||
// Storage init is done in the QML
|
||||
// Build config from settings if onboarding was done, otherwise use empty config
|
||||
// QString configJson = StorageBackend::getUserConfig();
|
||||
// qDebug() << "UserConfig" << StorageBackend::getUserConfigPath();
|
||||
// qDebug() << "configJson" << configJson;
|
||||
|
||||
// LogosResult result = backend->init(configJson);
|
||||
|
||||
// if (!result.success) {
|
||||
// qWarning() << "StorageUIPlugin: Failed to init backend:" << result.getError();
|
||||
// }
|
||||
|
||||
return quickWidget;
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import Logos.Controls
|
||||
LogosStorageLayout {
|
||||
id: root
|
||||
|
||||
property var backend: null
|
||||
property var backend: MockBackend
|
||||
|
||||
signal back
|
||||
signal completed
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
56
src/qml/ArcGauge.qml
Normal file
56
src/qml/ArcGauge.qml
Normal file
@ -0,0 +1,56 @@
|
||||
import QtQuick
|
||||
import Logos.Theme
|
||||
|
||||
// Reusable arc gauge — same visual style as the Nothing OS ring widgets.
|
||||
//
|
||||
// Usage:
|
||||
// ArcGauge {
|
||||
// anchors.fill: parent
|
||||
// fraction: 0.65
|
||||
// fillColor: Theme.palette.success
|
||||
// }
|
||||
Canvas {
|
||||
id: root
|
||||
|
||||
// 0.0 – 1.0 (clamped internally)
|
||||
property real fraction: 0.0
|
||||
property color trackColor: Theme.palette.textMuted
|
||||
property color fillColor: Theme.palette.text
|
||||
property real arcRadius: 46
|
||||
property real arcWidth: 8
|
||||
|
||||
onFractionChanged: requestPaint()
|
||||
onTrackColorChanged: requestPaint()
|
||||
onFillColorChanged: requestPaint()
|
||||
|
||||
Component.onCompleted: requestPaint()
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d")
|
||||
ctx.reset()
|
||||
|
||||
var cx = width / 2
|
||||
var cy = height / 2
|
||||
var startRad = 130 * Math.PI / 180
|
||||
var totalRad = 280 * Math.PI / 180
|
||||
|
||||
// ── Track (full arc, muted) ───────────────────────────────────────────
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, root.arcRadius, startRad, startRad + totalRad)
|
||||
ctx.strokeStyle = root.trackColor.toString()
|
||||
ctx.lineWidth = root.arcWidth
|
||||
ctx.lineCap = "round"
|
||||
ctx.stroke()
|
||||
|
||||
// ── Fill (proportional) ───────────────────────────────────────────────
|
||||
var f = Math.min(Math.max(root.fraction, 0.0), 1.0)
|
||||
if (f > 0) {
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, root.arcRadius, startRad, startRad + totalRad * f)
|
||||
ctx.strokeStyle = root.fillColor.toString()
|
||||
ctx.lineWidth = root.arcWidth
|
||||
ctx.lineCap = "round"
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/qml/ArcWidget.qml
Normal file
46
src/qml/ArcWidget.qml
Normal file
@ -0,0 +1,46 @@
|
||||
import QtQuick
|
||||
import Logos.Theme
|
||||
|
||||
// Reusable ring widget — Rectangle + ArcGauge + content overlay.
|
||||
//
|
||||
// Usage:
|
||||
// ArcWidget {
|
||||
// fraction: 0.65
|
||||
// fillColor: Theme.palette.success
|
||||
//
|
||||
// ColumnLayout { anchors.centerIn: parent; ... }
|
||||
// }
|
||||
//
|
||||
// Children are placed inside an overlay Item that fills the widget,
|
||||
// so anchors such as `anchors.centerIn: parent` work as expected.
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
width: 140
|
||||
height: 140
|
||||
radius: 14
|
||||
color: Theme.palette.backgroundSecondary
|
||||
border.color: Theme.palette.borderSecondary
|
||||
border.width: 1
|
||||
|
||||
// ── Arc properties ────────────────────────────────────────────────────────
|
||||
property real fraction: 0.0
|
||||
property color fillColor: Theme.palette.text
|
||||
property color trackColor: Theme.palette.textMuted
|
||||
|
||||
// ── Content slot ──────────────────────────────────────────────────────────
|
||||
// Children declared inside ArcWidget { … } land here, on top of the arc.
|
||||
default property alias content: overlay.data
|
||||
|
||||
ArcGauge {
|
||||
anchors.fill: parent
|
||||
fraction: root.fraction
|
||||
trackColor: root.trackColor
|
||||
fillColor: root.fillColor
|
||||
}
|
||||
|
||||
Item {
|
||||
id: overlay
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
@ -119,6 +119,8 @@ target_link_libraries(appqml PRIVATE
|
||||
)
|
||||
|
||||
# Define the qml module and the StorageBackend sources.
|
||||
set_source_files_properties(MockBackend.qml PROPERTIES QML_SINGLETON TRUE)
|
||||
|
||||
qt_add_qml_module(appqml
|
||||
URI StorageBackend
|
||||
VERSION 1.0
|
||||
@ -138,12 +140,31 @@ 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
|
||||
Widgets.qml
|
||||
DebugPanel.qml
|
||||
Utils.js
|
||||
MockBackend.qml
|
||||
)
|
||||
|
||||
# Set up QML module directory for runtime
|
||||
|
||||
92
src/qml/DebugPanel.qml
Normal file
92
src/qml/DebugPanel.qml
Normal file
@ -0,0 +1,92 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
// qmllint disable unqualified
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property var backend: MockBackend
|
||||
property bool running: false
|
||||
|
||||
color: Theme.palette.backgroundElevated
|
||||
border.color: Theme.palette.borderSecondary
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 10
|
||||
Layout.topMargin: 6
|
||||
Layout.bottomMargin: 4
|
||||
spacing: 6
|
||||
|
||||
LogosStorageButton {
|
||||
text: "Debug"
|
||||
enabled: root.running
|
||||
onClicked: root.backend.logDebugInfo()
|
||||
}
|
||||
LogosStorageButton {
|
||||
text: "Peer ID"
|
||||
enabled: root.running
|
||||
onClicked: root.backend.logPeerId()
|
||||
}
|
||||
LogosStorageButton {
|
||||
text: "Data dir"
|
||||
enabled: root.running
|
||||
onClicked: root.backend.logDataDir()
|
||||
}
|
||||
LogosStorageButton {
|
||||
text: "SPR"
|
||||
enabled: root.running
|
||||
onClicked: root.backend.logSpr()
|
||||
}
|
||||
LogosStorageButton {
|
||||
text: "Version"
|
||||
enabled: root.running
|
||||
onClicked: root.backend.logVersion()
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: Theme.palette.borderSecondary
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: logFlick
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
contentWidth: width
|
||||
contentHeight: debugText.implicitHeight
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
src/qml/DeleteIcon.qml
Normal file
6
src/qml/DeleteIcon.qml
Normal file
@ -0,0 +1,6 @@
|
||||
import QtQuick
|
||||
|
||||
// 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]
|
||||
}
|
||||
52
src/qml/DiskWidget.qml
Normal file
52
src/qml/DiskWidget.qml
Normal file
@ -0,0 +1,52 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
import "Utils.js" as Utils
|
||||
|
||||
ArcWidget {
|
||||
id: root
|
||||
|
||||
property var backend: MockBackend
|
||||
property double total: 0
|
||||
property double used: 0
|
||||
|
||||
fraction: root.total > 0 ? Math.min(root.used / root.total, 1.0) : 0
|
||||
|
||||
function refreshSpace() {
|
||||
let space = root.backend.space()
|
||||
root.total = space.total
|
||||
root.used = space.used
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.backend
|
||||
|
||||
function onSpaceUpdated(total, used) {
|
||||
root.total = total
|
||||
root.used = used
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 2
|
||||
|
||||
LogosText {
|
||||
text: root.total > 0 ? Utils.formatBytes(root.used) : "—"
|
||||
font.pixelSize: 15
|
||||
font.bold: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
LogosText {
|
||||
text: root.total > 0 ? "/ " + Utils.formatBytes(root.total) : "STORAGE"
|
||||
font.pixelSize: 9
|
||||
color: Theme.palette.textTertiary
|
||||
font.letterSpacing: 1.3
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
12
src/qml/DownloadIcon.qml
Normal file
12
src/qml/DownloadIcon.qml
Normal file
@ -0,0 +1,12 @@
|
||||
import QtQuick
|
||||
|
||||
// Downward arrow — download (mirrored UploadIcon)
|
||||
DotIcon {
|
||||
pattern: [
|
||||
0, 0, 1, 0, 0,
|
||||
0, 0, 1, 0, 0,
|
||||
1, 1, 1, 1, 1,
|
||||
0, 1, 1, 1, 0,
|
||||
0, 0, 1, 0, 0
|
||||
]
|
||||
}
|
||||
@ -1,31 +1,45 @@
|
||||
import QtQuick
|
||||
import Logos.Theme
|
||||
|
||||
Item {
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property var backend: MockBackend
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
57
src/qml/JsonEditor.qml
Normal file
57
src/qml/JsonEditor.qml
Normal file
@ -0,0 +1,57 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Logos.Theme
|
||||
|
||||
// Reusable JSON editor with live validation.
|
||||
// Usage:
|
||||
// JsonEditor {
|
||||
// id: editor
|
||||
// Layout.fillWidth: true
|
||||
// Layout.fillHeight: true
|
||||
// }
|
||||
// // Load content (e.g. when a popup opens):
|
||||
// editor.load(backend.configJson() || "{}")
|
||||
// // Read back:
|
||||
// editor.text // current text
|
||||
// editor.isValid // false when JSON.parse would throw
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property alias text: jsonArea.text
|
||||
property bool isValid: true
|
||||
|
||||
Component.onCompleted: root.validate()
|
||||
|
||||
color: Theme.palette.backgroundElevated
|
||||
radius: 8
|
||||
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)
|
||||
isValid = true
|
||||
} catch (e) {
|
||||
isValid = false
|
||||
}
|
||||
}
|
||||
|
||||
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 {}
|
||||
onTextChanged: root.validate()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -5,8 +5,6 @@ import Logos.Theme
|
||||
Rectangle {
|
||||
id: root
|
||||
color: Theme.palette.background
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: 600
|
||||
implicitHeight: 400
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Logos.Theme
|
||||
|
||||
// qmllint disable unqualified
|
||||
TextField {
|
||||
id: root
|
||||
|
||||
@ -11,6 +12,7 @@ TextField {
|
||||
color: isValid ? Theme.palette.text : Theme.palette.error
|
||||
selectByMouse: true
|
||||
background: Rectangle {
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.palette.backgroundSecondary
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtCore
|
||||
import Logos.Theme
|
||||
|
||||
@ -22,24 +23,14 @@ Item {
|
||||
id: root
|
||||
implicitWidth: 800
|
||||
implicitHeight: 800
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
property var backend: mockBackend
|
||||
property var backend: MockBackend
|
||||
|
||||
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.
|
||||
@ -168,46 +159,4 @@ Item {
|
||||
anchors.bottomMargin: Theme.spacing.medium
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: mockBackend
|
||||
|
||||
readonly property bool isMock: true
|
||||
property int status
|
||||
|
||||
signal startCompleted
|
||||
signal startFailed
|
||||
signal stopCompleted
|
||||
signal initCompleted
|
||||
signal ready
|
||||
signal error
|
||||
signal natExtConfigCompleted
|
||||
signal nodeIsUp
|
||||
signal nodeIsntUp
|
||||
|
||||
function start() {
|
||||
console.log("mock start called")
|
||||
}
|
||||
|
||||
function saveUserConfig() {}
|
||||
|
||||
function loadUserConfig() {}
|
||||
|
||||
function reloadIfChanged() {}
|
||||
|
||||
function enableUpnpConfig() {}
|
||||
|
||||
function enableNatExtConfig() {
|
||||
natExtConfigCompleted()
|
||||
}
|
||||
|
||||
function saveCurrentConfig() {}
|
||||
|
||||
function stop() {}
|
||||
|
||||
function checkNodeIsUp() {}
|
||||
|
||||
function guessResolution() {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
282
src/qml/ManifestTable.qml
Normal file
282
src/qml/ManifestTable.qml
Normal file
@ -0,0 +1,282 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Dialogs
|
||||
import QtQuick.Layouts
|
||||
import QtCore
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
import "Utils.js" as Utils
|
||||
|
||||
// qmllint disable unqualified
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property var backend: MockBackend
|
||||
property bool running: false
|
||||
property var manifests: []
|
||||
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
FileDialog {
|
||||
id: saveDialog
|
||||
|
||||
property var pendingManifest: null
|
||||
|
||||
fileMode: FileDialog.SaveFile
|
||||
onAccepted: {
|
||||
if (pendingManifest) {
|
||||
root.backend.downloadFile(pendingManifest.cid, selectedFile)
|
||||
pendingManifest = null
|
||||
}
|
||||
}
|
||||
onRejected: pendingManifest = null
|
||||
}
|
||||
|
||||
|
||||
Connections {
|
||||
target: root.backend
|
||||
|
||||
onManifestsUpdated: function (manifests) {
|
||||
root.manifests = manifests
|
||||
}
|
||||
}
|
||||
|
||||
LogosText {
|
||||
text: "MANIFESTS"
|
||||
font.pixelSize: 11
|
||||
color: Theme.palette.textTertiary
|
||||
font.letterSpacing: 1.5
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
LogosTextField {
|
||||
id: cidInput
|
||||
Layout.fillWidth: true
|
||||
height: getManifestBtn.implicitHeight
|
||||
placeholderText: "Enter CID to download manifest…"
|
||||
isValid: true
|
||||
}
|
||||
|
||||
LogosStorageButton {
|
||||
id: getManifestBtn
|
||||
text: "GET MANIFEST"
|
||||
enabled: root.running && cidInput.text.length > 0
|
||||
onClicked: {
|
||||
root.backend.downloadManifest(cidInput.text)
|
||||
cidInput.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 30
|
||||
color: Theme.palette.backgroundElevated
|
||||
radius: 4
|
||||
|
||||
Row {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 240
|
||||
color: Theme.palette.background
|
||||
border.color: Theme.palette.borderSecondary
|
||||
border.width: 1
|
||||
radius: 4
|
||||
clip: true
|
||||
|
||||
ListView {
|
||||
id: manifestList
|
||||
anchors.fill: parent
|
||||
model: root.manifests
|
||||
clip: true
|
||||
|
||||
delegate: Rectangle {
|
||||
id: delegateItem
|
||||
width: manifestList.width
|
||||
height: 36
|
||||
color: index % 2
|
||||
=== 0 ? Theme.palette.background : Theme.palette.backgroundSecondary
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 8
|
||||
|
||||
Text {
|
||||
width: 160
|
||||
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
|
||||
}
|
||||
}
|
||||
Text {
|
||||
width: 130
|
||||
text: modelData.filename
|
||||
color: Theme.palette.textSecondary
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
width: 90
|
||||
text: modelData.mimetype
|
||||
color: Theme.palette.textSecondary
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
width: 80
|
||||
text: Utils.formatBytes(parseInt(modelData.datasetSize))
|
||||
color: Theme.palette.textSecondary
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 6
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
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
|
||||
|
||||
DownloadIcon {
|
||||
anchors.centerIn: parent
|
||||
dotColor: Theme.palette.text
|
||||
dotSize: 3
|
||||
dotSpacing: 1
|
||||
}
|
||||
HoverHandler {
|
||||
id: dlHover
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.running
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
saveDialog.pendingManifest = modelData
|
||||
saveDialog.currentFile = StandardPaths.writableLocation(
|
||||
StandardPaths.HomeLocation)
|
||||
+ "/" + (modelData.filename
|
||||
|| modelData.cid
|
||||
|| "download")
|
||||
saveDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
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
|
||||
|
||||
DeleteIcon {
|
||||
anchors.centerIn: parent
|
||||
dotColor: Theme.palette.error
|
||||
dotSize: 3
|
||||
dotSpacing: 1
|
||||
}
|
||||
HoverHandler {
|
||||
id: rmHover
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.running
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData.cid.length > 0) {
|
||||
root.backend.remove(modelData.cid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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]
|
||||
dotColor: Theme.palette.textMuted
|
||||
activeOpacity: 0.25
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
LogosText {
|
||||
text: "No manifests yet"
|
||||
color: Theme.palette.textMuted
|
||||
font.pixelSize: 12
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
47
src/qml/MockBackend.qml
Normal file
47
src/qml/MockBackend.qml
Normal file
@ -0,0 +1,47 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
readonly property bool isMock: true
|
||||
property int status: 0
|
||||
property string debugLogs: "Hello!"
|
||||
|
||||
signal ready
|
||||
signal startCompleted
|
||||
signal startFailed(string error)
|
||||
signal error(string message)
|
||||
signal natExtConfigCompleted
|
||||
signal nodeIsUp
|
||||
signal nodeIsntUp(string reason)
|
||||
signal peersUpdated(int count)
|
||||
signal uploadStarted(real totalBytes)
|
||||
signal uploadChunk(real len)
|
||||
signal uploadCompleted(string cid)
|
||||
signal downloadCompleted(string cid)
|
||||
signal spaceUpdated(real total, real used)
|
||||
signal manifestsUpdated(var manifests)
|
||||
signal stopCompleted
|
||||
|
||||
function start() { status = 2 }
|
||||
function stop() { status = 0 }
|
||||
function destroy() {}
|
||||
function checkNodeIsUp() {}
|
||||
function fetchWidgetsData() {}
|
||||
function uploadFile(url) {}
|
||||
function downloadFile(cid, url) {}
|
||||
function downloadManifest(cid) {}
|
||||
function downloadManifests() {}
|
||||
function remove(cid) {}
|
||||
function logDebugInfo() {}
|
||||
function logPeerId() {}
|
||||
function logDataDir() {}
|
||||
function logSpr() {}
|
||||
function logVersion() {}
|
||||
function saveUserConfig(json) {}
|
||||
function saveCurrentConfig() {}
|
||||
function loadUserConfig() {}
|
||||
function reloadIfChanged(json) {}
|
||||
function enableUpnpConfig() {}
|
||||
function enableNatExtConfig(tcpPort) { natExtConfigCompleted() }
|
||||
function configJson() { return "{}" }
|
||||
}
|
||||
@ -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"
|
||||
|
||||
147
src/qml/NodeHeader.qml
Normal file
147
src/qml/NodeHeader.qml
Normal file
@ -0,0 +1,147 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
// qmllint disable unqualified
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property var backend: MockBackend
|
||||
property bool nodeIsUp: false
|
||||
property bool blinkOn: false
|
||||
|
||||
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
|
||||
|
||||
signal settingsRequested()
|
||||
|
||||
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.backend.status !== root.running)
|
||||
return Theme.palette.textMuted
|
||||
return root.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.backend.status !== root.running)
|
||||
return Theme.palette.textMuted
|
||||
return root.nodeIsUp ? Theme.palette.success : Theme.palette.error
|
||||
}
|
||||
opacity: root.backend.status === root.running
|
||||
? (root.blinkOn ? 1.0 : 0.15) : 1.0
|
||||
}
|
||||
|
||||
LogosText {
|
||||
text: {
|
||||
switch (root.backend.status) {
|
||||
case root.stopped: return "Stopped"
|
||||
case root.starting: return "Starting…"
|
||||
case root.running: return "Running"
|
||||
case root.stopping: return "Stopping…"
|
||||
case root.destroyed: return "Not initialised"
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
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: root.settingsRequested()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -6,7 +6,7 @@ import Logos.Controls
|
||||
LogosStorageLayout {
|
||||
id: root
|
||||
|
||||
property var backend: mockBackend
|
||||
property var backend: MockBackend
|
||||
|
||||
signal back
|
||||
signal completed(bool upnpEnabled)
|
||||
@ -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
|
||||
@ -152,9 +156,4 @@ LogosStorageLayout {
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: mockBackend
|
||||
|
||||
function enableUpnpConfig() {}
|
||||
}
|
||||
}
|
||||
|
||||
42
src/qml/PeersWidget.qml
Normal file
42
src/qml/PeersWidget.qml
Normal file
@ -0,0 +1,42 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
ArcWidget {
|
||||
id: root
|
||||
|
||||
property var backend: MockBackend
|
||||
property int peers: 0
|
||||
property int maxPeers: 20
|
||||
|
||||
fraction: root.maxPeers > 0 ? Math.min(root.peers / root.maxPeers, 1.0) : 0
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 2
|
||||
|
||||
LogosText {
|
||||
text: root.peers
|
||||
font.pixelSize: 22
|
||||
font.bold: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
LogosText {
|
||||
text: "PEERS"
|
||||
font.pixelSize: 9
|
||||
color: Theme.palette.textTertiary
|
||||
font.letterSpacing: 1.3
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.backend
|
||||
|
||||
function onPeersUpdated(peers) {
|
||||
root.peers = peers
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/qml/PlayIcon.qml
Normal file
12
src/qml/PlayIcon.qml
Normal file
@ -0,0 +1,12 @@
|
||||
import QtQuick
|
||||
|
||||
// Right-pointing triangle — play / start
|
||||
DotIcon {
|
||||
pattern: [
|
||||
1, 0, 0, 0, 0,
|
||||
1, 1, 0, 0, 0,
|
||||
1, 1, 1, 0, 0,
|
||||
1, 1, 0, 0, 0,
|
||||
1, 0, 0, 0, 0
|
||||
]
|
||||
}
|
||||
@ -8,7 +8,7 @@ LogosStorageLayout {
|
||||
|
||||
property var tcpPort: 0
|
||||
property bool loading: false
|
||||
property var backend: mockBackend
|
||||
property var backend: MockBackend
|
||||
|
||||
signal back
|
||||
signal completed(int port)
|
||||
@ -50,6 +50,7 @@ LogosStorageLayout {
|
||||
|
||||
LogosTextField {
|
||||
Layout.fillWidth: true
|
||||
height: 60
|
||||
id: tcpPortTextField
|
||||
placeholderText: "Enter the TCP port"
|
||||
text: root.tcpPort
|
||||
@ -95,9 +96,4 @@ LogosStorageLayout {
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: mockBackend
|
||||
|
||||
function enableNatExtConfig(port) {}
|
||||
}
|
||||
}
|
||||
|
||||
11
src/qml/SettingsIcon.qml
Normal file
11
src/qml/SettingsIcon.qml
Normal file
@ -0,0 +1,11 @@
|
||||
import QtQuick
|
||||
|
||||
// Gear / cog icon — 4 cardinal teeth + ring with center hole
|
||||
// . . ●. .
|
||||
// . ● ● ● .
|
||||
// ● ● . ● ●
|
||||
// . ● ● ● .
|
||||
// . . ● . .
|
||||
DotIcon {
|
||||
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]
|
||||
}
|
||||
75
src/qml/SettingsPopup.qml
Normal file
75
src/qml/SettingsPopup.qml
Normal file
@ -0,0 +1,75 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
property var backend: MockBackend
|
||||
|
||||
modal: true
|
||||
width: 520
|
||||
height: 400
|
||||
anchors.centerIn: Overlay.overlay
|
||||
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
|
||||
border.width: 1
|
||||
radius: 14
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
LogosText {
|
||||
text: "Configuration"
|
||||
font.pixelSize: Theme.typography.titleText
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
LogosText {
|
||||
text: "Edit the JSON configuration below, then click Save."
|
||||
font.pixelSize: Theme.typography.primaryText
|
||||
color: Theme.palette.textSecondary
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
JsonEditor {
|
||||
id: jsonEditor
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
LogosStorageButton {
|
||||
text: "Cancel"
|
||||
onClicked: root.close()
|
||||
}
|
||||
|
||||
LogosStorageButton {
|
||||
text: "Save"
|
||||
variant: "success"
|
||||
enabled: jsonEditor.isValid
|
||||
onClicked: {
|
||||
root.backend.saveUserConfig(jsonEditor.text)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,12 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Logos.Controls
|
||||
import Logos.Theme
|
||||
|
||||
LogosStorageLayout {
|
||||
id: root
|
||||
|
||||
property var backend: mockBackend
|
||||
property var backend: MockBackend
|
||||
property string status: ""
|
||||
property string title: "Starting your node"
|
||||
property string resolution: ""
|
||||
@ -132,19 +131,4 @@ LogosStorageLayout {
|
||||
onTriggered: root.onNodeStarted()
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: mockBackend
|
||||
|
||||
readonly property bool isMock: true
|
||||
|
||||
signal startCompleted
|
||||
signal startFailed(string error)
|
||||
signal nodeIsUp
|
||||
signal nodeIsntUp(string reason)
|
||||
|
||||
function checkNodeIsUp() {}
|
||||
function stop() {}
|
||||
function saveCurrentConfig() {}
|
||||
function start() {}
|
||||
}
|
||||
}
|
||||
|
||||
12
src/qml/StopIcon.qml
Normal file
12
src/qml/StopIcon.qml
Normal file
@ -0,0 +1,12 @@
|
||||
import QtQuick
|
||||
|
||||
// Filled square — stop
|
||||
DotIcon {
|
||||
pattern: [
|
||||
0, 0, 0, 0, 0,
|
||||
0, 1, 1, 1, 0,
|
||||
0, 1, 1, 1, 0,
|
||||
0, 1, 1, 1, 0,
|
||||
0, 0, 0, 0, 0
|
||||
]
|
||||
}
|
||||
14
src/qml/StorageIcon.qml
Normal file
14
src/qml/StorageIcon.qml
Normal file
@ -0,0 +1,14 @@
|
||||
import QtQuick
|
||||
|
||||
// Ring / node pattern — used in the StorageView header
|
||||
DotIcon {
|
||||
pattern: [
|
||||
0, 1, 1, 1, 0,
|
||||
1, 0, 0, 0, 1,
|
||||
1, 0, 1, 0, 1,
|
||||
1, 0, 0, 0, 1,
|
||||
0, 1, 1, 1, 0
|
||||
]
|
||||
dotSize: 7
|
||||
dotSpacing: 5
|
||||
}
|
||||
@ -1,888 +1,115 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Dialogs
|
||||
import QtQuick.Layouts
|
||||
import QtCore
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
// qmllint disable unqualified
|
||||
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 string peerId: ""
|
||||
property string downloadDestination: ""
|
||||
property url downloadCid: ""
|
||||
property string logLevel: ""
|
||||
property var backend: MockBackend
|
||||
property bool showDebug: false
|
||||
property var pendingDownloadManifest: null
|
||||
property url uploadCid: root.backend.cid
|
||||
property url configJson: root.backend.configJson
|
||||
|
||||
function getStatusLabel() {
|
||||
switch (backend.status) {
|
||||
case stopped:
|
||||
return "Logos Storage stopped."
|
||||
case starting:
|
||||
return "Logos Storage is starting..."
|
||||
case running:
|
||||
return "Logos Storage started."
|
||||
case stopping:
|
||||
return "Logos Storage is stopping..."
|
||||
case destroyed:
|
||||
return "Logos Storage is not initialised."
|
||||
}
|
||||
}
|
||||
|
||||
function startStopText() {
|
||||
if (backend.status == running) {
|
||||
return "Stop"
|
||||
}
|
||||
return "Start"
|
||||
}
|
||||
|
||||
function canStartStop() {
|
||||
return backend.status == running || backend.status == stopped
|
||||
}
|
||||
|
||||
function isRunning() {
|
||||
return backend.status == running
|
||||
return backend.status === 2 // StorageBackend.Running
|
||||
}
|
||||
|
||||
Component.onCompleted: root.backend.start()
|
||||
|
||||
QtObject {
|
||||
id: mockBackend
|
||||
|
||||
property var status: root.stopped
|
||||
property var debugLogs: "Hello !"
|
||||
property var configJson: "{}"
|
||||
property string uploadStatus: ""
|
||||
property int uploadProgress: 0
|
||||
property var manifests: []
|
||||
property var quotaMaxBytes: 20 * 1024 * 1024 * 1024 // 20 GB default
|
||||
property var quotaUsedBytes: 0
|
||||
property var quotaReservedBytes: 0
|
||||
|
||||
function start(newConfigJson) {
|
||||
status = root.running
|
||||
Component.onCompleted: function () {
|
||||
if (isRunning()) {
|
||||
root.backend.fetchWidgetsData()
|
||||
} else {
|
||||
root.backend.start()
|
||||
}
|
||||
|
||||
function stop() {
|
||||
status = root.stopped
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB"
|
||||
}
|
||||
|
||||
HealthIndicator {
|
||||
id: health
|
||||
backend: root.backend
|
||||
}
|
||||
|
||||
SettingsPopup {
|
||||
id: settingsPopup
|
||||
backend: root.backend
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+D"
|
||||
onActivated: root.showDebug = !root.showDebug
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
id: mainScroll
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
Text {
|
||||
id: statusTextElement
|
||||
objectName: "status"
|
||||
text: root.getStatusLabel()
|
||||
color: "white"
|
||||
font.pointSize: 20
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 20
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Button {
|
||||
property var isStopped: root.backend.status == root.stopped
|
||||
|
||||
id: startStopButton
|
||||
objectName: "startStopButton"
|
||||
anchors.leftMargin: 50
|
||||
text: root.startStopText()
|
||||
enabled: root.canStartStop()
|
||||
onClicked: isStopped ? root.backend.start(
|
||||
jsonEditor.text) : root.backend.stop()
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: statusTextElement.bottom
|
||||
anchors.topMargin: 10
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
id: cidTextEdit
|
||||
objectName: "cid"
|
||||
color: "white"
|
||||
font.pointSize: 14
|
||||
readOnly: true
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: startStopButton.bottom
|
||||
anchors.topMargin: 10
|
||||
text: root.uploadCid
|
||||
}
|
||||
|
||||
Button {
|
||||
id: openFile
|
||||
text: "Open file"
|
||||
onClicked: fileDialog.open()
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: cidTextEdit.bottom
|
||||
anchors.topMargin: 15
|
||||
enabled: root.isRunning
|
||||
}
|
||||
|
||||
Column {
|
||||
id: uploadProgressColumn
|
||||
anchors.top: openFile.bottom
|
||||
anchors.topMargin: 10
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 300
|
||||
spacing: 5
|
||||
visible: root.backend.uploadProgress > 0
|
||||
|
||||
ProgressBar {
|
||||
width: parent.width
|
||||
value: root.backend.uploadProgress / 100.0
|
||||
|
||||
background: Rectangle {
|
||||
color: "#333333"
|
||||
radius: 3
|
||||
implicitWidth: 300
|
||||
implicitHeight: 6
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
implicitWidth: 300
|
||||
implicitHeight: 6
|
||||
|
||||
Rectangle {
|
||||
width: parent.width * control.visualPosition
|
||||
height: parent.height
|
||||
radius: 3
|
||||
color: "#4CAF50"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.backend.uploadStatus
|
||||
color: "#888888"
|
||||
font.pixelSize: 10
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
// TextField {
|
||||
// id: peerIdField
|
||||
// placeholderText: "Enter the peer Id"
|
||||
// placeholderTextColor: "#999999"
|
||||
// color: "#000000"
|
||||
// selectByMouse: true
|
||||
// text: root.peerId
|
||||
// onTextChanged: root.peerId = text
|
||||
// anchors.top: uploadProgressColumn.bottom
|
||||
// anchors.topMargin: 50
|
||||
// anchors.horizontalCenter: parent.horizontalCenter
|
||||
// }
|
||||
|
||||
// Button {
|
||||
// id: peerConnectButton
|
||||
// objectName: "peerConnectButton"
|
||||
// text: "Peer connect"
|
||||
// onClicked: root.backend.tryPeerConnect(root.peerId)
|
||||
// anchors.top: peerIdField.bottom
|
||||
// anchors.horizontalCenter: parent.horizontalCenter
|
||||
// enabled: root.isRunning
|
||||
// anchors.topMargin: 10
|
||||
// }
|
||||
Button {
|
||||
id: debugButton
|
||||
objectName: "debugButton"
|
||||
text: "Debug"
|
||||
onClicked: root.backend.tryDebug()
|
||||
anchors.top: uploadProgressColumn.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
enabled: root.isRunning
|
||||
anchors.topMargin: 50
|
||||
}
|
||||
|
||||
Button {
|
||||
id: peerIdButton
|
||||
objectName: "peerIdButton"
|
||||
text: "Peer Id"
|
||||
onClicked: root.backend.showPeerId()
|
||||
anchors.top: uploadProgressColumn.bottom
|
||||
anchors.right: debugButton.left
|
||||
enabled: root.isRunning
|
||||
anchors.topMargin: 50
|
||||
}
|
||||
|
||||
Button {
|
||||
id: dataDirButton
|
||||
objectName: "dataDirButton"
|
||||
text: "Data dir"
|
||||
onClicked: root.backend.dataDir()
|
||||
anchors.top: uploadProgressColumn.bottom
|
||||
anchors.right: peerIdButton.left
|
||||
enabled: root.isRunning
|
||||
anchors.topMargin: 50
|
||||
}
|
||||
|
||||
Button {
|
||||
id: sprButton
|
||||
objectName: "sprButton"
|
||||
text: "SPR"
|
||||
onClicked: root.backend.spr()
|
||||
anchors.top: uploadProgressColumn.bottom
|
||||
anchors.left: debugButton.right
|
||||
enabled: root.isRunning
|
||||
anchors.topMargin: 50
|
||||
}
|
||||
|
||||
Button {
|
||||
id: versionButton
|
||||
objectName: "versionButton"
|
||||
text: "Version"
|
||||
onClicked: root.backend.version()
|
||||
anchors.top: uploadProgressColumn.bottom
|
||||
anchors.left: sprButton.right
|
||||
enabled: root.isRunning
|
||||
anchors.topMargin: 50
|
||||
}
|
||||
|
||||
// TextField {
|
||||
// id: cidDownloadField
|
||||
// placeholderTextColor: "#999999"
|
||||
// placeholderText: "Enter the cid to download"
|
||||
// color: "black"
|
||||
// // text: root.downloadCid
|
||||
// onTextChanged: root.downloadCid = text
|
||||
// anchors.top: debugButton.bottom
|
||||
// anchors.topMargin: 50
|
||||
// anchors.horizontalCenter: parent.horizontalCenter
|
||||
// }
|
||||
|
||||
// Button {
|
||||
// id: openFile2
|
||||
// text: "Open file"
|
||||
// onClicked: fileDialog2.open()
|
||||
// anchors.horizontalCenter: parent.horizontalCenter
|
||||
// anchors.top: cidDownloadField.bottom
|
||||
// anchors.topMargin: 15
|
||||
// enabled: root.isRunning
|
||||
// }
|
||||
|
||||
// Button {
|
||||
// id: cidDownloadButton
|
||||
// objectName: "cidDownloadButton"
|
||||
// text: "Download"
|
||||
// onClicked: root.backend.tryDownloadFile(root.downloadCid,
|
||||
// root.downloadDestination)
|
||||
// anchors.top: openFile2.bottom
|
||||
// anchors.horizontalCenter: parent.horizontalCenter
|
||||
// enabled: root.isRunning
|
||||
// anchors.topMargin: 10
|
||||
// }
|
||||
|
||||
// Button {
|
||||
// id: existsButton
|
||||
// objectName: "existsButton"
|
||||
// text: "Exists"
|
||||
// onClicked: root.backend.exists(root.downloadCid)
|
||||
// anchors.top: openFile2.bottom
|
||||
// anchors.left: cidDownloadButton.right
|
||||
// enabled: root.isRunning
|
||||
// anchors.topMargin: 10
|
||||
// }
|
||||
|
||||
// Button {
|
||||
// id: fetchButton
|
||||
// objectName: "fetchButton"
|
||||
// text: "Fetch"
|
||||
// onClicked: root.backend.fetch(root.downloadCid)
|
||||
// anchors.top: openFile2.bottom
|
||||
// anchors.left: existsButton.right
|
||||
// enabled: root.isRunning
|
||||
// anchors.topMargin: 10
|
||||
// }
|
||||
|
||||
// Button {
|
||||
// id: removeButton
|
||||
// objectName: "removeButton"
|
||||
// text: "Remove"
|
||||
// onClicked: root.backend.remove(root.downloadCid)
|
||||
// anchors.top: openFile2.bottom
|
||||
// anchors.right: cidDownloadButton.left
|
||||
// enabled: root.isRunning
|
||||
// anchors.topMargin: 10
|
||||
// }
|
||||
|
||||
// Button {
|
||||
// id: downloadManifestButton
|
||||
// objectName: "downloadManifestButton"
|
||||
// text: "Download manifest"
|
||||
// onClicked: root.backend.downloadManifest(root.downloadCid)
|
||||
// anchors.top: openFile2.bottom
|
||||
// anchors.right: removeButton.left
|
||||
// enabled: root.isRunning
|
||||
// anchors.topMargin: 10
|
||||
// }
|
||||
|
||||
|
||||
/*Button {
|
||||
id: downloadManifestsButton
|
||||
objectName: "downloadManifestsButton"
|
||||
text: "Manifests"
|
||||
onClicked: root.backend.downloadManifests()
|
||||
anchors.top: cidDownloadButton.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
enabled: root.isRunning
|
||||
anchors.topMargin: 10
|
||||
}*/
|
||||
|
||||
// ── Manifests section ──────────────────────────────────────────────────
|
||||
Text {
|
||||
id: manifestsTitle
|
||||
text: "Manifests"
|
||||
color: "white"
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
anchors.top: versionButton.bottom
|
||||
anchors.topMargin: 30
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
// ── Disk space bar ────────────────────────────────────────────────────
|
||||
Item {
|
||||
id: spaceBarSection
|
||||
anchors.top: manifestsTitle.bottom
|
||||
anchors.topMargin: 10
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
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 reserved: root.backend.quotaReservedBytes
|
||||
|
||||
// No quota configured
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "No quota configured"
|
||||
color: "#555555"
|
||||
font.pixelSize: 11
|
||||
visible: spaceBarSection.total <= 0
|
||||
}
|
||||
|
||||
// Background track
|
||||
Rectangle {
|
||||
id: spaceBarTrack
|
||||
visible: spaceBarSection.total > 0
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: 14
|
||||
radius: 7
|
||||
color: "#2a2a2a"
|
||||
border.color: "#3a3a3a"
|
||||
border.width: 1
|
||||
clip: true
|
||||
|
||||
// Used (green)
|
||||
Rectangle {
|
||||
width: Math.min(
|
||||
parent.width * (spaceBarSection.used / spaceBarSection.total),
|
||||
parent.width)
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: "#4CAF50"
|
||||
}
|
||||
|
||||
// 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)
|
||||
height: parent.height
|
||||
color: "#FF9800"
|
||||
}
|
||||
}
|
||||
|
||||
// Labels
|
||||
Row {
|
||||
visible: spaceBarSection.total > 0
|
||||
anchors.top: spaceBarTrack.bottom
|
||||
anchors.topMargin: 4
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: "Used: " + root.formatBytes(spaceBarSection.used)
|
||||
color: "#4CAF50"
|
||||
font.pixelSize: 10
|
||||
}
|
||||
Text {
|
||||
text: "Reserved: " + root.formatBytes(spaceBarSection.reserved)
|
||||
color: "#FF9800"
|
||||
font.pixelSize: 10
|
||||
}
|
||||
Text {
|
||||
text: "Free: " + root.formatBytes(
|
||||
spaceBarSection.total - spaceBarSection.used - spaceBarSection.reserved)
|
||||
color: "#888888"
|
||||
font.pixelSize: 10
|
||||
}
|
||||
Text {
|
||||
text: "Total: " + root.formatBytes(spaceBarSection.total)
|
||||
color: "#555555"
|
||||
font.pixelSize: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: manifestInputRow
|
||||
spacing: 8
|
||||
anchors.top: spaceBarSection.bottom
|
||||
anchors.topMargin: 16
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
TextField {
|
||||
id: manifestCidField
|
||||
width: 380
|
||||
placeholderText: "Enter CID to download manifest"
|
||||
placeholderTextColor: "#999999"
|
||||
color: "#000000"
|
||||
selectByMouse: true
|
||||
}
|
||||
|
||||
Button {
|
||||
id: addManifestButton
|
||||
text: "Download Manifest"
|
||||
enabled: root.isRunning() && manifestCidField.text.length > 0
|
||||
onClicked: {
|
||||
root.backend.downloadManifest(manifestCidField.text)
|
||||
manifestCidField.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Table header
|
||||
Rectangle {
|
||||
id: manifestTableHeader
|
||||
anchors.top: manifestInputRow.bottom
|
||||
anchors.topMargin: 8
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: parent.width - 40
|
||||
height: 28
|
||||
color: "#222222"
|
||||
radius: 2
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 6
|
||||
|
||||
Text {
|
||||
width: 150
|
||||
text: "CID"
|
||||
color: "#aaaaaa"
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
width: 120
|
||||
text: "Filename"
|
||||
color: "#aaaaaa"
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
width: 85
|
||||
text: "MIME type"
|
||||
color: "#aaaaaa"
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
width: 75
|
||||
text: "Size (bytes)"
|
||||
color: "#aaaaaa"
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
width: 110
|
||||
text: ""
|
||||
color: "#aaaaaa"
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: manifestTableContainer
|
||||
anchors.top: manifestTableHeader.bottom
|
||||
anchors.left: manifestTableHeader.left
|
||||
anchors.right: manifestTableHeader.right
|
||||
height: 280
|
||||
color: "#111111"
|
||||
border.color: "#333333"
|
||||
border.width: 1
|
||||
anchors.bottomMargin: root.showDebug ? debugPanel.height : 0
|
||||
contentWidth: availableWidth
|
||||
clip: true
|
||||
|
||||
ListView {
|
||||
id: manifestListView
|
||||
anchors.fill: parent
|
||||
model: root.backend.manifests
|
||||
clip: true
|
||||
ColumnLayout {
|
||||
width: mainScroll.availableWidth
|
||||
spacing: 0
|
||||
|
||||
delegate: Rectangle {
|
||||
width: manifestListView.width
|
||||
height: 36
|
||||
color: index % 2 === 0 ? "#181818" : "#1e1e1e"
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 6
|
||||
anchors.rightMargin: 4
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
width: 150
|
||||
text: modelData["cid"] ?? ""
|
||||
color: "#dddddd"
|
||||
font.pixelSize: 11
|
||||
font.family: "monospace"
|
||||
elide: Text.ElideMiddle
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
// ToolTip.visible: hovered
|
||||
ToolTip.text: modelData["cid"] ?? ""
|
||||
HoverHandler {}
|
||||
}
|
||||
Text {
|
||||
width: 120
|
||||
text: modelData["filename"] ?? ""
|
||||
color: "#dddddd"
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
width: 85
|
||||
text: modelData["mimetype"] ?? ""
|
||||
color: "#dddddd"
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
width: 75
|
||||
text: modelData["datasetSize"] ?? ""
|
||||
color: "#dddddd"
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Row {
|
||||
spacing: 4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Button {
|
||||
width: 50
|
||||
height: 26
|
||||
text: "↓"
|
||||
enabled: root.isRunning()
|
||||
onClicked: {
|
||||
root.pendingDownloadManifest = modelData
|
||||
var filename = modelData["filename"]
|
||||
|| modelData["cid"] || "download"
|
||||
manifestSaveDialog.currentFile = StandardPaths.writableLocation(
|
||||
StandardPaths.HomeLocation) + "/" + filename
|
||||
manifestSaveDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
width: 50
|
||||
height: 26
|
||||
text: "🗑"
|
||||
enabled: root.isRunning()
|
||||
onClicked: root.backend.remove(
|
||||
modelData["cid"] ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
NodeHeader {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 24
|
||||
Layout.rightMargin: 24
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 20
|
||||
backend: root.backend
|
||||
nodeIsUp: health.nodeIsUp
|
||||
blinkOn: health.blinkOn
|
||||
onSettingsRequested: settingsPopup.open()
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "No manifests yet"
|
||||
color: "#555555"
|
||||
font.pixelSize: 12
|
||||
visible: manifestListView.count === 0
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 24
|
||||
Layout.rightMargin: 24
|
||||
Layout.preferredHeight: 1
|
||||
color: Theme.palette.borderSecondary
|
||||
}
|
||||
|
||||
Widgets {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 24
|
||||
Layout.rightMargin: 24
|
||||
Layout.topMargin: 20
|
||||
backend: root.backend
|
||||
running: root.isRunning()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 24
|
||||
Layout.rightMargin: 24
|
||||
Layout.preferredHeight: 1
|
||||
color: Theme.palette.borderSecondary
|
||||
}
|
||||
|
||||
ManifestTable {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 24
|
||||
Layout.rightMargin: 24
|
||||
Layout.topMargin: 20
|
||||
Layout.bottomMargin: 20
|
||||
backend: root.backend
|
||||
running: root.isRunning()
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 20
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
id: spaceButton
|
||||
objectName: "spaceButton"
|
||||
text: "Space"
|
||||
onClicked: root.backend.space()
|
||||
anchors.top: manifestTableContainer.bottom
|
||||
enabled: root.isRunning
|
||||
anchors.topMargin: 10
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
// ── Log level section ──────────────────────────────────────────────────
|
||||
// TextField {
|
||||
// id: logLevelField
|
||||
// placeholderTextColor: "#999999"
|
||||
// placeholderText: "Enter the log level to download"
|
||||
// color: "black"
|
||||
// // text: root.downloadCid
|
||||
// onTextChanged: root.logLevel = text
|
||||
// anchors.top: manifestTableContainer.bottom
|
||||
// anchors.topMargin: 30
|
||||
// anchors.horizontalCenter: parent.horizontalCenter
|
||||
// }
|
||||
|
||||
// Button {
|
||||
// id: logLevelButton
|
||||
// objectName: "logLevelButton"
|
||||
// text: "Log level"
|
||||
// onClicked: root.backend.updateLogLevel(root.logLevel)
|
||||
// anchors.top: logLevelField.bottom
|
||||
// anchors.horizontalCenter: parent.horizontalCenter
|
||||
// enabled: root.isRunning
|
||||
// anchors.topMargin: 10
|
||||
// }
|
||||
|
||||
// TextEdit {
|
||||
// id: selectableText
|
||||
// anchors.fill: parent
|
||||
// anchors.margins: 10
|
||||
// text: "This text is selectable. You can copy it, but not edit it."
|
||||
// readOnly: true // Makes the text non-editable
|
||||
// selectByMouse: true // Enables selection by mouse drag (often the default for desktop)
|
||||
// // Optional: Change cursor shape to IBeam when hovering
|
||||
// MouseArea {
|
||||
// anchors.fill: parent
|
||||
// cursorShape: Qt.IBeamCursor
|
||||
// acceptedButtons: Qt.NoButton // Allows TextEdit to handle mouse events
|
||||
// }
|
||||
// }
|
||||
|
||||
// Button {
|
||||
// anchors.left: parent.left
|
||||
// anchors.bottom: parent.bottom
|
||||
// objectName: "uploadButton"
|
||||
// text: "Upload"
|
||||
// anchors.bottomMargin: 80
|
||||
// onClicked: root.backend.tryUpload()
|
||||
// }
|
||||
|
||||
// Button {
|
||||
// anchors.left: parent.left
|
||||
// anchors.bottom: parent.bottom
|
||||
// objectName: "finalizeButton"
|
||||
// text: "Finalize"
|
||||
// onClicked: root.backend.tryUploadFinalize()
|
||||
// }
|
||||
// Button {
|
||||
// anchors.left: parent.left
|
||||
// anchors.bottom: parent.bottom
|
||||
// objectName: "uploadFileButton"
|
||||
// text: "Upload file"
|
||||
// onClicked: root.backend.tryUploadFile()
|
||||
// anchors.bottomMargin: 30
|
||||
// }
|
||||
FileDialog {
|
||||
id: fileDialog
|
||||
onAccepted: root.backend.tryUploadFile(fileDialog.selectedFile)
|
||||
}
|
||||
|
||||
FileDialog {
|
||||
id: fileDialog2
|
||||
fileMode: FileDialog.SaveFile
|
||||
onAccepted: {
|
||||
root.downloadDestination = fileDialog2.selectedFile
|
||||
console.log("Destination selected:",
|
||||
root.backend.downloadDestination)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
DebugPanel {
|
||||
id: debugPanel
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: 150
|
||||
color: "#111111"
|
||||
visible: root.showDebug // or: visible: showDebug
|
||||
|
||||
TabBar {
|
||||
id: bar
|
||||
width: parent.width
|
||||
|
||||
TabButton {
|
||||
text: qsTr("Logs")
|
||||
}
|
||||
|
||||
TabButton {
|
||||
text: qsTr("Config")
|
||||
}
|
||||
}
|
||||
|
||||
StackLayout {
|
||||
id: stackLayout
|
||||
currentIndex: bar.currentIndex
|
||||
anchors.top: bar.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
Item {
|
||||
id: homeTab
|
||||
|
||||
Flickable {
|
||||
id: flick
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
contentWidth: width
|
||||
contentHeight: debugText.paintedHeight
|
||||
|
||||
TextEdit {
|
||||
id: debugText
|
||||
width: flick.width
|
||||
text: root.backend.debugLogs
|
||||
color: "#dddddd"
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 12
|
||||
wrapMode: Text.WrapAnywhere
|
||||
readOnly: true
|
||||
|
||||
onTextChanged: Qt.callLater(function () {
|
||||
flick.contentY = Math.max(
|
||||
0, flick.contentHeight - flick.height)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
id: discoverTab
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
|
||||
TextArea {
|
||||
id: jsonEditor
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 12
|
||||
color: "#d4d4d4"
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
wrapMode: Text.WrapAnywhere
|
||||
|
||||
background: Rectangle {
|
||||
color: "#1e1e1e"
|
||||
border.color: jsonEditor.isValid ? "#3a3a3a" : "#ff0000"
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
property bool isValid: true
|
||||
|
||||
Connections {
|
||||
target: root.backend
|
||||
|
||||
function onConfigJsonChanged() {
|
||||
jsonEditor.text = root.backend.configJson
|
||||
try {
|
||||
const jsonData = JSON.parse(jsonEditor.text)
|
||||
jsonEditor.isValid = true
|
||||
} catch (e) {
|
||||
jsonEditor.isValid = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
text = root.backend.configJson
|
||||
|
||||
try {
|
||||
const jsonData = JSON.parse(text)
|
||||
isValid = true
|
||||
} catch (e) {
|
||||
isValid = false
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Shortcut {
|
||||
sequence: "Ctrl+D"
|
||||
onActivated: root.showDebug = !root.showDebug
|
||||
}
|
||||
height: 220
|
||||
visible: root.showDebug
|
||||
backend: root.backend
|
||||
running: root.isRunning()
|
||||
}
|
||||
}
|
||||
|
||||
12
src/qml/UploadIcon.qml
Normal file
12
src/qml/UploadIcon.qml
Normal file
@ -0,0 +1,12 @@
|
||||
import QtQuick
|
||||
|
||||
// Upward arrow — upload
|
||||
DotIcon {
|
||||
pattern: [
|
||||
0, 0, 1, 0, 0,
|
||||
0, 1, 1, 1, 0,
|
||||
1, 1, 1, 1, 1,
|
||||
0, 0, 1, 0, 0,
|
||||
0, 0, 1, 0, 0
|
||||
]
|
||||
}
|
||||
96
src/qml/UploadWidget.qml
Normal file
96
src/qml/UploadWidget.qml
Normal file
@ -0,0 +1,96 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
ArcWidget {
|
||||
id: root
|
||||
|
||||
property var backend: MockBackend
|
||||
property bool running: false
|
||||
property real totalBytes: 0
|
||||
property real uploadedBytes: 0
|
||||
|
||||
readonly property int uploadProgress: {
|
||||
if (totalBytes <= 0) {
|
||||
return 0
|
||||
}
|
||||
return Math.min(Math.round(uploadedBytes / totalBytes * 100), 100)
|
||||
}
|
||||
|
||||
signal uploadRequested
|
||||
|
||||
readonly property bool isUploading: uploadProgress > 0
|
||||
&& uploadProgress < 100
|
||||
readonly property bool isDone: uploadProgress >= 100
|
||||
|
||||
Connections {
|
||||
target: root.backend
|
||||
|
||||
function onUploadStarted(totalBytes) {
|
||||
root.totalBytes = totalBytes
|
||||
root.uploadedBytes = 0
|
||||
}
|
||||
|
||||
function onUploadChunk(len) {
|
||||
root.uploadedBytes += len
|
||||
}
|
||||
|
||||
function onUploadCompleted(cid) {
|
||||
root.uploadedBytes = root.totalBytes // force 100%
|
||||
}
|
||||
}
|
||||
|
||||
fraction: root.uploadProgress / 100.0
|
||||
fillColor: root.isDone ? Theme.palette.success : Theme.palette.text
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 2
|
||||
|
||||
UploadIcon {
|
||||
dotColor: Theme.palette.textSecondary
|
||||
dotSize: 4
|
||||
dotSpacing: 3
|
||||
activeOpacity: 0.5
|
||||
visible: !root.isUploading
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
// Uploading: percentage
|
||||
LogosText {
|
||||
text: root.uploadProgress + "%"
|
||||
font.pixelSize: 22
|
||||
font.bold: true
|
||||
visible: root.isUploading
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
LogosText {
|
||||
text: root.isDone ? "DONE" : "UPLOAD"
|
||||
font.pixelSize: 9
|
||||
color: Theme.palette.textTertiary
|
||||
font.letterSpacing: 1.2
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: widgetHover
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: root.radius
|
||||
color: widgetHover.hovered
|
||||
&& root.running ? Qt.rgba(1, 1, 1, 0.04) : "transparent"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: root.running ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: if (root.running) {
|
||||
root.uploadRequested()
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/qml/Utils.js
Normal file
13
src/qml/Utils.js
Normal file
@ -0,0 +1,13 @@
|
||||
.pragma library
|
||||
|
||||
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"
|
||||
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB"
|
||||
}
|
||||
158
src/qml/Widgets.qml
Normal file
158
src/qml/Widgets.qml
Normal file
@ -0,0 +1,158 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Dialogs
|
||||
import QtQuick.Layouts
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
// qmllint disable unqualified
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property var backend: MockBackend
|
||||
property bool running: false
|
||||
property string _lastUploadedCid: ""
|
||||
|
||||
spacing: 0
|
||||
|
||||
TextEdit {
|
||||
id: clipHelper
|
||||
visible: false
|
||||
function copyText(str) {
|
||||
clipHelper.text = str
|
||||
clipHelper.selectAll()
|
||||
clipHelper.copy()
|
||||
}
|
||||
}
|
||||
|
||||
FileDialog {
|
||||
id: uploadDialog
|
||||
onAccepted: root.backend.uploadFile(selectedFile)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.backend
|
||||
function onUploadCompleted(cid) {
|
||||
root._lastUploadedCid = cid
|
||||
}
|
||||
|
||||
function onDownloadCompleted() {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
UploadWidget {
|
||||
backend: root.backend
|
||||
running: root.running
|
||||
onUploadRequested: uploadDialog.open()
|
||||
}
|
||||
|
||||
DiskWidget {
|
||||
backend: root.backend
|
||||
}
|
||||
|
||||
PeersWidget {
|
||||
backend: root.backend
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 10
|
||||
Layout.bottomMargin: 10
|
||||
Layout.preferredHeight: 36
|
||||
|
||||
opacity: root._lastUploadedCid.length > 0 ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
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 = root._lastUploadedCid
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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(root._lastUploadedCid)
|
||||
copyFlashAnim.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/qml/icons/AdvancedIcon.qml
Normal file
17
src/qml/icons/AdvancedIcon.qml
Normal file
@ -0,0 +1,17 @@
|
||||
import QtQuick
|
||||
|
||||
// X pattern — advanced / expert mode
|
||||
// ● . . . ●
|
||||
// . ● . ● .
|
||||
// . . ● . .
|
||||
// . ● . ● .
|
||||
// ● . . . ●
|
||||
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
|
||||
]
|
||||
}
|
||||
56
src/qml/icons/ArcGauge.qml
Normal file
56
src/qml/icons/ArcGauge.qml
Normal file
@ -0,0 +1,56 @@
|
||||
import QtQuick
|
||||
import Logos.Theme
|
||||
|
||||
// Reusable arc gauge — same visual style as the Nothing OS ring widgets.
|
||||
//
|
||||
// Usage:
|
||||
// ArcGauge {
|
||||
// anchors.fill: parent
|
||||
// fraction: 0.65
|
||||
// fillColor: Theme.palette.success
|
||||
// }
|
||||
Canvas {
|
||||
id: root
|
||||
|
||||
// 0.0 – 1.0 (clamped internally)
|
||||
property real fraction: 0.0
|
||||
property color trackColor: Theme.palette.textMuted
|
||||
property color fillColor: Theme.palette.text
|
||||
property real arcRadius: 46
|
||||
property real arcWidth: 8
|
||||
|
||||
onFractionChanged: requestPaint()
|
||||
onTrackColorChanged: requestPaint()
|
||||
onFillColorChanged: requestPaint()
|
||||
|
||||
Component.onCompleted: requestPaint()
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d")
|
||||
ctx.reset()
|
||||
|
||||
var cx = width / 2
|
||||
var cy = height / 2
|
||||
var startRad = 130 * Math.PI / 180
|
||||
var totalRad = 280 * Math.PI / 180
|
||||
|
||||
// ── Track (full arc, muted) ───────────────────────────────────────────
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, root.arcRadius, startRad, startRad + totalRad)
|
||||
ctx.strokeStyle = root.trackColor.toString()
|
||||
ctx.lineWidth = root.arcWidth
|
||||
ctx.lineCap = "round"
|
||||
ctx.stroke()
|
||||
|
||||
// ── Fill (proportional) ───────────────────────────────────────────────
|
||||
var f = Math.min(Math.max(root.fraction, 0.0), 1.0)
|
||||
if (f > 0) {
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, root.arcRadius, startRad, startRad + totalRad * f)
|
||||
ctx.strokeStyle = root.fillColor.toString()
|
||||
ctx.lineWidth = root.arcWidth
|
||||
ctx.lineCap = "round"
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
}
|
||||
76
src/qml/icons/ArcWidget.qml
Normal file
76
src/qml/icons/ArcWidget.qml
Normal file
@ -0,0 +1,76 @@
|
||||
import QtQuick
|
||||
import Logos.Theme
|
||||
|
||||
// Reusable ring widget — Rectangle + arc canvas + content overlay.
|
||||
// Usage:
|
||||
// ArcWidget {
|
||||
// fraction: 0.65
|
||||
// fillColor: Theme.palette.success
|
||||
// ColumnLayout { anchors.centerIn: parent; ... }
|
||||
// }
|
||||
// Children are placed inside an overlay Item that fills the widget,
|
||||
// so anchors such as `anchors.centerIn: parent` work as expected.
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
width: 140
|
||||
height: 140
|
||||
radius: 14
|
||||
color: Theme.palette.backgroundSecondary
|
||||
border.color: Theme.palette.borderSecondary
|
||||
border.width: 1
|
||||
|
||||
property real fraction: 0.0
|
||||
property color fillColor: Theme.palette.text
|
||||
property color trackColor: Theme.palette.textMuted
|
||||
property real arcRadius: 46
|
||||
property real arcWidth: 8
|
||||
|
||||
onFractionChanged: arc.requestPaint()
|
||||
onFillColorChanged: arc.requestPaint()
|
||||
onTrackColorChanged: arc.requestPaint()
|
||||
|
||||
default property alias content: overlay.data
|
||||
|
||||
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 startRad = 130 * Math.PI / 180
|
||||
var totalRad = 280 * Math.PI / 180
|
||||
|
||||
// Track (full arc, muted)
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, root.arcRadius, startRad, startRad + totalRad)
|
||||
ctx.strokeStyle = root.trackColor.toString()
|
||||
ctx.lineWidth = root.arcWidth
|
||||
ctx.lineCap = "round"
|
||||
ctx.stroke()
|
||||
|
||||
// Fill (proportional)
|
||||
var f = Math.min(Math.max(root.fraction, 0.0), 1.0)
|
||||
if (f > 0) {
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, root.arcRadius, startRad,
|
||||
startRad + totalRad * f)
|
||||
ctx.strokeStyle = root.fillColor.toString()
|
||||
ctx.lineWidth = root.arcWidth
|
||||
ctx.lineCap = "round"
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: overlay
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
18
src/qml/icons/DeleteIcon.qml
Normal file
18
src/qml/icons/DeleteIcon.qml
Normal file
@ -0,0 +1,18 @@
|
||||
import QtQuick
|
||||
|
||||
// X pattern — delete / remove
|
||||
// ● . . . ●
|
||||
// . ● . ● .
|
||||
// . . ● . .
|
||||
// . ● . ● .
|
||||
// ● . . . ●
|
||||
// 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
|
||||
]
|
||||
}
|
||||
74
src/qml/icons/DotIcon.qml
Normal file
74
src/qml/icons/DotIcon.qml
Normal file
@ -0,0 +1,74 @@
|
||||
import QtQuick
|
||||
|
||||
// qmllint disable unqualified
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Static pattern — flat array of 0/1, row-major
|
||||
property var pattern: []
|
||||
|
||||
// Dimensions
|
||||
property int columns: 5
|
||||
property int dotSize: 6
|
||||
property int dotSpacing: 4
|
||||
|
||||
// Appearance
|
||||
property color dotColor: "white"
|
||||
property real inactiveOpacity: 0.1
|
||||
property real activeOpacity: 0.9
|
||||
|
||||
// Animation
|
||||
property bool animated: false
|
||||
property int animPhase: 0
|
||||
|
||||
readonly property int rows: Math.max(1, Math.ceil(pattern.length / columns))
|
||||
readonly property int count: animated ? columns * columns : pattern.length
|
||||
|
||||
implicitWidth: columns * dotSize + Math.max(0, columns - 1) * dotSpacing
|
||||
implicitHeight: rows * dotSize + Math.max(0, rows - 1) * dotSpacing
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
|
||||
Timer {
|
||||
interval: 140
|
||||
repeat: true
|
||||
running: root.animated
|
||||
onTriggered: root.animPhase = (root.animPhase + 1) % (root.columns * 2)
|
||||
}
|
||||
|
||||
Grid {
|
||||
columns: root.columns
|
||||
spacing: root.dotSpacing
|
||||
|
||||
Repeater {
|
||||
model: root.count
|
||||
|
||||
Rectangle {
|
||||
width: root.dotSize
|
||||
height: root.dotSize
|
||||
radius: root.dotSize * 0.3
|
||||
color: root.dotColor
|
||||
|
||||
opacity: {
|
||||
if (!root.animated) {
|
||||
return (index < root.pattern.length
|
||||
&& root.pattern[index]) ? root.activeOpacity : root.inactiveOpacity
|
||||
}
|
||||
// Wave from center
|
||||
const cx = Math.floor(root.columns / 2)
|
||||
const cy = Math.floor(root.columns / 2)
|
||||
const col = index % root.columns
|
||||
const row = Math.floor(index / root.columns)
|
||||
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
|
||||
return root.inactiveOpacity
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/qml/icons/DownloadIcon.qml
Normal file
17
src/qml/icons/DownloadIcon.qml
Normal file
@ -0,0 +1,17 @@
|
||||
import QtQuick
|
||||
|
||||
// Downward arrow — download
|
||||
// . . ● . .
|
||||
// . . ● . .
|
||||
// ● ● ● ● ●
|
||||
// . ● ● ● .
|
||||
// . . ● . .
|
||||
DotIcon {
|
||||
pattern: [
|
||||
0, 0, 1, 0, 0,
|
||||
0, 0, 1, 0, 0,
|
||||
1, 1, 1, 1, 1,
|
||||
0, 1, 1, 1, 0,
|
||||
0, 0, 1, 0, 0
|
||||
]
|
||||
}
|
||||
17
src/qml/icons/GuideIcon.qml
Normal file
17
src/qml/icons/GuideIcon.qml
Normal file
@ -0,0 +1,17 @@
|
||||
import QtQuick
|
||||
|
||||
// Crosshair pattern — step-by-step guide
|
||||
// . . ● . .
|
||||
// . . ● . .
|
||||
// ● ● . ● ●
|
||||
// . . ● . .
|
||||
// . . ● . .
|
||||
DotIcon {
|
||||
pattern: [
|
||||
0, 0, 1, 0, 0,
|
||||
0, 0, 1, 0, 0,
|
||||
1, 1, 0, 1, 1,
|
||||
0, 0, 1, 0, 0,
|
||||
0, 0, 1, 0, 0
|
||||
]
|
||||
}
|
||||
72
src/qml/icons/NodeStatusIcon.qml
Normal file
72
src/qml/icons/NodeStatusIcon.qml
Normal file
@ -0,0 +1,72 @@
|
||||
import QtQuick
|
||||
import Logos.Theme
|
||||
|
||||
// qmllint disable unqualified
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property bool starting: true
|
||||
property bool success: false
|
||||
property int animPhase: 0
|
||||
|
||||
readonly property int columns: 7
|
||||
readonly property int dotSize: 8
|
||||
readonly property int dotSpacing: 5
|
||||
|
||||
implicitWidth: columns * dotSize + (columns - 1) * dotSpacing
|
||||
implicitHeight: columns * dotSize + (columns - 1) * dotSpacing
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
|
||||
Timer {
|
||||
interval: 120
|
||||
repeat: true
|
||||
running: root.starting
|
||||
onTriggered: root.animPhase = (root.animPhase + 1) % 14
|
||||
}
|
||||
|
||||
Grid {
|
||||
columns: root.columns
|
||||
spacing: root.dotSpacing
|
||||
|
||||
Repeater {
|
||||
model: root.columns * root.columns
|
||||
|
||||
Rectangle {
|
||||
width: root.dotSize
|
||||
height: root.dotSize
|
||||
radius: root.dotSize * 0.25
|
||||
|
||||
color: {
|
||||
if (root.success)
|
||||
return Theme.palette.success
|
||||
if (!root.starting)
|
||||
return Theme.palette.error
|
||||
return Theme.palette.text
|
||||
}
|
||||
|
||||
opacity: {
|
||||
const col = index % root.columns
|
||||
const row = Math.floor(index / root.columns)
|
||||
const d = Math.abs(col - 3) + Math.abs(row - 3)
|
||||
|
||||
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
|
||||
return 0.1
|
||||
}
|
||||
|
||||
if (root.success)
|
||||
return 0.85
|
||||
|
||||
// Error — X pattern
|
||||
return (col === row || col + row === 6) ? 0.9 : 0.1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/qml/icons/PlayIcon.qml
Normal file
17
src/qml/icons/PlayIcon.qml
Normal file
@ -0,0 +1,17 @@
|
||||
import QtQuick
|
||||
|
||||
// Right-pointing triangle — play / start
|
||||
// ● . . . .
|
||||
// ● ● . . .
|
||||
// ● ● ● . .
|
||||
// ● ● . . .
|
||||
// ● . . . .
|
||||
DotIcon {
|
||||
pattern: [
|
||||
1, 0, 0, 0, 0,
|
||||
1, 1, 0, 0, 0,
|
||||
1, 1, 1, 0, 0,
|
||||
1, 1, 0, 0, 0,
|
||||
1, 0, 0, 0, 0
|
||||
]
|
||||
}
|
||||
17
src/qml/icons/PortIcon.qml
Normal file
17
src/qml/icons/PortIcon.qml
Normal file
@ -0,0 +1,17 @@
|
||||
import QtQuick
|
||||
|
||||
// Right arrow pattern — manual port forwarding
|
||||
// . . ● . .
|
||||
// . . . ● .
|
||||
// ● ● ● ● ●
|
||||
// . . . ● .
|
||||
// . . ● . .
|
||||
DotIcon {
|
||||
pattern: [
|
||||
0, 0, 1, 0, 0,
|
||||
0, 0, 0, 1, 0,
|
||||
1, 1, 1, 1, 1,
|
||||
0, 0, 0, 1, 0,
|
||||
0, 0, 1, 0, 0
|
||||
]
|
||||
}
|
||||
11
src/qml/icons/SettingsIcon.qml
Normal file
11
src/qml/icons/SettingsIcon.qml
Normal file
@ -0,0 +1,11 @@
|
||||
import QtQuick
|
||||
|
||||
// Gear / cog icon — 4 cardinal teeth + ring with center hole
|
||||
// . . ● . .
|
||||
// . ● ● ● .
|
||||
// ● ● . ● ●
|
||||
// . ● ● ● .
|
||||
// . . ● . .
|
||||
DotIcon {
|
||||
pattern: [0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0]
|
||||
}
|
||||
17
src/qml/icons/StopIcon.qml
Normal file
17
src/qml/icons/StopIcon.qml
Normal file
@ -0,0 +1,17 @@
|
||||
import QtQuick
|
||||
|
||||
// Filled square — stop
|
||||
// . . . . .
|
||||
// . ● ● ● .
|
||||
// . ● ● ● .
|
||||
// . ● ● ● .
|
||||
// . . . . .
|
||||
DotIcon {
|
||||
pattern: [
|
||||
0, 0, 0, 0, 0,
|
||||
0, 1, 1, 1, 0,
|
||||
0, 1, 1, 1, 0,
|
||||
0, 1, 1, 1, 0,
|
||||
0, 0, 0, 0, 0
|
||||
]
|
||||
}
|
||||
19
src/qml/icons/StorageIcon.qml
Normal file
19
src/qml/icons/StorageIcon.qml
Normal file
@ -0,0 +1,19 @@
|
||||
import QtQuick
|
||||
|
||||
// Ring / node pattern — used in the StorageView header
|
||||
// . ● ● ● .
|
||||
// ● . . . ●
|
||||
// ● . ● . ●
|
||||
// ● . . . ●
|
||||
// . ● ● ● .
|
||||
DotIcon {
|
||||
pattern: [
|
||||
0, 1, 1, 1, 0,
|
||||
1, 0, 0, 0, 1,
|
||||
1, 0, 1, 0, 1,
|
||||
1, 0, 0, 0, 1,
|
||||
0, 1, 1, 1, 0
|
||||
]
|
||||
dotSize: 7
|
||||
dotSpacing: 5
|
||||
}
|
||||
17
src/qml/icons/UploadIcon.qml
Normal file
17
src/qml/icons/UploadIcon.qml
Normal file
@ -0,0 +1,17 @@
|
||||
import QtQuick
|
||||
|
||||
// Upward arrow — upload
|
||||
// . . ● . .
|
||||
// . ● ● ● .
|
||||
// ● ● ● ● ●
|
||||
// . . ● . .
|
||||
// . . ● . .
|
||||
DotIcon {
|
||||
pattern: [
|
||||
0, 0, 1, 0, 0,
|
||||
0, 1, 1, 1, 0,
|
||||
1, 1, 1, 1, 1,
|
||||
0, 0, 1, 0, 0,
|
||||
0, 0, 1, 0, 0
|
||||
]
|
||||
}
|
||||
17
src/qml/icons/UpnpIcon.qml
Normal file
17
src/qml/icons/UpnpIcon.qml
Normal file
@ -0,0 +1,17 @@
|
||||
import QtQuick
|
||||
|
||||
// Diamond / network pattern — UPnP automatic port forwarding
|
||||
// . . ● . .
|
||||
// . ● . ● .
|
||||
// ● . ● . ●
|
||||
// . ● . ● .
|
||||
// . . ● . .
|
||||
DotIcon {
|
||||
pattern: [
|
||||
0, 0, 1, 0, 0,
|
||||
0, 1, 0, 1, 0,
|
||||
1, 0, 1, 0, 1,
|
||||
0, 1, 0, 1, 0,
|
||||
0, 0, 1, 0, 0
|
||||
]
|
||||
}
|
||||
@ -12,12 +12,31 @@
|
||||
<file alias="HealthIndicator.qml">qml/HealthIndicator.qml</file>
|
||||
<file alias="ModeSelector.qml">qml/ModeSelector.qml</file>
|
||||
<file alias="AdvancedSetup.qml">qml/AdvancedSetup.qml</file>
|
||||
<file alias="DotIcon.qml">qml/DotIcon.qml</file>
|
||||
<file alias="NodeStatusIcon.qml">qml/NodeStatusIcon.qml</file>
|
||||
<file alias="GuideIcon.qml">qml/GuideIcon.qml</file>
|
||||
<file alias="AdvancedIcon.qml">qml/AdvancedIcon.qml</file>
|
||||
<file alias="UpnpIcon.qml">qml/UpnpIcon.qml</file>
|
||||
<file alias="PortIcon.qml">qml/PortIcon.qml</file>
|
||||
<file alias="ManifestTable.qml">qml/ManifestTable.qml</file>
|
||||
<file alias="NodeHeader.qml">qml/NodeHeader.qml</file>
|
||||
<file alias="Widgets.qml">qml/Widgets.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="DiskWidget.qml">qml/DiskWidget.qml</file>
|
||||
<file alias="UploadWidget.qml">qml/UploadWidget.qml</file>
|
||||
<file alias="PeersWidget.qml">qml/PeersWidget.qml</file>
|
||||
<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>
|
||||
<file alias="AdvancedIcon.qml">qml/icons/AdvancedIcon.qml</file>
|
||||
<file alias="UpnpIcon.qml">qml/icons/UpnpIcon.qml</file>
|
||||
<file alias="PortIcon.qml">qml/icons/PortIcon.qml</file>
|
||||
<file alias="StorageIcon.qml">qml/icons/StorageIcon.qml</file>
|
||||
<file alias="PlayIcon.qml">qml/icons/PlayIcon.qml</file>
|
||||
<file alias="StopIcon.qml">qml/icons/StopIcon.qml</file>
|
||||
<file alias="SettingsIcon.qml">qml/icons/SettingsIcon.qml</file>
|
||||
<file alias="UploadIcon.qml">qml/icons/UploadIcon.qml</file>
|
||||
<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>
|
||||
<file alias="Utils.js">qml/Utils.js</file>
|
||||
<file alias="MockBackend.qml">qml/MockBackend.qml</file>
|
||||
<file>icons/storage.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
1
vendor/logos-design-system
vendored
Submodule
1
vendor/logos-design-system
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 063c4b46accc621bc85fa8baab46b31ef65f3957
|
||||
Loading…
x
Reference in New Issue
Block a user