able to receive file
This commit is contained in:
+199
-3
@@ -1,5 +1,11 @@
|
||||
#include "AppController.h"
|
||||
#include <QJsonArray>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <QStandardPaths>
|
||||
#include <QUrl>
|
||||
#include <QDebug>
|
||||
|
||||
AppController::AppController(QObject* parent)
|
||||
: QObject(parent)
|
||||
@@ -33,8 +39,20 @@ void AppController::initialize()
|
||||
this, &AppController::onDeviceDiscovered);
|
||||
connect(m_discovery, &LocalSend::DiscoveryManager::deviceLost,
|
||||
this, &AppController::onDeviceLost);
|
||||
connect(m_server, &LocalSend::HttpServer::registerRequest,
|
||||
this, &AppController::onRegisterRequest);
|
||||
connect(m_server, &LocalSend::HttpServer::prepareUploadRequest,
|
||||
this, &AppController::onPrepareUploadRequest);
|
||||
connect(m_server, &LocalSend::HttpServer::uploadRequest,
|
||||
this, &AppController::onUploadRequest);
|
||||
connect(m_sessions, &LocalSend::SessionManager::receiveSessionAccepted,
|
||||
this, &AppController::onSessionAccepted);
|
||||
connect(m_sessions, &LocalSend::SessionManager::receiveSessionDeclined,
|
||||
this, &AppController::onSessionDeclined);
|
||||
connect(m_sessions, &LocalSend::SessionManager::receiveProgress,
|
||||
this, &AppController::onReceiveProgress);
|
||||
connect(m_sessions, &LocalSend::SessionManager::receiveSessionCompleted,
|
||||
this, &AppController::onReceiveCompleted);
|
||||
|
||||
startDiscovery();
|
||||
}
|
||||
@@ -76,6 +94,32 @@ void AppController::setPort(quint16 port)
|
||||
}
|
||||
}
|
||||
|
||||
QString AppController::downloadPath() const
|
||||
{
|
||||
return m_settings->downloadDir();
|
||||
}
|
||||
|
||||
void AppController::setDownloadPath(const QString& path)
|
||||
{
|
||||
if (m_settings->downloadDir() != path) {
|
||||
m_settings->setDownloadDir(path);
|
||||
emit downloadPathChanged();
|
||||
}
|
||||
}
|
||||
|
||||
bool AppController::quickSave() const
|
||||
{
|
||||
return m_settings->quickSave();
|
||||
}
|
||||
|
||||
void AppController::setQuickSave(bool enabled)
|
||||
{
|
||||
if (m_settings->quickSave() != enabled) {
|
||||
m_settings->setQuickSave(enabled);
|
||||
emit quickSaveChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QVariantList AppController::devices() const
|
||||
{
|
||||
QVariantList result;
|
||||
@@ -112,6 +156,64 @@ void AppController::refreshDevices()
|
||||
m_discovery->startScan();
|
||||
}
|
||||
|
||||
void AppController::acceptReceive(const QString& sessionId)
|
||||
{
|
||||
qDebug() << "[AppController] acceptReceive called with sessionId:" << sessionId;
|
||||
|
||||
LocalSend::ReceiveSession session = m_sessions->receiveSession(sessionId);
|
||||
if (session.sessionId.isEmpty()) {
|
||||
qWarning() << "[AppController] Session not found for sessionId:" << sessionId;
|
||||
return;
|
||||
}
|
||||
|
||||
QString baseDir = downloadPath();
|
||||
QDir dir(baseDir);
|
||||
if (!dir.exists()) {
|
||||
dir.mkpath(QStringLiteral("."));
|
||||
}
|
||||
|
||||
QMap<QString, QString> destinationPaths;
|
||||
for (auto it = session.files.constBegin(); it != session.files.constEnd(); ++it) {
|
||||
QString filePath = generateUniqueFilePath(baseDir, it->file.fileName);
|
||||
destinationPaths.insert(it.key(), filePath);
|
||||
qDebug() << "[AppController] File" << it.key() << "->" << filePath;
|
||||
}
|
||||
|
||||
m_sessions->acceptReceiveSession(sessionId, destinationPaths);
|
||||
}
|
||||
|
||||
void AppController::declineReceive(const QString& sessionId)
|
||||
{
|
||||
qDebug() << "[AppController] declineReceive called with sessionId:" << sessionId;
|
||||
m_sessions->declineReceiveSession(sessionId);
|
||||
}
|
||||
|
||||
QString AppController::generateUniqueFilePath(const QString& baseDir, const QString& fileName) const
|
||||
{
|
||||
QString filePath = baseDir + QDir::separator() + fileName;
|
||||
QFileInfo info(filePath);
|
||||
|
||||
if (!info.exists()) {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
QString baseName = info.completeBaseName();
|
||||
QString suffix = info.suffix();
|
||||
|
||||
int counter = 1;
|
||||
while (info.exists()) {
|
||||
QString newName = QString(QStringLiteral("%1 (%2)")).arg(baseName).arg(counter);
|
||||
if (!suffix.isEmpty()) {
|
||||
newName += QStringLiteral(".") + suffix;
|
||||
}
|
||||
filePath = baseDir + QDir::separator() + newName;
|
||||
info.setFile(filePath);
|
||||
counter++;
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
void AppController::onDeviceDiscovered(const LocalSend::Device& device)
|
||||
{
|
||||
m_devices.insert(device.fingerprint, device);
|
||||
@@ -124,9 +226,33 @@ void AppController::onDeviceLost(const QString& fingerprint)
|
||||
emit devicesChanged();
|
||||
}
|
||||
|
||||
void AppController::onPrepareUploadRequest(const LocalSend::PrepareUploadRequestDto& dto,
|
||||
void AppController::onRegisterRequest(const LocalSend::RegisterDto& dto, const QHostAddress& sender)
|
||||
{
|
||||
qDebug() << "[AppController] onRegisterRequest from:" << sender.toString()
|
||||
<< "alias:" << dto.alias << "fingerprint:" << dto.fingerprint;
|
||||
|
||||
LocalSend::Device device;
|
||||
device.ip = sender.toString();
|
||||
device.port = dto.port;
|
||||
device.protocol = dto.protocol;
|
||||
device.alias = dto.alias;
|
||||
device.fingerprint = dto.fingerprint;
|
||||
device.deviceModel = dto.deviceModel;
|
||||
device.deviceType = dto.deviceType;
|
||||
device.version = dto.version;
|
||||
device.download = dto.download;
|
||||
device.lastSeen = QDateTime::currentDateTime();
|
||||
|
||||
m_devices.insert(device.fingerprint, device);
|
||||
emit devicesChanged();
|
||||
}
|
||||
|
||||
void AppController::onPrepareUploadRequest(const QString& httpSessionId,
|
||||
const LocalSend::PrepareUploadRequestDto& dto,
|
||||
const QHostAddress& sender)
|
||||
{
|
||||
qDebug() << "[AppController] onPrepareUploadRequest received, httpSessionId:" << httpSessionId;
|
||||
|
||||
LocalSend::Device device;
|
||||
device.ip = sender.toString();
|
||||
device.alias = dto.info.alias;
|
||||
@@ -135,7 +261,8 @@ void AppController::onPrepareUploadRequest(const LocalSend::PrepareUploadRequest
|
||||
device.deviceType = dto.info.deviceType;
|
||||
device.version = dto.info.version;
|
||||
|
||||
QString sessionId = m_sessions->createReceiveSession(device, dto.files);
|
||||
QString sessionId = m_sessions->createReceiveSession(device, dto.files, httpSessionId);
|
||||
qDebug() << "[AppController] Created receive session, sessionId:" << sessionId;
|
||||
|
||||
QVariantList filesList;
|
||||
for (auto it = dto.files.constBegin(); it != dto.files.constEnd(); ++it) {
|
||||
@@ -147,5 +274,74 @@ void AppController::onPrepareUploadRequest(const LocalSend::PrepareUploadRequest
|
||||
filesList.append(file);
|
||||
}
|
||||
|
||||
emit receiveRequest(sessionId, dto.info.alias, filesList);
|
||||
emit receiveRequest(sessionId, dto.info.alias, sender.toString(), filesList);
|
||||
}
|
||||
|
||||
void AppController::onUploadRequest(const QString& sessionId, const QString& fileId,
|
||||
const QString& token, const QByteArray& data)
|
||||
{
|
||||
LocalSend::ReceiveSession session = m_sessions->receiveSession(sessionId);
|
||||
if (session.sessionId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session.files.contains(fileId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const LocalSend::FileTransfer& transfer = session.files[fileId];
|
||||
if (transfer.token != token) {
|
||||
emit receiveError(sessionId, QStringLiteral("Invalid token"));
|
||||
return;
|
||||
}
|
||||
|
||||
QString filePath = transfer.destinationPath;
|
||||
if (filePath.isEmpty()) {
|
||||
emit receiveError(sessionId, QStringLiteral("No destination path"));
|
||||
return;
|
||||
}
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
emit receiveError(sessionId, QStringLiteral("Cannot open file: ") + file.errorString());
|
||||
return;
|
||||
}
|
||||
|
||||
qint64 written = file.write(data);
|
||||
file.close();
|
||||
|
||||
if (written != data.size()) {
|
||||
m_sessions->failReceiveFile(sessionId, fileId);
|
||||
emit receiveError(sessionId, QStringLiteral("Write error"));
|
||||
return;
|
||||
}
|
||||
|
||||
m_sessions->updateReceiveProgress(sessionId, fileId, written);
|
||||
|
||||
if (written >= transfer.file.size) {
|
||||
m_sessions->completeReceiveFile(sessionId, fileId);
|
||||
}
|
||||
}
|
||||
|
||||
void AppController::onSessionAccepted(const QString& sessionId, const QMap<QString, QString>& tokens)
|
||||
{
|
||||
qDebug() << "[AppController] onSessionAccepted, sessionId:" << sessionId;
|
||||
qDebug() << "[AppController] Tokens:" << tokens;
|
||||
m_server->respondToPrepareUpload(sessionId, true, tokens);
|
||||
}
|
||||
|
||||
void AppController::onSessionDeclined(const QString& sessionId)
|
||||
{
|
||||
qDebug() << "[AppController] onSessionDeclined, sessionId:" << sessionId;
|
||||
m_server->respondToPrepareUpload(sessionId, false);
|
||||
}
|
||||
|
||||
void AppController::onReceiveProgress(const QString& sessionId, const QString& fileId, double progress)
|
||||
{
|
||||
emit receiveProgress(sessionId, fileId, progress);
|
||||
}
|
||||
|
||||
void AppController::onReceiveCompleted(const QString& sessionId)
|
||||
{
|
||||
emit receiveCompleted(sessionId);
|
||||
}
|
||||
|
||||
+30
-2
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariantList>
|
||||
#include <QVariantMap>
|
||||
#include "LocalSendCore/DiscoveryManager.h"
|
||||
#include "LocalSendCore/HttpServer.h"
|
||||
#include "LocalSendCore/HttpClient.h"
|
||||
@@ -16,6 +17,8 @@ class AppController : public QObject
|
||||
Q_PROPERTY(quint16 port READ port WRITE setPort NOTIFY portChanged)
|
||||
Q_PROPERTY(QVariantList devices READ devices NOTIFY devicesChanged)
|
||||
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)
|
||||
|
||||
public:
|
||||
explicit AppController(QObject* parent = nullptr);
|
||||
@@ -29,26 +32,50 @@ public:
|
||||
quint16 port() const;
|
||||
void setPort(quint16 port);
|
||||
|
||||
QString downloadPath() const;
|
||||
void setDownloadPath(const QString& path);
|
||||
|
||||
bool quickSave() const;
|
||||
void setQuickSave(bool enabled);
|
||||
|
||||
QVariantList devices() const;
|
||||
bool serverRunning() 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);
|
||||
|
||||
signals:
|
||||
void aliasChanged();
|
||||
void portChanged();
|
||||
void downloadPathChanged();
|
||||
void quickSaveChanged();
|
||||
void devicesChanged();
|
||||
void serverRunningChanged();
|
||||
void receiveRequest(const QString& sessionId, const QString& senderAlias, const QVariantList& files);
|
||||
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);
|
||||
|
||||
private slots:
|
||||
void onDeviceDiscovered(const LocalSend::Device& device);
|
||||
void onDeviceLost(const QString& fingerprint);
|
||||
void onPrepareUploadRequest(const LocalSend::PrepareUploadRequestDto& dto, const QHostAddress& sender);
|
||||
void onRegisterRequest(const LocalSend::RegisterDto& dto, const QHostAddress& sender);
|
||||
void onPrepareUploadRequest(const QString& httpSessionId,
|
||||
const LocalSend::PrepareUploadRequestDto& dto,
|
||||
const QHostAddress& sender);
|
||||
void onUploadRequest(const QString& sessionId, const QString& fileId,
|
||||
const QString& token, const QByteArray& data);
|
||||
void onSessionAccepted(const QString& sessionId, const QMap<QString, QString>& tokens);
|
||||
void onSessionDeclined(const QString& sessionId);
|
||||
void onReceiveProgress(const QString& sessionId, const QString& fileId, double progress);
|
||||
void onReceiveCompleted(const QString& sessionId);
|
||||
|
||||
private:
|
||||
LocalSend::Settings* m_settings = nullptr;
|
||||
@@ -60,4 +87,5 @@ private:
|
||||
QMap<QString, LocalSend::Device> m_devices;
|
||||
|
||||
LocalSend::InfoDto buildInfoDto() const;
|
||||
QString generateUniqueFilePath(const QString& baseDir, const QString& fileName) const;
|
||||
};
|
||||
|
||||
+207
-2
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs
|
||||
|
||||
ApplicationWindow {
|
||||
id: root
|
||||
@@ -9,6 +10,12 @@ ApplicationWindow {
|
||||
height: 600
|
||||
title: qsTr("LocalSend")
|
||||
|
||||
property string currentSessionId: ""
|
||||
property var currentFiles: []
|
||||
property string currentSenderAlias: ""
|
||||
property string currentSenderIp: ""
|
||||
property var receiveProgress: ({})
|
||||
|
||||
StackView {
|
||||
id: stackView
|
||||
anchors.fill: parent
|
||||
@@ -18,6 +25,174 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: receiveDialog
|
||||
anchors.centerIn: parent
|
||||
modal: true
|
||||
closePolicy: Popup.NoAutoClose
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
title: qsTr("Receive Files")
|
||||
|
||||
onAccepted: {
|
||||
appController.acceptReceive(currentSessionId)
|
||||
currentSessionId = ""
|
||||
}
|
||||
|
||||
onRejected: {
|
||||
appController.declineReceive(currentSessionId)
|
||||
currentSessionId = ""
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 12
|
||||
|
||||
Label {
|
||||
text: qsTr("From: %1 (%2)").arg(currentSenderAlias).arg(currentSenderIp)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Files:")
|
||||
}
|
||||
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.min(200, contentHeight)
|
||||
width: 400
|
||||
model: currentFiles
|
||||
spacing: 4
|
||||
|
||||
delegate: RowLayout {
|
||||
width: ListView.view.width
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
text: modelData.fileName
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideMiddle
|
||||
}
|
||||
Label {
|
||||
text: formatSize(modelData.size)
|
||||
color: "gray"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Save to: %1").arg(appController.downloadPath)
|
||||
color: palette.mid
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: progressDialog
|
||||
anchors.centerIn: parent
|
||||
modal: true
|
||||
closePolicy: Popup.NoAutoClose
|
||||
title: qsTr("Receiving Files")
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 12
|
||||
|
||||
Label {
|
||||
id: progressLabel
|
||||
text: qsTr("Receiving from %1...").arg(currentSenderAlias)
|
||||
}
|
||||
|
||||
ProgressBar {
|
||||
id: totalProgressBar
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 100
|
||||
value: calculateTotalProgress()
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("%1% complete").arg(Math.round(totalProgressBar.value))
|
||||
color: "gray"
|
||||
}
|
||||
}
|
||||
|
||||
property var progressData: ({})
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: appController
|
||||
|
||||
function onReceiveRequest(sessionId, senderAlias, senderIp, files) {
|
||||
currentSessionId = sessionId
|
||||
currentSenderAlias = senderAlias
|
||||
currentSenderIp = senderIp
|
||||
currentFiles = files
|
||||
|
||||
if (appController.quickSave) {
|
||||
appController.acceptReceive(sessionId)
|
||||
} else {
|
||||
receiveDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
function onReceiveProgress(sessionId, fileId, progress) {
|
||||
if (sessionId === currentSessionId) {
|
||||
receiveProgress[fileId] = progress
|
||||
receiveProgress = Object.assign({}, receiveProgress)
|
||||
progressDialog.progressData = receiveProgress
|
||||
}
|
||||
}
|
||||
|
||||
function onReceiveCompleted(sessionId) {
|
||||
if (sessionId === currentSessionId) {
|
||||
progressDialog.close()
|
||||
receiveProgress = {}
|
||||
currentSessionId = ""
|
||||
}
|
||||
}
|
||||
|
||||
function onReceiveError(sessionId, error) {
|
||||
if (sessionId === currentSessionId) {
|
||||
progressDialog.close()
|
||||
errorDialog.text = error
|
||||
errorDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: errorDialog
|
||||
anchors.centerIn: parent
|
||||
modal: true
|
||||
standardButtons: Dialog.Ok
|
||||
title: qsTr("Error")
|
||||
property alias text: errorLabel.text
|
||||
|
||||
Label {
|
||||
id: errorLabel
|
||||
}
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (bytes < 1024) return bytes + " B"
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"
|
||||
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + " MB"
|
||||
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB"
|
||||
}
|
||||
|
||||
function calculateTotalProgress() {
|
||||
if (!currentFiles || currentFiles.length === 0) return 0
|
||||
var total = 0
|
||||
var count = 0
|
||||
for (var i = 0; i < currentFiles.length; i++) {
|
||||
var fileId = currentFiles[i].id
|
||||
if (receiveProgress[fileId] !== undefined) {
|
||||
total += receiveProgress[fileId] * 100
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count > 0 ? total / currentFiles.length : 0
|
||||
}
|
||||
|
||||
Component {
|
||||
id: homePageComponent
|
||||
Page {
|
||||
@@ -90,7 +265,7 @@ ApplicationWindow {
|
||||
Button {
|
||||
text: qsTr("Send")
|
||||
onClicked: {
|
||||
// send to this device
|
||||
// TODO: send to this device
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,7 +281,7 @@ ApplicationWindow {
|
||||
Item { Layout.fillWidth: true }
|
||||
Label {
|
||||
text: qsTr("Alias: %1").arg(appController.alias)
|
||||
color: "gray"
|
||||
color: palette.mid
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,6 +337,36 @@ ApplicationWindow {
|
||||
to: 65535
|
||||
onValueChanged: appController.port = value
|
||||
}
|
||||
|
||||
Label { text: qsTr("Download Path:") }
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
TextField {
|
||||
id: downloadPathField
|
||||
text: appController.downloadPath
|
||||
onEditingFinished: appController.downloadPath = text
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Button {
|
||||
text: qsTr("Browse")
|
||||
onClicked: folderDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
Label { text: qsTr("Quick Save:") }
|
||||
CheckBox {
|
||||
id: quickSaveCheck
|
||||
checked: appController.quickSave
|
||||
onCheckedChanged: appController.quickSave = checked
|
||||
}
|
||||
}
|
||||
|
||||
FolderDialog {
|
||||
id: folderDialog
|
||||
onAccepted: {
|
||||
downloadPathField.text = selectedFolder.toString().replace("file://", "")
|
||||
appController.downloadPath = downloadPathField.text
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
|
||||
Reference in New Issue
Block a user