2026-04-24 20:20:24 +08:00
|
|
|
#include "AppController.h"
|
|
|
|
|
#include <QJsonArray>
|
2026-04-27 15:06:59 +08:00
|
|
|
#include <QFile>
|
|
|
|
|
#include <QFileInfo>
|
|
|
|
|
#include <QDir>
|
|
|
|
|
#include <QStandardPaths>
|
|
|
|
|
#include <QUrl>
|
|
|
|
|
#include <QDebug>
|
2026-04-27 16:08:40 +08:00
|
|
|
#include <QMimeDatabase>
|
2026-04-24 20:20:24 +08:00
|
|
|
|
|
|
|
|
AppController::AppController(QObject* parent)
|
|
|
|
|
: QObject(parent)
|
|
|
|
|
, m_settings(new LocalSend::Settings(this))
|
|
|
|
|
, m_security(new LocalSend::SecurityContext(this))
|
|
|
|
|
, m_discovery(new LocalSend::DiscoveryManager(this))
|
|
|
|
|
, m_server(new LocalSend::HttpServer(this))
|
|
|
|
|
, m_sessions(new LocalSend::SessionManager(this))
|
2026-04-27 16:08:40 +08:00
|
|
|
, m_httpClient(new LocalSend::HttpClient(this))
|
2026-04-24 20:20:24 +08:00
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AppController::~AppController()
|
|
|
|
|
{
|
|
|
|
|
stopDiscovery();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AppController::initialize()
|
|
|
|
|
{
|
|
|
|
|
m_security->initialize();
|
|
|
|
|
|
|
|
|
|
LocalSend::InfoDto info = buildInfoDto();
|
|
|
|
|
m_server->setLocalInfo(info, m_security->fingerprint());
|
|
|
|
|
m_discovery->setLocalInfo(info, m_security->fingerprint(), m_settings->port(),
|
|
|
|
|
m_settings->https() ? LocalSend::ProtocolType::Https : LocalSend::ProtocolType::Http);
|
|
|
|
|
|
|
|
|
|
if (m_server->start(m_settings->port(), m_settings->https())) {
|
|
|
|
|
emit serverRunningChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connect(m_discovery, &LocalSend::DiscoveryManager::deviceDiscovered,
|
|
|
|
|
this, &AppController::onDeviceDiscovered);
|
|
|
|
|
connect(m_discovery, &LocalSend::DiscoveryManager::deviceLost,
|
|
|
|
|
this, &AppController::onDeviceLost);
|
2026-04-27 15:06:59 +08:00
|
|
|
connect(m_server, &LocalSend::HttpServer::registerRequest,
|
|
|
|
|
this, &AppController::onRegisterRequest);
|
2026-04-24 20:20:24 +08:00
|
|
|
connect(m_server, &LocalSend::HttpServer::prepareUploadRequest,
|
|
|
|
|
this, &AppController::onPrepareUploadRequest);
|
2026-04-27 15:06:59 +08:00
|
|
|
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);
|
2026-04-24 20:20:24 +08:00
|
|
|
|
2026-04-27 16:08:40 +08:00
|
|
|
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);
|
|
|
|
|
|
2026-04-24 20:20:24 +08:00
|
|
|
startDiscovery();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LocalSend::InfoDto AppController::buildInfoDto() const
|
|
|
|
|
{
|
|
|
|
|
LocalSend::InfoDto info;
|
|
|
|
|
info.alias = m_settings->alias();
|
|
|
|
|
info.version = m_settings->version();
|
|
|
|
|
info.deviceModel = m_settings->deviceModel();
|
|
|
|
|
info.deviceType = m_settings->deviceType();
|
|
|
|
|
info.download = false;
|
|
|
|
|
return info;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString AppController::alias() const
|
|
|
|
|
{
|
|
|
|
|
return m_settings->alias();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AppController::setAlias(const QString& alias)
|
|
|
|
|
{
|
|
|
|
|
if (m_settings->alias() != alias) {
|
|
|
|
|
m_settings->setAlias(alias);
|
|
|
|
|
emit aliasChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
quint16 AppController::port() const
|
|
|
|
|
{
|
|
|
|
|
return m_settings->port();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AppController::setPort(quint16 port)
|
|
|
|
|
{
|
|
|
|
|
if (m_settings->port() != port) {
|
|
|
|
|
m_settings->setPort(port);
|
|
|
|
|
emit portChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 15:06:59 +08:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 20:20:24 +08:00
|
|
|
QVariantList AppController::devices() const
|
|
|
|
|
{
|
|
|
|
|
QVariantList result;
|
|
|
|
|
for (const LocalSend::Device& device : m_devices) {
|
|
|
|
|
QVariantMap map;
|
|
|
|
|
map[QStringLiteral("ip")] = device.ip;
|
|
|
|
|
map[QStringLiteral("port")] = device.port;
|
|
|
|
|
map[QStringLiteral("alias")] = device.alias;
|
|
|
|
|
map[QStringLiteral("fingerprint")] = device.fingerprint;
|
|
|
|
|
map[QStringLiteral("deviceModel")] = device.deviceModel;
|
|
|
|
|
map[QStringLiteral("deviceType")] = LocalSend::deviceTypeToString(device.deviceType);
|
|
|
|
|
result.append(map);
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool AppController::serverRunning() const
|
|
|
|
|
{
|
|
|
|
|
return m_server->isRunning();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AppController::startDiscovery()
|
|
|
|
|
{
|
|
|
|
|
m_discovery->start();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AppController::stopDiscovery()
|
|
|
|
|
{
|
|
|
|
|
m_discovery->stop();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AppController::refreshDevices()
|
|
|
|
|
{
|
|
|
|
|
m_discovery->startScan();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 15:06:59 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 20:20:24 +08:00
|
|
|
void AppController::onDeviceDiscovered(const LocalSend::Device& device)
|
|
|
|
|
{
|
|
|
|
|
m_devices.insert(device.fingerprint, device);
|
|
|
|
|
emit devicesChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AppController::onDeviceLost(const QString& fingerprint)
|
|
|
|
|
{
|
|
|
|
|
m_devices.remove(fingerprint);
|
|
|
|
|
emit devicesChanged();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 15:06:59 +08:00
|
|
|
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,
|
2026-04-24 20:20:24 +08:00
|
|
|
const QHostAddress& sender)
|
|
|
|
|
{
|
2026-04-27 15:06:59 +08:00
|
|
|
qDebug() << "[AppController] onPrepareUploadRequest received, httpSessionId:" << httpSessionId;
|
|
|
|
|
|
2026-04-24 20:20:24 +08:00
|
|
|
LocalSend::Device device;
|
|
|
|
|
device.ip = sender.toString();
|
|
|
|
|
device.alias = dto.info.alias;
|
|
|
|
|
device.fingerprint = dto.info.fingerprint;
|
|
|
|
|
device.deviceModel = dto.info.deviceModel;
|
|
|
|
|
device.deviceType = dto.info.deviceType;
|
|
|
|
|
device.version = dto.info.version;
|
|
|
|
|
|
2026-04-27 15:06:59 +08:00
|
|
|
QString sessionId = m_sessions->createReceiveSession(device, dto.files, httpSessionId);
|
|
|
|
|
qDebug() << "[AppController] Created receive session, sessionId:" << sessionId;
|
2026-04-24 20:20:24 +08:00
|
|
|
|
|
|
|
|
QVariantList filesList;
|
|
|
|
|
for (auto it = dto.files.constBegin(); it != dto.files.constEnd(); ++it) {
|
|
|
|
|
QVariantMap file;
|
|
|
|
|
file[QStringLiteral("id")] = it.key();
|
|
|
|
|
file[QStringLiteral("fileName")] = it.value().fileName;
|
|
|
|
|
file[QStringLiteral("size")] = it.value().size;
|
|
|
|
|
file[QStringLiteral("fileType")] = it.value().fileType;
|
|
|
|
|
filesList.append(file);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 15:06:59 +08:00
|
|
|
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);
|
2026-04-24 20:20:24 +08:00
|
|
|
}
|
2026-04-27 16:08:40 +08:00
|
|
|
|
|
|
|
|
bool AppController::sending() const
|
|
|
|
|
{
|
|
|
|
|
return !m_currentSendSessionId.isEmpty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double AppController::sendProgress() const
|
|
|
|
|
{
|
|
|
|
|
return m_sendProgress;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 17:38:50 +08:00
|
|
|
QVariantList AppController::pendingFiles() const
|
|
|
|
|
{
|
|
|
|
|
return m_pendingFilesList;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool AppController::hasPendingFiles() const
|
|
|
|
|
{
|
|
|
|
|
return !m_pendingFilesList.isEmpty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AppController::addFiles(const QStringList& filePaths)
|
|
|
|
|
{
|
|
|
|
|
QMimeDatabase mimeDb;
|
|
|
|
|
for (const QString& rawPath : filePaths) {
|
|
|
|
|
QString filePath = rawPath;
|
|
|
|
|
if (filePath.startsWith(QStringLiteral("file://"))) {
|
|
|
|
|
filePath = filePath.mid(7);
|
|
|
|
|
}
|
|
|
|
|
QFileInfo info(filePath);
|
|
|
|
|
if (!info.exists() || info.isDir()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
QVariantMap file;
|
|
|
|
|
file[QStringLiteral("path")] = info.absoluteFilePath();
|
|
|
|
|
file[QStringLiteral("fileName")] = info.fileName();
|
|
|
|
|
file[QStringLiteral("size")] = info.size();
|
|
|
|
|
file[QStringLiteral("fileType")] = mimeDb.mimeTypeForFile(info).name();
|
|
|
|
|
m_pendingFilesList.append(file);
|
|
|
|
|
m_pendingSendPaths.append(info.absoluteFilePath());
|
|
|
|
|
}
|
|
|
|
|
emit pendingFilesChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AppController::removePendingFile(int index)
|
|
|
|
|
{
|
|
|
|
|
if (index < 0 || index >= m_pendingFilesList.size()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
m_pendingFilesList.removeAt(index);
|
|
|
|
|
m_pendingSendPaths.removeAt(index);
|
|
|
|
|
emit pendingFilesChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AppController::clearPendingFiles()
|
|
|
|
|
{
|
|
|
|
|
m_pendingFilesList.clear();
|
|
|
|
|
m_pendingSendPaths.clear();
|
|
|
|
|
emit pendingFilesChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AppController::sendTo(const QString& deviceFingerprint)
|
|
|
|
|
{
|
|
|
|
|
if (m_pendingSendPaths.isEmpty()) {
|
|
|
|
|
emit sendError(QStringLiteral("No files selected"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
sendFiles(deviceFingerprint, m_pendingSendPaths);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 16:08:40 +08:00
|
|
|
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;
|
2026-04-27 17:38:50 +08:00
|
|
|
m_pendingSendPaths = filePaths;
|
2026-04-27 16:08:40 +08:00
|
|
|
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<QString, LocalSend::FileDto> 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<QString, QString> 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();
|
2026-04-27 17:38:50 +08:00
|
|
|
m_pendingSendPaths.clear();
|
|
|
|
|
m_pendingFilesList.clear();
|
2026-04-27 16:08:40 +08:00
|
|
|
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<double>(sent) / total;
|
2026-04-27 17:38:50 +08:00
|
|
|
int totalFiles = m_pendingSendPaths.size();
|
2026-04-27 16:08:40 +08:00
|
|
|
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++;
|
|
|
|
|
|
2026-04-27 17:38:50 +08:00
|
|
|
if (m_currentFileIndex < m_pendingSendPaths.size()) {
|
2026-04-27 16:08:40 +08:00
|
|
|
sendNextFile();
|
|
|
|
|
} else {
|
|
|
|
|
qDebug() << "[AppController] All files sent successfully";
|
|
|
|
|
m_sendProgress = 100.0;
|
|
|
|
|
emit sendProgressChanged();
|
|
|
|
|
emit sendCompleted(m_currentSendSessionId);
|
|
|
|
|
m_currentSendSessionId.clear();
|
2026-04-27 17:38:50 +08:00
|
|
|
m_pendingFilesList.clear();
|
|
|
|
|
m_pendingSendPaths.clear();
|
2026-04-27 16:08:40 +08:00
|
|
|
emit sendingChanged();
|
2026-04-27 17:38:50 +08:00
|
|
|
emit pendingFilesChanged();
|
2026-04-27 16:08:40 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
{
|
2026-04-27 17:38:50 +08:00
|
|
|
if (m_currentFileIndex >= m_pendingSendPaths.size()) {
|
2026-04-27 16:08:40 +08:00
|
|
|
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;
|
2026-04-27 17:38:50 +08:00
|
|
|
QString filePath = m_pendingSendPaths[m_currentFileIndex];
|
2026-04-27 16:08:40 +08:00
|
|
|
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;
|
|
|
|
|
}
|