mirror of
https://github.com/kidfromjupiter/nearby.git
synced 2026-09-16 15:36:12 -04:00
added incoming transfer progress card. more blob eye candy
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user