Copy image to clipboard

Fixes #599
This commit is contained in:
Nicolas Werner 2023-04-23 20:55:28 +02:00
parent 0ded15315e
commit 6a03615413
No known key found for this signature in database
GPG key ID: C8D75E610773F2D9
8 changed files with 129 additions and 2 deletions

View file

@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M5.503 4.627 5.5 6.75v10.504a3.25 3.25 0 0 0 3.25 3.25h8.616a2.251 2.251 0 0 1-2.122 1.5H8.75A4.75 4.75 0 0 1 4 17.254V6.75c0-.98.627-1.815 1.503-2.123ZM17.75 2A2.25 2.25 0 0 1 20 4.25v13a2.25 2.25 0 0 1-2.25 2.25h-9a2.25 2.25 0 0 1-2.25-2.25v-13A2.25 2.25 0 0 1 8.75 2h9Zm0 1.5h-9a.75.75 0 0 0-.75.75v13c0 .414.336.75.75.75h9a.75.75 0 0 0 .75-.75v-13a.75.75 0 0 0-.75-.75Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 501 B

View file

@ -29,6 +29,17 @@ Window {
onActivated: imageOverlay.close() onActivated: imageOverlay.close()
} }
Shortcut {
sequence: StandardKey.Copy
onActivated: {
if (room) {
room.copyMedia(eventId);
} else {
TimelineManager.copyImage(url);
}
}
}
TapHandler { TapHandler {
onSingleTapped: imageOverlay.close(); onSingleTapped: imageOverlay.close();
} }
@ -107,14 +118,37 @@ Window {
anchors.margins: Nheko.paddingLarge anchors.margins: Nheko.paddingLarge
spacing: Nheko.paddingMedium 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 { ImageButton {
height: 48 height: 48
width: 48 width: 48
hoverEnabled: true hoverEnabled: true
image: ":/icons/icons/ui/download.svg" image: ":/icons/icons/ui/download.svg"
//ToolTip.visible: hovered //ToolTip.visible: hovered
//ToolTip.delay: Nheko.tooltipDelay //ToolTip.delay: Nheko.tooltipDelay
//ToolTip.text: qsTr("Download") //ToolTip.text: qsTr("Download")
onClicked: { onClicked: {
imageOverlay.hide(); imageOverlay.hide();
if (room) { if (room) {
@ -130,9 +164,11 @@ Window {
width: 48 width: 48
hoverEnabled: true hoverEnabled: true
image: ":/icons/icons/ui/dismiss.svg" image: ":/icons/icons/ui/dismiss.svg"
//ToolTip.visible: hovered //ToolTip.visible: hovered
//ToolTip.delay: Nheko.tooltipDelay //ToolTip.delay: Nheko.tooltipDelay
//ToolTip.text: qsTr("Close") //ToolTip.text: qsTr("Close")
onClicked: imageOverlay.close() onClicked: imageOverlay.close()
} }
} }

View file

@ -9,6 +9,7 @@
<file>icons/ui/checkmark.svg</file> <file>icons/ui/checkmark.svg</file>
<file>icons/ui/clock.svg</file> <file>icons/ui/clock.svg</file>
<file>icons/ui/collapsed.svg</file> <file>icons/ui/collapsed.svg</file>
<file>icons/ui/copy.svg</file>
<file>icons/ui/delete.svg</file> <file>icons/ui/delete.svg</file>
<file>icons/ui/dismiss.svg</file> <file>icons/ui/dismiss.svg</file>
<file>icons/ui/dismiss_edit.svg</file> <file>icons/ui/dismiss_edit.svg</file>

View file

@ -148,6 +148,7 @@ InputBar::insertMimeData(const QMimeData *md)
nhlog::ui()->debug("Got mime formats: {}", nhlog::ui()->debug("Got mime formats: {}",
md->formats().join(QStringLiteral(", ")).toStdString()); md->formats().join(QStringLiteral(", ")).toStdString());
nhlog::ui()->debug("Has image: {}", md->hasImage());
const auto formats = md->formats().filter(QStringLiteral("/")); const auto formats = md->formats().filter(QStringLiteral("/"));
const auto image = formats.filter(QStringLiteral("image/"), Qt::CaseInsensitive); const auto image = formats.filter(QStringLiteral("image/"), Qt::CaseInsensitive);
const auto audio = formats.filter(QStringLiteral("audio/"), Qt::CaseInsensitive); const auto audio = formats.filter(QStringLiteral("audio/"), Qt::CaseInsensitive);

View file

@ -12,6 +12,7 @@
#include <QDesktopServices> #include <QDesktopServices>
#include <QFileDialog> #include <QFileDialog>
#include <QGuiApplication> #include <QGuiApplication>
#include <QMimeData>
#include <QMimeDatabase> #include <QMimeDatabase>
#include <QRegularExpression> #include <QRegularExpression>
#include <QStandardPaths> #include <QStandardPaths>
@ -1860,6 +1861,60 @@ TimelineModel::saveMedia(const QString &eventId) const
return true; 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<int>(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 void
TimelineModel::cacheMedia(const QString &eventId, TimelineModel::cacheMedia(const QString &eventId,
const std::function<void(const QString)> &callback) const std::function<void(const QString)> &callback)

View file

@ -320,6 +320,7 @@ public:
Q_INVOKABLE void openMedia(const QString &eventId); Q_INVOKABLE void openMedia(const QString &eventId);
Q_INVOKABLE void cacheMedia(const QString &eventId); Q_INVOKABLE void cacheMedia(const QString &eventId);
Q_INVOKABLE bool saveMedia(const QString &eventId) const; 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 showEvent(QString eventId);
Q_INVOKABLE void copyLinkToEvent(const QString &eventId) const; Q_INVOKABLE void copyLinkToEvent(const QString &eventId) const;

View file

@ -5,7 +5,9 @@
#include "TimelineViewManager.h" #include "TimelineViewManager.h"
#include <QApplication> #include <QApplication>
#include <QClipboard>
#include <QFileDialog> #include <QFileDialog>
#include <QMimeData>
#include <QStandardPaths> #include <QStandardPaths>
#include <QString> #include <QString>
@ -29,8 +31,6 @@
#include "voip/CallManager.h" #include "voip/CallManager.h"
#include "voip/WebRTCSession.h" #include "voip/WebRTCSession.h"
namespace msgs = mtx::events::msg;
namespace { namespace {
template<template<class...> class Op, class... Args> template<template<class...> class Op, class... Args>
using is_detected = typename nheko::detail::detector<nheko::nonesuch, void, Op, Args...>::value_t; using is_detected = typename nheko::detail::detector<nheko::nonesuch, void, Op, Args...>::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<int>(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 void
TimelineViewManager::updateReadReceipts(const QString &room_id, TimelineViewManager::updateReadReceipts(const QString &room_id,
const std::vector<QString> &event_ids) const std::vector<QString> &event_ids)

View file

@ -56,6 +56,7 @@ public:
double proportionalHeight); double proportionalHeight);
Q_INVOKABLE void openImagePackSettings(QString roomid); Q_INVOKABLE void openImagePackSettings(QString roomid);
Q_INVOKABLE void saveMedia(QString mxcUrl); Q_INVOKABLE void saveMedia(QString mxcUrl);
Q_INVOKABLE void copyImage(const QString &mxcUrl) const;
Q_INVOKABLE QColor userColor(QString id, QColor background); Q_INVOKABLE QColor userColor(QString id, QColor background);
Q_INVOKABLE QString escapeEmoji(QString str) const; Q_INVOKABLE QString escapeEmoji(QString str) const;
Q_INVOKABLE QString htmlEscape(QString str) const { return str.toHtmlEscaped(); } Q_INVOKABLE QString htmlEscape(QString str) const { return str.toHtmlEscaped(); }