added incoming transfer progress card. more blob eye candy

This commit is contained in:
Lasan Mahaliyana
2026-03-22 09:14:30 +05:30
parent 19cf94faca
commit 7c3eae2db2
7 changed files with 285 additions and 36 deletions
@@ -8,7 +8,109 @@ Item {
readonly property color textPrimary: "#111827"
readonly property color textMuted: "#6b7280"
readonly property color textSoft: "#4b5563"
readonly property color cardSurface: "#ffffff"
readonly property color cardBorder: "#d1fae5"
readonly property bool isSendMode: fileShareController.pendingSendFilePath.length > 0
readonly property var incomingTransfer: findIncomingTransfer()
readonly property var incomingTarget: findTargetForTransfer(incomingTransfer)
readonly property bool hasIncomingTransfer: incomingTransfer !== null
&& incomingTarget !== null
readonly property bool isReceivingActive: hasIncomingTransfer
&& String(incomingTransfer.status || "") !== "Complete"
readonly property real receivingIntensity: {
if (!isReceivingActive)
return 0
var numeric = Number(incomingTransfer.progress)
if (!isFinite(numeric) || numeric < 0)
numeric = 0
return Math.max(0.35, Math.min(1.0, numeric))
}
property real blobChaos: receivingIntensity
property string dismissedTransferKey: ""
Behavior on blobChaos {
NumberAnimation {
duration: 220
easing.type: Easing.OutCubic
}
}
function isIncomingTransferActive(status) {
return status === "InProgress"
|| status === "Queued"
|| status === "Connecting"
|| status === "AwaitingLocalConfirmation"
|| status === "AwaitingRemoteAcceptance"
|| status === "Complete"
}
function transferKey(transfer) {
if (!transfer)
return ""
return String(transfer.targetId || "")
+ "|" + String(transfer.status || "")
+ "|" + String(transfer.filePath || "")
+ "|" + String(transfer.fileName || "")
}
function findIncomingTransfer() {
var transfers = fileShareController.transfers
var latestCompleted = null
for (var i = transfers.length - 1; i >= 0; --i) {
var entry = transfers[i]
if (!entry)
continue
if (String(entry.direction || "") !== "incoming")
continue
var status = String(entry.status || "")
if (!isIncomingTransferActive(status))
continue
if (status === "Complete") {
if (latestCompleted === null)
latestCompleted = entry
continue
}
return entry
}
return latestCompleted
}
function findTargetForTransfer(transfer) {
if (!transfer)
return null
var targets = fileShareController.discoveredTargets
for (var i = 0; i < targets.length; ++i) {
var entry = targets[i]
if (entry && entry.id === transfer.targetId)
return entry
}
return {
id: transfer.targetId,
name: String(transfer.targetName || "Incoming device"),
isIncoming: true
}
}
function incomingHeadline(status) {
if (status === "Complete")
return "Received"
if (status === "AwaitingLocalConfirmation")
return "Incoming transfer"
if (status === "AwaitingRemoteAcceptance")
return "Preparing transfer"
if (status === "Connecting")
return "Connecting"
return "Receiving"
}
// The card only becomes actionable once the file has landed on disk and we
// have a local path to open.
function incomingClickReady() {
return hasIncomingTransfer
&& String(incomingTransfer.status || "") === "Complete"
&& String(incomingTransfer.filePath || "").length > 0
}
Label {
x: 48; y: 48
@@ -27,15 +129,17 @@ Item {
visible: !isSendMode
property real t: 0
property double startMs: Date.now()
property double lastMs: Date.now()
Timer {
interval: 16
running: true
repeat: true
onTriggered: {
// seconds -> radians scale (t grows forever, no loop jump)
blobCanvas3.t = (Date.now() - blobCanvas3.startMs) * 0.001 * (Math.PI * 2 / 8.0)
var now = Date.now()
var dt = Math.min(0.05, Math.max(0.0, (now - blobCanvas3.lastMs) * 0.001))
blobCanvas3.lastMs = now
blobCanvas3.t += dt * (Math.PI * 2 / Math.max(4.6, 8.0 - blobChaos * 2.6))
blobCanvas3.requestPaint()
}
}
@@ -44,18 +148,21 @@ Item {
var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height)
var cx = width / 2
var cy = height / 2
var chaos = blobChaos
var cx = width / 2 + Math.sin(t * 0.33) * chaos * 10
var cy = height / 2 + Math.cos(t * 0.27) * chaos * 8
var n = 10
var pts = []
for (var i = 0; i < n; i++) {
var a = (i / n) * Math.PI * 2 - Math.PI / 2
var r = 150
+ Math.sin(a * 2 + t) * 11
+ Math.cos(a * 3 - t * 0.2) * 8
+ Math.sin(a * 1.5 + t * 0.7) * 6
+ Math.sin(a * 1.5 + t * 1) * 4
var r = 150 + chaos * 22
+ Math.sin(a * 2 + t) * (11 + chaos * 14)
+ Math.cos(a * 3 - t * 0.2) * (8 + chaos * 10)
+ Math.sin(a * 1.5 + t * 0.7) * (6 + chaos * 8)
+ Math.sin(a * 1.5 + t) * (4 + chaos * 6)
+ Math.cos(a * 5 - t * 1.2) * (chaos * 9)
+ Math.sin(a * 7 + t * 0.85) * (chaos * 6)
pts.push({ x: cx + Math.cos(a) * r, y: cy + Math.sin(a) * r })
}
@@ -89,15 +196,17 @@ Item {
visible: !isSendMode
property real t: 0
property double startMs: Date.now()
property double lastMs: Date.now()
Timer {
interval: 16
running: true
repeat: true
onTriggered: {
// seconds -> radians scale (t grows forever, no loop jump)
blobCanvas2.t = (Date.now() - blobCanvas2.startMs) * 0.001 * (Math.PI * 2 / 8.0)
var now = Date.now()
var dt = Math.min(0.05, Math.max(0.0, (now - blobCanvas2.lastMs) * 0.001))
blobCanvas2.lastMs = now
blobCanvas2.t += dt * (Math.PI * 2 / Math.max(4.9, 8.0 - blobChaos * 2.2))
blobCanvas2.requestPaint()
}
}
@@ -106,18 +215,21 @@ Item {
var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height)
var cx = width / 2
var cy = height / 2
var chaos = blobChaos
var cx = width / 2 + Math.cos(t * 0.29) * chaos * 8
var cy = height / 2 + Math.sin(t * 0.41) * chaos * 11
var n = 10
var pts = []
for (var i = 0; i < n; i++) {
var a = (i / n) * Math.PI * 2 - Math.PI / 2
var r = 140
+ Math.sin(a * 2 + t) * 11
+ Math.cos(a * 3 - t * 0.8) * 8
+ Math.sin(a * 1.5 + t * 0.23) * 6
+ Math.sin(a * 1.5 + t * 0.85) * 4
var r = 140 + chaos * 18
+ Math.sin(a * 2 + t) * (11 + chaos * 12)
+ Math.cos(a * 3 - t * 0.8) * (8 + chaos * 9)
+ Math.sin(a * 1.5 + t * 0.23) * (6 + chaos * 7)
+ Math.sin(a * 1.5 + t * 0.85) * (4 + chaos * 6)
+ Math.cos(a * 4 + t * 1.1) * (chaos * 7)
+ Math.sin(a * 6 - t * 0.95) * (chaos * 5)
pts.push({ x: cx + Math.cos(a) * r, y: cy + Math.sin(a) * r })
}
@@ -153,15 +265,17 @@ Item {
visible: !isSendMode
property real t: 0
property double startMs: Date.now()
property double lastMs: Date.now()
Timer {
interval: 16
running: true
repeat: true
onTriggered: {
// seconds -> radians scale (t grows forever, no loop jump)
blobCanvas.t = (Date.now() - blobCanvas.startMs) * 0.001 * (Math.PI * 2 / 8.0)
var now = Date.now()
var dt = Math.min(0.05, Math.max(0.0, (now - blobCanvas.lastMs) * 0.001))
blobCanvas.lastMs = now
blobCanvas.t += dt * (Math.PI * 2 / Math.max(4.4, 8.0 - blobChaos * 3.0))
blobCanvas.requestPaint()
}
}
@@ -170,18 +284,21 @@ Item {
var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height)
var cx = width / 2
var cy = height / 2
var chaos = blobChaos
var cx = width / 2 + Math.sin(t * 0.55) * chaos * 12
var cy = height / 2 + Math.cos(t * 0.47) * chaos * 10
var n = 10
var pts = []
for (var i = 0; i < n; i++) {
var a = (i / n) * Math.PI * 2 - Math.PI / 2
var r = 130
+ Math.sin(a * 2 + t) * 11
+ Math.cos(a * 3 - t * 0.6) * 8
+ Math.sin(a * 1.5 + t * 0.35) * 6
+ Math.sin(a + t * 0.75) * 3
var r = 130 + chaos * 15
+ Math.sin(a * 2 + t) * (11 + chaos * 15)
+ Math.cos(a * 3 - t * 0.6) * (8 + chaos * 12)
+ Math.sin(a * 1.5 + t * 0.35) * (6 + chaos * 9)
+ Math.sin(a + t * 0.75) * (3 + chaos * 6)
+ Math.cos(a * 5 - t * 1.35) * (chaos * 10)
+ Math.sin(a * 8 + t * 0.92) * (chaos * 6)
pts.push({ x: cx + Math.cos(a) * r, y: cy + Math.sin(a) * r })
}
@@ -208,6 +325,83 @@ Item {
}
}
Rectangle {
id: incomingTransferCard
anchors.centerIn: parent
visible: !isSendMode && hasIncomingTransfer
&& transferKey(incomingTransfer) !== dismissedTransferKey
width: 200
height: 210
radius: 34
color: cardSurface
border.color: cardBorder
border.width: 1
z: 10
Rectangle {
anchors.fill: parent
anchors.margins: 10
radius: parent.radius - 10
color: "#ffffff"
opacity: 0.84
}
Column {
anchors.fill: parent
anchors.margins: 24
spacing: 10
Label {
anchors.horizontalCenter: parent.horizontalCenter
text: incomingHeadline(String(incomingTransfer.status || ""))
font.pixelSize: 12
font.weight: Font.DemiBold
color: "#059669"
horizontalAlignment: Text.AlignHCenter
}
DeviceCard {
anchors.horizontalCenter: parent.horizontalCenter
modelData: incomingTarget
}
Label {
width: parent.width
visible: String(incomingTransfer.fileName || "").length > 0
text: String(incomingTransfer.fileName || "")
font.pixelSize: 13
font.weight: Font.StyleItalic
color: textPrimary
wrapMode: Text.Wrap
maximumLineCount: 2
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
}
}
MouseArea {
anchors.fill: parent
enabled: incomingClickReady()
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
dismissedTransferKey = transferKey(incomingTransfer)
fileShareController.openFileLocation(String(incomingTransfer.filePath || ""))
}
}
}
onIncomingTransferChanged: {
if (!incomingTransfer) {
dismissedTransferKey = ""
return
}
if (transferKey(incomingTransfer) !== dismissedTransferKey)
return
if (String(incomingTransfer.status || "") !== "Complete")
dismissedTransferKey = ""
}
Label {
@@ -185,7 +185,7 @@ Item {
horizontalAlignment: Text.AlignHCenter
text: targetName
font.pixelSize: 13
font.weight: Font.Medium
font.weight: Font.Bold
elide: Text.ElideRight
maximumLineCount: 2
wrapMode: Text.Wrap
@@ -66,7 +66,7 @@ bool FileShareState::HasTarget(qlonglong id) const {
void FileShareState::AddOrUpdateTransfer(
qlonglong target_id, const QString& target_name, const QString& status,
double progress, qulonglong transferred_bytes, const QString& direction,
const QString& file_name) {
const QString& file_name, const QString& file_path) {
QVariantMap transfer{
{QStringLiteral("targetId"), target_id},
{QStringLiteral("targetName"), target_name},
@@ -75,8 +75,10 @@ void FileShareState::AddOrUpdateTransfer(
{QStringLiteral("transferredBytes"), transferred_bytes},
{QStringLiteral("direction"), direction},
{QStringLiteral("fileName"), file_name},
{QStringLiteral("filePath"), file_path},
};
qInfo() << "qDiscovered targets" << discoveredTargets();
if (transfer_row_by_target_.contains(target_id)) {
const int row_index = transfer_row_by_target_.value(target_id);
if (row_index >= 0 && row_index < transfers_.size()) {
@@ -72,7 +72,8 @@ class FileShareState {
void AddOrUpdateTransfer(qlonglong target_id, const QString& target_name,
const QString& status, double progress,
qulonglong transferred_bytes,
const QString& direction, const QString& file_name);
const QString& direction, const QString& file_name,
const QString& file_path);
void RemoveTransfer(qlonglong target_id);
bool HasActiveTransferForTarget(qlonglong target_id) const;
bool HasActiveTransfers() const;
@@ -1,13 +1,17 @@
#include "file_share_tray_controller.h"
#include <iostream>
#include <QClipboard>
#include <QDesktopServices>
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QGuiApplication>
#include <QMetaObject>
#include <QSettings>
#include <QSysInfo>
#include <QTimer>
#include <QUrl>
#include "string_utils.h"
#include "status_mapper.h"
@@ -38,6 +42,10 @@ void FileShareTrayController::initializeService() {
emit qrCodeUrlChanged();
emit qrCodeChanged();
attachServiceListeners();
// service_ ->StartFastInitiationScanning([](auto a)
// {
// std::cout << "Probably fine";
// });
}
void FileShareTrayController::updateQrCodeData() {
@@ -105,7 +113,8 @@ void FileShareTrayController::handleTransferUpdate(
}
state_.AddOrUpdateTransfer(update.share_target_id, name, status, update.progress,
update.transferred_bytes, direction, file_name);
update.transferred_bytes, direction, file_name,
StringUtils::FromStdString(update.first_file_path));
emit transfersChanged();
setStatus(QStringLiteral("%1 (%2)").arg(status, name));
@@ -429,7 +438,8 @@ void FileShareTrayController::sendPendingFileToTarget(qlonglong share_target_id)
state_.SetPendingSendFile(file_path, file_info.fileName(), share_target_id);
state_.AddOrUpdateTransfer(share_target_id, target_name, QStringLiteral("Queued"), 0.0, 0,
QStringLiteral("outgoing"), file_info.fileName());
QStringLiteral("outgoing"), file_info.fileName(),
file_info.absoluteFilePath());
emit transfersChanged();
service_->SendFile(
@@ -452,7 +462,8 @@ void FileShareTrayController::sendPendingFileToTarget(qlonglong share_target_id)
state_.AddOrUpdateTransfer(share_target_id, target_name,
QStringLiteral("Failed"), 0.0, 0,
QStringLiteral("outgoing"),
state_.pendingSendFileName());
state_.pendingSendFileName(),
state_.pendingSendFilePath());
emit transfersChanged();
state_.SetPendingSendFile("", "", 0);
},
@@ -479,6 +490,45 @@ void FileShareTrayController::copyTextToClipboard(const QString& text) {
QStringLiteral("Link copied to clipboard."));
}
void FileShareTrayController::openFileLocation(const QString& file_path) {
const QString trimmed = file_path.trimmed();
if (trimmed.isEmpty()) {
emit requestTrayMessage(QStringLiteral("Open location failed"),
QStringLiteral("No received file location is available."));
return;
}
QFileInfo info(trimmed);
QString target_path;
if (info.exists() && info.isFile()) {
// Open the containing folder so the file is visible in the user's file
// manager regardless of the desktop environment.
target_path = info.absolutePath();
} else if (info.exists() && info.isDir()) {
target_path = info.absoluteFilePath();
} else {
// Some transfer updates can outlive the exact file entry we saw earlier;
// fall back to the parent directory when it still exists.
const QFileInfo parent_info(info.absolutePath());
if (parent_info.exists() && parent_info.isDir()) {
target_path = parent_info.absoluteFilePath();
}
}
if (target_path.isEmpty()) {
emit requestTrayMessage(QStringLiteral("Open location failed"),
QStringLiteral("The file location is no longer available."));
return;
}
const bool opened =
QDesktopServices::openUrl(QUrl::fromLocalFile(target_path));
if (!opened) {
emit requestTrayMessage(QStringLiteral("Open location failed"),
QStringLiteral("Could not open the file location."));
}
}
void FileShareTrayController::clearTransfers() {
state_.ClearAll();
emit discoveredTargetsChanged();
@@ -55,6 +55,7 @@ class FileShareTrayController : public QObject {
Q_INVOKABLE void switchToSendModeWithFile(const QString& file_path);
Q_INVOKABLE void sendPendingFileToTarget(qlonglong share_target_id);
Q_INVOKABLE void copyTextToClipboard(const QString& text);
Q_INVOKABLE void openFileLocation(const QString& file_path);
Q_INVOKABLE void clearTransfers();
Q_INVOKABLE void hideToTray();
@@ -233,6 +233,7 @@ int main(int argc, char* argv[]) {
tray.show();
controller.start();
//controller.
controller.switchToReceiveMode();
return app.exec();