more ui stuff

This commit is contained in:
Lasan Mahaliyana
2026-06-30 22:57:02 +05:30
parent ce37bcef6d
commit 0afaffdec4
6 changed files with 488 additions and 41 deletions
+72 -19
View File
@@ -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
}
}
}
}
}
+1
View File
@@ -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);
}
+87 -7
View File
@@ -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
+2 -2
View File
@@ -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)
+263 -13
View File
@@ -1,9 +1,13 @@
#include "sharing/linux/app/backend.h"
#include <algorithm>
#include <functional>
#include <thread>
#include <utility>
#include <QMetaObject>
#include <QPointer>
#include <QUrl>
#include <sdbus-c++/sdbus-c++.h>
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<nearby::sharing::linux::app::Transfer>& transfer) {
if (!transfer.has_value()) {
return 0;
}
return std::clamp(transfer->progress / 100.0, 0.0, 1.0);
}
bool IsAwaitingLocalConfirmation(
const std::optional<nearby::sharing::linux::app::Transfer>& transfer) {
return transfer.has_value() &&
transfer->status == "kAwaitingLocalConfirmation";
}
QString DeviceNameFor(
const std::optional<nearby::sharing::linux::app::ShareTarget>& 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<int, QByteArray> ShareTargetModel::roleNames() const {
return {
{IdRole, "id"},
{IdRole, "targetId"},
{DeviceNameRole, "deviceName"},
{TypeRole, "type"},
{IsIncomingRole, "isIncoming"},
@@ -104,6 +137,15 @@ void ShareTargetModel::ResetTargets(const std::vector<ShareTarget>& targets) {
endResetModel();
}
std::optional<ShareTargetModel::ShareTarget> 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<int>(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<int>(transfers_.size());
}
QVariant ShareTransferModel::data(const QModelIndex& index, int role) const {
if (!index.isValid() || index.row() < 0 ||
index.row() >= static_cast<int>(transfers_.size())) {
return {};
}
return DataForRow(transfers_[index.row()], role);
}
QHash<int, QByteArray> 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<int>(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<ShareTarget>& target) {
int row = IndexOf(target_id);
if (row < 0) {
row = static_cast<int>(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<int>(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<qint64>(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<qint64>(transfer.transferred_bytes);
case TotalBytesRole:
return QVariant::fromValue<qint64>(transfer.total_bytes);
case TransferSpeedRole:
return QVariant::fromValue<qint64>(transfer.transfer_speed);
case EstimatedTimeRemainingRole:
return QVariant::fromValue<qint64>(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<Client>(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> 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();
}
+63
View File
@@ -3,6 +3,7 @@
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <tuple>
#include <vector>
@@ -42,6 +43,7 @@ class ShareTargetModel : public QAbstractListModel {
void ApplyTarget(const ShareTarget& target);
void RemoveTarget(int64_t target_id);
void ResetTargets(const std::vector<ShareTarget>& targets);
std::optional<ShareTarget> FindTarget(int64_t target_id) const;
private:
int IndexOf(int64_t target_id) const;
@@ -49,6 +51,62 @@ class ShareTargetModel : public QAbstractListModel {
std::vector<ShareTarget> 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<int, QByteArray> 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<ShareTarget>& target);
void RemoveTransfer(int64_t target_id);
private:
struct Row {
int64_t id = 0;
QString direction;
std::optional<ShareTarget> target;
std::optional<Transfer> 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<Row> 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> client_;
bool is_incoming_transfer_ = false;
};