From 0afaffdec4ec47148eaf592bc0ee511ac40a0935 Mon Sep 17 00:00:00 2001 From: Lasan Mahaliyana Date: Tue, 30 Jun 2026 22:57:02 +0530 Subject: [PATCH] more ui stuff --- sharing/linux/app/AppContent.qml | 91 +++++++-- sharing/linux/app/Drop.qml | 1 + sharing/linux/app/IncomingShare.qml | 94 +++++++++- sharing/linux/app/Targets.qml | 4 +- sharing/linux/app/backend.cc | 276 ++++++++++++++++++++++++++-- sharing/linux/app/backend.h | 63 +++++++ 6 files changed, 488 insertions(+), 41 deletions(-) diff --git a/sharing/linux/app/AppContent.qml b/sharing/linux/app/AppContent.qml index 5e0bc922..ed34c6fa 100644 --- a/sharing/linux/app/AppContent.qml +++ b/sharing/linux/app/AppContent.qml @@ -5,20 +5,15 @@ import QtQuick.Shapes import "." RowLayout { - id: top + id: topLayout anchors.fill: parent spacing: 0 property string pendingPath: "" property bool pendingTransfer: false property bool incomingShare: false - property int currentIndex: 1 - - property string filename: "VacationPhoto_2026.jpg" - property string targetname: "Lasan's A55" - property real progressValue: 0.64 - property string statusText: "Receiving 1 of 1 items" - property bool transferring: true + property int currentIndex: 0 + property var selectedTransferId: 0 Connections { target: backend @@ -28,8 +23,14 @@ RowLayout { } function onIncomingTransfer(share_target_id, device_name, status) { console.log("Getting incoming share"); + console.log("selected transfer: " + share_target_id) + selectedTransferId = share_target_id; currentIndex = 2; - transferring = false; + } + function onTransferUpdate(share_target_id, device_name, status, progress) { + if (selectedTransferId === share_target_id) { + currentIndex = 2; + } } } @@ -37,12 +38,22 @@ RowLayout { target: EventBus function onFileSelected(path) { - top.pendingPath = path; + topLayout.pendingPath = path; startSharing(); } + function onShareTargetSelected(shareTargetId) { + if (topLayout.pendingPath.length === 0) { + return; + } + topLayout.selectedTransferId = shareTargetId; + backend.prepareOutgoingTransfer(shareTargetId, topLayout.pendingPath); + backend.sendFile(shareTargetId, topLayout.pendingPath); + topLayout.currentIndex = 2; + } + function onCancelPendingShareRequested() { - top.cancelPendingShare(); + topLayout.cancelPendingShare(); } } @@ -52,6 +63,7 @@ RowLayout { function cancelPendingShare() { pendingPath = ""; + selectedTransferId = 0; currentIndex = 0; backend.stopDiscovery(); backend.startReceive(); @@ -64,24 +76,65 @@ RowLayout { } Sidebar { - pendingPath: top.pendingPath + pendingPath: topLayout.pendingPath } StackLayout { id: contentStack Layout.fillWidth: true Layout.fillHeight: true - currentIndex: top.currentIndex + currentIndex: topLayout.currentIndex Drop {} SearchingDevices {} - IncomingShare { - filename: top.filename - targetname: top.targetname - progressValue: top.progressValue - statusText: top.statusText - transferring: top.transferring + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + Repeater { + id: transferRepeater + model: backend.transfers + + IncomingShare { + anchors.fill: parent + + Component.onCompleted: { + console.log("transferId: " + model.transferId) + console.log("selectedTransferId: " + topLayout.selectedTransferId) + console.log(model.transferId == topLayout.selectedTransferId) + + } + + visible: topLayout.selectedTransferId == 0 || model.transferId == topLayout.selectedTransferId + shareTargetId: model.transferId + direction: model.direction + filename: model.localPath + targetname: model.deviceName + progressValue: model.progress + status: model.status + totalBytes: model.totalBytes + transferredBytes: model.transferredBytes + totalAttachmentsCount: model.totalAttachmentsCount + transferredAttachmentsCount: model.transferredAttachmentsCount + isFinalStatus: model.isFinalStatus + awaitingLocalConfirmation: model.awaitingLocalConfirmation + } + } + + Rectangle { + anchors.fill: parent + color: "#DCF5FF" + visible: transferRepeater.count === 0 + + Text { + anchors.centerIn: parent + text: "Preparing transfer..." + color: "#377B95" + font.pointSize: 18 + font.weight: 600 + } + } } } } diff --git a/sharing/linux/app/Drop.qml b/sharing/linux/app/Drop.qml index 97ea2332..5617626f 100644 --- a/sharing/linux/app/Drop.qml +++ b/sharing/linux/app/Drop.qml @@ -40,6 +40,7 @@ Rectangle { drop.acceptProposedAction(); if (drop.hasUrls) { console.log(drop.urls[0].toString()); + EventBus.fileSelected(drop.urls[0].toString()); for (let i of drop.formats) { console.log(i); } diff --git a/sharing/linux/app/IncomingShare.qml b/sharing/linux/app/IncomingShare.qml index 67ded9e7..45fca8c8 100644 --- a/sharing/linux/app/IncomingShare.qml +++ b/sharing/linux/app/IncomingShare.qml @@ -9,11 +9,87 @@ Rectangle { Layout.fillWidth: true Layout.fillHeight: true + property var shareTargetId: 0 + property string direction: "receive" property string filename: "ThisIsAnImage.jpg" property string targetname: "Lasan's A55" - property bool transferring: true + property bool transferring: !awaitingLocalConfirmation property real progressValue: 0.64 - property string statusText: "Receiving file" + property string status: "kUnknown" + property bool isFinalStatus: false + property bool awaitingLocalConfirmation: false + property int totalAttachmentsCount: 0 + property int transferredAttachmentsCount: 0 + property var totalBytes: 0 + property var transferredBytes: 0 + + function baseName(path) { + if (!path || path.length === 0) { + return direction === "send" ? "Selected file" : transferSummary() + } + const normalized = decodeURIComponent(path).replace("file://", "") + const parts = normalized.split("/") + return parts.length === 0 ? normalized : parts[parts.length - 1] + } + + function transferSummary() { + if (totalAttachmentsCount > 0) { + return totalAttachmentsCount === 1 ? "1 item" : totalAttachmentsCount + " items" + } + if (totalBytes > 0) { + return formatBytes(totalBytes) + } + return "Shared items" + } + + function formatBytes(bytes) { + let value = Number(bytes) + if (!isFinite(value) || value <= 0) { + return "" + } + const units = ["B", "KB", "MB", "GB", "TB"] + let unitIndex = 0 + while (value >= 1024 && unitIndex < units.length - 1) { + value /= 1024 + unitIndex += 1 + } + return (unitIndex === 0 ? Math.round(value) : value.toFixed(value >= 10 ? 1 : 2)) + " " + units[unitIndex] + } + + function statusText() { + if (status === "kAwaitingLocalConfirmation") { + return "Incoming share request" + } + if (status === "kConnecting") { + return direction === "send" ? "Connecting" : "Preparing transfer" + } + if (status === "kAwaitingRemoteAcceptance") { + return "Waiting for receiver" + } + if (status === "kInProgress") { + const verb = direction === "send" ? "Sending" : "Receiving" + if (totalAttachmentsCount > 0) { + return verb + " " + Math.min(transferredAttachmentsCount + 1, totalAttachmentsCount) + " of " + totalAttachmentsCount + " items" + } + return verb + } + if (status === "kComplete") { + return direction === "send" ? "Sent" : "Received" + } + if (status === "kReject") { + return "Rejected" + } + if (status === "kCancelled") { + return "Cancelled" + } + if (status === "kTimedOut") { + return "Timed out" + } + if (isFinalStatus) { + return "Transfer failed" + } + return direction === "send" ? "Starting send" : "Preparing transfer" + } Item { visible: root.transferring @@ -68,7 +144,7 @@ Rectangle { } Text { - text: root.filename + text: root.baseName(root.filename) color: "#57707A" font.pointSize: 12 elide: Text.ElideMiddle @@ -102,7 +178,7 @@ Rectangle { Layout.fillWidth: true Text { - text: root.statusText + text: root.statusText() color: "#57707A" font.pointSize: 12 Layout.fillWidth: true @@ -127,6 +203,8 @@ Rectangle { Button { id: cancelButton text: "Cancel" + visible: !root.isFinalStatus + onClicked: backend.cancel(root.shareTargetId) contentItem: Text { text: cancelButton.text @@ -174,7 +252,7 @@ Rectangle { font.pointSize: 18 font.weight: 700 color: "#377B95" - text: "Incoming share" + text: root.direction === "send" ? "Outgoing share" : "Incoming share" horizontalAlignment: Text.AlignHCenter } @@ -194,7 +272,7 @@ Rectangle { color: "#57707A" font.pointSize: 11 wrapMode: Text.WrapAnywhere - text: root.filename + text: root.baseName(root.filename) horizontalAlignment: Text.AlignHCenter maximumLineCount: 2 elide: Text.ElideMiddle @@ -216,7 +294,8 @@ Rectangle { Button { id: cancelbutton Layout.fillWidth: true - text: "Cancel" + text: "Reject" + onClicked: backend.reject(root.shareTargetId) contentItem: Text { text: cancelbutton.text @@ -240,6 +319,7 @@ Rectangle { id: acceptButton Layout.fillWidth: true text: "Accept" + onClicked: backend.accept(root.shareTargetId) contentItem: Text { text: acceptButton.text diff --git a/sharing/linux/app/Targets.qml b/sharing/linux/app/Targets.qml index 1a88c61d..752a4596 100644 --- a/sharing/linux/app/Targets.qml +++ b/sharing/linux/app/Targets.qml @@ -6,7 +6,7 @@ Row { id: shareTargetsRow spacing: 10 - property bool useDummyTargets: true + property bool useDummyTargets: false ListModel { id: dummyTargets @@ -53,7 +53,7 @@ Row { spacing: 5 ShareTarget { - shareTargetId: shareTargetsRow.useDummyTargets ? model.targetId : model.id + shareTargetId: model.targetId deviceName: model.deviceName iconSource: { if (model.type === 2) diff --git a/sharing/linux/app/backend.cc b/sharing/linux/app/backend.cc index cea2b667..ad79916e 100644 --- a/sharing/linux/app/backend.cc +++ b/sharing/linux/app/backend.cc @@ -1,9 +1,13 @@ #include "sharing/linux/app/backend.h" +#include #include +#include #include #include +#include +#include #include namespace { @@ -12,6 +16,35 @@ QString ToQString(const std::string& value) { return QString::fromStdString(value); } +QString NormalizeLocalPath(const QString& path) { + const QUrl url(path); + if (url.isValid() && url.isLocalFile()) { + return url.toLocalFile(); + } + return path; +} + +double ProgressFraction(const std::optional& transfer) { + if (!transfer.has_value()) { + return 0; + } + return std::clamp(transfer->progress / 100.0, 0.0, 1.0); +} + +bool IsAwaitingLocalConfirmation( + const std::optional& transfer) { + return transfer.has_value() && + transfer->status == "kAwaitingLocalConfirmation"; +} + +QString DeviceNameFor( + const std::optional& target) { + if (!target.has_value() || target->device_name.empty()) { + return QStringLiteral("Unknown device"); + } + return ToQString(target->device_name); +} + } // namespace ShareTargetModel::ShareTargetModel(QObject* parent) @@ -57,7 +90,7 @@ QVariant ShareTargetModel::data(const QModelIndex& index, int role) const { QHash ShareTargetModel::roleNames() const { return { - {IdRole, "id"}, + {IdRole, "targetId"}, {DeviceNameRole, "deviceName"}, {TypeRole, "type"}, {IsIncomingRole, "isIncoming"}, @@ -104,6 +137,15 @@ void ShareTargetModel::ResetTargets(const std::vector& targets) { endResetModel(); } +std::optional ShareTargetModel::FindTarget( + int64_t target_id) const { + const int row = IndexOf(target_id); + if (row < 0) { + return std::nullopt; + } + return targets_[row]; +} + int ShareTargetModel::IndexOf(int64_t target_id) const { for (int i = 0; i < static_cast(targets_.size()); ++i) { if (targets_[i].id == target_id) { @@ -113,7 +155,178 @@ int ShareTargetModel::IndexOf(int64_t target_id) const { return -1; } -Backend::Backend(QObject* parent) : QObject(parent), targets_(this) { +ShareTransferModel::ShareTransferModel(QObject* parent) + : QAbstractListModel(parent) {} + +int ShareTransferModel::rowCount(const QModelIndex& parent) const { + if (parent.isValid()) { + return 0; + } + return static_cast(transfers_.size()); +} + +QVariant ShareTransferModel::data(const QModelIndex& index, int role) const { + if (!index.isValid() || index.row() < 0 || + index.row() >= static_cast(transfers_.size())) { + return {}; + } + return DataForRow(transfers_[index.row()], role); +} + +QHash ShareTransferModel::roleNames() const { + return { + {IdRole, "transferId"}, + {DirectionRole, "direction"}, + {DeviceNameRole, "deviceName"}, + {TypeRole, "type"}, + {StatusRole, "status"}, + {ProgressRole, "progress"}, + {TransferredBytesRole, "transferredBytes"}, + {TotalBytesRole, "totalBytes"}, + {TransferSpeedRole, "transferSpeed"}, + {EstimatedTimeRemainingRole, "estimatedTimeRemaining"}, + {TotalAttachmentsCountRole, "totalAttachmentsCount"}, + {TransferredAttachmentsCountRole, "transferredAttachmentsCount"}, + {IsFinalStatusRole, "isFinalStatus"}, + {HasTargetRole, "hasTarget"}, + {HasTransferRole, "hasTransfer"}, + {AwaitingLocalConfirmationRole, "awaitingLocalConfirmation"}, + {LocalPathRole, "localPath"}, + }; +} + +void ShareTransferModel::ApplyTarget(const ShareTarget& target) { + const int row = IndexOf(target.id); + if (row < 0) { + return; + } + transfers_[row].target = target; + EmitRowChanged(row); +} + +void ShareTransferModel::ApplyTransfer(const QString& direction, + const ShareTarget& target, + const Transfer& transfer) { + int row = IndexOf(target.id); + if (row < 0) { + row = static_cast(transfers_.size()); + beginInsertRows(QModelIndex(), row, row); + transfers_.push_back(Row{target.id, direction, target, transfer, {}}); + endInsertRows(); + return; + } + + transfers_[row].direction = direction; + transfers_[row].target = target; + transfers_[row].transfer = transfer; + EmitRowChanged(row); +} + +void ShareTransferModel::PrepareOutgoingTransfer( + int64_t target_id, const QString& local_path, + const std::optional& target) { + int row = IndexOf(target_id); + if (row < 0) { + row = static_cast(transfers_.size()); + beginInsertRows(QModelIndex(), row, row); + Row transfer; + transfer.id = target_id; + transfer.direction = QStringLiteral("send"); + transfer.target = target; + transfer.local_path = local_path; + transfers_.push_back(std::move(transfer)); + endInsertRows(); + return; + } + + transfers_[row].direction = QStringLiteral("send"); + transfers_[row].target = target; + transfers_[row].local_path = local_path; + EmitRowChanged(row); +} + +void ShareTransferModel::RemoveTransfer(int64_t target_id) { + const int row = IndexOf(target_id); + if (row < 0) { + return; + } + beginRemoveRows(QModelIndex(), row, row); + transfers_.erase(transfers_.begin() + row); + endRemoveRows(); +} + +int ShareTransferModel::IndexOf(int64_t target_id) const { + for (int i = 0; i < static_cast(transfers_.size()); ++i) { + if (transfers_[i].id == target_id) { + return i; + } + } + return -1; +} + +QVariant ShareTransferModel::DataForRow(const Row& row, int role) const { + const Transfer empty_transfer; + const Transfer& transfer = row.transfer.value_or(empty_transfer); + + switch (role) { + case IdRole: + return QVariant::fromValue(row.id); + case DirectionRole: + return row.direction; + case DeviceNameRole: + return DeviceNameFor(row.target); + case TypeRole: + return row.target.has_value() ? row.target->type : 0; + case StatusRole: + if (row.transfer.has_value()) { + return ToQString(transfer.status); + } + return row.direction == QStringLiteral("send") + ? QStringLiteral("kConnecting") + : QStringLiteral("kUnknown"); + case ProgressRole: + return ProgressFraction(row.transfer); + case TransferredBytesRole: + return QVariant::fromValue(transfer.transferred_bytes); + case TotalBytesRole: + return QVariant::fromValue(transfer.total_bytes); + case TransferSpeedRole: + return QVariant::fromValue(transfer.transfer_speed); + case EstimatedTimeRemainingRole: + return QVariant::fromValue(transfer.estimated_time_remaining); + case TotalAttachmentsCountRole: + return transfer.total_attachments_count; + case TransferredAttachmentsCountRole: + return transfer.transferred_attachments_count; + case IsFinalStatusRole: + return transfer.is_final_status; + case HasTargetRole: + return row.target.has_value(); + case HasTransferRole: + return row.transfer.has_value(); + case AwaitingLocalConfirmationRole: + return IsAwaitingLocalConfirmation(row.transfer); + case LocalPathRole: + return row.local_path; + default: + return {}; + } +} + +void ShareTransferModel::EmitRowChanged(int row) { + const QModelIndex changed_index = index(row); + emit dataChanged(changed_index, changed_index, + {IdRole, DirectionRole, DeviceNameRole, TypeRole, + StatusRole, ProgressRole, TransferredBytesRole, + TotalBytesRole, TransferSpeedRole, + EstimatedTimeRemainingRole, TotalAttachmentsCountRole, + TransferredAttachmentsCountRole, IsFinalStatusRole, + HasTargetRole, HasTransferRole, + AwaitingLocalConfirmationRole, LocalPathRole}); +} + +Backend::Backend(QObject* parent) + : QObject(parent), targets_(this), transfers_(this) { try { client_ = std::make_unique(this); SetStatusText(QStringLiteral("Connected to nearby sharing daemon")); @@ -146,11 +359,18 @@ void Backend::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()); + const QString local_path = NormalizeLocalPath(path); + RunCommand(QStringLiteral("Send file"), [this, share_target_id, local_path]() { + return client_->SendFile(share_target_id, local_path.toStdString()); }); } +void Backend::prepareOutgoingTransfer(qint64 share_target_id, + const QString& path) { + transfers_.PrepareOutgoingTransfer(share_target_id, NormalizeLocalPath(path), + targets_.FindTarget(share_target_id)); +} + void Backend::accept(qint64 share_target_id) { RunCommand(QStringLiteral("Accept transfer"), [this, share_target_id]() { return client_->Accept(share_target_id); @@ -189,8 +409,9 @@ void Backend::OnIncomingTransfer(const std::string& direction, const Transfer& transfer) { QMetaObject::invokeMethod( this, - [this, target, transfer]() { + [this, direction, target, transfer]() { ApplyTarget(target); + transfers_.ApplyTransfer(ToQString(direction), target, transfer); emit incomingTransfer(target.id, ToQString(target.device_name), ToQString(transfer.status)); }, @@ -202,8 +423,9 @@ void Backend::OnTransferUpdate(const std::string& direction, const Transfer& transfer) { QMetaObject::invokeMethod( this, - [this, target, transfer]() { + [this, direction, target, transfer]() { ApplyTarget(target); + transfers_.ApplyTransfer(ToQString(direction), target, transfer); emit transferUpdate(target.id, ToQString(target.device_name), ToQString(transfer.status), transfer.progress); }, @@ -217,6 +439,7 @@ void Backend::OnStatusChanged(const Status& status) { void Backend::ApplyTarget(const ShareTarget& target) { targets_.ApplyTarget(target); + transfers_.ApplyTarget(target); } void Backend::RemoveTarget(const ShareTarget& target) { @@ -246,13 +469,40 @@ void Backend::RunCommand( return; } - try { - ApplyCommandResult(command, operation()); - } catch (const sdbus::Error& error) { - SetStatusText( - QStringLiteral("%1 failed: %2") - .arg(command, QString::fromStdString(error.getMessage()))); - } + QPointer backend(this); + std::thread([backend, command, operation]() { + try { + auto result = operation(); + if (!backend) { + return; + } + QMetaObject::invokeMethod( + backend, + [backend, command, result = std::move(result)]() { + if (!backend) { + return; + } + backend->ApplyCommandResult(command, result); + }, + Qt::QueuedConnection); + } catch (const sdbus::Error& error) { + const QString message = QStringLiteral("%1 failed: %2") + .arg(command, QString::fromStdString( + error.getMessage())); + if (!backend) { + return; + } + QMetaObject::invokeMethod( + backend, + [backend, message]() { + if (!backend) { + return; + } + backend->SetStatusText(message); + }, + Qt::QueuedConnection); + } + }).detach(); } diff --git a/sharing/linux/app/backend.h b/sharing/linux/app/backend.h index 330ccd86..61f97052 100644 --- a/sharing/linux/app/backend.h +++ b/sharing/linux/app/backend.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,7 @@ class ShareTargetModel : public QAbstractListModel { void ApplyTarget(const ShareTarget& target); void RemoveTarget(int64_t target_id); void ResetTargets(const std::vector& targets); + std::optional FindTarget(int64_t target_id) const; private: int IndexOf(int64_t target_id) const; @@ -49,6 +51,62 @@ class ShareTargetModel : public QAbstractListModel { std::vector targets_; }; +class ShareTransferModel : public QAbstractListModel { + Q_OBJECT + + public: + using ShareTarget = nearby::sharing::linux::app::ShareTarget; + using Transfer = nearby::sharing::linux::app::Transfer; + + enum Role { + IdRole = Qt::UserRole + 1, + DirectionRole, + DeviceNameRole, + TypeRole, + StatusRole, + ProgressRole, + TransferredBytesRole, + TotalBytesRole, + TransferSpeedRole, + EstimatedTimeRemainingRole, + TotalAttachmentsCountRole, + TransferredAttachmentsCountRole, + IsFinalStatusRole, + HasTargetRole, + HasTransferRole, + AwaitingLocalConfirmationRole, + LocalPathRole, + }; + + explicit ShareTransferModel(QObject* parent = nullptr); + + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role) const override; + QHash roleNames() const override; + + void ApplyTarget(const ShareTarget& target); + void ApplyTransfer(const QString& direction, const ShareTarget& target, + const Transfer& transfer); + void PrepareOutgoingTransfer(int64_t target_id, const QString& local_path, + const std::optional& target); + void RemoveTransfer(int64_t target_id); + + private: + struct Row { + int64_t id = 0; + QString direction; + std::optional target; + std::optional transfer; + QString local_path; + }; + + int IndexOf(int64_t target_id) const; + QVariant DataForRow(const Row& row, int role) const; + void EmitRowChanged(int row); + + std::vector transfers_; +}; + class Backend : public QObject, public nearby::sharing::linux::app::NearbySharingDbusClient::Observer { @@ -61,6 +119,7 @@ class Backend Q_PROPERTY(bool scanning READ scanning NOTIFY statusChanged) Q_PROPERTY(bool transferring READ transferring NOTIFY statusChanged) Q_PROPERTY(QAbstractListModel* targets READ targets CONSTANT) + Q_PROPERTY(QAbstractListModel* transfers READ transfers CONSTANT) public: explicit Backend(QObject* parent = nullptr); @@ -72,12 +131,15 @@ class Backend bool scanning() const { return status_.is_scanning; } bool transferring() const { return status_.is_transferring; } QAbstractListModel* targets() { return &targets_; } + QAbstractListModel* transfers() { return &transfers_; } 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 prepareOutgoingTransfer(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); @@ -119,6 +181,7 @@ class Backend QString status_text_; Status status_; ShareTargetModel targets_; + ShareTransferModel transfers_; std::unique_ptr client_; bool is_incoming_transfer_ = false; };