diff --git a/src/app/AppController.cpp b/src/app/AppController.cpp index 982f53d..02d944c 100644 --- a/src/app/AppController.cpp +++ b/src/app/AppController.cpp @@ -369,6 +369,65 @@ double AppController::sendProgress() const return m_sendProgress; } +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); +} + void AppController::sendFiles(const QString& deviceFingerprint, const QStringList& filePaths) { if (!m_devices.contains(deviceFingerprint)) { @@ -387,7 +446,7 @@ void AppController::sendFiles(const QString& deviceFingerprint, const QStringLis } m_currentSendDeviceFingerprint = deviceFingerprint; - m_pendingFiles = filePaths; + m_pendingSendPaths = filePaths; m_currentFileIndex = 0; m_sendProgress = 0.0; emit sendingChanged(); @@ -449,7 +508,8 @@ void AppController::cancelSend() m_sessions->cancelSendSession(m_currentSendSessionId); m_currentSendSessionId.clear(); - m_pendingFiles.clear(); + m_pendingSendPaths.clear(); + m_pendingFilesList.clear(); m_sendProgress = 0.0; emit sendingChanged(); emit sendProgressChanged(); @@ -496,7 +556,7 @@ void AppController::onUploadProgress(qint64 sent, qint64 total) { if (total > 0) { double fileProgress = static_cast(sent) / total; - int totalFiles = m_pendingFiles.size(); + int totalFiles = m_pendingSendPaths.size(); double overallProgress = (m_currentFileIndex + fileProgress) / totalFiles; m_sendProgress = overallProgress * 100.0; emit sendProgressChanged(); @@ -511,7 +571,7 @@ void AppController::onUploadCompleted() m_currentFileIndex++; - if (m_currentFileIndex < m_pendingFiles.size()) { + if (m_currentFileIndex < m_pendingSendPaths.size()) { sendNextFile(); } else { qDebug() << "[AppController] All files sent successfully"; @@ -519,7 +579,10 @@ void AppController::onUploadCompleted() emit sendProgressChanged(); emit sendCompleted(m_currentSendSessionId); m_currentSendSessionId.clear(); + m_pendingFilesList.clear(); + m_pendingSendPaths.clear(); emit sendingChanged(); + emit pendingFilesChanged(); } } @@ -534,7 +597,7 @@ void AppController::onUploadError(const QString& error) void AppController::sendNextFile() { - if (m_currentFileIndex >= m_pendingFiles.size()) { + if (m_currentFileIndex >= m_pendingSendPaths.size()) { return; } @@ -551,7 +614,7 @@ void AppController::sendNextFile() } m_currentSendFileId = fileId; - QString filePath = m_pendingFiles[m_currentFileIndex]; + QString filePath = m_pendingSendPaths[m_currentFileIndex]; QString token = session.files[fileId].token; LocalSend::Device target = m_devices.value(m_currentSendDeviceFingerprint); diff --git a/src/app/AppController.h b/src/app/AppController.h index 7b3f668..5b704ad 100644 --- a/src/app/AppController.h +++ b/src/app/AppController.h @@ -21,6 +21,8 @@ class AppController : public QObject 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) + Q_PROPERTY(QVariantList pendingFiles READ pendingFiles NOTIFY pendingFilesChanged) + Q_PROPERTY(bool hasPendingFiles READ hasPendingFiles NOTIFY pendingFilesChanged) public: explicit AppController(QObject* parent = nullptr); @@ -45,6 +47,8 @@ public: bool sending() const; double sendProgress() const; + QVariantList pendingFiles() const; + bool hasPendingFiles() const; Q_INVOKABLE void startDiscovery(); Q_INVOKABLE void stopDiscovery(); @@ -54,7 +58,11 @@ public: Q_INVOKABLE void declineReceive(const QString& sessionId); Q_INVOKABLE void sendFiles(const QString& deviceFingerprint, const QStringList& filePaths); + Q_INVOKABLE void sendTo(const QString& deviceFingerprint); Q_INVOKABLE void cancelSend(); + Q_INVOKABLE void addFiles(const QStringList& filePaths); + Q_INVOKABLE void removePendingFile(int index); + Q_INVOKABLE void clearPendingFiles(); signals: void aliasChanged(); @@ -65,6 +73,7 @@ signals: void serverRunningChanged(); void sendingChanged(); void sendProgressChanged(); + void pendingFilesChanged(); void receiveRequest(const QString& sessionId, const QString& senderAlias, const QString& senderIp, const QVariantList& files); void receiveProgress(const QString& sessionId, const QString& fileId, double progress); @@ -106,7 +115,8 @@ private: QString m_currentSendSessionId; QString m_currentSendFileId; QString m_currentSendDeviceFingerprint; - QStringList m_pendingFiles; + QStringList m_pendingSendPaths; + QVariantList m_pendingFilesList; int m_currentFileIndex = 0; double m_sendProgress = 0.0; diff --git a/src/app/qml/main.qml b/src/app/qml/main.qml index dcf6ee5..37fee6e 100644 --- a/src/app/qml/main.qml +++ b/src/app/qml/main.qml @@ -15,7 +15,63 @@ ApplicationWindow { property string currentSenderAlias: "" property string currentSenderIp: "" property var receiveProgress: ({}) - property string selectedDeviceFingerprint: "" + + DropArea { + id: dropArea + anchors.fill: parent + z: 9999 + + onEntered: function(drag) { + if (drag.hasUrls) { + drag.accepted = true + dropOverlay.visible = true + } else { + drag.accepted = false + } + } + + onDropped: function(drop) { + dropOverlay.visible = false + if (drop.hasUrls) { + var paths = [] + for (var i = 0; i < drop.urls.length; i++) { + paths.push(drop.urls[i].toString()) + } + if (paths.length > 0) { + appController.addFiles(paths) + } + } + } + + onExited: { + dropOverlay.visible = false + } + + Rectangle { + id: dropOverlay + visible: false + anchors.fill: parent + color: "#200080FF" + z: 10000 + + Label { + anchors.centerIn: parent + text: qsTr("Drop files here to add them") + font.pixelSize: 24 + font.bold: true + color: "#0080FF" + } + + Rectangle { + anchors.fill: parent + anchors.margins: 8 + color: "transparent" + radius: 12 + border.color: "#0080FF" + border.width: 3 + } + } + } StackView { id: stackView @@ -28,15 +84,15 @@ ApplicationWindow { FileDialog { id: fileDialog - title: qsTr("Select Files to Send") + title: qsTr("Select Files") fileMode: FileDialog.OpenFiles onAccepted: { var paths = [] for (var i = 0; i < selectedFiles.length; i++) { - paths.push(selectedFiles[i].toString().replace("file://", "")) + paths.push(selectedFiles[i].toString()) } - if (paths.length > 0 && selectedDeviceFingerprint !== "") { - appController.sendFiles(selectedDeviceFingerprint, paths) + if (paths.length > 0) { + appController.addFiles(paths) } } } @@ -311,6 +367,86 @@ ApplicationWindow { anchors.margins: 16 spacing: 16 + ColumnLayout { + Layout.fillWidth: true + spacing: 8 + + RowLayout { + Layout.fillWidth: true + + Label { + text: qsTr("Selected Files") + font.bold: true + font.pixelSize: 16 + } + + Item { Layout.fillWidth: true } + + Button { + text: qsTr("Add Files") + onClicked: fileDialog.open() + } + + Button { + text: qsTr("Clear All") + visible: appController.hasPendingFiles + onClicked: appController.clearPendingFiles() + } + } + + ListView { + id: pendingFilesList + Layout.fillWidth: true + Layout.preferredHeight: Math.min(180, contentHeight) + model: appController.pendingFiles + spacing: 4 + visible: appController.hasPendingFiles + clip: true + + delegate: Pane { + width: ListView.view.width + padding: 8 + + background: Rectangle { + color: "transparent" + radius: 6 + border.color: palette.mid + border.width: 1 + } + + RowLayout { + anchors.fill: parent + spacing: 8 + + Label { + text: modelData.fileName + Layout.fillWidth: true + elide: Text.ElideMiddle + } + Label { + text: formatSize(modelData.size) + color: palette.mid + font.pixelSize: 12 + } + ToolButton { + text: "\u2715" + font.pixelSize: 14 + onClicked: appController.removePendingFile(index) + } + } + } + } + + Label { + visible: !appController.hasPendingFiles + text: qsTr("No files selected. Click \"Add Files\" or drag and drop files here.") + color: palette.mid + Layout.fillWidth: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + } + Label { text: qsTr("Nearby Devices") font.bold: true @@ -345,6 +481,7 @@ ApplicationWindow { Label { text: modelData.alias || modelData.ip font.bold: true + color: palette.text } Label { text: "%1:%2".arg(modelData.ip).arg(modelData.port) @@ -352,13 +489,14 @@ ApplicationWindow { font.pixelSize: 12 } } - Button { + visible: appController.hasPendingFiles && !appController.sending text: qsTr("Send") enabled: !appController.sending onClicked: { - selectedDeviceFingerprint = modelData.fingerprint - fileDialog.open() + if (appController.hasPendingFiles && !appController.sending) { + appController.sendTo(modelData.fingerprint) + } } } }