diff --git a/resources/icons/ui/copy.svg b/resources/icons/ui/copy.svg new file mode 100644 index 00000000..ae358603 --- /dev/null +++ b/resources/icons/ui/copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/qml/dialogs/ImageOverlay.qml b/resources/qml/dialogs/ImageOverlay.qml index 6f56e79a..fa874529 100644 --- a/resources/qml/dialogs/ImageOverlay.qml +++ b/resources/qml/dialogs/ImageOverlay.qml @@ -29,6 +29,17 @@ Window { onActivated: imageOverlay.close() } + Shortcut { + sequence: StandardKey.Copy + onActivated: { + if (room) { + room.copyMedia(eventId); + } else { + TimelineManager.copyImage(url); + } + } + } + TapHandler { onSingleTapped: imageOverlay.close(); } @@ -107,14 +118,37 @@ Window { anchors.margins: Nheko.paddingLarge spacing: Nheko.paddingMedium + ImageButton { + height: 48 + width: 48 + hoverEnabled: true + image: ":/icons/icons/ui/copy.svg" + + //ToolTip.visible: hovered + //ToolTip.delay: Nheko.tooltipDelay + //ToolTip.text: qsTr("Copy to clipboard") + + onClicked: { + imageOverlay.hide(); + if (room) { + room.copyMedia(eventId); + } else { + TimelineManager.copyImage(url); + } + imageOverlay.close(); + } + } + ImageButton { height: 48 width: 48 hoverEnabled: true image: ":/icons/icons/ui/download.svg" + //ToolTip.visible: hovered //ToolTip.delay: Nheko.tooltipDelay //ToolTip.text: qsTr("Download") + onClicked: { imageOverlay.hide(); if (room) { @@ -130,9 +164,11 @@ Window { width: 48 hoverEnabled: true image: ":/icons/icons/ui/dismiss.svg" + //ToolTip.visible: hovered //ToolTip.delay: Nheko.tooltipDelay //ToolTip.text: qsTr("Close") + onClicked: imageOverlay.close() } } diff --git a/resources/res.qrc b/resources/res.qrc index 3f1b2b65..412cdb83 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -9,6 +9,7 @@ icons/ui/checkmark.svg icons/ui/clock.svg icons/ui/collapsed.svg + icons/ui/copy.svg icons/ui/delete.svg icons/ui/dismiss.svg icons/ui/dismiss_edit.svg diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index 879ec7cc..3a626a3c 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -148,6 +148,7 @@ InputBar::insertMimeData(const QMimeData *md) nhlog::ui()->debug("Got mime formats: {}", md->formats().join(QStringLiteral(", ")).toStdString()); + nhlog::ui()->debug("Has image: {}", md->hasImage()); const auto formats = md->formats().filter(QStringLiteral("/")); const auto image = formats.filter(QStringLiteral("image/"), Qt::CaseInsensitive); const auto audio = formats.filter(QStringLiteral("audio/"), Qt::CaseInsensitive); diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index f80f2ee9..5996bea8 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -1860,6 +1861,60 @@ TimelineModel::saveMedia(const QString &eventId) const return true; } +bool +TimelineModel::copyMedia(const QString &eventId) const +{ + auto event = events.get(eventId.toStdString(), ""); + if (!event) + return false; + + QString mxcUrl = QString::fromStdString(mtx::accessors::url(*event)); + QString mimeType = QString::fromStdString(mtx::accessors::mimetype(*event)); + qml_mtx_events::EventType eventType = toRoomEventType(*event); + + auto encryptionInfo = mtx::accessors::file(*event); + + const auto url = mxcUrl.toStdString(); + + http::client()->download( + url, + [url, mimeType, eventType, encryptionInfo](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve media {}: {} {}", + url, + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + try { + auto temp = data; + if (encryptionInfo) + temp = + mtx::crypto::to_string(mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + + auto by = QByteArray(temp.data(), (qsizetype)temp.size()); + QMimeData *clipContents = new QMimeData(); + clipContents->setData(mimeType, by); + + if (eventType == qml_mtx_events::EventType::ImageMessage) { + auto img = utils::readImage(QByteArray(data.data(), (qsizetype)data.size())); + clipContents->setImageData(img); + } + + QGuiApplication::clipboard()->setMimeData(clipContents); + + return; + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while copying file to clipboard: {}", e.what()); + } + }); + return true; +} + void TimelineModel::cacheMedia(const QString &eventId, const std::function &callback) diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 0244c1b1..b0d81441 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -320,6 +320,7 @@ public: Q_INVOKABLE void openMedia(const QString &eventId); Q_INVOKABLE void cacheMedia(const QString &eventId); Q_INVOKABLE bool saveMedia(const QString &eventId) const; + Q_INVOKABLE bool copyMedia(const QString &eventId) const; Q_INVOKABLE void showEvent(QString eventId); Q_INVOKABLE void copyLinkToEvent(const QString &eventId) const; diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index b949e4c3..44f288c6 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -5,7 +5,9 @@ #include "TimelineViewManager.h" #include +#include #include +#include #include #include @@ -29,8 +31,6 @@ #include "voip/CallManager.h" #include "voip/WebRTCSession.h" -namespace msgs = mtx::events::msg; - namespace { template class Op, class... Args> using is_detected = typename nheko::detail::detector::value_t; @@ -318,6 +318,37 @@ TimelineViewManager::saveMedia(QString mxcUrl) }); } +void +TimelineViewManager::copyImage(const QString &mxcUrl) const +{ + const auto url = mxcUrl.toStdString(); + QString mimeType; + + http::client()->download( + url, + [url, mimeType](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve media {}: {} {}", + url, + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + try { + auto img = utils::readImage(QByteArray(data.data(), (qsizetype)data.size())); + QGuiApplication::clipboard()->setImage(img); + + return; + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while copying file to clipboard: {}", e.what()); + } + }); +} + void TimelineViewManager::updateReadReceipts(const QString &room_id, const std::vector &event_ids) diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index ee5cf031..e3279e21 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -56,6 +56,7 @@ public: double proportionalHeight); Q_INVOKABLE void openImagePackSettings(QString roomid); Q_INVOKABLE void saveMedia(QString mxcUrl); + Q_INVOKABLE void copyImage(const QString &mxcUrl) const; Q_INVOKABLE QColor userColor(QString id, QColor background); Q_INVOKABLE QString escapeEmoji(QString str) const; Q_INVOKABLE QString htmlEscape(QString str) const { return str.toHtmlEscaped(); }