diff --git a/src/app/AppController.cpp b/src/app/AppController.cpp index 4656984..f8ab143 100644 --- a/src/app/AppController.cpp +++ b/src/app/AppController.cpp @@ -8,6 +8,10 @@ #include #include +#include +#include +#include + AppController::AppController(QObject* parent) : QObject(parent) , m_settings(new LocalSend::Settings(this)) @@ -146,6 +150,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 { return LocalSend::deviceTypeToString(m_settings->deviceType()); @@ -327,6 +344,17 @@ void AppController::onRegisterRequest(const LocalSend::RegisterDto& dto, const Q 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, const LocalSend::PrepareUploadRequestDto& dto, const QHostAddress& sender) @@ -356,7 +384,12 @@ void AppController::onPrepareUploadRequest(const QString& httpSessionId, 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, @@ -570,6 +603,21 @@ void AppController::clearPendingFiles() 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) { if (m_pendingSendPaths.isEmpty()) { diff --git a/src/app/AppController.h b/src/app/AppController.h index 2554309..ca88163 100644 --- a/src/app/AppController.h +++ b/src/app/AppController.h @@ -19,6 +19,7 @@ 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 autoFinish READ autoFinish WRITE setAutoFinish NOTIFY autoFinishChanged) Q_PROPERTY(bool sending READ sending NOTIFY sendingChanged) Q_PROPERTY(double sendProgress READ sendProgress NOTIFY sendProgressChanged) Q_PROPERTY(QString currentSendFileName READ currentSendFileName NOTIFY sendProgressChanged) @@ -54,6 +55,9 @@ public: bool quickSave() const; void setQuickSave(bool enabled); + bool autoFinish() const; + void setAutoFinish(bool enabled); + QVariantList devices() const; bool serverRunning() const; @@ -96,6 +100,8 @@ public: Q_INVOKABLE void removePendingFile(int index); Q_INVOKABLE void clearPendingFiles(); Q_INVOKABLE void retryWithPin(const QString& pin); + Q_INVOKABLE void copyToClipboard(const QString& text); + Q_INVOKABLE void openLink(const QString& url); signals: void aliasChanged(); @@ -104,11 +110,12 @@ signals: void quickSaveChanged(); void devicesChanged(); void serverRunningChanged(); + void autoFinishChanged(); void sendingChanged(); void sendProgressChanged(); void pendingFilesChanged(); 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 receiveCompleted(const QString& sessionId); void receiveError(const QString& sessionId, const QString& error); diff --git a/src/app/qml/main.qml b/src/app/qml/main.qml index cb51f93..9f1ad2f 100644 --- a/src/app/qml/main.qml +++ b/src/app/qml/main.qml @@ -14,6 +14,7 @@ ApplicationWindow { property var currentFiles: [] property string currentSenderAlias: "" property string currentSenderIp: "" + property string currentMessage: "" function getDeviceTypeIcon(deviceType) { switch (deviceType) { @@ -173,7 +174,9 @@ ApplicationWindow { anchors.centerIn: parent modal: true closePolicy: Popup.NoAutoClose - title: qsTr("Receiving Files") + title: succeeded ? qsTr("Receive Complete") : qsTr("Receiving Files") + + property bool succeeded: false ColumnLayout { spacing: 12 @@ -184,6 +187,7 @@ ApplicationWindow { } Label { + visible: !receiveProgressDialog.succeeded text: appController.currentReceiveFileName ? qsTr("%1 (%2/%3)").arg(appController.currentReceiveFileName) .arg(appController.currentReceiveFileIndex) @@ -197,17 +201,29 @@ ApplicationWindow { Layout.fillWidth: true from: 0 to: 100 - value: appController.receiveProgress + value: receiveProgressDialog.succeeded ? 100 : appController.receiveProgress } Label { - text: qsTr("%1% complete").arg(Math.round(appController.receiveProgress)) + text: receiveProgressDialog.succeeded + ? qsTr("Files received successfully.") + : qsTr("%1% complete").arg(Math.round(appController.receiveProgress)) + color: receiveProgressDialog.succeeded ? palette.mid : palette.mid + } + + 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 { Button { + visible: !receiveProgressDialog.succeeded text: qsTr("Cancel") DialogButtonBox.buttonRole: DialogButtonBox.RejectRole onClicked: { @@ -215,6 +231,85 @@ ApplicationWindow { 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,12 +318,15 @@ ApplicationWindow { anchors.centerIn: parent modal: true closePolicy: Popup.NoAutoClose - title: qsTr("Sending Files") + title: succeeded ? qsTr("Send Complete") : qsTr("Sending Files") + + property bool succeeded: false ColumnLayout { spacing: 12 Label { + visible: !sendProgressDialog.succeeded text: appController.currentSendFileName ? qsTr("%1 (%2/%3)").arg(appController.currentSendFileName) .arg(appController.currentSendFileIndex) @@ -242,17 +340,20 @@ ApplicationWindow { Layout.fillWidth: true from: 0 to: 100 - value: appController.sendProgress + value: sendProgressDialog.succeeded ? 100 : appController.sendProgress } Label { - text: qsTr("%1% complete").arg(Math.round(appController.sendProgress)) + text: sendProgressDialog.succeeded + ? qsTr("Files sent successfully.") + : qsTr("%1% complete").arg(Math.round(appController.sendProgress)) color: palette.mid } } footer: DialogButtonBox { Button { + visible: !sendProgressDialog.succeeded text: qsTr("Cancel") DialogButtonBox.buttonRole: DialogButtonBox.RejectRole onClicked: { @@ -260,18 +361,35 @@ ApplicationWindow { sendProgressDialog.close() } } + Button { + visible: sendProgressDialog.succeeded + text: qsTr("Done") + DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + onClicked: sendProgressDialog.close() + } + } + + onClosed: { + succeeded = false } } Connections { target: appController - function onReceiveRequest(sessionId, senderAlias, senderIp, files) { + function onReceiveRequest(sessionId, senderAlias, senderIp, files, message) { currentSessionId = sessionId currentSenderAlias = senderAlias currentSenderIp = senderIp 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) { appController.acceptReceive(sessionId) receiveProgressDialog.open() @@ -281,7 +399,10 @@ ApplicationWindow { } function onReceiveCompleted(sessionId) { - receiveProgressDialog.close() + receiveProgressDialog.succeeded = true + if (appController.autoFinish || appController.quickSave) { + receiveProgressDialog.close() + } } function onReceiveError(sessionId, error) { @@ -297,9 +418,10 @@ ApplicationWindow { } function onSendCompleted(sessionId) { - sendProgressDialog.close() - successDialog.text = qsTr("Files sent successfully!") - successDialog.open() + sendProgressDialog.succeeded = true + if (appController.autoFinish) { + sendProgressDialog.close() + } } function onSendError(error) { @@ -506,6 +628,12 @@ ApplicationWindow { 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) + } + Component { id: homePageComponent Page { @@ -788,6 +916,13 @@ ApplicationWindow { onCheckedChanged: appController.quickSave = checked } + Label { text: qsTr("Auto Finish:") } + CheckBox { + id: autoFinishCheck + checked: appController.autoFinish + onCheckedChanged: appController.autoFinish = checked + } + Label { text: qsTr("Receive PIN:") } RowLayout { Layout.fillWidth: true diff --git a/src/core/include/LocalSendCore/Settings.h b/src/core/include/LocalSendCore/Settings.h index a69d82a..2ecbe3e 100644 --- a/src/core/include/LocalSendCore/Settings.h +++ b/src/core/include/LocalSendCore/Settings.h @@ -29,6 +29,9 @@ public: bool quickSave() const; void setQuickSave(bool enabled); + bool autoFinish() const; + void setAutoFinish(bool enabled); + QString receivePin() const; void setReceivePin(const QString& pin); diff --git a/src/core/src/Settings.cpp b/src/core/src/Settings.cpp index aa4d622..3c28930 100644 --- a/src/core/src/Settings.cpp +++ b/src/core/src/Settings.cpp @@ -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 { return m_settings.value(QStringLiteral("receivePin")).toString();