Merge pull request #669 from Nheko-Reborn/sticker-editor

Sticker editor
This commit is contained in:
DeepBlueV7.X 2021-08-06 03:00:19 +00:00 committed by GitHub
commit 11f9a9d044
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 896 additions and 168 deletions

View file

@ -381,7 +381,7 @@ if(USE_BUNDLED_MTXCLIENT)
FetchContent_Declare(
MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
GIT_TAG 316a4040785ee2eabac7ef5ce7b4acb71c48f6eb
GIT_TAG e5688a2c5987a614b5055595f991f18568127bd2
)
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")

View file

@ -161,7 +161,7 @@ modules:
buildsystem: cmake-ninja
name: mtxclient
sources:
- commit: 316a4040785ee2eabac7ef5ce7b4acb71c48f6eb
- commit: e5688a2c5987a614b5055595f991f18568127bd2
type: git
url: https://github.com/Nheko-Reborn/mtxclient.git
- config-opts:

View file

@ -11,10 +11,11 @@ import im.nheko 1.0
Rectangle {
id: avatar
property alias url: img.source
property string url
property string userid
property string displayName
property alias textColor: label.color
property bool crop: true
signal clicked(var mouse)
@ -44,12 +45,13 @@ Rectangle {
anchors.fill: parent
asynchronous: true
fillMode: Image.PreserveAspectCrop
fillMode: avatar.crop ? Image.PreserveAspectCrop : Image.PreserveAspectFit
mipmap: true
smooth: true
sourceSize.width: avatar.width
sourceSize.height: avatar.height
layer.enabled: true
source: avatar.url + ((avatar.crop || !avatar.url) ? "" : "?scale")
MouseArea {
id: mouseArea

View file

@ -154,7 +154,7 @@ ApplicationWindow {
GridLayout {
columns: 2
rowSpacing: 10
rowSpacing: Nheko.paddingLarge
MatrixText {
text: qsTr("SETTINGS")
@ -180,7 +180,7 @@ ApplicationWindow {
}
MatrixText {
text: "Room access"
text: qsTr("Room access")
Layout.fillWidth: true
}

View file

@ -23,6 +23,9 @@ MouseArea {
// console.warn("Delta: ", wheel.pixelDelta.y);
// console.warn("Old position: ", flickable.contentY);
// console.warn("New position: ", newPos);
// breaks ListView's with headers...
//if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem)
// minYExtent += flickableItem.headerItem.height;
id: root
@ -55,9 +58,6 @@ MouseArea {
var minYExtent = flickableItem.originY + flickableItem.topMargin;
var maxYExtent = (flickableItem.contentHeight + flickableItem.bottomMargin + flickableItem.originY) - flickableItem.height;
if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem)
minYExtent += flickableItem.headerItem.height;
//Avoid overscrolling
return Math.max(minYExtent, Math.min(maxYExtent, flickableItem.contentY - pixelDelta));
}

View file

@ -0,0 +1,133 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import ".."
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import im.nheko 1.0
Rectangle {
id: tile
property color background: Nheko.colors.window
property color importantText: Nheko.colors.text
property color unimportantText: Nheko.colors.buttonText
property color bubbleBackground: Nheko.colors.highlight
property color bubbleText: Nheko.colors.highlightedText
property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3)
required property string avatarUrl
required property string title
required property string subtitle
required property int index
required property int selectedIndex
property bool crop: true
color: background
height: avatarSize + 2 * Nheko.paddingMedium
width: ListView.view.width
state: "normal"
states: [
State {
name: "highlight"
when: hovered.hovered && !(index == selectedIndex)
PropertyChanges {
target: tile
background: Nheko.colors.dark
importantText: Nheko.colors.brightText
unimportantText: Nheko.colors.brightText
bubbleBackground: Nheko.colors.highlight
bubbleText: Nheko.colors.highlightedText
}
},
State {
name: "selected"
when: index == selectedIndex
PropertyChanges {
target: tile
background: Nheko.colors.highlight
importantText: Nheko.colors.highlightedText
unimportantText: Nheko.colors.highlightedText
bubbleBackground: Nheko.colors.highlightedText
bubbleText: Nheko.colors.highlight
}
}
]
HoverHandler {
id: hovered
}
RowLayout {
spacing: Nheko.paddingMedium
anchors.fill: parent
anchors.margins: Nheko.paddingMedium
Avatar {
id: avatar
enabled: false
Layout.alignment: Qt.AlignVCenter
height: avatarSize
width: avatarSize
url: tile.avatarUrl.replace("mxc://", "image://MxcImage/")
displayName: title
crop: tile.crop
}
ColumnLayout {
id: textContent
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
Layout.minimumWidth: 100
width: parent.width - avatar.width
Layout.preferredWidth: parent.width - avatar.width
spacing: Nheko.paddingSmall
RowLayout {
Layout.fillWidth: true
spacing: 0
ElidedLabel {
Layout.alignment: Qt.AlignBottom
color: tile.importantText
elideWidth: textContent.width - Nheko.paddingMedium
fullText: title
textFormat: Text.PlainText
}
Item {
Layout.fillWidth: true
}
}
RowLayout {
Layout.fillWidth: true
spacing: 0
ElidedLabel {
color: tile.unimportantText
font.pixelSize: fontMetrics.font.pixelSize * 0.9
elideWidth: textContent.width - Nheko.paddingSmall
fullText: subtitle
textFormat: Text.PlainText
}
Item {
Layout.fillWidth: true
}
}
}
}
}

View file

@ -0,0 +1,301 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import ".."
import "../components"
import Qt.labs.platform 1.1
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import im.nheko 1.0
ApplicationWindow {
//Component.onCompleted: Nheko.reparent(win)
id: win
property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3)
property SingleImagePackModel imagePack
property int currentImageIndex: -1
readonly property int stickerDim: 128
readonly property int stickerDimPad: 128 + Nheko.paddingSmall
title: qsTr("Editing image pack")
height: 600
width: 600
palette: Nheko.colors
color: Nheko.colors.base
modality: Qt.WindowModal
flags: Qt.Dialog | Qt.WindowCloseButtonHint
AdaptiveLayout {
id: adaptiveView
anchors.fill: parent
singlePageMode: false
pageIndex: 0
AdaptiveLayoutElement {
id: packlistC
visible: Settings.groupView
minimumWidth: 200
collapsedWidth: 200
preferredWidth: 300
maximumWidth: 300
clip: true
ListView {
//required property bool isEmote
//required property bool isSticker
model: imagePack
ScrollHelper {
flickable: parent
anchors.fill: parent
enabled: !Settings.mobileMode
}
header: AvatarListTile {
title: imagePack.packname
avatarUrl: imagePack.avatarUrl
subtitle: imagePack.statekey
index: -1
selectedIndex: currentImageIndex
TapHandler {
onSingleTapped: currentImageIndex = -1
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
height: parent.height - Nheko.paddingSmall * 2
width: 3
color: Nheko.colors.highlight
}
}
footer: Button {
palette: Nheko.colors
onClicked: addFilesDialog.open()
width: ListView.view.width
text: qsTr("Add images")
FileDialog {
id: addFilesDialog
folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
fileMode: FileDialog.OpenFiles
nameFilters: [qsTr("Stickers (*.png *.webp)")]
onAccepted: imagePack.addStickers(files)
}
}
delegate: AvatarListTile {
id: packItem
property color background: Nheko.colors.window
property color importantText: Nheko.colors.text
property color unimportantText: Nheko.colors.buttonText
property color bubbleBackground: Nheko.colors.highlight
property color bubbleText: Nheko.colors.highlightedText
required property string shortCode
required property string url
required property string body
title: shortCode
subtitle: body
avatarUrl: url
selectedIndex: currentImageIndex
crop: false
TapHandler {
onSingleTapped: currentImageIndex = index
}
}
}
}
AdaptiveLayoutElement {
id: packinfoC
Rectangle {
color: Nheko.colors.window
GridLayout {
anchors.fill: parent
anchors.margins: Nheko.paddingMedium
visible: currentImageIndex == -1
enabled: visible
columns: 2
rowSpacing: Nheko.paddingLarge
Avatar {
Layout.columnSpan: 2
url: imagePack.avatarUrl.replace("mxc://", "image://MxcImage/")
displayName: imagePack.packname
height: 130
width: 130
crop: false
Layout.alignment: Qt.AlignHCenter
}
MatrixText {
visible: imagePack.roomid
text: qsTr("State key")
}
MatrixTextField {
visible: imagePack.roomid
Layout.fillWidth: true
text: imagePack.statekey
onTextEdited: imagePack.statekey = text
}
MatrixText {
text: qsTr("Packname")
}
MatrixTextField {
Layout.fillWidth: true
text: imagePack.packname
onTextEdited: imagePack.packname = text
}
MatrixText {
text: qsTr("Attrbution")
}
MatrixTextField {
Layout.fillWidth: true
text: imagePack.attribution
onTextEdited: imagePack.attribution = text
}
MatrixText {
text: qsTr("Use as Emoji")
}
ToggleButton {
checked: imagePack.isEmotePack
onClicked: imagePack.isEmotePack = checked
Layout.alignment: Qt.AlignRight
}
MatrixText {
text: qsTr("Use as Sticker")
}
ToggleButton {
checked: imagePack.isStickerPack
onClicked: imagePack.isStickerPack = checked
Layout.alignment: Qt.AlignRight
}
Item {
Layout.columnSpan: 2
Layout.fillHeight: true
}
}
GridLayout {
anchors.fill: parent
anchors.margins: Nheko.paddingMedium
visible: currentImageIndex >= 0
enabled: visible
columns: 2
rowSpacing: Nheko.paddingLarge
Avatar {
Layout.columnSpan: 2
url: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.Url).replace("mxc://", "image://MxcImage/")
displayName: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.ShortCode)
height: 130
width: 130
crop: false
Layout.alignment: Qt.AlignHCenter
}
MatrixText {
text: qsTr("Shortcode")
}
MatrixTextField {
Layout.fillWidth: true
text: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.ShortCode)
onTextEdited: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.ShortCode)
}
MatrixText {
text: qsTr("Body")
}
MatrixTextField {
Layout.fillWidth: true
text: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.Body)
onTextEdited: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.Body)
}
MatrixText {
text: qsTr("Use as Emoji")
}
ToggleButton {
checked: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.IsEmote)
onClicked: imagePack.setData(imagePack.index(currentImageIndex, 0), checked, SingleImagePackModel.IsEmote)
Layout.alignment: Qt.AlignRight
}
MatrixText {
text: qsTr("Use as Sticker")
}
ToggleButton {
checked: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.IsSticker)
onClicked: imagePack.setData(imagePack.index(currentImageIndex, 0), checked, SingleImagePackModel.IsSticker)
Layout.alignment: Qt.AlignRight
}
Item {
Layout.columnSpan: 2
Layout.fillHeight: true
}
}
}
}
}
footer: DialogButtonBox {
id: buttons
Button {
text: qsTr("Cancel")
DialogButtonBox.buttonRole: DialogButtonBox.DestructiveRole
onClicked: win.close()
}
Button {
text: qsTr("Save")
DialogButtonBox.buttonRole: DialogButtonBox.ApplyRole
onClicked: {
imagePack.save();
win.close();
}
}
}
}

