Compare commits
2
Commits
3a0529edf8
...
da06f1fd51
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da06f1fd51 | ||
|
|
7bcb02babe |
No files matched your search
+205
-10
@@ -8,6 +8,10 @@
|
|||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QMimeDatabase>
|
#include <QMimeDatabase>
|
||||||
|
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QClipboard>
|
||||||
|
#include <QDesktopServices>
|
||||||
|
|
||||||
AppController::AppController(QObject* parent)
|
AppController::AppController(QObject* parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_settings(new LocalSend::Settings(this))
|
, m_settings(new LocalSend::Settings(this))
|
||||||
@@ -28,6 +32,11 @@ void AppController::initialize()
|
|||||||
{
|
{
|
||||||
m_security->initialize();
|
m_security->initialize();
|
||||||
m_server->setSslConfiguration(m_security->sslConfiguration());
|
m_server->setSslConfiguration(m_security->sslConfiguration());
|
||||||
|
// The LocalSend protocol requires mutual TLS: sending our files to another
|
||||||
|
// device means our HTTP client must present this device's certificate during
|
||||||
|
// the handshake, or the receiver rejects the connection with
|
||||||
|
// "tlsv13 alert certificate required".
|
||||||
|
m_httpClient->setSslConfiguration(m_security->sslConfiguration());
|
||||||
|
|
||||||
LocalSend::InfoDto info = buildInfoDto();
|
LocalSend::InfoDto info = buildInfoDto();
|
||||||
m_server->setLocalInfo(info, m_security->fingerprint());
|
m_server->setLocalInfo(info, m_security->fingerprint());
|
||||||
@@ -146,6 +155,19 @@ void AppController::setQuickSave(bool enabled)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool AppController::autoFinish() const
|
||||||
|
{
|
||||||
|
return m_settings->autoFinish();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppController::setAutoFinish(bool enabled)
|
||||||
|
{
|
||||||
|
if (m_settings->autoFinish() != enabled) {
|
||||||
|
m_settings->setAutoFinish(enabled);
|
||||||
|
emit autoFinishChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QString AppController::deviceType() const
|
QString AppController::deviceType() const
|
||||||
{
|
{
|
||||||
return LocalSend::deviceTypeToString(m_settings->deviceType());
|
return LocalSend::deviceTypeToString(m_settings->deviceType());
|
||||||
@@ -238,10 +260,21 @@ void AppController::acceptReceive(const QString& sessionId)
|
|||||||
m_currentReceiveSenderAlias = session.sender.alias;
|
m_currentReceiveSenderAlias = session.sender.alias;
|
||||||
m_totalReceiveSize = 0;
|
m_totalReceiveSize = 0;
|
||||||
m_receiveFileNames.clear();
|
m_receiveFileNames.clear();
|
||||||
|
m_receiveFileStates.clear();
|
||||||
|
m_receiveFinishedCount = 0;
|
||||||
for (auto it = session.files.constBegin(); it != session.files.constEnd(); ++it) {
|
for (auto it = session.files.constBegin(); it != session.files.constEnd(); ++it) {
|
||||||
m_totalReceiveSize += it->file.size;
|
m_totalReceiveSize += it->file.size;
|
||||||
m_receiveFileNames.insert(it.key(), it->file.fileName);
|
m_receiveFileNames.insert(it.key(), it->file.fileName);
|
||||||
|
QVariantMap file;
|
||||||
|
file[QStringLiteral("id")] = it.key();
|
||||||
|
file[QStringLiteral("fileName")] = it.value().file.fileName;
|
||||||
|
file[QStringLiteral("size")] = it.value().file.size;
|
||||||
|
file[QStringLiteral("status")] = QStringLiteral("queue");
|
||||||
|
m_receiveFileStates.append(file);
|
||||||
}
|
}
|
||||||
|
m_receiveStartedAt = QDateTime::currentDateTime();
|
||||||
|
m_receiveElapsed.start();
|
||||||
|
emit receiveFileStatesChanged();
|
||||||
emit receivingChanged();
|
emit receivingChanged();
|
||||||
emit receiveProgressChanged();
|
emit receiveProgressChanged();
|
||||||
|
|
||||||
@@ -327,6 +360,17 @@ void AppController::onRegisterRequest(const LocalSend::RegisterDto& dto, const Q
|
|||||||
emit devicesChanged();
|
emit devicesChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool isMessageRequest(const LocalSend::PrepareUploadRequestDto& dto)
|
||||||
|
{
|
||||||
|
// The original LocalSend treats a request with a single "text/plain" file
|
||||||
|
// that carries a preview as a message/link share instead of a file transfer.
|
||||||
|
if (dto.files.size() != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const LocalSend::FileDto& file = dto.files.constBegin().value();
|
||||||
|
return file.fileType == QStringLiteral("text/plain") && !file.preview.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
void AppController::onPrepareUploadRequest(const QString& httpSessionId,
|
void AppController::onPrepareUploadRequest(const QString& httpSessionId,
|
||||||
const LocalSend::PrepareUploadRequestDto& dto,
|
const LocalSend::PrepareUploadRequestDto& dto,
|
||||||
const QHostAddress& sender)
|
const QHostAddress& sender)
|
||||||
@@ -356,7 +400,12 @@ void AppController::onPrepareUploadRequest(const QString& httpSessionId,
|
|||||||
filesList.append(file);
|
filesList.append(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
emit receiveRequest(sessionId, dto.info.alias, sender.toString(), filesList);
|
QString message;
|
||||||
|
if (isMessageRequest(dto)) {
|
||||||
|
message = dto.files.constBegin().value().preview;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit receiveRequest(sessionId, dto.info.alias, sender.toString(), filesList, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AppController::onUploadRequest(const QString& sessionId, const QString& fileId,
|
void AppController::onUploadRequest(const QString& sessionId, const QString& fileId,
|
||||||
@@ -400,17 +449,32 @@ void AppController::onUploadRequest(const QString& sessionId, const QString& fil
|
|||||||
|
|
||||||
m_sessions->updateReceiveProgress(sessionId, fileId, written);
|
m_sessions->updateReceiveProgress(sessionId, fileId, written);
|
||||||
|
|
||||||
if (sessionId == m_currentReceiveSessionId) {
|
// Accumulate session-level stats. No m_currentReceiveSessionId guard here:
|
||||||
m_currentReceiveFileName = m_receiveFileNames.value(fileId);
|
// the server only accepts one active receive session, so this upload always
|
||||||
m_currentReceiveFileIndex++;
|
// belongs to the currently shown dialog.
|
||||||
m_receivedSize += written;
|
m_currentReceiveFileName = m_receiveFileNames.value(fileId);
|
||||||
if (m_totalReceiveSize > 0) {
|
m_currentReceiveFileIndex++;
|
||||||
m_receiveProgressValue = (static_cast<double>(m_receivedSize) / m_totalReceiveSize) * 100.0;
|
m_receivedSize += written;
|
||||||
}
|
if (m_totalReceiveSize > 0) {
|
||||||
emit receiveProgressChanged();
|
m_receiveProgressValue = (static_cast<double>(m_receivedSize) / m_totalReceiveSize) * 100.0;
|
||||||
}
|
}
|
||||||
|
emit receiveProgressChanged();
|
||||||
|
|
||||||
if (written >= transfer.file.size) {
|
if (written >= transfer.file.size) {
|
||||||
|
// Mark this file finished in the UI state *before* completing the session:
|
||||||
|
// completing the last file synchronously emits receiveSessionCompleted, which
|
||||||
|
// resets m_currentReceiveSessionId so we can no longer match the id afterwards.
|
||||||
|
for (int i = 0; i < m_receiveFileStates.size(); ++i) {
|
||||||
|
QVariantMap f = m_receiveFileStates[i].toMap();
|
||||||
|
if (f.value(QStringLiteral("id")) == fileId) {
|
||||||
|
f[QStringLiteral("status")] = QStringLiteral("finished");
|
||||||
|
m_receiveFileStates[i] = f;
|
||||||
|
m_receiveFinishedCount++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit receiveFileStatesChanged();
|
||||||
|
|
||||||
m_sessions->completeReceiveFile(sessionId, fileId);
|
m_sessions->completeReceiveFile(sessionId, fileId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -511,6 +575,83 @@ QString AppController::currentReceiveSenderAlias() const
|
|||||||
return m_currentReceiveSenderAlias;
|
return m_currentReceiveSenderAlias;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QVariantList AppController::receiveFileStates() const
|
||||||
|
{
|
||||||
|
return m_receiveFileStates;
|
||||||
|
}
|
||||||
|
|
||||||
|
int AppController::receiveFinishedCount() const
|
||||||
|
{
|
||||||
|
return m_receiveFinishedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList AppController::sendFileStates() const
|
||||||
|
{
|
||||||
|
return m_sendFileStates;
|
||||||
|
}
|
||||||
|
|
||||||
|
int AppController::sendFinishedCountFiles() const
|
||||||
|
{
|
||||||
|
return m_sendFinishedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
double AppController::receiveThroughput() const
|
||||||
|
{
|
||||||
|
// Bytes/second for the current receive session. Falls back to 0 before any data arrives.
|
||||||
|
if (!m_receiveElapsed.isValid() || m_receiveElapsed.nsecsElapsed() == 0 || m_receivedSize <= 0) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
double seconds = m_receiveElapsed.nsecsElapsed() / 1e9;
|
||||||
|
if (seconds <= 0) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
return m_receivedSize / seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
double AppController::sendThroughput() const
|
||||||
|
{
|
||||||
|
if (!m_sendElapsed.isValid() || m_sendElapsed.nsecsElapsed() == 0) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
qint64 total = 0;
|
||||||
|
for (const auto& f : m_sendFileStates) {
|
||||||
|
total += f.toMap().value(QStringLiteral("size")).toLongLong();
|
||||||
|
}
|
||||||
|
if (total <= 0) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
double seconds = m_sendElapsed.nsecsElapsed() / 1e9;
|
||||||
|
if (seconds <= 0) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
double transferred = (m_sendProgress / 100.0) * total;
|
||||||
|
return transferred / seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 AppController::receiveTotalSize() const
|
||||||
|
{
|
||||||
|
return m_totalReceiveSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 AppController::receivedSizeBytes() const
|
||||||
|
{
|
||||||
|
return m_receivedSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 AppController::sendTotalBytes() const
|
||||||
|
{
|
||||||
|
qint64 total = 0;
|
||||||
|
for (const auto& f : m_sendFileStates) {
|
||||||
|
total += f.toMap().value(QStringLiteral("size")).toLongLong();
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 AppController::sendTransferredBytes() const
|
||||||
|
{
|
||||||
|
return static_cast<qint64>((m_sendProgress / 100.0) * sendTotalBytes());
|
||||||
|
}
|
||||||
|
|
||||||
QString AppController::currentSendFileName() const
|
QString AppController::currentSendFileName() const
|
||||||
{
|
{
|
||||||
if (m_currentFileIndex >= 0 && m_currentFileIndex < m_pendingSendPaths.size()) {
|
if (m_currentFileIndex >= 0 && m_currentFileIndex < m_pendingSendPaths.size()) {
|
||||||
@@ -570,6 +711,21 @@ void AppController::clearPendingFiles()
|
|||||||
emit pendingFilesChanged();
|
emit pendingFilesChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AppController::copyToClipboard(const QString& text)
|
||||||
|
{
|
||||||
|
if (auto* clipboard = QGuiApplication::clipboard()) {
|
||||||
|
clipboard->setText(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppController::openLink(const QString& url)
|
||||||
|
{
|
||||||
|
QUrl qUrl(url);
|
||||||
|
if (qUrl.isValid() && (qUrl.scheme() == QStringLiteral("http") || qUrl.scheme() == QStringLiteral("https"))) {
|
||||||
|
QDesktopServices::openUrl(qUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void AppController::sendTo(const QString& deviceFingerprint)
|
void AppController::sendTo(const QString& deviceFingerprint)
|
||||||
{
|
{
|
||||||
if (m_pendingSendPaths.isEmpty()) {
|
if (m_pendingSendPaths.isEmpty()) {
|
||||||
@@ -641,6 +797,8 @@ void AppController::sendFiles(const QString& deviceFingerprint, const QStringLis
|
|||||||
m_currentFileIndex = 0;
|
m_currentFileIndex = 0;
|
||||||
m_sendProgress = 0.0;
|
m_sendProgress = 0.0;
|
||||||
m_pinFirstAttempt = true;
|
m_pinFirstAttempt = true;
|
||||||
|
m_sendFileStates.clear();
|
||||||
|
m_sendFinishedCount = 0;
|
||||||
|
|
||||||
qDebug() << "[AppController] sendFiles: device=" << deviceFingerprint
|
qDebug() << "[AppController] sendFiles: device=" << deviceFingerprint
|
||||||
<< "files=" << filePaths.size();
|
<< "files=" << filePaths.size();
|
||||||
@@ -660,6 +818,13 @@ void AppController::sendFiles(const QString& deviceFingerprint, const QStringLis
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QVariantMap fstate;
|
||||||
|
fstate[QStringLiteral("id")] = QString::number(i);
|
||||||
|
fstate[QStringLiteral("fileName")] = info.fileName();
|
||||||
|
fstate[QStringLiteral("size")] = info.size();
|
||||||
|
fstate[QStringLiteral("status")] = QStringLiteral("queue");
|
||||||
|
m_sendFileStates.append(fstate);
|
||||||
|
|
||||||
LocalSend::FileDto fileDto;
|
LocalSend::FileDto fileDto;
|
||||||
fileDto.id = QString::number(i);
|
fileDto.id = QString::number(i);
|
||||||
fileDto.fileName = info.fileName();
|
fileDto.fileName = info.fileName();
|
||||||
@@ -680,6 +845,9 @@ void AppController::sendFiles(const QString& deviceFingerprint, const QStringLis
|
|||||||
|
|
||||||
emit sendingChanged();
|
emit sendingChanged();
|
||||||
emit sendProgressChanged();
|
emit sendProgressChanged();
|
||||||
|
m_sendStartedAt = QDateTime::currentDateTime();
|
||||||
|
m_sendElapsed.start();
|
||||||
|
emit sendFileStatesChanged();
|
||||||
|
|
||||||
LocalSend::PrepareUploadRequestDto request;
|
LocalSend::PrepareUploadRequestDto request;
|
||||||
request.info = buildRegisterDto();
|
request.info = buildRegisterDto();
|
||||||
@@ -754,6 +922,16 @@ void AppController::onPrepareUploadResponse(const LocalSend::PrepareUploadRespon
|
|||||||
sendNextFile();
|
sendNextFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AppController::markSendFileSending(int index)
|
||||||
|
{
|
||||||
|
if (index >= 0 && index < m_sendFileStates.size()) {
|
||||||
|
QVariantMap f = m_sendFileStates[index].toMap();
|
||||||
|
f[QStringLiteral("status")] = QStringLiteral("sending");
|
||||||
|
m_sendFileStates[index] = f;
|
||||||
|
emit sendFileStatesChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void AppController::onPrepareUploadError(const QString& error)
|
void AppController::onPrepareUploadError(const QString& error)
|
||||||
{
|
{
|
||||||
qWarning() << "[AppController] onPrepareUploadError:" << error;
|
qWarning() << "[AppController] onPrepareUploadError:" << error;
|
||||||
@@ -829,6 +1007,19 @@ void AppController::onUploadCompleted()
|
|||||||
|
|
||||||
m_sessions->completeSendFile(m_currentSendSessionId, m_currentSendFileId);
|
m_sessions->completeSendFile(m_currentSendSessionId, m_currentSendFileId);
|
||||||
|
|
||||||
|
// Mark this file finished in the live state list.
|
||||||
|
QString finishedId = m_currentSendFileId;
|
||||||
|
for (int i = 0; i < m_sendFileStates.size(); ++i) {
|
||||||
|
QVariantMap f = m_sendFileStates[i].toMap();
|
||||||
|
if (f.value(QStringLiteral("id")) == finishedId) {
|
||||||
|
f[QStringLiteral("status")] = QStringLiteral("finished");
|
||||||
|
m_sendFinishedCount++;
|
||||||
|
m_sendFileStates[i] = f;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit sendFileStatesChanged();
|
||||||
|
|
||||||
m_currentFileIndex++;
|
m_currentFileIndex++;
|
||||||
|
|
||||||
if (m_currentFileIndex < m_pendingSendPaths.size()) {
|
if (m_currentFileIndex < m_pendingSendPaths.size()) {
|
||||||
@@ -871,6 +1062,8 @@ void AppController::sendNextFile()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markSendFileSending(m_currentFileIndex);
|
||||||
|
|
||||||
LocalSend::SendSession session = m_sessions->sendSession(m_currentSendSessionId);
|
LocalSend::SendSession session = m_sessions->sendSession(m_currentSendSessionId);
|
||||||
if (session.sessionId.isEmpty()) {
|
if (session.sessionId.isEmpty()) {
|
||||||
qWarning() << "[AppController] Session not found:" << m_currentSendSessionId;
|
qWarning() << "[AppController] Session not found:" << m_currentSendSessionId;
|
||||||
@@ -943,6 +1136,7 @@ void AppController::resetSendState()
|
|||||||
m_currentSendDeviceFingerprint.clear();
|
m_currentSendDeviceFingerprint.clear();
|
||||||
m_currentFileIndex = 0;
|
m_currentFileIndex = 0;
|
||||||
m_sendProgress = 0.0;
|
m_sendProgress = 0.0;
|
||||||
|
m_sendElapsed.invalidate();
|
||||||
emit sendingChanged();
|
emit sendingChanged();
|
||||||
emit sendProgressChanged();
|
emit sendProgressChanged();
|
||||||
}
|
}
|
||||||
@@ -956,8 +1150,9 @@ void AppController::resetReceiveState()
|
|||||||
m_totalReceiveFiles = 0;
|
m_totalReceiveFiles = 0;
|
||||||
m_totalReceiveSize = 0;
|
m_totalReceiveSize = 0;
|
||||||
m_receivedSize = 0;
|
m_receivedSize = 0;
|
||||||
m_currentReceiveSenderAlias.clear();
|
// Keep m_currentReceiveSenderAlias so the completed dialog still shows "From".
|
||||||
m_receiveFileNames.clear();
|
m_receiveFileNames.clear();
|
||||||
|
m_receiveElapsed.invalidate();
|
||||||
emit receivingChanged();
|
emit receivingChanged();
|
||||||
emit receiveProgressChanged();
|
emit receiveProgressChanged();
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-1
@@ -3,6 +3,8 @@
|
|||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QVariantList>
|
#include <QVariantList>
|
||||||
#include <QVariantMap>
|
#include <QVariantMap>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QElapsedTimer>
|
||||||
#include "LocalSendCore/DiscoveryManager.h"
|
#include "LocalSendCore/DiscoveryManager.h"
|
||||||
#include "LocalSendCore/HttpServer.h"
|
#include "LocalSendCore/HttpServer.h"
|
||||||
#include "LocalSendCore/HttpClient.h"
|
#include "LocalSendCore/HttpClient.h"
|
||||||
@@ -19,6 +21,7 @@ class AppController : public QObject
|
|||||||
Q_PROPERTY(bool serverRunning READ serverRunning NOTIFY serverRunningChanged)
|
Q_PROPERTY(bool serverRunning READ serverRunning NOTIFY serverRunningChanged)
|
||||||
Q_PROPERTY(QString downloadPath READ downloadPath WRITE setDownloadPath NOTIFY downloadPathChanged)
|
Q_PROPERTY(QString downloadPath READ downloadPath WRITE setDownloadPath NOTIFY downloadPathChanged)
|
||||||
Q_PROPERTY(bool quickSave READ quickSave WRITE setQuickSave NOTIFY quickSaveChanged)
|
Q_PROPERTY(bool quickSave READ quickSave WRITE setQuickSave NOTIFY quickSaveChanged)
|
||||||
|
Q_PROPERTY(bool autoFinish READ autoFinish WRITE setAutoFinish NOTIFY autoFinishChanged)
|
||||||
Q_PROPERTY(bool sending READ sending NOTIFY sendingChanged)
|
Q_PROPERTY(bool sending READ sending NOTIFY sendingChanged)
|
||||||
Q_PROPERTY(double sendProgress READ sendProgress NOTIFY sendProgressChanged)
|
Q_PROPERTY(double sendProgress READ sendProgress NOTIFY sendProgressChanged)
|
||||||
Q_PROPERTY(QString currentSendFileName READ currentSendFileName NOTIFY sendProgressChanged)
|
Q_PROPERTY(QString currentSendFileName READ currentSendFileName NOTIFY sendProgressChanged)
|
||||||
@@ -33,6 +36,16 @@ class AppController : public QObject
|
|||||||
Q_PROPERTY(int currentReceiveFileIndex READ currentReceiveFileIndex NOTIFY receiveProgressChanged)
|
Q_PROPERTY(int currentReceiveFileIndex READ currentReceiveFileIndex NOTIFY receiveProgressChanged)
|
||||||
Q_PROPERTY(int totalReceiveFiles READ totalReceiveFiles NOTIFY receivingChanged)
|
Q_PROPERTY(int totalReceiveFiles READ totalReceiveFiles NOTIFY receivingChanged)
|
||||||
Q_PROPERTY(QString currentReceiveSenderAlias READ currentReceiveSenderAlias NOTIFY receivingChanged)
|
Q_PROPERTY(QString currentReceiveSenderAlias READ currentReceiveSenderAlias NOTIFY receivingChanged)
|
||||||
|
Q_PROPERTY(QVariantList receiveFileStates READ receiveFileStates NOTIFY receiveFileStatesChanged)
|
||||||
|
Q_PROPERTY(int receiveFinishedCount READ receiveFinishedCount NOTIFY receiveFileStatesChanged)
|
||||||
|
Q_PROPERTY(QVariantList sendFileStates READ sendFileStates NOTIFY sendFileStatesChanged)
|
||||||
|
Q_PROPERTY(int sendFinishedCount READ sendFinishedCountFiles NOTIFY sendFileStatesChanged)
|
||||||
|
Q_PROPERTY(double receiveThroughput READ receiveThroughput NOTIFY receiveProgressChanged)
|
||||||
|
Q_PROPERTY(qint64 receiveTotalSize READ receiveTotalSize NOTIFY receiveProgressChanged)
|
||||||
|
Q_PROPERTY(qint64 receivedSize READ receivedSizeBytes NOTIFY receiveProgressChanged)
|
||||||
|
Q_PROPERTY(double sendThroughput READ sendThroughput NOTIFY sendProgressChanged)
|
||||||
|
Q_PROPERTY(qint64 sendTotalBytes READ sendTotalBytes NOTIFY sendProgressChanged)
|
||||||
|
Q_PROPERTY(qint64 sendTransferredBytes READ sendTransferredBytes NOTIFY sendProgressChanged)
|
||||||
Q_PROPERTY(QString deviceType READ deviceType WRITE setDeviceType NOTIFY deviceTypeChanged)
|
Q_PROPERTY(QString deviceType READ deviceType WRITE setDeviceType NOTIFY deviceTypeChanged)
|
||||||
Q_PROPERTY(bool https READ https WRITE setHttps NOTIFY httpsChanged)
|
Q_PROPERTY(bool https READ https WRITE setHttps NOTIFY httpsChanged)
|
||||||
|
|
||||||
@@ -54,6 +67,9 @@ public:
|
|||||||
bool quickSave() const;
|
bool quickSave() const;
|
||||||
void setQuickSave(bool enabled);
|
void setQuickSave(bool enabled);
|
||||||
|
|
||||||
|
bool autoFinish() const;
|
||||||
|
void setAutoFinish(bool enabled);
|
||||||
|
|
||||||
QVariantList devices() const;
|
QVariantList devices() const;
|
||||||
bool serverRunning() const;
|
bool serverRunning() const;
|
||||||
|
|
||||||
@@ -73,6 +89,16 @@ public:
|
|||||||
int currentReceiveFileIndex() const;
|
int currentReceiveFileIndex() const;
|
||||||
int totalReceiveFiles() const;
|
int totalReceiveFiles() const;
|
||||||
QString currentReceiveSenderAlias() const;
|
QString currentReceiveSenderAlias() const;
|
||||||
|
QVariantList receiveFileStates() const;
|
||||||
|
int receiveFinishedCount() const;
|
||||||
|
QVariantList sendFileStates() const;
|
||||||
|
int sendFinishedCountFiles() const;
|
||||||
|
double receiveThroughput() const;
|
||||||
|
qint64 receiveTotalSize() const;
|
||||||
|
qint64 receivedSizeBytes() const;
|
||||||
|
double sendThroughput() const;
|
||||||
|
qint64 sendTotalBytes() const;
|
||||||
|
qint64 sendTransferredBytes() const;
|
||||||
|
|
||||||
QString deviceType() const;
|
QString deviceType() const;
|
||||||
void setDeviceType(const QString& type);
|
void setDeviceType(const QString& type);
|
||||||
@@ -96,6 +122,8 @@ public:
|
|||||||
Q_INVOKABLE void removePendingFile(int index);
|
Q_INVOKABLE void removePendingFile(int index);
|
||||||
Q_INVOKABLE void clearPendingFiles();
|
Q_INVOKABLE void clearPendingFiles();
|
||||||
Q_INVOKABLE void retryWithPin(const QString& pin);
|
Q_INVOKABLE void retryWithPin(const QString& pin);
|
||||||
|
Q_INVOKABLE void copyToClipboard(const QString& text);
|
||||||
|
Q_INVOKABLE void openLink(const QString& url);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void aliasChanged();
|
void aliasChanged();
|
||||||
@@ -104,11 +132,14 @@ signals:
|
|||||||
void quickSaveChanged();
|
void quickSaveChanged();
|
||||||
void devicesChanged();
|
void devicesChanged();
|
||||||
void serverRunningChanged();
|
void serverRunningChanged();
|
||||||
|
void autoFinishChanged();
|
||||||
|
void sendFileStatesChanged();
|
||||||
|
void receiveFileStatesChanged();
|
||||||
void sendingChanged();
|
void sendingChanged();
|
||||||
void sendProgressChanged();
|
void sendProgressChanged();
|
||||||
void pendingFilesChanged();
|
void pendingFilesChanged();
|
||||||
void receiveRequest(const QString& sessionId, const QString& senderAlias,
|
void receiveRequest(const QString& sessionId, const QString& senderAlias,
|
||||||
const QString& senderIp, const QVariantList& files);
|
const QString& senderIp, const QVariantList& files, const QString& message);
|
||||||
void receiveProgress(const QString& sessionId, const QString& fileId, double progress);
|
void receiveProgress(const QString& sessionId, const QString& fileId, double progress);
|
||||||
void receiveCompleted(const QString& sessionId);
|
void receiveCompleted(const QString& sessionId);
|
||||||
void receiveError(const QString& sessionId, const QString& error);
|
void receiveError(const QString& sessionId, const QString& error);
|
||||||
@@ -166,6 +197,10 @@ private:
|
|||||||
int m_currentFileIndex = 0;
|
int m_currentFileIndex = 0;
|
||||||
double m_sendProgress = 0.0;
|
double m_sendProgress = 0.0;
|
||||||
bool m_pinFirstAttempt = true;
|
bool m_pinFirstAttempt = true;
|
||||||
|
QVariantList m_sendFileStates;
|
||||||
|
int m_sendFinishedCount = 0;
|
||||||
|
QDateTime m_sendStartedAt;
|
||||||
|
QElapsedTimer m_sendElapsed;
|
||||||
|
|
||||||
QString m_currentReceiveSessionId;
|
QString m_currentReceiveSessionId;
|
||||||
double m_receiveProgressValue = 0.0;
|
double m_receiveProgressValue = 0.0;
|
||||||
@@ -176,10 +211,15 @@ private:
|
|||||||
qint64 m_receivedSize = 0;
|
qint64 m_receivedSize = 0;
|
||||||
QString m_currentReceiveSenderAlias;
|
QString m_currentReceiveSenderAlias;
|
||||||
QMap<QString, QString> m_receiveFileNames;
|
QMap<QString, QString> m_receiveFileNames;
|
||||||
|
QVariantList m_receiveFileStates;
|
||||||
|
int m_receiveFinishedCount = 0;
|
||||||
|
QDateTime m_receiveStartedAt;
|
||||||
|
QElapsedTimer m_receiveElapsed;
|
||||||
|
|
||||||
LocalSend::InfoDto buildInfoDto() const;
|
LocalSend::InfoDto buildInfoDto() const;
|
||||||
QString generateUniqueFilePath(const QString& baseDir, const QString& fileName) const;
|
QString generateUniqueFilePath(const QString& baseDir, const QString& fileName) const;
|
||||||
void sendNextFile();
|
void sendNextFile();
|
||||||
|
void markSendFileSending(int index);
|
||||||
LocalSend::RegisterDto buildRegisterDto() const;
|
LocalSend::RegisterDto buildRegisterDto() const;
|
||||||
void resetSendState();
|
void resetSendState();
|
||||||
void resetReceiveState();
|
void resetReceiveState();
|
||||||
|
|||||||
+370
-29
@@ -14,6 +14,11 @@ ApplicationWindow {
|
|||||||
property var currentFiles: []
|
property var currentFiles: []
|
||||||
property string currentSenderAlias: ""
|
property string currentSenderAlias: ""
|
||||||
property string currentSenderIp: ""
|
property string currentSenderIp: ""
|
||||||
|
property string currentMessage: ""
|
||||||
|
property double displayReceiveThroughput: 0
|
||||||
|
property string displayReceiveEta: "-"
|
||||||
|
property double displaySendThroughput: 0
|
||||||
|
property string displaySendEta: "-"
|
||||||
|
|
||||||
function getDeviceTypeIcon(deviceType) {
|
function getDeviceTypeIcon(deviceType) {
|
||||||
switch (deviceType) {
|
switch (deviceType) {
|
||||||
@@ -173,41 +178,132 @@ ApplicationWindow {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
modal: true
|
modal: true
|
||||||
closePolicy: Popup.NoAutoClose
|
closePolicy: Popup.NoAutoClose
|
||||||
title: qsTr("Receiving Files")
|
title: succeeded ? qsTr("Receive Complete") : qsTr("Receiving Files")
|
||||||
|
width: 520
|
||||||
|
implicitWidth: 520
|
||||||
|
|
||||||
|
property bool succeeded: false
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: 12
|
spacing: 12
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
text: qsTr("From: %1").arg(appController.currentReceiveSenderAlias)
|
text: qsTr("From: %1").arg(appController.currentReceiveSenderAlias)
|
||||||
font.bold: true
|
font.bold: true
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
|
||||||
text: appController.currentReceiveFileName
|
|
||||||
? qsTr("%1 (%2/%3)").arg(appController.currentReceiveFileName)
|
|
||||||
.arg(appController.currentReceiveFileIndex)
|
|
||||||
.arg(appController.totalReceiveFiles)
|
|
||||||
: qsTr("Waiting for data...")
|
|
||||||
elide: Text.ElideMiddle
|
|
||||||
Layout.maximumWidth: 400
|
|
||||||
}
|
|
||||||
|
|
||||||
ProgressBar {
|
ProgressBar {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
from: 0
|
from: 0
|
||||||
to: 100
|
to: 100
|
||||||
value: appController.receiveProgress
|
value: receiveProgressDialog.succeeded ? 100 : appController.receiveProgress
|
||||||
|
indeterminate: !receiveProgressDialog.succeeded && appController.receiveProgress === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: receiveProgressDialog.succeeded
|
||||||
|
? qsTr("Files received successfully.")
|
||||||
|
: qsTr("%1% complete").arg(Math.round(appController.receiveProgress))
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: qsTr("%1 / %2 files")
|
||||||
|
.arg(appController.receiveFinishedCount)
|
||||||
|
.arg(appController.receiveFileStates.length)
|
||||||
|
color: palette.mid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: !receiveProgressDialog.succeeded
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Speed: %1").arg(formatThroughput(displayReceiveThroughput))
|
||||||
|
color: palette.mid
|
||||||
|
}
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
Label {
|
||||||
|
text: qsTr("Remaining: %1").arg(displayReceiveEta)
|
||||||
|
color: palette.mid
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
text: qsTr("%1% complete").arg(Math.round(appController.receiveProgress))
|
visible: !receiveProgressDialog.succeeded && appController.currentReceiveFileName.length > 0
|
||||||
|
text: qsTr("Current: %1").arg(appController.currentReceiveFileName)
|
||||||
color: palette.mid
|
color: palette.mid
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
Layout.fillWidth: true
|
||||||
|
font.pixelSize: 12
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: receiveProgressDialog.succeeded || appController.receiveFileStates.length > 0
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: Math.min(260, appController.receiveFileStates.length * 26 + 8)
|
||||||
|
color: palette.base
|
||||||
|
border.color: palette.mid
|
||||||
|
radius: 6
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 4
|
||||||
|
model: appController.receiveFileStates
|
||||||
|
spacing: 2
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
delegate: RowLayout {
|
||||||
|
width: ListView.view.width
|
||||||
|
height: 22
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: modelData.fileName
|
||||||
|
Layout.fillWidth: true
|
||||||
|
color: palette.text
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.pixelSize: 12
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: formatSize(modelData.size)
|
||||||
|
Layout.preferredWidth: 80
|
||||||
|
color: palette.mid
|
||||||
|
font.pixelSize: 12
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: modelData.status === "finished" ? qsTr("Done")
|
||||||
|
: modelData.status === "sending" ? qsTr("Active")
|
||||||
|
: qsTr("Waiting")
|
||||||
|
Layout.preferredWidth: 60
|
||||||
|
color: modelData.status === "finished" ? "green"
|
||||||
|
: modelData.status === "sending" ? palette.text
|
||||||
|
: palette.mid
|
||||||
|
font.pixelSize: 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
visible: receiveProgressDialog.succeeded
|
||||||
|
text: qsTr("Saved to: %1").arg(appController.downloadPath)
|
||||||
|
color: palette.mid
|
||||||
|
font.pixelSize: 12
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
footer: DialogButtonBox {
|
footer: DialogButtonBox {
|
||||||
Button {
|
Button {
|
||||||
|
visible: !receiveProgressDialog.succeeded
|
||||||
text: qsTr("Cancel")
|
text: qsTr("Cancel")
|
||||||
DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
|
DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
|
||||||
onClicked: {
|
onClicked: {
|
||||||
@@ -215,6 +311,85 @@ ApplicationWindow {
|
|||||||
receiveProgressDialog.close()
|
receiveProgressDialog.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Button {
|
||||||
|
visible: receiveProgressDialog.succeeded
|
||||||
|
text: qsTr("Done")
|
||||||
|
DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
|
||||||
|
onClicked: receiveProgressDialog.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClosed: {
|
||||||
|
succeeded = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog {
|
||||||
|
id: receiveMessageDialog
|
||||||
|
anchors.centerIn: parent
|
||||||
|
modal: true
|
||||||
|
closePolicy: Popup.NoAutoClose
|
||||||
|
title: qsTr("Message")
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 12
|
||||||
|
Layout.minimumWidth: 440
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("From: %1 (%2)").arg(currentSenderAlias).arg(currentSenderIp)
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 200
|
||||||
|
color: palette.base
|
||||||
|
border.color: palette.mid
|
||||||
|
radius: 6
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 8
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
TextArea {
|
||||||
|
id: messageBody
|
||||||
|
width: ScrollView.availableWidth - 4
|
||||||
|
text: root.currentMessage
|
||||||
|
readOnly: true
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
selectByMouse: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: qsTr("Copy")
|
||||||
|
onClicked: {
|
||||||
|
appController.copyToClipboard(root.currentMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
visible: isLink(root.currentMessage)
|
||||||
|
text: qsTr("Open Link")
|
||||||
|
onClicked: {
|
||||||
|
appController.openLink(root.currentMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
Button {
|
||||||
|
text: qsTr("Close")
|
||||||
|
onClicked: {
|
||||||
|
appController.declineReceive(currentSessionId)
|
||||||
|
currentSessionId = ""
|
||||||
|
receiveMessageDialog.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,36 +398,115 @@ ApplicationWindow {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
modal: true
|
modal: true
|
||||||
closePolicy: Popup.NoAutoClose
|
closePolicy: Popup.NoAutoClose
|
||||||
title: qsTr("Sending Files")
|
title: succeeded ? qsTr("Send Complete") : qsTr("Sending Files")
|
||||||
|
width: 520
|
||||||
|
implicitWidth: 520
|
||||||
|
|
||||||
|
property bool succeeded: false
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: 12
|
spacing: 12
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
text: appController.currentSendFileName
|
visible: !sendProgressDialog.succeeded && appController.currentSendFileName.length > 0
|
||||||
? qsTr("%1 (%2/%3)").arg(appController.currentSendFileName)
|
text: qsTr("Current: %1").arg(appController.currentSendFileName)
|
||||||
.arg(appController.currentSendFileIndex)
|
|
||||||
.arg(appController.totalSendFiles)
|
|
||||||
: qsTr("Preparing...")
|
|
||||||
elide: Text.ElideMiddle
|
elide: Text.ElideMiddle
|
||||||
Layout.maximumWidth: 400
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
ProgressBar {
|
ProgressBar {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
from: 0
|
from: 0
|
||||||
to: 100
|
to: 100
|
||||||
value: appController.sendProgress
|
value: sendProgressDialog.succeeded ? 100 : appController.sendProgress
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
RowLayout {
|
||||||
text: qsTr("%1% complete").arg(Math.round(appController.sendProgress))
|
Layout.fillWidth: true
|
||||||
color: palette.mid
|
|
||||||
|
Label {
|
||||||
|
text: sendProgressDialog.succeeded
|
||||||
|
? qsTr("Files sent successfully.")
|
||||||
|
: qsTr("%1% complete").arg(Math.round(appController.sendProgress))
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: qsTr("%1 / %2 files")
|
||||||
|
.arg(appController.sendFinishedCount)
|
||||||
|
.arg(appController.sendFileStates.length)
|
||||||
|
color: palette.mid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: !sendProgressDialog.succeeded
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Speed: %1").arg(formatThroughput(displaySendThroughput))
|
||||||
|
color: palette.mid
|
||||||
|
}
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
Label {
|
||||||
|
text: qsTr("Remaining: %1").arg(displaySendEta)
|
||||||
|
color: palette.mid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: sendProgressDialog.succeeded || appController.sendFileStates.length > 0
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: Math.min(260, appController.sendFileStates.length * 26 + 8)
|
||||||
|
color: palette.base
|
||||||
|
border.color: palette.mid
|
||||||
|
radius: 6
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 4
|
||||||
|
model: appController.sendFileStates
|
||||||
|
spacing: 2
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
delegate: RowLayout {
|
||||||
|
width: ListView.view.width
|
||||||
|
height: 22
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: modelData.fileName
|
||||||
|
Layout.fillWidth: true
|
||||||
|
color: palette.text
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.pixelSize: 12
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: formatSize(modelData.size)
|
||||||
|
Layout.preferredWidth: 80
|
||||||
|
color: palette.mid
|
||||||
|
font.pixelSize: 12
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: modelData.status === "finished" ? qsTr("Done")
|
||||||
|
: modelData.status === "sending" ? qsTr("Active")
|
||||||
|
: qsTr("Waiting")
|
||||||
|
Layout.preferredWidth: 60
|
||||||
|
color: modelData.status === "finished" ? "green"
|
||||||
|
: modelData.status === "sending" ? palette.text
|
||||||
|
: palette.mid
|
||||||
|
font.pixelSize: 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
footer: DialogButtonBox {
|
footer: DialogButtonBox {
|
||||||
Button {
|
Button {
|
||||||
|
visible: !sendProgressDialog.succeeded
|
||||||
text: qsTr("Cancel")
|
text: qsTr("Cancel")
|
||||||
DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
|
DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
|
||||||
onClicked: {
|
onClicked: {
|
||||||
@@ -260,18 +514,35 @@ ApplicationWindow {
|
|||||||
sendProgressDialog.close()
|
sendProgressDialog.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Button {
|
||||||
|
visible: sendProgressDialog.succeeded
|
||||||
|
text: qsTr("Done")
|
||||||
|
DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
|
||||||
|
onClicked: sendProgressDialog.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClosed: {
|
||||||
|
succeeded = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: appController
|
target: appController
|
||||||
|
|
||||||
function onReceiveRequest(sessionId, senderAlias, senderIp, files) {
|
function onReceiveRequest(sessionId, senderAlias, senderIp, files, message) {
|
||||||
currentSessionId = sessionId
|
currentSessionId = sessionId
|
||||||
currentSenderAlias = senderAlias
|
currentSenderAlias = senderAlias
|
||||||
currentSenderIp = senderIp
|
currentSenderIp = senderIp
|
||||||
currentFiles = files
|
currentFiles = files
|
||||||
|
|
||||||
|
if (message !== undefined && message.length > 0) {
|
||||||
|
// A message (or link) share request: show the message view instead.
|
||||||
|
currentMessage = message
|
||||||
|
receiveMessageDialog.open()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (appController.quickSave) {
|
if (appController.quickSave) {
|
||||||
appController.acceptReceive(sessionId)
|
appController.acceptReceive(sessionId)
|
||||||
receiveProgressDialog.open()
|
receiveProgressDialog.open()
|
||||||
@@ -281,7 +552,10 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onReceiveCompleted(sessionId) {
|
function onReceiveCompleted(sessionId) {
|
||||||
receiveProgressDialog.close()
|
receiveProgressDialog.succeeded = true
|
||||||
|
if (appController.autoFinish || appController.quickSave) {
|
||||||
|
receiveProgressDialog.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onReceiveError(sessionId, error) {
|
function onReceiveError(sessionId, error) {
|
||||||
@@ -297,9 +571,10 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onSendCompleted(sessionId) {
|
function onSendCompleted(sessionId) {
|
||||||
sendProgressDialog.close()
|
sendProgressDialog.succeeded = true
|
||||||
successDialog.text = qsTr("Files sent successfully!")
|
if (appController.autoFinish) {
|
||||||
successDialog.open()
|
sendProgressDialog.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSendError(error) {
|
function onSendError(error) {
|
||||||
@@ -506,6 +781,65 @@ ApplicationWindow {
|
|||||||
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB"
|
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isLink(text) {
|
||||||
|
var trimmed = text.trim()
|
||||||
|
if (trimmed.length === 0 || /\s/.test(trimmed)) return false
|
||||||
|
return /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(trimmed)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatThroughput(bytesPerSec) {
|
||||||
|
if (bytesPerSec <= 0) return "-"
|
||||||
|
var sec = bytesPerSec
|
||||||
|
if (sec < 1024) return Math.round(sec) + " B/s"
|
||||||
|
if (sec < 1024 * 1024) return (sec / 1024).toFixed(1) + " KB/s"
|
||||||
|
return (sec / (1024 * 1024)).toFixed(1) + " MB/s"
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatEta(remainingSec) {
|
||||||
|
if (!isFinite(remainingSec) || remainingSec <= 0) return "-"
|
||||||
|
var total = Math.ceil(remainingSec)
|
||||||
|
var h = Math.floor(total / 3600)
|
||||||
|
var m = Math.floor((total % 3600) / 60)
|
||||||
|
var s = total % 60
|
||||||
|
if (h > 0) return h + "h " + m + "m"
|
||||||
|
if (m > 0) return m + "m " + s + "s"
|
||||||
|
return s + "s"
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: receiveRateTimer
|
||||||
|
interval: 1000
|
||||||
|
repeat: true
|
||||||
|
running: receiveProgressDialog.visible
|
||||||
|
onTriggered: {
|
||||||
|
displayReceiveThroughput = appController.receiveThroughput
|
||||||
|
var total = appController.totalReceiveSize
|
||||||
|
var received = appController.receivedSize
|
||||||
|
if (displayReceiveThroughput > 0 && total > received) {
|
||||||
|
displayReceiveEta = formatEta((total - received) / displayReceiveThroughput)
|
||||||
|
} else {
|
||||||
|
displayReceiveEta = "-"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: sendRateTimer
|
||||||
|
interval: 1000
|
||||||
|
repeat: true
|
||||||
|
running: sendProgressDialog.visible
|
||||||
|
onTriggered: {
|
||||||
|
displaySendThroughput = appController.sendThroughput
|
||||||
|
var total = appController.sendTotalBytes
|
||||||
|
var sent = appController.sendTransferredBytes
|
||||||
|
if (displaySendThroughput > 0 && total > sent) {
|
||||||
|
displaySendEta = formatEta((total - sent) / displaySendThroughput)
|
||||||
|
} else {
|
||||||
|
displaySendEta = "-"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: homePageComponent
|
id: homePageComponent
|
||||||
Page {
|
Page {
|
||||||
@@ -788,6 +1122,13 @@ ApplicationWindow {
|
|||||||
onCheckedChanged: appController.quickSave = checked
|
onCheckedChanged: appController.quickSave = checked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Label { text: qsTr("Auto Finish:") }
|
||||||
|
CheckBox {
|
||||||
|
id: autoFinishCheck
|
||||||
|
checked: appController.autoFinish
|
||||||
|
onCheckedChanged: appController.autoFinish = checked
|
||||||
|
}
|
||||||
|
|
||||||
Label { text: qsTr("Receive PIN:") }
|
Label { text: qsTr("Receive PIN:") }
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ public:
|
|||||||
bool quickSave() const;
|
bool quickSave() const;
|
||||||
void setQuickSave(bool enabled);
|
void setQuickSave(bool enabled);
|
||||||
|
|
||||||
|
bool autoFinish() const;
|
||||||
|
void setAutoFinish(bool enabled);
|
||||||
|
|
||||||
QString receivePin() const;
|
QString receivePin() const;
|
||||||
void setReceivePin(const QString& pin);
|
void setReceivePin(const QString& pin);
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,19 @@ void Settings::setQuickSave(bool enabled)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Settings::autoFinish() const
|
||||||
|
{
|
||||||
|
return m_settings.value(QStringLiteral("autoFinish"), false).toBool();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Settings::setAutoFinish(bool enabled)
|
||||||
|
{
|
||||||
|
if (autoFinish() != enabled) {
|
||||||
|
m_settings.setValue(QStringLiteral("autoFinish"), enabled);
|
||||||
|
m_settings.sync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QString Settings::receivePin() const
|
QString Settings::receivePin() const
|
||||||
{
|
{
|
||||||
return m_settings.value(QStringLiteral("receivePin")).toString();
|
return m_settings.value(QStringLiteral("receivePin")).toString();
|
||||||
|
|||||||
@@ -21,3 +21,7 @@ add_test(NAME TestSessionManager COMMAND TestSessionManager)
|
|||||||
add_executable(TestDtoTypesExtended core/TestDtoTypesExtended.cpp)
|
add_executable(TestDtoTypesExtended core/TestDtoTypesExtended.cpp)
|
||||||
target_link_libraries(TestDtoTypesExtended PRIVATE LocalSendCore Qt6::Test)
|
target_link_libraries(TestDtoTypesExtended PRIVATE LocalSendCore Qt6::Test)
|
||||||
add_test(NAME TestDtoTypesExtended COMMAND TestDtoTypesExtended)
|
add_test(NAME TestDtoTypesExtended COMMAND TestDtoTypesExtended)
|
||||||
|
|
||||||
|
add_executable(TestHttpClientMtls core/TestHttpClientMtls.cpp)
|
||||||
|
target_link_libraries(TestHttpClientMtls PRIVATE LocalSendCore Qt6::Test)
|
||||||
|
add_test(NAME TestHttpClientMtls COMMAND TestHttpClientMtls)
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
#include <QtTest>
|
||||||
|
#include <QHttpServer>
|
||||||
|
#include <QHttpServerResponse>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QHostAddress>
|
||||||
|
#include <QSslServer>
|
||||||
|
#include <QSslConfiguration>
|
||||||
|
#include <QDir>
|
||||||
|
|
||||||
|
#include <LocalSendCore/HttpClient.h>
|
||||||
|
#include <LocalSendCore/SecurityContext.h>
|
||||||
|
#include <LocalSendCore/Device.h>
|
||||||
|
#include <LocalSendCore/Constants.h>
|
||||||
|
|
||||||
|
// The modern LocalSend protocol server (the Rust implementation) requires
|
||||||
|
// mutual TLS: it only accepts a sender that presents a valid client
|
||||||
|
// certificate during the TLS handshake. These tests spin up a QSslServer with
|
||||||
|
// mandatory client-certificate verification and assert that LocalSend's
|
||||||
|
// HttpClient can complete the handshake *only when* its SSL configuration
|
||||||
|
// carries the device certificate (regression test for
|
||||||
|
// "tlsv13 alert certificate required" when sending files).
|
||||||
|
class TestHttpClientMtls : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void initTestCase();
|
||||||
|
void cleanupTestCase();
|
||||||
|
void testSslConfigurationCarriesDeviceIdentity();
|
||||||
|
void testGetInfoSucceedsWithClientCertificate();
|
||||||
|
void testGetInfoFailsWithoutClientCertificate();
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Starts a QHttpServer over TLS that REQUIRES a client certificate.
|
||||||
|
// The trust store is seeded with our device certificate, so only a client
|
||||||
|
// presenting that certificate passes. Returns the listening port.
|
||||||
|
quint16 startMtlsServer(QHttpServer& http, QSslServer& ssl,
|
||||||
|
const LocalSend::SecurityContext& sec);
|
||||||
|
|
||||||
|
QString m_configDir;
|
||||||
|
LocalSend::SecurityContext* m_sec = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
void TestHttpClientMtls::initTestCase()
|
||||||
|
{
|
||||||
|
// Isolated storage in the (writable) build directory so the test never
|
||||||
|
// touches the real app certificate or depends on the user's HOME.
|
||||||
|
m_configDir = QDir::currentPath() + QStringLiteral("/.mtls-test-cfg");
|
||||||
|
QDir().mkpath(m_configDir);
|
||||||
|
qputenv("XDG_CONFIG_HOME", m_configDir.toUtf8());
|
||||||
|
|
||||||
|
m_sec = new LocalSend::SecurityContext(this);
|
||||||
|
m_sec->initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestHttpClientMtls::cleanupTestCase()
|
||||||
|
{
|
||||||
|
delete m_sec;
|
||||||
|
m_sec = nullptr;
|
||||||
|
QDir(m_configDir).removeRecursively();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestHttpClientMtls::testSslConfigurationCarriesDeviceIdentity()
|
||||||
|
{
|
||||||
|
QVERIFY(m_sec);
|
||||||
|
QSslConfiguration config = m_sec->sslConfiguration();
|
||||||
|
QVERIFY(!config.isNull());
|
||||||
|
QVERIFY(!config.localCertificate().isNull());
|
||||||
|
QVERIFY(!m_sec->privateKey().isNull());
|
||||||
|
// The config doubles as the client identity: it must carry the device
|
||||||
|
// certificate that the handshake presents to the receiver.
|
||||||
|
QCOMPARE(config.localCertificate().digest(QCryptographicHash::Sha256),
|
||||||
|
m_sec->certificate().digest(QCryptographicHash::Sha256));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestHttpClientMtls::testGetInfoSucceedsWithClientCertificate()
|
||||||
|
{
|
||||||
|
QVERIFY(m_sec);
|
||||||
|
QVERIFY(!m_sec->sslConfiguration().isNull());
|
||||||
|
|
||||||
|
QHttpServer http;
|
||||||
|
QSslServer ssl;
|
||||||
|
quint16 port = startMtlsServer(http, ssl, *m_sec);
|
||||||
|
QVERIFY(port != 0);
|
||||||
|
|
||||||
|
LocalSend::HttpClient client;
|
||||||
|
// The app wires the same security context into the client (AppController).
|
||||||
|
client.setSslConfiguration(m_sec->sslConfiguration());
|
||||||
|
|
||||||
|
LocalSend::Device device(QStringLiteral("127.0.0.1"), port);
|
||||||
|
device.protocol = LocalSend::ProtocolType::Https;
|
||||||
|
|
||||||
|
bool gotInfo = false;
|
||||||
|
bool gotError = false;
|
||||||
|
QObject::connect(&client, &LocalSend::HttpClient::infoReceived,
|
||||||
|
[&](const LocalSend::InfoDto&) { gotInfo = true; });
|
||||||
|
QObject::connect(&client, &LocalSend::HttpClient::infoError,
|
||||||
|
[&](const QString&) { gotError = true; });
|
||||||
|
|
||||||
|
client.getInfo(device);
|
||||||
|
QTRY_VERIFY_WITH_TIMEOUT(gotInfo || gotError, 5000);
|
||||||
|
|
||||||
|
QVERIFY(!gotError);
|
||||||
|
QVERIFY(gotInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestHttpClientMtls::testGetInfoFailsWithoutClientCertificate()
|
||||||
|
{
|
||||||
|
QVERIFY(m_sec);
|
||||||
|
|
||||||
|
QHttpServer http;
|
||||||
|
QSslServer ssl;
|
||||||
|
quint16 port = startMtlsServer(http, ssl, *m_sec);
|
||||||
|
QVERIFY(port != 0);
|
||||||
|
|
||||||
|
// NO client SSL configuration: like the app before the fix, the client
|
||||||
|
// does not present a certificate and the handshake must be rejected.
|
||||||
|
LocalSend::HttpClient client;
|
||||||
|
|
||||||
|
LocalSend::Device device(QStringLiteral("127.0.0.1"), port);
|
||||||
|
device.protocol = LocalSend::ProtocolType::Https;
|
||||||
|
|
||||||
|
bool gotInfo = false;
|
||||||
|
bool gotError = false;
|
||||||
|
QObject::connect(&client, &LocalSend::HttpClient::infoReceived,
|
||||||
|
[&](const LocalSend::InfoDto&) { gotInfo = true; });
|
||||||
|
QObject::connect(&client, &LocalSend::HttpClient::infoError,
|
||||||
|
[&](const QString&) { gotError = true; });
|
||||||
|
|
||||||
|
client.getInfo(device);
|
||||||
|
QTRY_VERIFY_WITH_TIMEOUT(gotInfo || gotError, 5000);
|
||||||
|
|
||||||
|
QVERIFY(!gotInfo);
|
||||||
|
QVERIFY(gotError);
|
||||||
|
}
|
||||||
|
|
||||||
|
quint16 TestHttpClientMtls::startMtlsServer(QHttpServer& http, QSslServer& ssl,
|
||||||
|
const LocalSend::SecurityContext& sec)
|
||||||
|
{
|
||||||
|
http.route(QString::fromLatin1(LocalSend::ApiRoute::INFO), QHttpServerRequest::Method::Get,
|
||||||
|
[](const QHttpServerRequest&) {
|
||||||
|
QJsonObject o;
|
||||||
|
o[QStringLiteral("alias")] = QStringLiteral("test-receiver");
|
||||||
|
o[QStringLiteral("version")] = QStringLiteral("1.0");
|
||||||
|
o[QStringLiteral("fingerprint")] = QStringLiteral("AABB");
|
||||||
|
return QHttpServerResponse(QJsonDocument(o).toJson(QJsonDocument::Compact),
|
||||||
|
QHttpServerResponse::StatusCode::Ok);
|
||||||
|
});
|
||||||
|
|
||||||
|
QSslConfiguration serverSsl;
|
||||||
|
serverSsl.setLocalCertificate(sec.certificate());
|
||||||
|
serverSsl.setPrivateKey(sec.privateKey());
|
||||||
|
serverSsl.setPeerVerifyMode(QSslSocket::VerifyPeer); // mandatory client cert
|
||||||
|
serverSsl.setCaCertificates({ sec.certificate() }); // trust our device cert
|
||||||
|
ssl.setSslConfiguration(serverSsl);
|
||||||
|
|
||||||
|
if (!ssl.listen(QHostAddress::LocalHost, 0)) {
|
||||||
|
qWarning() << "Failed to listen for MTLS test server";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
http.bind(&ssl);
|
||||||
|
return ssl.serverPort();
|
||||||
|
}
|
||||||
|
|
||||||
|
QTEST_MAIN(TestHttpClientMtls)
|
||||||
|
#include "TestHttpClientMtls.moc"
|
||||||
Reference in new issue
Block a user