diff --git a/sharing/linux/app/AppContent.qml b/sharing/linux/app/AppContent.qml new file mode 100644 index 00000000..aaccea23 --- /dev/null +++ b/sharing/linux/app/AppContent.qml @@ -0,0 +1,38 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import QtQuick.Shapes + +RowLayout { + anchors.fill: parent + spacing: 0 + + Sidebar {} + StackLayout { + id: contentStack + Layout.fillWidth: true + Layout.fillHeight: true + currentIndex: 1 + + Drop {} + + Rectangle { + color: "transparent" + Layout.fillWidth: true + Layout.fillHeight: true + + Targets{} + + } + Rectangle { + color: "transparent" + + Text { + text: "User Profile Management" + anchors.centerIn: parent + font.pointSize: 22 + color: "#333333" + } + } + } +} diff --git a/sharing/linux/app/BUILD b/sharing/linux/app/BUILD new file mode 100644 index 00000000..a7d6593f --- /dev/null +++ b/sharing/linux/app/BUILD @@ -0,0 +1,83 @@ +load("@rules_cc//cc:cc_binary.bzl", "cc_binary") +load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc:cc_test.bzl", "cc_test") + +load("@hedron_compile_commands//:refresh_compile_commands.bzl", "refresh_compile_commands") + +refresh_compile_commands( + name = "refresh_compile_commands", + + # Specify the targets of interest. + # For example, specify a dict of targets and any flags required to build. + targets = { + ":app" : "", + }, + # No need to add flags already in .bazelrc. They're automatically picked up. + # If you don't need flags, a list of targets is also okay, as is a single target string. + # Wildcard patterns, like //... for everything, *are* allowed here, just like a build. + # As are additional targets (+) and subtractions (-), like in bazel query https://docs.bazel.build/versions/main/query.html#expressions + # And if you're working on a header-only library, specify a test or binary target that compiles it. +) + +# 1. Compile QML files into a C++ source file using Qt's Resource Compiler (rcc) +genrule( + name = "qrc_generation", + srcs = [ + "resources.qrc", + "main.qml", + "Drop.qml", + "Sidebar.qml", + "googlesans_var.ttf", + "icons/file.svg", + "icons/up_file.svg" + ], + outs = ["qrc_resources.cpp"], + cmd = "/usr/lib64/qt6/libexec/rcc $(location resources.qrc) -o $(location qrc_resources.cpp)", +) + +# 2. MOC generation for backend.h +genrule( + name = "moc_backend", + srcs = ["backend.h"], + outs = ["moc_backend.cpp"], + cmd = "/usr/lib64/qt6/libexec/moc $(location backend.h) -o $(location moc_backend.cpp) ", +) + +cc_binary( + name = "app", + srcs = [ + "backend.cc", + "main.cc", + "backend.h", + ":qrc_generation", # Include the generated QRC file + ":moc_backend", + ], + copts = [ + # Base Qt6 directory + "-I/usr/include/qt6", + # Individual module directories required for QGuiApplication and QQmlApplicationEngine + "-I/usr/include/qt6/QtCore", + "-I/usr/include/qt6/QtGui", + "-I/usr/include/qt6/QtQml", + "-I/usr/include/qt6/QtQuick", + ], + linkopts = [ + "-lQt6Core", + "-lQt6Gui", + "-lQt6Qml", + "-lQt6Quick", + ], + deps = [ + ":nearby_sharing_dbus_client", + ], +) + +cc_library( + name = "nearby_sharing_dbus_client", + srcs = ["nearby_sharing_dbus_client.cc"], + hdrs = ["nearby_sharing_dbus_client.h"], + deps = [ + "//sharing/linux/daemon:nearby_sharing_dbus_client_glue", + "@sdbus_cpp", + ], +) diff --git a/sharing/linux/app/Drop.qml b/sharing/linux/app/Drop.qml new file mode 100644 index 00000000..3923d64e --- /dev/null +++ b/sharing/linux/app/Drop.qml @@ -0,0 +1,74 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import QtQuick.Shapes + +Rectangle { + id: dropZone + color: "transparent" + DropArea { + anchors.fill: parent + // Visual feedback: Change color when an item is hovered over it + onEntered: drag => { + dropZone.color = "#91C8DE"; + } + + onExited: { + dropText.text = "Drag and drop files to share"; + dropZone.color = "transparent"; + } + + // The action: What happens when the user releases the item here + onDropped: drop => { + dropZone.color = "#98FB98"; // Success Mint Green + dropText.text = "Dropped: " + drop.source.objectName; + + // Snap the dragged item exactly to the center of the drop zone + drop.source.x = dropZone.x + (dropZone.width - drop.source.width) / 2; + drop.source.y = dropZone.y + (dropZone.height - drop.source.height) / 2; + + drop.accept(); // Tell Qt the drop event was handled successfully + } + } + ColumnLayout { + + anchors.centerIn: parent + Button { + icon.source: "qrc:icons/up_file.svg" + icon.height: 80 + icon.width: 80 + icon.color: "#195871" + Layout.fillWidth: true + background: Rectangle { + color: "transparent" + } + } + + Text { + id: dropText + text: "Drag and drop files to share" + font.pointSize: 18 + color: "#333333" + } + } + Canvas { + id: dashedBorderCanvas + anchors.fill: parent + + onPaint: { + var ctx = getContext("2d"); + ctx.clearRect(0, 0, width, height); + + // Setup styling parameters + ctx.strokeStyle = "#91C8DE"; + ctx.lineWidth = 3; + + // Set the dash pattern array: [length of dash, length of space] + ctx.setLineDash([8, 6]); + + // Draw a rectangle border (X, Y, Width, Height) + // Offset by half line-width (1px) so the line doesn't get clipped on the edges + ctx.strokeRect(80, 80, width - 160, height - 160); + } + } +} diff --git a/sharing/linux/app/ShareTarget.qml b/sharing/linux/app/ShareTarget.qml new file mode 100644 index 00000000..734e4efa --- /dev/null +++ b/sharing/linux/app/ShareTarget.qml @@ -0,0 +1,119 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import QtQuick.Shapes + +Rectangle { + id: rootItem + // Explicit sizes so parent containers know how to space them + width: 100 + height: 130 + color: "transparent" + + // --- CUSTOM ARGUMENTS (PROPERTIES) --- + property string deviceName: "Unknown Device" + property real progressValue: 0.0 // Value between 0.0 and 1.0 + property string iconSource: "qrc:icons/smartphone.svg" + + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + spacing: 5 + + // --- ICON CONTAINER WITH PROGRESS RINGS --- + Item { + id: iconContainer + implicitWidth: 72 + implicitHeight: 72 + Layout.alignment: Qt.AlignHCenter + + // Target angle calculated using the custom dynamic argument + property real targetAngle: 360 * rootItem.progressValue + + Behavior on targetAngle { + NumberAnimation { + duration: 300 + easing.type: Easing.OutQuad + } + } + + // Background Gray Track Ring + Shape { + anchors.fill: parent + layer.enabled: true + layer.samples: 8 + antialiasing: true + + ShapePath { + strokeColor: "#E0E4E8" + strokeWidth: 4 + fillColor: "transparent" + + PathAngleArc { + centerX: iconContainer.width / 2 + centerY: iconContainer.height / 2 + radiusX: 32 + radiusY: 32 + startAngle: -90 + sweepAngle: 360 + } + } + } + + // Active Progress Ring + Shape { + anchors.fill: parent + layer.enabled: true + layer.samples: 8 + antialiasing: true + + ShapePath { + strokeColor: "#00658F" + strokeWidth: 4 + fillColor: "transparent" + capStyle: ShapePath.RoundCap + + PathAngleArc { + centerX: iconContainer.width / 2 + centerY: iconContainer.height / 2 + radiusX: 32 + radiusY: 32 + startAngle: -90 + sweepAngle: iconContainer.targetAngle + } + } + } + + // Icon Circle + Rectangle { + width: 60 + height: 60 + anchors.centerIn: parent + color: "#6EA7B6" + radius: 30 + + Button { + icon.source: rootItem.iconSource + anchors.fill: parent + anchors.margins: 5 + icon.color: "white" + icon.height: height + icon.width: width + background: Rectangle { + color: "transparent" + } + } + } + } + + // --- TEXT COMPONENT --- + Text { + text: rootItem.deviceName + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + color: "#1A1C1E" // Using the text charcoal color from your palette + } + } +} diff --git a/sharing/linux/app/Sidebar.qml b/sharing/linux/app/Sidebar.qml new file mode 100644 index 00000000..ff265fff --- /dev/null +++ b/sharing/linux/app/Sidebar.qml @@ -0,0 +1,128 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import QtQuick.Shapes + +Rectangle { + id: sidebar + Layout.fillHeight: true + Layout.preferredWidth: 300 + + color: "#CBF0FF" + + ColumnLayout { + anchors.margins: 20 + anchors.fill: parent + + Rectangle { + Layout.preferredHeight: innerColumn.implicitHeight + 20 + Layout.fillWidth: true + color: "transparent" + + ColumnLayout { + id: innerColumn + Text { + text: "Device name" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + Text { + text: "lasans-laptop" + font.weight: 500 + font.pointSize: 17 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: "transparent" + + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + //anchors.fill: parent + + Text { + text: "Visible to Everyone" + font.weight: 700 + color: "#377B95" + } + + Text { + Layout.preferredWidth: parent.width + wrapMode: Text.WordWrap + text: "This cannot be changed due to limitations in QuickShare on Linux" + color: "gray" + } + } + } + + Rectangle { + Layout.preferredHeight: 350 + Layout.fillWidth: true + border.color: "#91C8DE" + border.width: 2 + radius: 20 + color: "#DCF5FF" + ColumnLayout { + anchors.fill: parent + anchors.margins: 15 + + Text { + Layout.fillWidth: true + font.pointSize: 15 + + font.weight: 700 + color: "#377B95" + text: "Sharing" + } + Image { + source: "qrc:icons/file.svg" + Layout.fillHeight: true + Layout.fillWidth: true + fillMode: Image.PreserveAspectCrop + sourceSize.height: height + sourceSize.width: width + smooth: true + antialiasing: true + } + + Text { + Layout.fillWidth: true + color: "gray" + wrapMode: Text.WordWrap + text: "/home/lasan/test/this/is/a/very/long/file/path/file.pp" + horizontalAlignment: Text.AlignHCenter + } + Button { + id: cancelbutton + Layout.fillWidth: true + text: "Cancel" + contentItem: Text { + text: cancelbutton.text + font.pointSize: 14 + font.weight: 500 + color: "white" // Dims text when pressed + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + background: Rectangle { + implicitWidth: 100 + implicitHeight: 40 + radius: 10 + color: cancelbutton.hovered ? "#195871" : "#06384C" + border.color: "#91C8DE" + border.width: 2 + } + } + } + } + } +} diff --git a/sharing/linux/app/Targets.qml b/sharing/linux/app/Targets.qml new file mode 100644 index 00000000..1ededfec --- /dev/null +++ b/sharing/linux/app/Targets.qml @@ -0,0 +1,34 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +Row { + id: shareTargetsRow + spacing: 10 + + Repeater { + model: ["Lasan's A55", "Lasan's Tab S9+", "lasan-laptop", "Somethign else"] + + // Your original delegate structure + Rectangle { + // Note: You must give the Rectangle an explicit size + // so the Row knows how to position them. + width: 100 + height: 130 + color: "transparent" + + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + spacing: 5 + + ShareTarget{ + progressValue: 0.7 + + } + + } + } + } +} diff --git a/sharing/linux/app/backend.cc b/sharing/linux/app/backend.cc new file mode 100644 index 00000000..ef26bfcf --- /dev/null +++ b/sharing/linux/app/backend.cc @@ -0,0 +1,196 @@ +#include "sharing/linux/app/backend.h" + +#include +#include + +#include +#include +#include + +namespace { + +QString ToQString(const std::string& value) { + return QString::fromStdString(value); +} + +QVariantMap ToVariantMap( + const nearby::sharing::linux::app::ShareTarget& target) { + QVariantMap map; + map.insert("id", QVariant::fromValue(target.id)); + map.insert("deviceName", ToQString(target.device_name)); + map.insert("type", target.type); + map.insert("isIncoming", target.is_incoming); + map.insert("isKnown", target.is_known); + map.insert("deviceId", ToQString(target.device_id)); + map.insert("forSelfShare", target.for_self_share); + map.insert("vendorId", target.vendor_id); + map.insert("receiveDisabled", target.receive_disabled); + return map; +} + +} // namespace + +Backend::Backend(QObject* parent) : QObject(parent) { + try { + client_ = std::make_unique(this); + SetStatusText(QStringLiteral("Connected to nearby sharing daemon")); + } catch (const sdbus::Error& error) { + SetStatusText(QStringLiteral("D-Bus connection failed: %1") + .arg(QString::fromStdString(error.getMessage()))); + } +} + +Backend::~Backend() = default; + +QVariantList Backend::targets() const { + QVariantList list; + for (const auto& [id, target] : targets_) { + list.push_back(ToVariantMap(target)); + } + return list; +} + +void Backend::startReceive() { + RunCommand(QStringLiteral("Start receive"), + [this]() { return client_->StartReceive(); }); +} + +void Backend::stopReceive() { + RunCommand(QStringLiteral("Stop receive"), + [this]() { return client_->StopReceive(); }); +} + +void Backend::startDiscovery() { + RunCommand(QStringLiteral("Start discovery"), + [this]() { return client_->StartDiscovery(); }); +} + +void Backend::stopDiscovery() { + RunCommand(QStringLiteral("Stop discovery"), + [this]() { return client_->StopDiscovery(); }); +} + +void Backend::sendFile(qint64 share_target_id, const QString& path) { + RunCommand(QStringLiteral("Send file"), [this, share_target_id, path]() { + return client_->SendFile(share_target_id, path.toStdString()); + }); +} + +void Backend::accept(qint64 share_target_id) { + RunCommand(QStringLiteral("Accept transfer"), [this, share_target_id]() { + return client_->Accept(share_target_id); + }); +} + +void Backend::reject(qint64 share_target_id) { + RunCommand(QStringLiteral("Reject transfer"), [this, share_target_id]() { + return client_->Reject(share_target_id); + }); +} + +void Backend::cancel(qint64 share_target_id) { + RunCommand(QStringLiteral("Cancel transfer"), [this, share_target_id]() { + return client_->Cancel(share_target_id); + }); +} + +void Backend::OnTargetDiscovered(const ShareTarget& target) { + QMetaObject::invokeMethod( + this, [this, target]() { ApplyTarget(target); }, Qt::QueuedConnection); +} + +void Backend::OnTargetUpdated(const ShareTarget& target) { + QMetaObject::invokeMethod( + this, [this, target]() { ApplyTarget(target); }, Qt::QueuedConnection); +} + +void Backend::OnTargetLost(const ShareTarget& target) { + QMetaObject::invokeMethod( + this, [this, target]() { RemoveTarget(target); }, Qt::QueuedConnection); +} + +void Backend::OnIncomingTransfer(const std::string& direction, + const ShareTarget& target, + const Transfer& transfer) { + QMetaObject::invokeMethod( + this, + [this, target, transfer]() { + ApplyTarget(target); + emit incomingTransfer(target.id, ToQString(target.device_name), + ToQString(transfer.status)); + }, + Qt::QueuedConnection); +} + +void Backend::OnTransferUpdate(const std::string& direction, + const ShareTarget& target, + const Transfer& transfer) { + QMetaObject::invokeMethod( + this, + [this, target, transfer]() { + ApplyTarget(target); + emit transferUpdate(target.id, ToQString(target.device_name), + ToQString(transfer.status), transfer.progress); + }, + Qt::QueuedConnection); +} + +void Backend::OnStatusChanged(const Status& status) { + QMetaObject::invokeMethod( + this, [this, status]() { ApplyStatus(status); }, Qt::QueuedConnection); +} + +void Backend::ApplyTarget(const ShareTarget& target) { + targets_[target.id] = target; + emit targetsChanged(); +} + +void Backend::RemoveTarget(const ShareTarget& target) { + targets_.erase(target.id); + emit targetsChanged(); +} + +void Backend::ApplyStatus(const Status& status) { + status_ = status; + targets_.clear(); + for (const auto& target : status.targets) { + targets_[target.id] = target; + } + emit statusChanged(); + emit targetsChanged(); +} + +void Backend::ApplyCommandResult(const QString& command, + const std::tuple& result) { + const auto& [ok, message] = result; + const QString detail = + message.empty() ? (ok ? QStringLiteral("ok") : QStringLiteral("failed")) + : ToQString(message); + SetStatusText(QStringLiteral("%1: %2").arg(command, detail)); +} + +void Backend::RunCommand( + const QString& command, + const std::function()>& operation) { + if (!client_) { + SetStatusText(QStringLiteral("%1 failed: not connected").arg(command)); + return; + } + + try { + ApplyCommandResult(command, operation()); + } catch (const sdbus::Error& error) { + SetStatusText( + QStringLiteral("%1 failed: %2") + .arg(command, QString::fromStdString(error.getMessage()))); + } +} + + +void Backend::SetStatusText(const QString& text) { + if (status_text_ == text) { + return; + } + status_text_ = text; + emit statusTextChanged(); +} diff --git a/sharing/linux/app/backend.h b/sharing/linux/app/backend.h new file mode 100644 index 00000000..d2c06caf --- /dev/null +++ b/sharing/linux/app/backend.h @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "sharing/linux/app/nearby_sharing_dbus_client.h" + +class Backend + : public QObject, + public nearby::sharing::linux::app::NearbySharingDbusClient::Observer { + Q_OBJECT + + Q_PROPERTY(QString statusText READ statusText NOTIFY statusTextChanged) + Q_PROPERTY(bool receiveRegistered READ receiveRegistered NOTIFY statusChanged) + Q_PROPERTY( + bool discoveryRegistered READ discoveryRegistered NOTIFY statusChanged) + Q_PROPERTY(bool scanning READ scanning NOTIFY statusChanged) + Q_PROPERTY(bool transferring READ transferring NOTIFY statusChanged) + Q_PROPERTY(QVariantList targets READ targets NOTIFY targetsChanged) + + public: + explicit Backend(QObject* parent = nullptr); + ~Backend() override; + + QString statusText() const { return status_text_; } + bool receiveRegistered() const { return status_.receive_registered; } + bool discoveryRegistered() const { return status_.discovery_registered; } + bool scanning() const { return status_.is_scanning; } + bool transferring() const { return status_.is_transferring; } + QVariantList targets() const; + + Q_INVOKABLE void startReceive(); + Q_INVOKABLE void stopReceive(); + Q_INVOKABLE void startDiscovery(); + Q_INVOKABLE void stopDiscovery(); + Q_INVOKABLE void sendFile(qint64 share_target_id, const QString& path); + Q_INVOKABLE void accept(qint64 share_target_id); + Q_INVOKABLE void reject(qint64 share_target_id); + Q_INVOKABLE void cancel(qint64 share_target_id); + + signals: + void statusTextChanged(); + void statusChanged(); + void targetsChanged(); + void incomingTransfer(qint64 share_target_id, QString device_name, + QString status); + void transferUpdate(qint64 share_target_id, QString device_name, + QString status, double progress); + + private: + using Client = nearby::sharing::linux::app::NearbySharingDbusClient; + using ShareTarget = nearby::sharing::linux::app::ShareTarget; + using Status = nearby::sharing::linux::app::Status; + using Transfer = nearby::sharing::linux::app::Transfer; + + void OnTargetDiscovered(const ShareTarget& target) override; + void OnTargetUpdated(const ShareTarget& target) override; + void OnTargetLost(const ShareTarget& target) override; + void OnIncomingTransfer(const std::string& direction, + const ShareTarget& target, + const Transfer& transfer) override; + void OnTransferUpdate(const std::string& direction, const ShareTarget& target, + const Transfer& transfer) override; + void OnStatusChanged(const Status& status) override; + + void ApplyTarget(const ShareTarget& target); + void RemoveTarget(const ShareTarget& target); + void ApplyStatus(const Status& status); + void ApplyCommandResult(const QString& command, + const std::tuple& result); + void RunCommand( + const QString& command, + const std::function()>& operation); + void SetStatusText(const QString& text); + + int counter_ = 0; + QString status_text_; + Status status_; + std::map targets_; + std::unique_ptr client_; +}; diff --git a/sharing/linux/app/googlesans_var.ttf b/sharing/linux/app/googlesans_var.ttf new file mode 100644 index 00000000..6df533e0 Binary files /dev/null and b/sharing/linux/app/googlesans_var.ttf differ diff --git a/sharing/linux/app/icons/AdobeColor-My Color Theme.jpeg b/sharing/linux/app/icons/AdobeColor-My Color Theme.jpeg new file mode 100644 index 00000000..b0c0ab29 Binary files /dev/null and b/sharing/linux/app/icons/AdobeColor-My Color Theme.jpeg differ diff --git a/sharing/linux/app/icons/file.svg b/sharing/linux/app/icons/file.svg new file mode 100644 index 00000000..71d24d20 --- /dev/null +++ b/sharing/linux/app/icons/file.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/sharing/linux/app/icons/laptop.svg b/sharing/linux/app/icons/laptop.svg new file mode 100644 index 00000000..51edde5f --- /dev/null +++ b/sharing/linux/app/icons/laptop.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/sharing/linux/app/icons/smartphone.svg b/sharing/linux/app/icons/smartphone.svg new file mode 100644 index 00000000..11eb5b84 --- /dev/null +++ b/sharing/linux/app/icons/smartphone.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/sharing/linux/app/icons/tablet.svg b/sharing/linux/app/icons/tablet.svg new file mode 100644 index 00000000..e1f66e16 --- /dev/null +++ b/sharing/linux/app/icons/tablet.svg @@ -0,0 +1,12 @@ + + + + ic_fluent_tablet_24_regular + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/sharing/linux/app/icons/up_file.svg b/sharing/linux/app/icons/up_file.svg new file mode 100644 index 00000000..5ea1b168 --- /dev/null +++ b/sharing/linux/app/icons/up_file.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/sharing/linux/app/main.cc b/sharing/linux/app/main.cc new file mode 100644 index 00000000..f678a73e --- /dev/null +++ b/sharing/linux/app/main.cc @@ -0,0 +1,148 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr char kQmlHotReloadEnv[] = "NEARBY_QML_HOT_RELOAD"; +constexpr char kQmlSourceDirEnv[] = "NEARBY_QML_SOURCE_DIR"; +constexpr char kDefaultQmlSourceDir[] = + "/home/lasan/Dev/nearby_latest/sharing/linux/app"; + +bool IsHotReloadEnabled() { + const QByteArray value = qgetenv(kQmlHotReloadEnv); + return value == "1" || value.toLower() == "true"; +} + +QDir QmlSourceDir() { + const QByteArray configured_dir = qgetenv(kQmlSourceDirEnv); + if (!configured_dir.isEmpty()) { + return QDir(QString::fromLocal8Bit(configured_dir)); + } + return QDir(QString::fromUtf8(kDefaultQmlSourceDir)); +} + +void WatchQmlFiles(QFileSystemWatcher& watcher, const QDir& source_dir) { + const QStringList existing_files = watcher.files(); + if (!existing_files.isEmpty()) { + watcher.removePaths(existing_files); + } + + const QStringList files = + source_dir.entryList(QStringList() << "*.qml", QDir::Files, QDir::Name); + for (const QString& file : files) { + watcher.addPath(source_dir.absoluteFilePath(file)); + } +} + +void DestroyRootObjects(QQmlApplicationEngine& engine) { + const QList root_objects = engine.rootObjects(); + for (QObject* object : root_objects) { + object->deleteLater(); + } +} + +bool ReloadInnerContent(QQmlApplicationEngine& engine) { + const QList root_objects = engine.rootObjects(); + if (root_objects.isEmpty()) { + return false; + } + + QObject* root = root_objects.constFirst(); + return QMetaObject::invokeMethod(root, "reloadInnerContent"); +} + +bool CanReloadInnerContent(const QFileInfo& changed_file) { + return changed_file.fileName() == QStringLiteral("AppContent.qml"); +} + +} // namespace + +int main(int argc, char* argv[]) { + QGuiApplication app(argc, argv); + + QQmlApplicationEngine engine; + const int fontId = QFontDatabase::addApplicationFont(":/googlesans_var.ttf"); + + if (fontId != -1) { + const QStringList fontFamilies = + QFontDatabase::applicationFontFamilies(fontId); + if (!fontFamilies.isEmpty()) { + const QFont defaultFont(fontFamilies.at(0)); + app.setFont(defaultFont); + } + } + + if (IsHotReloadEnabled()) { + const QDir qml_source_dir = QmlSourceDir(); + const QUrl source_url = + QUrl::fromLocalFile(qml_source_dir.absoluteFilePath("main.qml")); + QFileSystemWatcher watcher; + QTimer reload_timer; + reload_timer.setSingleShot(true); + reload_timer.setInterval(75); + QString changed_path; + + const auto fullReload = [&engine, &watcher, qml_source_dir, source_url]() { + WatchQmlFiles(watcher, qml_source_dir); + DestroyRootObjects(engine); + engine.clearComponentCache(); + engine.load(source_url); + }; + + const auto reload = [&engine, &watcher, qml_source_dir, source_url, + &changed_path, &fullReload]() { + WatchQmlFiles(watcher, qml_source_dir); + const QFileInfo changed_file(changed_path); + + if (changed_file.fileName() == QStringLiteral("main.qml") || + !CanReloadInnerContent(changed_file) || + engine.rootObjects().isEmpty()) { + fullReload(); + return; + } + + engine.clearComponentCache(); + if (!ReloadInnerContent(engine)) { + fullReload(); + } + }; + + QObject::connect(&reload_timer, &QTimer::timeout, &app, reload); + QObject::connect(&watcher, &QFileSystemWatcher::fileChanged, &app, + [&reload_timer, &changed_path](const QString& path) { + changed_path = path; + reload_timer.start(); + }); + QObject::connect(&watcher, &QFileSystemWatcher::directoryChanged, &app, + [&reload_timer, &changed_path](const QString& path) { + changed_path = path; + reload_timer.start(); + }); + + watcher.addPath(qml_source_dir.absolutePath()); + fullReload(); + + return app.exec(); + } + + // Loaded via the qrc scheme since it's compiled into the binary + const QUrl url(QStringLiteral("qrc:/main.qml")); + + QObject::connect( + &engine, &QQmlApplicationEngine::objectCreated, &app, + [url](QObject* obj, const QUrl& objUrl) { + if (!obj && url == objUrl) QCoreApplication::exit(-1); + }, + Qt::QueuedConnection); + + engine.load(url); + + return app.exec(); +} diff --git a/sharing/linux/app/main.qml b/sharing/linux/app/main.qml new file mode 100644 index 00000000..010b4818 --- /dev/null +++ b/sharing/linux/app/main.qml @@ -0,0 +1,30 @@ +import QtQuick +import QtQuick.Window + +Window { + id: root + width: 640 + height: 480 + visible: true + title: qsTr("QuickShare") + color: "#DCF5FF" + + FontLoader { + id: googlesans + source: "qrc:/googlesans_var.ttf" + } + + Loader { + id: contentLoader + anchors.fill: parent + source: "AppContent.qml" + } + + function reloadInnerContent() { + const nextSource = "AppContent.qml?rev=" + Date.now() + contentLoader.active = false + contentLoader.source = "" + contentLoader.active = true + contentLoader.source = nextSource + } +} diff --git a/sharing/linux/app/nearby_sharing_dbus_client.cc b/sharing/linux/app/nearby_sharing_dbus_client.cc new file mode 100644 index 00000000..d84e3570 --- /dev/null +++ b/sharing/linux/app/nearby_sharing_dbus_client.cc @@ -0,0 +1,214 @@ +#include "sharing/linux/app/nearby_sharing_dbus_client.h" + +#include + +namespace nearby::sharing::linux::app { +namespace { + +template +T GetField(const DbusDictionary& map, const std::string& key, + T default_value = T{}) { + auto it = map.find(key); + if (it == map.end()) { + return default_value; + } + + try { + return it->second.get(); + } catch (const sdbus::Error&) { + return default_value; + } +} + +template +std::optional GetOptionalField(const DbusDictionary& map, + const std::string& key) { + auto it = map.find(key); + if (it == map.end()) { + return std::nullopt; + } + + try { + return it->second.get(); + } catch (const sdbus::Error&) { + return std::nullopt; + } +} + +double GetProgress(const DbusDictionary& map) { + if (auto progress = GetOptionalField(map, "progress")) { + return *progress; + } + if (auto progress = GetOptionalField(map, "progress")) { + return *progress; + } + if (auto progress = GetOptionalField(map, "progress")) { + return *progress; + } + return 0; +} + +} // namespace + +NearbySharingDbusClient::NearbySharingDbusClient(Observer* observer) + : sdbus::ProxyInterfaces( + sdbus::ServiceName("com.google.nearby.sharing"), + sdbus::ObjectPath("/com/google/nearby/sharing")), + observer_(observer) { + registerProxy(); +} + +NearbySharingDbusClient::~NearbySharingDbusClient() { + unregisterProxy(); +} + +std::tuple NearbySharingDbusClient::StartReceive() { + return com::google::nearby::sharing_proxy::StartReceive(); +} + +std::tuple NearbySharingDbusClient::StopReceive() { + return com::google::nearby::sharing_proxy::StopReceive(); +} + +std::tuple NearbySharingDbusClient::StartDiscovery() { + return com::google::nearby::sharing_proxy::StartDiscovery(); +} + +std::tuple NearbySharingDbusClient::StopDiscovery() { + return com::google::nearby::sharing_proxy::StopDiscovery(); +} + +std::tuple NearbySharingDbusClient::SendFile( + int64_t share_target_id, const std::string& path) { + return com::google::nearby::sharing_proxy::SendFile(share_target_id, path); +} + +std::tuple NearbySharingDbusClient::Accept( + int64_t share_target_id) { + return com::google::nearby::sharing_proxy::Accept(share_target_id); +} + +std::tuple NearbySharingDbusClient::Reject( + int64_t share_target_id) { + return com::google::nearby::sharing_proxy::Reject(share_target_id); +} + +std::tuple NearbySharingDbusClient::Cancel( + int64_t share_target_id) { + return com::google::nearby::sharing_proxy::Cancel(share_target_id); +} + +ShareTarget NearbySharingDbusClient::ConvertToShareTarget( + const DbusDictionary& map) { + ShareTarget target; + target.id = GetField(map, "id"); + target.device_name = GetField(map, "device_name"); + target.type = GetField(map, "type"); + target.is_incoming = GetField(map, "is_incoming"); + target.is_known = GetField(map, "is_known"); + target.device_id = GetField(map, "device_id"); + target.for_self_share = GetField(map, "for_self_share"); + target.vendor_id = GetField(map, "vendor_id"); + target.receive_disabled = GetField(map, "receive_disabled"); + return target; +} + +Transfer NearbySharingDbusClient::ConvertToTransfer(const DbusDictionary& map) { + Transfer transfer; + transfer.status = GetField(map, "status"); + transfer.progress = GetProgress(map); + transfer.transferred_bytes = GetField(map, "transferred_bytes"); + transfer.total_bytes = GetField(map, "total_bytes"); + transfer.transfer_speed = GetField(map, "transfer_speed"); + transfer.estimated_time_remaining = + GetField(map, "estimated_time_remaining"); + transfer.total_attachments_count = + GetField(map, "total_attachments_count"); + transfer.transferred_attachments_count = + GetField(map, "transferred_attachments_count"); + transfer.is_final_status = GetField(map, "is_final_status"); + transfer.is_self_share = GetField(map, "is_self_share"); + transfer.binding_id = GetField(map, "binding_id"); + transfer.token = GetOptionalField(map, "token"); + transfer.in_progress_attachment_id = + GetOptionalField(map, "in_progress_attachment_id"); + transfer.in_progress_attachment_transferred_bytes = GetOptionalField( + map, "in_progress_attachment_transferred_bytes"); + transfer.in_progress_attachment_total_bytes = + GetOptionalField(map, "in_progress_attachment_total_bytes"); + return transfer; +} + +Status NearbySharingDbusClient::ConvertToStatus(const DbusDictionary& map) { + Status status; + status.receive_registered = GetField(map, "receive_registered"); + status.discovery_registered = GetField(map, "discovery_registered"); + status.is_transferring = GetField(map, "is_transferring"); + status.is_scanning = GetField(map, "is_scanning"); + status.bluetooth_present = GetField(map, "bluetooth_present"); + status.bluetooth_powered = GetField(map, "bluetooth_powered"); + status.lan_connected = GetField(map, "lan_connected"); + + auto raw_targets = + GetOptionalField>(map, "targets"); + if (raw_targets.has_value()) { + status.targets.reserve(raw_targets->size()); + for (const auto& target : *raw_targets) { + status.targets.push_back(ConvertToShareTarget(target)); + } + } + + return status; +} + +void NearbySharingDbusClient::onTargetDiscovered( + const DbusDictionary& share_target) { + if (observer_ == nullptr) { + return; + } + observer_->OnTargetDiscovered(ConvertToShareTarget(share_target)); +} + +void NearbySharingDbusClient::onTargetUpdated( + const DbusDictionary& share_target) { + if (observer_ == nullptr) { + return; + } + observer_->OnTargetUpdated(ConvertToShareTarget(share_target)); +} + +void NearbySharingDbusClient::onTargetLost(const DbusDictionary& share_target) { + if (observer_ == nullptr) { + return; + } + observer_->OnTargetLost(ConvertToShareTarget(share_target)); +} + +void NearbySharingDbusClient::onIncomingTransfer( + const std::string& direction, const DbusDictionary& share_target, + const DbusDictionary& transfer) { + if (observer_ == nullptr) { + return; + } + observer_->OnIncomingTransfer(direction, ConvertToShareTarget(share_target), + ConvertToTransfer(transfer)); +} + +void NearbySharingDbusClient::onTransferUpdate( + const std::string& direction, const DbusDictionary& share_target, + const DbusDictionary& transfer) { + if (observer_ == nullptr) { + return; + } + observer_->OnTransferUpdate(direction, ConvertToShareTarget(share_target), + ConvertToTransfer(transfer)); +} + +void NearbySharingDbusClient::onStatusChanged(const DbusDictionary& status) { + if (observer_ == nullptr) { + return; + } + observer_->OnStatusChanged(ConvertToStatus(status)); +} + +} // namespace nearby::sharing::linux::app diff --git a/sharing/linux/app/nearby_sharing_dbus_client.h b/sharing/linux/app/nearby_sharing_dbus_client.h new file mode 100644 index 00000000..65fca9ed --- /dev/null +++ b/sharing/linux/app/nearby_sharing_dbus_client.h @@ -0,0 +1,111 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#include "sharing/linux/daemon/nearby_sharing_client.h" + +namespace nearby::sharing::linux::app { + +using DbusDictionary = std::map; + +struct ShareTarget { + int64_t id = 0; + std::string device_name; + int32_t type = 0; + bool is_incoming = false; + bool is_known = false; + std::string device_id; + bool for_self_share = false; + int32_t vendor_id = 0; + bool receive_disabled = false; +}; + +struct Transfer { + std::string status; + double progress = 0; + int64_t transferred_bytes = 0; + int64_t total_bytes = 0; + int64_t transfer_speed = 0; + int64_t estimated_time_remaining = 0; + int32_t total_attachments_count = 0; + int32_t transferred_attachments_count = 0; + bool is_final_status = false; + bool is_self_share = false; + std::string binding_id; + std::optional token; + std::optional in_progress_attachment_id; + std::optional in_progress_attachment_transferred_bytes; + std::optional in_progress_attachment_total_bytes; +}; + +struct Status { + bool receive_registered = false; + bool discovery_registered = false; + bool is_transferring = false; + bool is_scanning = false; + bool bluetooth_present = false; + bool bluetooth_powered = false; + bool lan_connected = false; + std::vector targets; +}; + +class NearbySharingDbusClient + : public sdbus::ProxyInterfaces { + public: + struct Observer { + virtual ~Observer() = default; + virtual void OnTargetDiscovered(const ShareTarget& target) {} + virtual void OnTargetUpdated(const ShareTarget& target) {} + virtual void OnTargetLost(const ShareTarget& target) {} + virtual void OnIncomingTransfer(const std::string& direction, + const ShareTarget& target, + const Transfer& transfer) {} + virtual void OnTransferUpdate(const std::string& direction, + const ShareTarget& target, + const Transfer& transfer) {} + virtual void OnStatusChanged(const Status& status) {} + }; + + explicit NearbySharingDbusClient(Observer* observer); + ~NearbySharingDbusClient(); + + NearbySharingDbusClient(const NearbySharingDbusClient&) = delete; + NearbySharingDbusClient& operator=(const NearbySharingDbusClient&) = delete; + + std::tuple StartReceive(); + std::tuple StopReceive(); + std::tuple StartDiscovery(); + std::tuple StopDiscovery(); + std::tuple SendFile(int64_t share_target_id, + const std::string& path); + std::tuple Accept(int64_t share_target_id); + std::tuple Reject(int64_t share_target_id); + std::tuple Cancel(int64_t share_target_id); + + static ShareTarget ConvertToShareTarget(const DbusDictionary& map); + static Transfer ConvertToTransfer(const DbusDictionary& map); + static Status ConvertToStatus(const DbusDictionary& map); + + private: + void onTargetDiscovered(const DbusDictionary& share_target) override; + void onTargetUpdated(const DbusDictionary& share_target) override; + void onTargetLost(const DbusDictionary& share_target) override; + void onIncomingTransfer(const std::string& direction, + const DbusDictionary& share_target, + const DbusDictionary& transfer) override; + void onTransferUpdate(const std::string& direction, + const DbusDictionary& share_target, + const DbusDictionary& transfer) override; + void onStatusChanged(const DbusDictionary& status) override; + + Observer* observer_ = nullptr; +}; + +} // namespace nearby::sharing::linux::app diff --git a/sharing/linux/app/resources.qrc b/sharing/linux/app/resources.qrc new file mode 100644 index 00000000..b90f6d80 --- /dev/null +++ b/sharing/linux/app/resources.qrc @@ -0,0 +1,17 @@ + + + main.qml + AppContent.qml + Sidebar.qml + Drop.qml + ShareTarget.qml + Targets.qml + googlesans_var.ttf + icons/file.svg + icons/up_file.svg + icons/tablet.svg + icons/laptop.svg + icons/smartphone.svg + + + diff --git a/sharing/linux/daemon/BUILD b/sharing/linux/daemon/BUILD index 681d734c..2f48b054 100644 --- a/sharing/linux/daemon/BUILD +++ b/sharing/linux/daemon/BUILD @@ -18,12 +18,20 @@ cc_binary( ], ) +cc_library( + name = "nearby_sharing_dbus_client_glue", + hdrs = ["nearby_sharing_client.h"], + visibility = ["//visibility:public"], + deps = ["@sdbus_cpp"], +) + cc_library( name = "nearby_sharing_dbus_service", srcs = ["nearby_sharing_dbus_service.cc"], hdrs = [ "nearby_sharing_dbus_service.h", "nearby_sharing_server.h", + "nearby_sharing_client.h", ], deps = [ "//internal/base:file_path", diff --git a/sharing/linux/daemon/nearby_sharing_client.h b/sharing/linux/daemon/nearby_sharing_client.h index ff3ef2cf..d67af2c3 100644 --- a/sharing/linux/daemon/nearby_sharing_client.h +++ b/sharing/linux/daemon/nearby_sharing_client.h @@ -3,8 +3,8 @@ * This file was automatically generated by sdbus-c++-xml2cpp; DO NOT EDIT! */ -#ifndef __sdbuscpp___sharing_linux_daemon_nearby_sharing_client_h__proxy__H__ -#define __sdbuscpp___sharing_linux_daemon_nearby_sharing_client_h__proxy__H__ +#ifndef __sdbuscpp__sharing_linux_daemon_nearby_sharing_client_h__proxy__H__ +#define __sdbuscpp__sharing_linux_daemon_nearby_sharing_client_h__proxy__H__ #include #include diff --git a/sharing/linux/tui/BUILD b/sharing/linux/tui/BUILD index 15d55bb3..4ef42336 100644 --- a/sharing/linux/tui/BUILD +++ b/sharing/linux/tui/BUILD @@ -25,7 +25,22 @@ cc_binary( "main.cc", ], deps = [ - ":app", + #":app", + "nearby_sharing_dbus_client" + ] +) + +cc_library( + name ="nearby_sharing_dbus_client", + hdrs = [ + 'nearby_sharing_dbus_client.h' + ], + srcs = [ + 'nearby_sharing_dbus_client.cc' + ], + deps = [ + "//sharing/linux/daemon:nearby_sharing_daemon", + "@sdbus_cpp", ] ) diff --git a/sharing/linux/tui/app.h b/sharing/linux/tui/app.h index 4e5a1415..5f4d047a 100644 --- a/sharing/linux/tui/app.h +++ b/sharing/linux/tui/app.h @@ -17,15 +17,11 @@ class TuiApp { int Run(); private: - bool HandleEvent(Event event); ScreenInteractive screen_; ZenityFilePicker file_picker_; - Page current_page_ = Page::FilePicker; std::string hostname_; std::string selected_file_; - std::string incoming_share_device_name_ = "Lasan's A55"; - ShareTargetType incoming_share_device_type_ = ShareTargetType::kPhone; }; } // namespace nearby::sharing::linux_tui diff --git a/sharing/linux/tui/main.cc b/sharing/linux/tui/main.cc index 788d8c11..db72303c 100644 --- a/sharing/linux/tui/main.cc +++ b/sharing/linux/tui/main.cc @@ -1,6 +1,15 @@ -#include "sharing/linux/tui/app.h" +#include "nearby_sharing_dbus_client.h" +#include int main() { - nearby::sharing::linux_tui::TuiApp app; - return app.Run(); + auto sharing = + NearbySharingService(sdbus::ServiceName("com.google.nearby.sharing"), + sdbus::ObjectPath("/com/google/nearby/sharing")); + + sharing.StartReceive(); + sleep(10); + sharing.StopReceive(); + // nearby::sharing::linux_tui::TuiApp app; + // return app.Run(); + return 0; } diff --git a/sharing/linux/tui/nearby_sharing_dbus_client.cc b/sharing/linux/tui/nearby_sharing_dbus_client.cc new file mode 100644 index 00000000..9ae3d107 --- /dev/null +++ b/sharing/linux/tui/nearby_sharing_dbus_client.cc @@ -0,0 +1,143 @@ +#include "sharing/linux/tui/nearby_sharing_dbus_client.h" + +// Helper to extract a value from the map with a fallback if missing or type +// mismatches +template +T get_field(const std::map& map, + const std::string& key, T default_value = T{}) { + auto it = map.find(key); + if (it != map.end()) { + try { + return it->second.get(); + } catch (const sdbus::Error& e) { + // Log type mismatch error if necessary + } + } + return default_value; +} + +// Helper for optional fields +template +std::optional get_optional_field( + const std::map& map, const std::string& key) { + auto it = map.find(key); + if (it != map.end()) { + try { + return it->second.get(); + } catch (const sdbus::Error& e) { + // Log type mismatch error if necessary + } + } + return std::nullopt; +} + +// Converter: Map -> ShareTarget +ShareTarget convertToShareTarget( + const std::map& map) { + ShareTarget target; + target.id = get_field(map, "id"); + target.device_name = get_field(map, "device_name"); + target.type = get_field(map, "type"); + target.is_incoming = get_field(map, "is_incoming"); + target.is_known = get_field(map, "is_known"); + target.device_id = get_field(map, "device_id"); + target.for_self_share = get_field(map, "for_self_share"); + target.vendor_id = get_field(map, "vendor_id"); + target.receive_disabled = get_field(map, "receive_disabled"); + return target; +} + +// Converter: Map -> Transfer +Transfer convertToTransfer(const std::map& map) { + Transfer transfer; + transfer.status = get_field(map, "status"); + transfer.progress = get_field(map, "progress"); + transfer.transferred_bytes = get_field(map, "transferred_bytes"); + transfer.total_bytes = get_field(map, "total_bytes"); + transfer.transfer_speed = get_field(map, "transfer_speed"); + transfer.estimated_time_remaining = + get_field(map, "estimated_time_remaining"); + transfer.total_attachments_count = + get_field(map, "total_attachments_count"); + transfer.transferred_attachments_count = + get_field(map, "transferred_attachments_count"); + transfer.is_final_status = get_field(map, "is_final_status"); + transfer.is_self_share = get_field(map, "is_self_share"); + transfer.binding_id = get_field(map, "binding_id"); + + // Optional fields + transfer.token = get_optional_field(map, "token"); + transfer.in_progress_attachment_id = + get_optional_field(map, "in_progress_attachment_id"); + transfer.in_progress_attachment_transferred_bytes = + get_optional_field(map, + "in_progress_attachment_transferred_bytes"); + transfer.in_progress_attachment_total_bytes = + get_optional_field(map, "in_progress_attachment_total_bytes"); + return transfer; +} + +// Converter: Map -> Status +Status convertToStatus(const std::map& map) { + Status status; + status.receive_registered = get_field(map, "receive_registered"); + status.discovery_registered = get_field(map, "discovery_registered"); + status.is_transferring = get_field(map, "is_transferring"); + status.is_scanning = get_field(map, "is_scanning"); + status.bluetooth_present = get_field(map, "bluetooth_present"); + status.bluetooth_powered = get_field(map, "bluetooth_powered"); + status.lan_connected = get_field(map, "lan_connected"); + + // Unpack aa{sv} (vector of maps) into vector of ShareTarget structs + auto it = map.find("targets"); + if (it != map.end()) { + try { + auto raw_targets = + it->second.get>>(); + for (const auto& target_map : raw_targets) { + status.targets.push_back(convertToShareTarget(target_map)); + } + } catch (const sdbus::Error& e) { + // Handle type mismatch for targets array + } + } + return status; +} + +void NearbySharingService::onTargetDiscovered( + const std::map& share_target) { + auto target = convertToShareTarget(share_target); + targets_[target.id] = target; +}; + +void NearbySharingService::onTargetUpdated( + const std::map& share_target) { + auto target = convertToShareTarget(share_target); + targets_[target.id] = target; +}; + +void NearbySharingService::onTargetLost( + const std::map& share_target) { + auto target = convertToShareTarget(share_target); + if (targets_.find(target.id) != targets_.end()) { + targets_.erase(target.id); + } +}; + +void NearbySharingService::onIncomingTransfer( + const std::string& direction, + const std::map& share_target, + const std::map& transfer) { + auto target = convertToShareTarget(share_target); + auto incoming_transfer = convertToTransfer(transfer); + + incoming_transfer_ = incoming_transfer; +}; + +void NearbySharingService::onTransferUpdate( + const std::string& direction, + const std::map& share_target, + const std::map& transfer) {}; + +void NearbySharingService::onStatusChanged( + const std::map& share_target) {}; diff --git a/sharing/linux/tui/nearby_sharing_dbus_client.h b/sharing/linux/tui/nearby_sharing_dbus_client.h new file mode 100644 index 00000000..346a7c4b --- /dev/null +++ b/sharing/linux/tui/nearby_sharing_dbus_client.h @@ -0,0 +1,78 @@ +#include "sharing/linux/daemon/nearby_sharing_client.h" + +struct ShareTarget { + int64_t id; + std::string device_name; + int32_t type; + bool is_incoming; + bool is_known; + std::string device_id; + bool for_self_share; + int32_t vendor_id; + bool receive_disabled; +}; + +struct Transfer { + std::string status; + int32_t progress; + int64_t transferred_bytes; + int64_t total_bytes; + int64_t transfer_speed; + int64_t estimated_time_remaining; + int32_t total_attachments_count; + int32_t transferred_attachments_count; + bool is_final_status; + bool is_self_share; + std::string binding_id; + std::optional token; + std::optional in_progress_attachment_id; + std::optional in_progress_attachment_transferred_bytes; + std::optional in_progress_attachment_total_bytes; +}; + +struct Status { + bool receive_registered; + bool discovery_registered; + bool is_transferring; + bool is_scanning; + bool bluetooth_present; + bool bluetooth_powered; + bool lan_connected; + + // aa{sv} maps to a vector of ShareTarget structs + std::vector targets; +}; + + +class NearbySharingService + : public sdbus::ProxyInterfaces { + public: + NearbySharingService(sdbus::ServiceName dest, sdbus::ObjectPath objectPath) + : ProxyInterfaces(std::move(dest), std::move(objectPath)) { + registerProxy(); + } + ~NearbySharingService() { unregisterProxy(); } + + void onTargetDiscovered( + const std::map& share_target) override; + void onTargetUpdated( + const std::map& share_target) override; + void onTargetLost( + const std::map& share_target) override; + void onIncomingTransfer( + const std::string& direction, + const std::map& share_target, + const std::map& transfer) override; + void onTransferUpdate( + const std::string& direction, + const std::map& share_target, + const std::map& transfer) override; + void onStatusChanged( + const std::map& status) override; + + private: + Status status_; + std::map targets_; + Transfer outgoing_transfer_; + Transfer incoming_transfer_; +};