diff --git a/src/app/AppController.cpp b/src/app/AppController.cpp index 34216fd..982f53d 100644 --- a/src/app/AppController.cpp +++ b/src/app/AppController.cpp @@ -6,6 +6,7 @@ #include #include #include +#include AppController::AppController(QObject* parent) : QObject(parent) @@ -14,6 +15,7 @@ AppController::AppController(QObject* parent) , m_discovery(new LocalSend::DiscoveryManager(this)) , m_server(new LocalSend::HttpServer(this)) , m_sessions(new LocalSend::SessionManager(this)) + , m_httpClient(new LocalSend::HttpClient(this)) { } @@ -54,6 +56,17 @@ void AppController::initialize() connect(m_sessions, &LocalSend::SessionManager::receiveSessionCompleted, this, &AppController::onReceiveCompleted); + connect(m_httpClient, &LocalSend::HttpClient::prepareUploadResponse, + this, &AppController::onPrepareUploadResponse); + connect(m_httpClient, &LocalSend::HttpClient::prepareUploadError, + this, &AppController::onPrepareUploadError); + connect(m_httpClient, &LocalSend::HttpClient::uploadProgress, + this, &AppController::onUploadProgress); + connect(m_httpClient, &LocalSend::HttpClient::uploadCompleted, + this, &AppController::onUploadCompleted); + connect(m_httpClient, &LocalSend::HttpClient::uploadError, + this, &AppController::onUploadError); + startDiscovery(); } @@ -345,3 +358,227 @@ void AppController::onReceiveCompleted(const QString& sessionId) { emit receiveCompleted(sessionId); } + +bool AppController::sending() const +{ + return !m_currentSendSessionId.isEmpty(); +} + +double AppController::sendProgress() const +{ + return m_sendProgress; +} + +void AppController::sendFiles(const QString& deviceFingerprint, const QStringList& filePaths) +{ + if (!m_devices.contains(deviceFingerprint)) { + emit sendError(QStringLiteral("Device not found")); + return; + } + + if (filePaths.isEmpty()) { + emit sendError(QStringLiteral("No files selected")); + return; + } + + if (sending()) { + emit sendError(QStringLiteral("Already sending files")); + return; + } + + m_currentSendDeviceFingerprint = deviceFingerprint; + m_pendingFiles = filePaths; + m_currentFileIndex = 0; + m_sendProgress = 0.0; + emit sendingChanged(); + emit sendProgressChanged(); + + qDebug() << "[AppController] sendFiles: device=" << deviceFingerprint + << "files=" << filePaths.size(); + + LocalSend::Device target = m_devices[deviceFingerprint]; + + QMap files; + QMimeDatabase mimeDb; + + for (int i = 0; i < filePaths.size(); ++i) { + QString filePath = filePaths[i]; + QFileInfo info(filePath); + + if (!info.exists()) { + emit sendError(QStringLiteral("File not found: ") + filePath); + m_currentSendSessionId.clear(); + emit sendingChanged(); + return; + } + + LocalSend::FileDto fileDto; + fileDto.id = QString::number(i); + fileDto.fileName = info.fileName(); + fileDto.size = info.size(); + fileDto.fileType = mimeDb.mimeTypeForFile(filePath).name(); + files.insert(fileDto.id, fileDto); + } + + m_currentSendSessionId = m_sessions->createSendSession(target, files, + [filePaths]() { + QMap paths; + for (int i = 0; i < filePaths.size(); ++i) { + paths.insert(QString::number(i), filePaths[i]); + } + return paths; + }() + ); + + LocalSend::PrepareUploadRequestDto request; + request.info = buildRegisterDto(); + request.files = files; + + qDebug() << "[AppController] Sending prepare-upload request to" << target.ip; + m_httpClient->prepareUpload(target, request); +} + +void AppController::cancelSend() +{ + if (m_currentSendSessionId.isEmpty()) { + return; + } + + LocalSend::Device target = m_devices.value(m_currentSendDeviceFingerprint); + m_httpClient->cancel(target, m_currentSendSessionId); + + m_sessions->cancelSendSession(m_currentSendSessionId); + m_currentSendSessionId.clear(); + m_pendingFiles.clear(); + m_sendProgress = 0.0; + emit sendingChanged(); + emit sendProgressChanged(); +} + +void AppController::onPrepareUploadResponse(const LocalSend::PrepareUploadResponseDto& response) +{ + qDebug() << "[AppController] onPrepareUploadResponse: sessionId=" << response.sessionId + << "files=" << response.files.size(); + + LocalSend::SendSession session = m_sessions->sendSession(m_currentSendSessionId); + if (session.sessionId.isEmpty()) { + emit sendError(QStringLiteral("Session not found")); + m_currentSendSessionId.clear(); + emit sendingChanged(); + return; + } + + if (response.files.isEmpty()) { + emit sendError(QStringLiteral("Receiver declined the transfer")); + m_currentSendSessionId.clear(); + emit sendingChanged(); + return; + } + + m_sessions->setSendSessionTokens(m_currentSendSessionId, response.sessionId, response.files); + + m_currentSendSessionId = response.sessionId; + m_sessions->startSendSession(m_currentSendSessionId); + + m_currentFileIndex = 0; + sendNextFile(); +} + +void AppController::onPrepareUploadError(const QString& error) +{ + qWarning() << "[AppController] onPrepareUploadError:" << error; + emit sendError(error); + m_currentSendSessionId.clear(); + emit sendingChanged(); +} + +void AppController::onUploadProgress(qint64 sent, qint64 total) +{ + if (total > 0) { + double fileProgress = static_cast(sent) / total; + int totalFiles = m_pendingFiles.size(); + double overallProgress = (m_currentFileIndex + fileProgress) / totalFiles; + m_sendProgress = overallProgress * 100.0; + emit sendProgressChanged(); + } +} + +void AppController::onUploadCompleted() +{ + qDebug() << "[AppController] onUploadCompleted, file index:" << m_currentFileIndex; + + m_sessions->completeSendFile(m_currentSendSessionId, m_currentSendFileId); + + m_currentFileIndex++; + + if (m_currentFileIndex < m_pendingFiles.size()) { + sendNextFile(); + } else { + qDebug() << "[AppController] All files sent successfully"; + m_sendProgress = 100.0; + emit sendProgressChanged(); + emit sendCompleted(m_currentSendSessionId); + m_currentSendSessionId.clear(); + emit sendingChanged(); + } +} + +void AppController::onUploadError(const QString& error) +{ + qWarning() << "[AppController] onUploadError:" << error; + emit sendError(error); + m_sessions->failSendFile(m_currentSendSessionId, m_currentSendFileId); + m_currentSendSessionId.clear(); + emit sendingChanged(); +} + +void AppController::sendNextFile() +{ + if (m_currentFileIndex >= m_pendingFiles.size()) { + return; + } + + LocalSend::SendSession session = m_sessions->sendSession(m_currentSendSessionId); + if (session.sessionId.isEmpty()) { + qWarning() << "[AppController] Session not found:" << m_currentSendSessionId; + return; + } + + QString fileId = QString::number(m_currentFileIndex); + if (!session.files.contains(fileId)) { + qWarning() << "[AppController] File not found in session:" << fileId; + return; + } + + m_currentSendFileId = fileId; + QString filePath = m_pendingFiles[m_currentFileIndex]; + QString token = session.files[fileId].token; + + LocalSend::Device target = m_devices.value(m_currentSendDeviceFingerprint); + + qDebug() << "[AppController] Uploading file" << fileId << ":" << filePath + << "token:" << token << "to" << target.ip; + + if (token.isEmpty()) { + emit sendError(QStringLiteral("No token for file: ") + fileId); + m_currentSendSessionId.clear(); + emit sendingChanged(); + return; + } + + m_httpClient->uploadFile(target, m_currentSendSessionId, fileId, token, filePath); +} + +LocalSend::RegisterDto AppController::buildRegisterDto() const +{ + LocalSend::RegisterDto dto; + dto.alias = m_settings->alias(); + dto.version = m_settings->version(); + dto.deviceModel = m_settings->deviceModel(); + dto.deviceType = m_settings->deviceType(); + dto.fingerprint = m_security->fingerprint(); + dto.port = m_settings->port(); + dto.protocol = m_settings->https() ? LocalSend::ProtocolType::Https : LocalSend::ProtocolType::Http; + dto.download = false; + return dto; +} diff --git a/src/app/AppController.h b/src/app/AppController.h index 3380bc8..7b3f668 100644 --- a/src/app/AppController.h +++ b/src/app/AppController.h @@ -19,6 +19,8 @@ class AppController : public QObject Q_PROPERTY(bool serverRunning READ serverRunning NOTIFY serverRunningChanged) Q_PROPERTY(QString downloadPath READ downloadPath WRITE setDownloadPath NOTIFY downloadPathChanged) Q_PROPERTY(bool quickSave READ quickSave WRITE setQuickSave NOTIFY quickSaveChanged) + Q_PROPERTY(bool sending READ sending NOTIFY sendingChanged) + Q_PROPERTY(double sendProgress READ sendProgress NOTIFY sendProgressChanged) public: explicit AppController(QObject* parent = nullptr); @@ -41,12 +43,18 @@ public: QVariantList devices() const; bool serverRunning() const; + bool sending() const; + double sendProgress() const; + Q_INVOKABLE void startDiscovery(); Q_INVOKABLE void stopDiscovery(); Q_INVOKABLE void refreshDevices(); Q_INVOKABLE void acceptReceive(const QString& sessionId); Q_INVOKABLE void declineReceive(const QString& sessionId); + + Q_INVOKABLE void sendFiles(const QString& deviceFingerprint, const QStringList& filePaths); + Q_INVOKABLE void cancelSend(); signals: void aliasChanged(); @@ -55,13 +63,15 @@ signals: void quickSaveChanged(); void devicesChanged(); void serverRunningChanged(); + void sendingChanged(); + void sendProgressChanged(); void receiveRequest(const QString& sessionId, const QString& senderAlias, const QString& senderIp, const QVariantList& files); void receiveProgress(const QString& sessionId, const QString& fileId, double progress); void receiveCompleted(const QString& sessionId); void receiveError(const QString& sessionId, const QString& error); - void sendProgress(const QString& sessionId, double progress); void sendCompleted(const QString& sessionId); + void sendError(const QString& error); private slots: void onDeviceDiscovered(const LocalSend::Device& device); @@ -76,6 +86,12 @@ private slots: void onSessionDeclined(const QString& sessionId); void onReceiveProgress(const QString& sessionId, const QString& fileId, double progress); void onReceiveCompleted(const QString& sessionId); + + void onPrepareUploadResponse(const LocalSend::PrepareUploadResponseDto& response); + void onPrepareUploadError(const QString& error); + void onUploadProgress(qint64 sent, qint64 total); + void onUploadCompleted(); + void onUploadError(const QString& error); private: LocalSend::Settings* m_settings = nullptr; @@ -83,9 +99,19 @@ private: LocalSend::DiscoveryManager* m_discovery = nullptr; LocalSend::HttpServer* m_server = nullptr; LocalSend::SessionManager* m_sessions = nullptr; + LocalSend::HttpClient* m_httpClient = nullptr; QMap m_devices; + QString m_currentSendSessionId; + QString m_currentSendFileId; + QString m_currentSendDeviceFingerprint; + QStringList m_pendingFiles; + int m_currentFileIndex = 0; + double m_sendProgress = 0.0; + LocalSend::InfoDto buildInfoDto() const; QString generateUniqueFilePath(const QString& baseDir, const QString& fileName) const; + void sendNextFile(); + LocalSend::RegisterDto buildRegisterDto() const; }; diff --git a/src/app/qml/main.qml b/src/app/qml/main.qml index e8e4ddd..dcf6ee5 100644 --- a/src/app/qml/main.qml +++ b/src/app/qml/main.qml @@ -15,6 +15,7 @@ ApplicationWindow { property string currentSenderAlias: "" property string currentSenderIp: "" property var receiveProgress: ({}) + property string selectedDeviceFingerprint: "" StackView { id: stackView @@ -25,6 +26,21 @@ ApplicationWindow { } } + FileDialog { + id: fileDialog + title: qsTr("Select Files to Send") + fileMode: FileDialog.OpenFiles + onAccepted: { + var paths = [] + for (var i = 0; i < selectedFiles.length; i++) { + paths.push(selectedFiles[i].toString().replace("file://", "")) + } + if (paths.length > 0 && selectedDeviceFingerprint !== "") { + appController.sendFiles(selectedDeviceFingerprint, paths) + } + } + } + Dialog { id: receiveDialog anchors.centerIn: parent @@ -73,7 +89,7 @@ ApplicationWindow { } Label { text: formatSize(modelData.size) - color: "gray" + color: palette.mid } } } @@ -87,7 +103,7 @@ ApplicationWindow { } Dialog { - id: progressDialog + id: receiveProgressDialog anchors.centerIn: parent modal: true closePolicy: Popup.NoAutoClose @@ -97,12 +113,10 @@ ApplicationWindow { spacing: 12 Label { - id: progressLabel text: qsTr("Receiving from %1...").arg(currentSenderAlias) } ProgressBar { - id: totalProgressBar Layout.fillWidth: true from: 0 to: 100 @@ -110,14 +124,53 @@ ApplicationWindow { } Label { - text: qsTr("%1% complete").arg(Math.round(totalProgressBar.value)) - color: "gray" + text: qsTr("%1% complete").arg(Math.round(calculateTotalProgress())) + color: palette.mid } } property var progressData: ({}) } + Dialog { + id: sendProgressDialog + anchors.centerIn: parent + modal: true + closePolicy: Popup.NoAutoClose + title: qsTr("Sending Files") + + ColumnLayout { + spacing: 12 + + Label { + text: qsTr("Sending files...") + } + + ProgressBar { + Layout.fillWidth: true + from: 0 + to: 100 + value: appController.sendProgress + } + + Label { + text: qsTr("%1% complete").arg(Math.round(appController.sendProgress)) + color: palette.mid + } + } + + footer: DialogButtonBox { + Button { + text: qsTr("Cancel") + DialogButtonBox.buttonRole: DialogButtonBox.RejectRole + onClicked: { + appController.cancelSend() + sendProgressDialog.close() + } + } + } + } + Connections { target: appController @@ -129,6 +182,7 @@ ApplicationWindow { if (appController.quickSave) { appController.acceptReceive(sessionId) + receiveProgressDialog.open() } else { receiveDialog.open() } @@ -138,13 +192,17 @@ ApplicationWindow { if (sessionId === currentSessionId) { receiveProgress[fileId] = progress receiveProgress = Object.assign({}, receiveProgress) - progressDialog.progressData = receiveProgress + receiveProgressDialog.progressData = receiveProgress + + if (!receiveProgressDialog.visible) { + receiveProgressDialog.open() + } } } function onReceiveCompleted(sessionId) { if (sessionId === currentSessionId) { - progressDialog.close() + receiveProgressDialog.close() receiveProgress = {} currentSessionId = "" } @@ -152,11 +210,29 @@ ApplicationWindow { function onReceiveError(sessionId, error) { if (sessionId === currentSessionId) { - progressDialog.close() + receiveProgressDialog.close() errorDialog.text = error errorDialog.open() } } + + function onSendProgress(progress) { + if (!sendProgressDialog.visible) { + sendProgressDialog.open() + } + } + + function onSendCompleted(sessionId) { + sendProgressDialog.close() + successDialog.text = qsTr("Files sent successfully!") + successDialog.open() + } + + function onSendError(error) { + sendProgressDialog.close() + errorDialog.text = error + errorDialog.open() + } } Dialog { @@ -172,6 +248,19 @@ ApplicationWindow { } } + Dialog { + id: successDialog + anchors.centerIn: parent + modal: true + standardButtons: Dialog.Ok + title: qsTr("Success") + property alias text: successLabel.text + + Label { + id: successLabel + } + } + function formatSize(bytes) { if (bytes < 1024) return bytes + " B" if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB" @@ -242,8 +331,10 @@ ApplicationWindow { padding: 12 background: Rectangle { - color: Qt.lighter("gray", 1.8) + color: "transparent" radius: 8 + border.color: palette.mid + border.width: 1 } RowLayout { @@ -257,19 +348,28 @@ ApplicationWindow { } Label { text: "%1:%2".arg(modelData.ip).arg(modelData.port) - color: "gray" + color: palette.mid font.pixelSize: 12 } } Button { text: qsTr("Send") + enabled: !appController.sending onClicked: { - // TODO: send to this device + selectedDeviceFingerprint = modelData.fingerprint + fileDialog.open() } } } } + + Label { + anchors.centerIn: parent + text: qsTr("No devices found") + color: palette.mid + visible: deviceListView.count === 0 + } } RowLayout { diff --git a/src/core/include/LocalSendCore/SessionManager.h b/src/core/include/LocalSendCore/SessionManager.h index d2d7917..e35116b 100644 --- a/src/core/include/LocalSendCore/SessionManager.h +++ b/src/core/include/LocalSendCore/SessionManager.h @@ -53,6 +53,8 @@ public: QString createSendSession(const Device& target, const QMap& files, const QMap& localPaths); + void setSendSessionTokens(const QString& sessionId, const QString& responseSessionId, + const QMap& tokens); void startSendSession(const QString& sessionId); void updateSendProgress(const QString& sessionId, const QString& fileId, qint64 bytes); void completeSendFile(const QString& sessionId, const QString& fileId); diff --git a/src/core/src/HttpClient.cpp b/src/core/src/HttpClient.cpp index 326542f..10665b0 100644 --- a/src/core/src/HttpClient.cpp +++ b/src/core/src/HttpClient.cpp @@ -3,6 +3,7 @@ #include #include #include +#include namespace LocalSend { @@ -10,6 +11,10 @@ HttpClient::HttpClient(QObject* parent) : QObject(parent) , m_manager(new QNetworkAccessManager(this)) { + connect(m_manager, &QNetworkAccessManager::sslErrors, + this, [](QNetworkReply* reply, const QList&) { + reply->ignoreSslErrors(); + }); } HttpClient::~HttpClient() @@ -98,20 +103,29 @@ void HttpClient::prepareUpload(const Device& device, const PrepareUploadRequestD { QUrl url = buildUrl(device, ApiRoute::PREPARE_UPLOAD); QByteArray data = QJsonDocument(dto.toJson()).toJson(QJsonDocument::Compact); + + qDebug() << "[HttpClient] prepareUpload to" << url.toString(); + qDebug() << "[HttpClient] request:" << data; + QNetworkReply* reply = sendPost(url, data); connect(reply, &QNetworkReply::finished, this, [this, reply]() { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { + qWarning() << "[HttpClient] prepareUpload error:" << reply->errorString(); emit prepareUploadError(reply->errorString()); return; } + QByteArray responseData = reply->readAll(); + qDebug() << "[HttpClient] prepareUpload response:" << responseData; + QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson(reply->readAll(), &error); + QJsonDocument doc = QJsonDocument::fromJson(responseData, &error); if (error.error != QJsonParseError::NoError) { + qWarning() << "[HttpClient] Invalid JSON response"; emit prepareUploadError(QStringLiteral("Invalid JSON response")); return; } @@ -131,14 +145,18 @@ void HttpClient::uploadFile(const Device& device, const QString& sessionId, query.addQueryItem(QStringLiteral("token"), token); url.setQuery(query); + qDebug() << "[HttpClient] uploadFile to" << url.toString(); + QFile* file = new QFile(filePath); if (!file->open(QIODevice::ReadOnly)) { + qWarning() << "[HttpClient] Cannot open file:" << filePath << file->errorString(); emit uploadError(QStringLiteral("Cannot open file: ") + file->errorString()); delete file; return; } qint64 fileSize = file->size(); + qDebug() << "[HttpClient] File size:" << fileSize; QNetworkRequest request(url); request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/octet-stream")); @@ -154,14 +172,16 @@ void HttpClient::uploadFile(const Device& device, const QString& sessionId, emit uploadProgress(sent, fileSize); }); - connect(reply, &QNetworkReply::finished, this, [this, reply]() { + connect(reply, &QNetworkReply::finished, this, [this, reply, filePath]() { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { + qWarning() << "[HttpClient] uploadFile error:" << reply->errorString(); emit uploadError(reply->errorString()); return; } + qDebug() << "[HttpClient] uploadFile completed:" << filePath; emit uploadCompleted(); }); } diff --git a/src/core/src/SessionManager.cpp b/src/core/src/SessionManager.cpp index 8477055..3ed06db 100644 --- a/src/core/src/SessionManager.cpp +++ b/src/core/src/SessionManager.cpp @@ -156,6 +156,25 @@ QString SessionManager::createSendSession(const Device& target, const QMap& tokens) +{ + if (!m_sendSessions.contains(sessionId)) { + return; + } + + SendSession session = m_sendSessions.take(sessionId); + session.sessionId = responseSessionId; + + for (auto it = tokens.constBegin(); it != tokens.constEnd(); ++it) { + if (session.files.contains(it.key())) { + session.files[it.key()].token = it.value(); + } + } + + m_sendSessions.insert(responseSessionId, session); +} + void SessionManager::startSendSession(const QString& sessionId) { if (!m_sendSessions.contains(sessionId)) {