View file

@ -20,14 +20,22 @@ ApplicationWindow {
readonly property int stickerDimPad: 128 + Nheko.paddingSmall
title: qsTr("Image pack settings")
height: 400
width: 600
height: 600
width: 800
palette: Nheko.colors
color: Nheko.colors.base
modality: Qt.NonModal
flags: Qt.Dialog | Qt.WindowCloseButtonHint
Component.onCompleted: Nheko.reparent(win)
Component {
id: packEditor
ImagePackEditorDialog {
}
}
AdaptiveLayout {
id: adaptiveView
@ -54,7 +62,35 @@ ApplicationWindow {
enabled: !Settings.mobileMode
}
delegate: Rectangle {
footer: ColumnLayout {
Button {
palette: Nheko.colors
onClicked: {
var dialog = packEditor.createObject(timelineRoot, {
"imagePack": packlist.newPack(false)
});
dialog.show();
}
width: packlist.width
visible: !packlist.containsAccountPack
text: qsTr("Create account pack")
}
Button {
palette: Nheko.colors
onClicked: {
var dialog = packEditor.createObject(timelineRoot, {
"imagePack": packlist.newPack(true)
});
dialog.show();
}
width: packlist.width
text: qsTr("New room pack")
}
}
delegate: AvatarListTile {
id: packItem
property color background: Nheko.colors.window
@ -63,131 +99,24 @@ ApplicationWindow {
property color bubbleBackground: Nheko.colors.highlight
property color bubbleText: Nheko.colors.highlightedText
required property string displayName
required property string avatarUrl
required property bool fromAccountData
required property bool fromCurrentRoom
required property int index
color: background
height: avatarSize + 2 * Nheko.paddingMedium
width: ListView.view.width
state: "normal"
states: [
State {
name: "highlight"
when: hovered.hovered && !(index == currentPackIndex)
PropertyChanges {
target: packItem
background: Nheko.colors.dark
importantText: Nheko.colors.brightText
unimportantText: Nheko.colors.brightText
bubbleBackground: Nheko.colors.highlight
bubbleText: Nheko.colors.highlightedText
}
},
State {
name: "selected"
when: index == currentPackIndex
PropertyChanges {
target: packItem
background: Nheko.colors.highlight
importantText: Nheko.colors.highlightedText
unimportantText: Nheko.colors.highlightedText
bubbleBackground: Nheko.colors.highlightedText
bubbleText: Nheko.colors.highlight
}
}
]
title: displayName
subtitle: {
if (fromAccountData)
return qsTr("Private pack");
else if (fromCurrentRoom)
return qsTr("Pack from this room");
else
return qsTr("Globally enabled pack");
}
selectedIndex: currentPackIndex
TapHandler {
margin: -Nheko.paddingSmall
onSingleTapped: currentPackIndex = index
}
HoverHandler {
id: hovered
}
RowLayout {
spacing: Nheko.paddingMedium
anchors.fill: parent
anchors.margins: Nheko.paddingMedium
Avatar {
// In the future we could show an online indicator by setting the userid for the avatar
//userid: Nheko.currentUser.userid
id: avatar
enabled: false
Layout.alignment: Qt.AlignVCenter
height: avatarSize
width: avatarSize
url: avatarUrl.replace("mxc://", "image://MxcImage/")
displayName: packItem.displayName
}
ColumnLayout {
id: textContent
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
Layout.minimumWidth: 100
width: parent.width - avatar.width
Layout.preferredWidth: parent.width - avatar.width
spacing: Nheko.paddingSmall
RowLayout {
Layout.fillWidth: true
spacing: 0
ElidedLabel {
Layout.alignment: Qt.AlignBottom
color: packItem.importantText
elideWidth: textContent.width - Nheko.paddingMedium
fullText: displayName
textFormat: Text.PlainText
}
Item {
Layout.fillWidth: true
}
}
RowLayout {
Layout.fillWidth: true
spacing: 0
ElidedLabel {
color: packItem.unimportantText
font.pixelSize: fontMetrics.font.pixelSize * 0.9
elideWidth: textContent.width - Nheko.paddingSmall
fullText: {
if (fromAccountData)
return qsTr("Private pack");
else if (fromCurrentRoom)
return qsTr("Pack from this room");
else
return qsTr("Globally enabled pack");
}
textFormat: Text.PlainText
}
Item {
Layout.fillWidth: true
}
}
}
}
}
}
@ -201,15 +130,10 @@ ApplicationWindow {
color: Nheko.colors.window
ColumnLayout {
//Button {
// Layout.alignment: Qt.AlignHCenter
// text: qsTr("Edit")
// enabled: currentPack.canEdit
//}
id: packinfo
property string packName: currentPack ? currentPack.packname : ""
property string attribution: currentPack ? currentPack.attribution : ""
property string avatarUrl: currentPack ? currentPack.avatarUrl : ""
anchors.fill: parent
@ -227,8 +151,18 @@ ApplicationWindow {
MatrixText {
text: packinfo.packName
font.pixelSize: 24
font.pixelSize: Math.ceil(fontMetrics.pixelSize * 1.1)
horizontalAlignment: TextEdit.AlignHCenter
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: packinfoC.width - Nheko.paddingLarge * 2
}
MatrixText {
text: packinfo.attribution
wrapMode: TextEdit.Wrap
horizontalAlignment: TextEdit.AlignHCenter
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: packinfoC.width - Nheko.paddingLarge * 2
}
GridLayout {
@ -250,6 +184,18 @@ ApplicationWindow {
}
Button {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Edit")
enabled: currentPack.canEdit
onClicked: {
var dialog = packEditor.createObject(timelineRoot, {
"imagePack": currentPack
});
dialog.show();
}
}
GridView {
Layout.fillHeight: true
Layout.fillWidth: true
@ -272,7 +218,7 @@ ApplicationWindow {
width: stickerDim
height: stickerDim
hoverEnabled: true
ToolTip.text: ":" + model.shortcode + ": - " + model.body
ToolTip.text: ":" + model.shortCode + ": - " + model.body
ToolTip.visible: hovered
contentItem: Image {

View file

@ -160,6 +160,7 @@
<file>qml/device-verification/Success.qml</file>
<file>qml/dialogs/InputDialog.qml</file>
<file>qml/dialogs/ImagePackSettingsDialog.qml</file>
<file>qml/dialogs/ImagePackEditorDialog.qml</file>
<file>qml/ui/Ripple.qml</file>
<file>qml/ui/Spinner.qml</file>
<file>qml/ui/animations/BlinkAnimation.qml</file>
@ -173,6 +174,7 @@
<file>qml/voip/VideoCall.qml</file>
<file>qml/components/AdaptiveLayout.qml</file>
<file>qml/components/AdaptiveLayoutElement.qml</file>
<file>qml/components/AvatarListTile.qml</file>
<file>qml/components/FlatButton.qml</file>
<file>qml/RoomMembers.qml</file>
<file>qml/InviteDialog.qml</file>

View file

@ -125,7 +125,7 @@ template<class T>
bool
containsStateUpdates(const T &e)
{
return std::visit([](const auto &ev) { return Cache::isStateEvent(ev); }, e);
return std::visit([](const auto &ev) { return Cache::isStateEvent_<decltype(ev)>; }, e);
}
bool
@ -3401,7 +3401,7 @@ Cache::getImagePacks(const std::string &room_id, std::optional<bool> stickers)
info.pack.pack = pack.pack;
for (const auto &img : pack.images) {
if (img.second.overrides_usage() &&
if (stickers.has_value() && img.second.overrides_usage() &&
(stickers ? !img.second.is_sticker() : !img.second.is_emoji()))
continue;

View file

@ -291,15 +291,9 @@ public:
std::optional<std::string> secret(const std::string name);
template<class T>
static constexpr bool isStateEvent(const mtx::events::StateEvent<T> &)
{
return true;
}
template<class T>
static constexpr bool isStateEvent(const mtx::events::Event<T> &)
{
return false;
}
constexpr static bool isStateEvent_ =
std::is_same_v<std::remove_cv_t<std::remove_reference_t<T>>,
mtx::events::StateEvent<decltype(std::declval<T>().content)>>;
static int compare_state_key(const MDB_val *a, const MDB_val *b)
{
@ -416,11 +410,27 @@ private:
}
std::visit(
[&txn, &statesdb, &stateskeydb, &eventsDb](auto e) {
if constexpr (isStateEvent(e)) {
[&txn, &statesdb, &stateskeydb, &eventsDb, &membersdb](const auto &e) {
if constexpr (isStateEvent_<decltype(e)>) {
eventsDb.put(txn, e.event_id, json(e).dump());
if (e.type != EventType::Unsupported) {
if (std::is_same_v<
std::remove_cv_t<std::remove_reference_t<decltype(e)>>,
StateEvent<mtx::events::msg::Redacted>>) {
if (e.type == EventType::RoomMember)
membersdb.del(txn, e.state_key, "");
else if (e.state_key.empty())
statesdb.del(txn, to_string(e.type));
else
stateskeydb.del(
txn,
to_string(e.type),
json::object({
{"key", e.state_key},
{"id", e.event_id},
})
.dump());
} else if (e.type != EventType::Unsupported) {
if (e.state_key.empty())
statesdb.put(
txn, to_string(e.type), json(e).dump());

View file

@ -74,3 +74,21 @@ ImagePackListModel::packAt(int row)
QQmlEngine::setObjectOwnership(e, QQmlEngine::CppOwnership);
return e;
}
SingleImagePackModel *
ImagePackListModel::newPack(bool inRoom)
{
ImagePackInfo info{};
if (inRoom)
info.source_room = room_id;
return new SingleImagePackModel(info);
}
bool
ImagePackListModel::containsAccountPack() const
{
for (const auto &p : packs)
if (p->roomid().isEmpty())
return true;
return false;
}

View file

@ -12,6 +12,7 @@ class SingleImagePackModel;
class ImagePackListModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(bool containsAccountPack READ containsAccountPack CONSTANT)
public:
enum Roles
{
@ -29,6 +30,9 @@ public:
QVariant data(const QModelIndex &index, int role) const override;
Q_INVOKABLE SingleImagePackModel *packAt(int row);
Q_INVOKABLE SingleImagePackModel *newPack(bool inRoom);
bool containsAccountPack() const;
private:
std::string room_id;

View file

@ -22,7 +22,14 @@ QHash<QString, mtx::crypto::EncryptedFile> infos;
QQuickImageResponse *
MxcImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
{
MxcImageResponse *response = new MxcImageResponse(id, requestedSize);
auto id_ = id;
bool crop = true;
if (id.endsWith("?scale")) {
crop = false;
id_.remove("?scale");
}
MxcImageResponse *response = new MxcImageResponse(id_, crop, requestedSize);
pool.start(response);
return response;
}
@ -36,20 +43,24 @@ void
MxcImageResponse::run()
{
MxcImageProvider::download(
m_id, m_requestedSize, [this](QString, QSize, QImage image, QString) {
m_id,
m_requestedSize,
[this](QString, QSize, QImage image, QString) {
if (image.isNull()) {
m_error = "Failed to download image.";
} else {
m_image = image;
}
emit finished();
});
},
m_crop);
}
void
MxcImageProvider::download(const QString &id,
const QSize &requestedSize,
std::function<void(QString, QSize, QImage, QString)> then)
std::function<void(QString, QSize, QImage, QString)> then,
bool crop)
{
std::optional<mtx::crypto::EncryptedFile> encryptionInfo;
auto temp = infos.find("mxc://" + id);
@ -58,11 +69,12 @@ MxcImageProvider::download(const QString &id,
if (requestedSize.isValid() && !encryptionInfo) {
QString fileName =
QString("%1_%2x%3_crop")
QString("%1_%2x%3_%4")
.arg(QString::fromUtf8(id.toUtf8().toBase64(QByteArray::Base64UrlEncoding |
QByteArray::OmitTrailingEquals)))
.arg(requestedSize.width())
.arg(requestedSize.height());
.arg(requestedSize.height())
.arg(crop ? "crop" : "scale");
QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
"/media_cache",
fileName);
@ -85,7 +97,7 @@ MxcImageProvider::download(const QString &id,
opts.mxc_url = "mxc://" + id.toStdString();
opts.width = requestedSize.width() > 0 ? requestedSize.width() : -1;
opts.height = requestedSize.height() > 0 ? requestedSize.height() : -1;
opts.method = "crop";
opts.method = crop ? "crop" : "scale";
http::client()->get_thumbnail(
opts,
[fileInfo, requestedSize, then, id](const std::string &res,

View file

@ -19,9 +19,10 @@ class MxcImageResponse
, public QRunnable
{
public:
MxcImageResponse(const QString &id, const QSize &requestedSize)
MxcImageResponse(const QString &id, bool crop, const QSize &requestedSize)
: m_id(id)
, m_requestedSize(requestedSize)
, m_crop(crop)
{
setAutoDelete(false);
}
@ -37,6 +38,7 @@ public:
QString m_id, m_error;
QSize m_requestedSize;
QImage m_image;
bool m_crop;
};
class MxcImageProvider
@ -51,7 +53,8 @@ public slots:
static void addEncryptionInfo(mtx::crypto::EncryptedFile info);
static void download(const QString &id,
const QSize &requestedSize,
std::function<void(QString, QSize, QImage, QString)> then);
std::function<void(QString, QSize, QImage, QString)> then,
bool crop = true);
private:
QThreadPool pool;

View file

@ -4,20 +4,35 @@
#include "SingleImagePackModel.h"
#include <QFile>
#include <QMimeDatabase>
#include "Cache_p.h"
#include "ChatPage.h"
#include "Logging.h"
#include "MatrixClient.h"
#include "Utils.h"
#include "timeline/Permissions.h"
#include "timeline/TimelineModel.h"
Q_DECLARE_METATYPE(mtx::common::ImageInfo)
SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent)
: QAbstractListModel(parent)
, roomid_(std::move(pack_.source_room))
, statekey_(std::move(pack_.state_key))
, old_statekey_(statekey_)
, pack(std::move(pack_.pack))
{
[[maybe_unused]] static auto imageInfoType = qRegisterMetaType<mtx::common::ImageInfo>();
if (!pack.pack)
pack.pack = mtx::events::msc2545::ImagePack::PackDescription{};
for (const auto &e : pack.images)
shortcodes.push_back(e.first);
connect(this, &SingleImagePackModel::addImage, this, &SingleImagePackModel::addImageCb);
}
int
@ -61,6 +76,73 @@ SingleImagePackModel::data(const QModelIndex &index, int role) const
return {};
}
bool
SingleImagePackModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
using mtx::events::msc2545::PackUsage;
if (hasIndex(index.row(), index.column(), index.parent())) {
auto &img = pack.images.at(shortcodes.at(index.row()));
switch (role) {
case ShortCode: {
auto newCode = value.toString().toStdString();
// otherwise we delete this by accident
if (pack.images.count(newCode))
return false;
auto tmp = img;
auto oldCode = shortcodes.at(index.row());
pack.images.erase(oldCode);
shortcodes[index.row()] = newCode;
pack.images.insert({newCode, tmp});
emit dataChanged(
this->index(index.row()), this->index(index.row()), {Roles::ShortCode});
return true;
}
case Body:
img.body = value.toString().toStdString();
emit dataChanged(
this->index(index.row()), this->index(index.row()), {Roles::Body});
return true;
case IsEmote: {
bool isEmote = value.toBool();
bool isSticker =
img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker();
img.usage.set(PackUsage::Emoji, isEmote);
img.usage.set(PackUsage::Sticker, isSticker);
if (img.usage == pack.pack->usage)
img.usage.reset();
emit dataChanged(
this->index(index.row()), this->index(index.row()), {Roles::IsEmote});
return true;
}
case IsSticker: {
bool isEmote =
img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji();
bool isSticker = value.toBool();
img.usage.set(PackUsage::Emoji, isEmote);
img.usage.set(PackUsage::Sticker, isSticker);
if (img.usage == pack.pack->usage)
img.usage.reset();
emit dataChanged(
this->index(index.row()), this->index(index.row()), {Roles::IsSticker});
return true;
}
}
}
return false;
}
bool
SingleImagePackModel::isGloballyEnabled() const
{
@ -98,3 +180,171 @@ SingleImagePackModel::setGloballyEnabled(bool enabled)
// emit this->globallyEnabledChanged();
});
}
bool
SingleImagePackModel::canEdit() const
{
if (roomid_.empty())
return true;
else
return Permissions(QString::fromStdString(roomid_))
.canChange(qml_mtx_events::ImagePackInRoom);
}
void
SingleImagePackModel::setPackname(QString val)
{
auto val_ = val.toStdString();
if (val_ != this->pack.pack->display_name) {
this->pack.pack->display_name = val_;
emit packnameChanged();
}
}
void
SingleImagePackModel::setAttribution(QString val)
{
auto val_ = val.toStdString();
if (val_ != this->pack.pack->attribution) {
this->pack.pack->attribution = val_;
emit attributionChanged();
}
}
void
SingleImagePackModel::setAvatarUrl(QString val)
{
auto val_ = val.toStdString();
if (val_ != this->pack.pack->avatar_url) {
this->pack.pack->avatar_url = val_;
emit avatarUrlChanged();
}
}
void
SingleImagePackModel::setStatekey(QString val)
{
auto val_ = val.toStdString();
if (val_ != statekey_) {
statekey_ = val_;
emit statekeyChanged();
}
}
void
SingleImagePackModel::setIsStickerPack(bool val)
{
using mtx::events::msc2545::PackUsage;
if (val != pack.pack->is_sticker()) {
pack.pack->usage.set(PackUsage::Sticker, val);
emit isStickerPackChanged();
}
}
void
SingleImagePackModel::setIsEmotePack(bool val)
{
using mtx::events::msc2545::PackUsage;
if (val != pack.pack->is_emoji()) {
pack.pack->usage.set(PackUsage::Emoji, val);
emit isEmotePackChanged();
}
}
void
SingleImagePackModel::save()
{
if (roomid_.empty()) {
http::client()->put_account_data(pack, [](mtx::http::RequestErr e) {
if (e)
ChatPage::instance()->showNotification(
tr("Failed to update image pack: {}")
.arg(QString::fromStdString(e->matrix_error.error)));
});
} else {
if (old_statekey_ != statekey_) {
http::client()->send_state_event(
roomid_,
to_string(mtx::events::EventType::ImagePackInRoom),
old_statekey_,
nlohmann::json::object(),
[](const mtx::responses::EventId &, mtx::http::RequestErr e) {
if (e)
ChatPage::instance()->showNotification(
tr("Failed to delete old image pack: {}")
.arg(QString::fromStdString(e->matrix_error.error)));
});
}
http::client()->send_state_event(
roomid_,
statekey_,
pack,
[this](const mtx::responses::EventId &, mtx::http::RequestErr e) {
if (e)
ChatPage::instance()->showNotification(
tr("Failed to update image pack: {}")
.arg(QString::fromStdString(e->matrix_error.error)));
nhlog::net()->info("Uploaded image pack: {}", statekey_);
});
}
}
void
SingleImagePackModel::addStickers(QList<QUrl> files)
{
for (const auto &f : files) {
auto file = QFile(f.toLocalFile());
if (!file.open(QFile::ReadOnly)) {
ChatPage::instance()->showNotification(
tr("Failed to open image: {}").arg(f.toLocalFile()));
return;
}
auto bytes = file.readAll();
auto img = utils::readImage(bytes);
mtx::common::ImageInfo info{};
auto sz = img.size() / 2;
if (sz.width() > 512 || sz.height() > 512) {
sz.scale(512, 512, Qt::AspectRatioMode::KeepAspectRatio);
}
info.h = sz.height();
info.w = sz.width();
info.size = bytes.size();
auto filename = f.fileName().toStdString();
http::client()->upload(
bytes.toStdString(),
QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString(),
filename,
[this, filename, info](const mtx::responses::ContentURI &uri,
mtx::http::RequestErr e) {
if (e) {
ChatPage::instance()->showNotification(
tr("Failed to upload image: {}")
.arg(QString::fromStdString(e->matrix_error.error)));
return;
}
emit addImage(uri.content_uri, filename, info);
});
}
}
void
SingleImagePackModel::addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info)
{
mtx::events::msc2545::PackImage img{};
img.url = uri;
img.info = info;
beginInsertRows(
QModelIndex(), static_cast<int>(shortcodes.size()), static_cast<int>(shortcodes.size()));
pack.images[filename] = img;
shortcodes.push_back(filename);
endInsertRows();
}

View file

@ -5,6 +5,8 @@
#pragma once
#include <QAbstractListModel>
#include <QList>
#include <QUrl>
#include <mtx/events/mscs/image_packs.hpp>
@ -15,14 +17,18 @@ class SingleImagePackModel : public QAbstractListModel
Q_OBJECT
Q_PROPERTY(QString roomid READ roomid CONSTANT)
Q_PROPERTY(QString statekey READ statekey CONSTANT)
Q_PROPERTY(QString attribution READ statekey CONSTANT)
Q_PROPERTY(QString packname READ packname CONSTANT)
Q_PROPERTY(QString avatarUrl READ avatarUrl CONSTANT)
Q_PROPERTY(bool isStickerPack READ isStickerPack CONSTANT)
Q_PROPERTY(bool isEmotePack READ isEmotePack CONSTANT)
Q_PROPERTY(QString statekey READ statekey WRITE setStatekey NOTIFY statekeyChanged)
Q_PROPERTY(
QString attribution READ attribution WRITE setAttribution NOTIFY attributionChanged)
Q_PROPERTY(QString packname READ packname WRITE setPackname NOTIFY packnameChanged)
Q_PROPERTY(QString avatarUrl READ avatarUrl WRITE setAvatarUrl NOTIFY avatarUrlChanged)
Q_PROPERTY(
bool isStickerPack READ isStickerPack WRITE setIsStickerPack NOTIFY isStickerPackChanged)
Q_PROPERTY(bool isEmotePack READ isEmotePack WRITE setIsEmotePack NOTIFY isEmotePackChanged)
Q_PROPERTY(bool isGloballyEnabled READ isGloballyEnabled WRITE setGloballyEnabled NOTIFY
globallyEnabledChanged)
Q_PROPERTY(bool canEdit READ canEdit CONSTANT)
public:
enum Roles
{
@ -32,11 +38,15 @@ public:
IsEmote,
IsSticker,
};
Q_ENUM(Roles);
SingleImagePackModel(ImagePackInfo pack_, QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
bool setData(const QModelIndex &index,
const QVariant &value,
int role = Qt::EditRole) override;
QString roomid() const { return QString::fromStdString(roomid_); }
QString statekey() const { return QString::fromStdString(statekey_); }
@ -47,14 +57,36 @@ public:
bool isEmotePack() const { return pack.pack->is_emoji(); }
bool isGloballyEnabled() const;
bool canEdit() const;
void setGloballyEnabled(bool enabled);
void setPackname(QString val);
void setAttribution(QString val);
void setAvatarUrl(QString val);
void setStatekey(QString val);
void setIsStickerPack(bool val);
void setIsEmotePack(bool val);
Q_INVOKABLE void save();
Q_INVOKABLE void addStickers(QList<QUrl> files);
signals:
void globallyEnabledChanged();
void statekeyChanged();
void attributionChanged();
void packnameChanged();
void avatarUrlChanged();
void isEmotePackChanged();
void isStickerPackChanged();
void addImage(std::string uri, std::string filename, mtx::common::ImageInfo info);
private slots:
void addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info);
private:
std::string roomid_;
std::string statekey_;
std::string statekey_, old_statekey_;
mtx::events::msc2545::ImagePack pack;
std::vector<std::string> shortcodes;

View file

@ -308,6 +308,15 @@ qml_mtx_events::fromRoomEventType(qml_mtx_events::EventType t)
case qml_mtx_events::KeyVerificationDone:
case qml_mtx_events::KeyVerificationReady:
return mtx::events::EventType::RoomMessage;
//! m.image_pack, currently im.ponies.room_emotes
case qml_mtx_events::ImagePackInRoom:
return mtx::events::EventType::ImagePackRooms;
//! m.image_pack, currently im.ponies.user_emotes
case qml_mtx_events::ImagePackInAccountData:
return mtx::events::EventType::ImagePackInAccountData;
//! m.image_pack.rooms, currently im.ponies.emote_rooms
case qml_mtx_events::ImagePackRooms:
return mtx::events::EventType::ImagePackRooms;
default:
return mtx::events::EventType::Unsupported;
};

View file

@ -107,7 +107,13 @@ enum EventType
KeyVerificationCancel,
KeyVerificationKey,
KeyVerificationDone,
KeyVerificationReady
KeyVerificationReady,
//! m.image_pack, currently im.ponies.room_emotes
ImagePackInRoom,
//! m.image_pack, currently im.ponies.user_emotes
ImagePackInAccountData,
//! m.image_pack.rooms, currently im.ponies.emote_rooms
ImagePackRooms,
};
Q_ENUM_NS(EventType)
mtx::events::EventType fromRoomEventType(qml_mtx_events::EventType);