added fly in outgoing transfer card and compact mode

This commit is contained in:
Lasan Mahaliyana
2026-07-18 20:45:27 +05:30
parent 27b635dac8
commit 5489fa334f
11 changed files with 485 additions and 178 deletions
+181 -39
View File
@@ -10,10 +10,11 @@ RowLayout {
spacing: 0
property string pendingPath: ""
property bool pendingTransfer: false
property bool incomingShare: false
property int currentIndex: 0
property var selectedTransferId: 0
property var activeOutgoingTransferId: 0
property bool outgoingStartFailed: false
readonly property bool outgoingTransferActive: activeOutgoingTransferId != 0
Connections {
target: backend
@@ -24,6 +25,12 @@ RowLayout {
selectedTransferId = share_target_id;
currentIndex = 2;
}
function onOutgoingTransferStartFailed(share_target_id) {
if (share_target_id == topLayout.activeOutgoingTransferId) {
topLayout.outgoingStartFailed = true;
}
}
}
Connections {
@@ -35,12 +42,14 @@ RowLayout {
}
function onShareTargetSelected(shareTargetId) {
if (topLayout.pendingPath.length === 0) {
if (topLayout.pendingPath.length === 0
|| topLayout.outgoingTransferActive) {
return;
}
topLayout.selectedTransferId = shareTargetId;
backend.sendFile(shareTargetId, topLayout.pendingPath);
topLayout.currentIndex = 2;
if (backend.sendFile(shareTargetId, topLayout.pendingPath)) {
topLayout.outgoingStartFailed = false;
topLayout.activeOutgoingTransferId = shareTargetId;
}
}
function onCancelPendingShareRequested() {
@@ -49,8 +58,25 @@ RowLayout {
}
function cancelPendingShare() {
if (outgoingTransferActive) {
return;
}
returnHome();
}
function finishOutgoingTransfer(shareTargetId) {
if (shareTargetId != activeOutgoingTransferId) {
return;
}
activeOutgoingTransferId = 0;
outgoingStartFailed = false;
}
function returnHome() {
pendingPath = "";
selectedTransferId = 0;
activeOutgoingTransferId = 0;
outgoingStartFailed = false;
currentIndex = 0;
backend.startReceive();
}
@@ -62,62 +88,178 @@ RowLayout {
Sidebar {
pendingPath: topLayout.pendingPath
cancelEnabled: !topLayout.outgoingTransferActive
}
StackLayout {
id: contentStack
Item {
id: contentPane
Layout.fillWidth: true
Layout.fillHeight: true
currentIndex: topLayout.currentIndex
Drop {}
StackLayout {
id: contentStack
anchors.fill: parent
currentIndex: topLayout.currentIndex
SearchingDevices {}
Drop {}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
SearchingDevices {
transferLocked: topLayout.outgoingTransferActive
activeTransferId: topLayout.activeOutgoingTransferId
}
Repeater {
id: transferRepeater
model: backend.transfers
Item {
Layout.fillWidth: true
Layout.fillHeight: true
IncomingShare {
anchors.fill: parent
Repeater {
id: transferRepeater
model: backend.transfers
Component.onCompleted: {
console.log("transferId: " + model.transferId)
console.log("selectedTransferId: " + topLayout.selectedTransferId)
console.log(model.transferId == topLayout.selectedTransferId)
IncomingShare {
anchors.fill: parent
visible: model.direction === "receive"
&& 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
onReturnHomeRequested: topLayout.returnHome()
}
}
visible: topLayout.selectedTransferId == 0 || model.transferId == topLayout.selectedTransferId
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
}
}
}
}
Repeater {
model: backend.transfers
Item {
id: outgoingNotification
readonly property bool terminal: model.isFinalStatus
|| topLayout.outgoingStartFailed
property bool cancelPending: false
property bool exiting: false
width: Math.min(contentPane.width * 0.9, 700)
height: 156
x: (contentPane.width - width) / 2
y: -height - 8
z: 100
visible: topLayout.currentIndex === 1
&& model.direction === "send"
&& model.transferId == topLayout.activeOutgoingTransferId
function showCard() {
if (!visible) {
return;
}
exiting = false;
cancelPending = false;
finalStatusTimer.stop();
exitAnimation.stop();
y = -height - 8;
enterAnimation.restart();
if (terminal) {
finalStatusTimer.restart();
}
}
onVisibleChanged: {
if (visible) {
Qt.callLater(showCard);
} else {
finalStatusTimer.stop();
}
}
onTerminalChanged: {
if (visible && terminal) {
finalStatusTimer.restart();
}
}
Component.onCompleted: {
if (visible) {
Qt.callLater(showCard);
}
}
TransferCard {
anchors.fill: parent
compact: true
shareTargetId: model.transferId
direction: model.direction
filename: model.localPath
targetname: model.deviceName
progressValue: model.progress
status: model.status
status: topLayout.outgoingStartFailed ? "kStartFailed" : model.status
isFinalStatus: outgoingNotification.terminal
cancelEnabled: !outgoingNotification.cancelPending
totalBytes: model.totalBytes
transferredBytes: model.transferredBytes
totalAttachmentsCount: model.totalAttachmentsCount
transferredAttachmentsCount: model.transferredAttachmentsCount
isFinalStatus: model.isFinalStatus
awaitingLocalConfirmation: model.awaitingLocalConfirmation
onCancelRequested: {
outgoingNotification.cancelPending = true;
backend.cancel(model.transferId);
}
}
}
Rectangle {
anchors.fill: parent
color: "#DCF5FF"
visible: transferRepeater.count === 0
NumberAnimation {
id: enterAnimation
target: outgoingNotification
property: "y"
to: 16
duration: 300
easing.type: Easing.OutCubic
}
Text {
anchors.centerIn: parent
text: "Preparing transfer..."
color: "#377B95"
font.pointSize: 18
font.weight: 600
Timer {
id: finalStatusTimer
interval: 2000
repeat: false
onTriggered: {
outgoingNotification.exiting = true;
exitAnimation.restart();
}
}
NumberAnimation {
id: exitAnimation
target: outgoingNotification
property: "y"
to: -outgoingNotification.height - 8
duration: 250
easing.type: Easing.InCubic
onStopped: {
if (outgoingNotification.exiting) {
outgoingNotification.exiting = false;
topLayout.finishOutgoingTransfer(model.transferId);
}
}
}
}
}
+1
View File
@@ -32,6 +32,7 @@ qt_resource_via_qrc(
"SearchingDevices.qml",
"Targets.qml",
"IncomingShare.qml",
"TransferCard.qml",
"googlesans_var.ttf",
"icons/file.svg",
"icons/laptop.svg",
+16 -130
View File
@@ -23,6 +23,8 @@ Rectangle {
property var totalBytes: 0
property var transferredBytes: 0
signal returnHomeRequested()
function baseName(path) {
if (!path || path.length === 0) {
return direction === "send" ? "Selected file" : transferSummary()
@@ -91,140 +93,24 @@ Rectangle {
return direction === "send" ? "Starting send" : "Preparing transfer"
}
Item {
TransferCard {
visible: root.transferring
anchors.centerIn: parent
width: Math.min(parent.width * 0.9, 700)
height: 212
Rectangle {
anchors.fill: parent
radius: 24
color: "#FFFFFF"
border.color: "#BCE5F5"
border.width: 1
}
RowLayout {
anchors.fill: parent
anchors.margins: 28
spacing: 24
Rectangle {
Layout.preferredWidth: 96
Layout.preferredHeight: 96
radius: 24
color: "#E8F7FC"
Image {
anchors.centerIn: parent
width: 60
height: 60
source: "qrc:/icons/file.svg"
fillMode: Image.PreserveAspectFit
sourceSize.width: width
sourceSize.height: height
smooth: true
antialiasing: true
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: 10
Text {
text: root.targetname
font.pointSize: 17
font.weight: 700
color: "#1A1C1E"
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: root.baseName(root.filename)
color: "#57707A"
font.pointSize: 12
elide: Text.ElideMiddle
Layout.fillWidth: true
}
ProgressBar {
id: transferProgress
Layout.fillWidth: true
from: 0
to: 1
value: Math.max(0, Math.min(1, root.progressValue))
background: Rectangle {
implicitHeight: 10
radius: 5
color: "#DDEEF5"
}
contentItem: Item {
Rectangle {
width: transferProgress.visualPosition * parent.width
height: 10
radius: 5
color: "#0D6D90"
}
}
}
RowLayout {
Layout.fillWidth: true
Text {
text: root.statusText()
color: "#57707A"
font.pointSize: 12
Layout.fillWidth: true
elide: Text.ElideRight
}
Text {
text: Math.round(Math.max(0, Math.min(1, root.progressValue)) * 100) + "%"
color: "#0D6D90"
font.pointSize: 13
font.weight: 700
}
}
RowLayout {
Layout.fillWidth: true
Item {
Layout.fillWidth: true
}
Button {
id: cancelButton
text: "Cancel"
visible: !root.isFinalStatus
onClicked: backend.cancel(root.shareTargetId)
contentItem: Text {
text: cancelButton.text
font.pointSize: 14
font.weight: 600
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
implicitWidth: 118
implicitHeight: 46
radius: 12
color: cancelButton.hovered ? "#B62828" : "#D94C4C"
}
}
}
}
}
shareTargetId: root.shareTargetId
direction: root.direction
filename: root.filename
targetname: root.targetname
progressValue: root.progressValue
status: root.status
isFinalStatus: root.isFinalStatus
totalBytes: root.totalBytes
totalAttachmentsCount: root.totalAttachmentsCount
transferredAttachmentsCount: root.transferredAttachmentsCount
finalActionText: root.isFinalStatus ? "Go back home" : ""
onCancelRequested: backend.cancel(root.shareTargetId)
onFinalActionRequested: root.returnHomeRequested()
}
Item {
+6
View File
@@ -3,10 +3,14 @@ import QtQuick.Layouts
import QtQuick.Controls
Rectangle {
id: root
color: "transparent"
Layout.fillWidth: true
Layout.fillHeight: true
property bool transferLocked: false
property var activeTransferId: 0
Rectangle {
id: pulseButton
property real pulseSize: Math.min(parent.width, parent.height) * 0.5
@@ -83,6 +87,8 @@ Rectangle {
Targets {
Layout.alignment: Qt.AlignHCenter
interactionLocked: root.transferLocked
activeTransferId: root.activeTransferId
}
}
}
+5 -2
View File
@@ -14,7 +14,8 @@ Rectangle {
property string deviceName: "Unknown Device"
property string iconSource: "qrc:icons/smartphone.svg"
property var shareTargetId: 0
property bool hovered: targetMouseArea.containsMouse
property bool interactionEnabled: true
property bool hovered: interactionEnabled && targetMouseArea.containsMouse
ColumnLayout {
anchors.top: parent.top
@@ -47,6 +48,7 @@ Rectangle {
Button {
id: iconButton
enabled: rootItem.interactionEnabled
icon.source: rootItem.iconSource
anchors.fill: parent
anchors.margins: 5
@@ -63,8 +65,9 @@ Rectangle {
MouseArea {
id: targetMouseArea
anchors.fill: parent
enabled: rootItem.interactionEnabled
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
cursorShape: rootItem.interactionEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
EventBus.shareTargetSelected(rootItem.shareTargetId);
}
+3
View File
@@ -10,6 +10,7 @@ Rectangle {
Layout.preferredWidth: 300
property string pendingPath: ""
property bool cancelEnabled: true
color: "#CBF0FF"
@@ -109,6 +110,8 @@ Rectangle {
id: cancelbutton
Layout.fillWidth: true
text: "Cancel"
enabled: sidebar.cancelEnabled
opacity: enabled ? 1 : 0.55
onClicked: EventBus.cancelPendingShareRequested()
contentItem: Text {
text: cancelbutton.text
+12
View File
@@ -7,6 +7,8 @@ Row {
spacing: 10
property bool useDummyTargets: false
property bool interactionLocked: false
property var activeTransferId: 0
ListModel {
id: dummyTargets
@@ -45,6 +47,15 @@ Row {
width: 100
height: 130
color: "transparent"
opacity: !shareTargetsRow.interactionLocked
|| model.targetId == shareTargetsRow.activeTransferId ? 1 : 0.32
Behavior on opacity {
NumberAnimation {
duration: 180
easing.type: Easing.OutQuad
}
}
ColumnLayout {
anchors.top: parent.top
@@ -55,6 +66,7 @@ Row {
ShareTarget {
shareTargetId: model.targetId
deviceName: model.deviceName
interactionEnabled: !shareTargetsRow.interactionLocked
iconSource: {
if (model.type === 2)
return "qrc:icons/laptop.svg"
+234
View File
@@ -0,0 +1,234 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
Item {
id: root
implicitWidth: 700
implicitHeight: compact ? 156 : 212
property bool compact: false
property var shareTargetId: 0
property string direction: "send"
property string filename: ""
property string targetname: "Unknown device"
property real progressValue: 0
property string status: "kUnknown"
property bool isFinalStatus: false
property bool cancelEnabled: true
property int totalAttachmentsCount: 0
property int transferredAttachmentsCount: 0
property var totalBytes: 0
property string finalActionText: ""
signal cancelRequested()
signal finalActionRequested()
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 === "kStartFailed") {
return "Transfer failed"
}
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"
}
Rectangle {
anchors.fill: parent
radius: 24
color: "#FFFFFF"
border.color: "#BCE5F5"
border.width: 1
}
RowLayout {
anchors.fill: parent
anchors.margins: root.compact ? 16 : 28
spacing: root.compact ? 16 : 24
Rectangle {
Layout.preferredWidth: root.compact ? 72 : 96
Layout.preferredHeight: root.compact ? 72 : 96
radius: root.compact ? 18 : 24
color: "#E8F7FC"
Image {
anchors.centerIn: parent
width: root.compact ? 44 : 60
height: root.compact ? 44 : 60
source: "qrc:/icons/file.svg"
fillMode: Image.PreserveAspectFit
sourceSize.width: width
sourceSize.height: height
smooth: true
antialiasing: true
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: root.compact ? 5 : 10
Text {
text: root.targetname
font.pointSize: root.compact ? 14 : 17
font.weight: 700
color: "#1A1C1E"
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: root.baseName(root.filename)
color: "#57707A"
font.pointSize: root.compact ? 10 : 12
elide: Text.ElideMiddle
Layout.fillWidth: true
}
ProgressBar {
id: transferProgress
Layout.fillWidth: true
from: 0
to: 1
value: Math.max(0, Math.min(1, root.progressValue))
background: Rectangle {
implicitHeight: root.compact ? 8 : 10
radius: root.compact ? 4 : 5
color: "#DDEEF5"
}
contentItem: Item {
Rectangle {
width: transferProgress.visualPosition * parent.width
height: root.compact ? 8 : 10
radius: root.compact ? 4 : 5
color: "#0D6D90"
}
}
}
RowLayout {
Layout.fillWidth: true
Text {
text: root.statusText()
color: "#57707A"
font.pointSize: root.compact ? 10 : 12
Layout.fillWidth: true
elide: Text.ElideRight
}
Text {
text: Math.round(Math.max(0, Math.min(1, root.progressValue)) * 100) + "%"
color: "#0D6D90"
font.pointSize: root.compact ? 11 : 13
font.weight: 700
}
}
RowLayout {
Layout.fillWidth: true
Item {
Layout.fillWidth: true
}
Button {
id: actionButton
visible: !root.isFinalStatus || root.finalActionText.length > 0
enabled: root.isFinalStatus || root.cancelEnabled
text: root.isFinalStatus ? root.finalActionText : "Cancel"
opacity: enabled ? 1 : 0.55
onClicked: {
if (root.isFinalStatus) {
root.finalActionRequested()
} else {
root.cancelRequested()
}
}
contentItem: Text {
text: actionButton.text
font.pointSize: root.compact ? 12 : 14
font.weight: 600
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
implicitWidth: root.compact ? 100 : 118
implicitHeight: root.compact ? 36 : 46
radius: root.compact ? 10 : 12
color: root.isFinalStatus
? (actionButton.hovered ? "#195871" : "#06384C")
: (actionButton.hovered ? "#B62828" : "#D94C4C")
}
}
}
}
}
}
+24 -6
View File
@@ -265,6 +265,8 @@ void ShareTransferModel::PrepareOutgoingTransfer(
transfers_[row].receive_mode = false;
transfers_[row].target = target;
transfers_[row].transfer.reset();
transfers_[row].total_bytes = 0;
transfers_[row].local_path = local_path;
EmitRowChanged(row);
}
@@ -394,26 +396,42 @@ void Backend::startDiscovery() {
SetDesiredMode(Mode::kDiscovery);
}
void Backend::sendFile(qint64 share_target_id, const QString& path) {
bool Backend::sendFile(qint64 share_target_id, const QString& path) {
if (service_ == nullptr || shutting_down_) {
return;
return false;
}
const QString local_path = NormalizeLocalPath(path);
const std::string native_path = local_path.toStdString();
std::error_code error;
if (!std::filesystem::is_regular_file(native_path, error)) {
qWarning() << "Send file failed: path is not a regular file" << local_path;
return;
return false;
}
const auto target = targets_.FindTarget(share_target_id);
if (!target.has_value()) {
qWarning() << "Send file failed: unknown target" << share_target_id;
return;
return false;
}
transfers_.PrepareOutgoingTransfer(share_target_id, local_path, target);
service_->SendAttachments(share_target_id, CreateFileAttachments(native_path),
StatusCallback(QStringLiteral("Send file")));
QPointer<Backend> backend(this);
service_->SendAttachments(
share_target_id, CreateFileAttachments(native_path),
[backend, share_target_id](NearbySharingService::StatusCodes status) {
if (!backend) {
return;
}
PostStatus(backend, [backend, share_target_id, status]() {
if (!backend || backend->shutting_down_) {
return;
}
backend->ReportStatus(QStringLiteral("Send file"), status);
if (status != NearbySharingService::StatusCodes::kOk) {
emit backend->outgoingTransferStartFailed(share_target_id);
}
});
});
return true;
}
void Backend::accept(qint64 share_target_id) {
+2 -1
View File
@@ -118,7 +118,7 @@ class Backend : public QObject,
Q_INVOKABLE void startReceive();
Q_INVOKABLE void startDiscovery();
Q_INVOKABLE void sendFile(qint64 share_target_id, const QString& path);
Q_INVOKABLE bool 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);
@@ -126,6 +126,7 @@ class Backend : public QObject,
signals:
void incomingTransfer(qint64 share_target_id);
void outgoingTransferStartFailed(qint64 share_target_id);
private:
using NearbySharingService = nearby::sharing::NearbySharingService;
+1
View File
@@ -9,6 +9,7 @@
<file>ShareTarget.qml</file>
<file>SearchingDevices.qml</file>
<file>IncomingShare.qml</file>
<file>TransferCard.qml</file>
<file>Targets.qml</file>
<file>googlesans_var.ttf</file>
<file>icons/file.svg</file>