Basic sticker pack editor

This commit is contained in:
Nicolas Werner 2021-08-06 01:45:47 +02:00
parent 6d83b7c675
commit a57a15a2e0
No known key found for this signature in database
GPG key ID: C8D75E610773F2D9
17 changed files with 751 additions and 167 deletions

View file

@ -381,7 +381,7 @@ if(USE_BUNDLED_MTXCLIENT)
FetchContent_Declare( FetchContent_Declare(
MatrixClient MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git 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_EXAMPLES OFF CACHE INTERNAL "")
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")

View file

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

View file

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

View file

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

View file

@ -30,6 +30,10 @@ MouseArea {
property alias enabled: root.enabled property alias enabled: root.enabled
function calculateNewPosition(flickableItem, wheel) { function calculateNewPosition(flickableItem, wheel) {
// breaks ListView's with headers...
//if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem)
// minYExtent += flickableItem.headerItem.height;
//Nothing to scroll //Nothing to scroll
if (flickableItem.contentHeight < flickableItem.height) if (flickableItem.contentHeight < flickableItem.height)
return flickableItem.contentY; return flickableItem.contentY;
@ -55,9 +59,6 @@ MouseArea {
var minYExtent = flickableItem.originY + flickableItem.topMargin; var minYExtent = flickableItem.originY + flickableItem.topMargin;
var maxYExtent = (flickableItem.contentHeight + flickableItem.bottomMargin + flickableItem.originY) - flickableItem.height; var maxYExtent = (flickableItem.contentHeight + flickableItem.bottomMargin + flickableItem.originY) - flickableItem.height;
if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem)
minYExtent += flickableItem.headerItem.height;
//Avoid overscrolling //Avoid overscrolling
return Math.max(minYExtent, Math.min(maxYExtent, flickableItem.contentY - pixelDelta)); 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,283 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import ".."
import "../components"
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
}
}
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
onToggled: imagePack.isEmotePack = checked
Layout.alignment: Qt.AlignRight
}
MatrixText {
text: qsTr("Use as Sticker")
}
ToggleButton {
checked: imagePack.isStickerPack
onToggled: 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)
onToggled: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.IsEmote)
Layout.alignment: Qt.AlignRight
}
MatrixText {
text: qsTr("Use as Sticker")
}
ToggleButton {
checked: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.IsSticker)
onToggled: imagePack.setData(imagePack.index(currentImageIndex, 0), text, 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 readonly property int stickerDimPad: 128 + Nheko.paddingSmall
title: qsTr("Image pack settings") title: qsTr("Image pack settings")
height: 400 height: 600
width: 600 width: 800
palette: Nheko.colors palette: Nheko.colors
color: Nheko.colors.base color: Nheko.colors.base
modality: Qt.NonModal modality: Qt.NonModal
flags: Qt.Dialog | Qt.WindowCloseButtonHint flags: Qt.Dialog | Qt.WindowCloseButtonHint
Component.onCompleted: Nheko.reparent(win) Component.onCompleted: Nheko.reparent(win)
Component {
id: packEditor
ImagePackEditorDialog {
}
}
AdaptiveLayout { AdaptiveLayout {
id: adaptiveView id: adaptiveView
@ -54,7 +62,7 @@ ApplicationWindow {
enabled: !Settings.mobileMode enabled: !Settings.mobileMode
} }
delegate: Rectangle { delegate: AvatarListTile {
id: packItem id: packItem
property color background: Nheko.colors.window property color background: Nheko.colors.window
@ -63,131 +71,24 @@ ApplicationWindow {
property color bubbleBackground: Nheko.colors.highlight property color bubbleBackground: Nheko.colors.highlight
property color bubbleText: Nheko.colors.highlightedText property color bubbleText: Nheko.colors.highlightedText
required property string displayName required property string displayName
required property string avatarUrl
required property bool fromAccountData required property bool fromAccountData
required property bool fromCurrentRoom required property bool fromCurrentRoom
required property int index
color: background title: displayName
height: avatarSize + 2 * Nheko.paddingMedium subtitle: {
width: ListView.view.width if (fromAccountData)
state: "normal" return qsTr("Private pack");
states: [ else if (fromCurrentRoom)
State { return qsTr("Pack from this room");
name: "highlight" else
when: hovered.hovered && !(index == currentPackIndex) return qsTr("Globally enabled pack");
}
PropertyChanges { selectedIndex: currentPackIndex
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
}
}
]
TapHandler { TapHandler {
margin: -Nheko.paddingSmall
onSingleTapped: currentPackIndex = index 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 +102,10 @@ ApplicationWindow {
color: Nheko.colors.window color: Nheko.colors.window
ColumnLayout { ColumnLayout {
//Button {
// Layout.alignment: Qt.AlignHCenter
// text: qsTr("Edit")
// enabled: currentPack.canEdit
//}
id: packinfo id: packinfo
property string packName: currentPack ? currentPack.packname : "" property string packName: currentPack ? currentPack.packname : ""
property string attribution: currentPack ? currentPack.attribution : ""
property string avatarUrl: currentPack ? currentPack.avatarUrl : "" property string avatarUrl: currentPack ? currentPack.avatarUrl : ""
anchors.fill: parent anchors.fill: parent
@ -227,8 +123,18 @@ ApplicationWindow {
MatrixText { MatrixText {
text: packinfo.packName text: packinfo.packName
font.pixelSize: 24 font.pixelSize: Math.ceil(fontMetrics.pixelSize * 1.1)
horizontalAlignment: TextEdit.AlignHCenter
Layout.alignment: Qt.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 { GridLayout {
@ -250,6 +156,18 @@ ApplicationWindow {
} }
Button {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Edit")
enabled: currentPack.canEdit
onClicked: {
var dialog = packEditor.createObject(timelineRoot, {
"imagePack": currentPack
});
dialog.show();
}
}
GridView { GridView {
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
@ -272,7 +190,7 @@ ApplicationWindow {
width: stickerDim width: stickerDim
height: stickerDim height: stickerDim
hoverEnabled: true hoverEnabled: true
ToolTip.text: ":" + model.shortcode + ": - " + model.body ToolTip.text: ":" + model.shortCode + ": - " + model.body
ToolTip.visible: hovered ToolTip.visible: hovered
contentItem: Image { contentItem: Image {

View file

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

View file

@ -125,7 +125,7 @@ template<class T>
bool bool
containsStateUpdates(const T &e) 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 bool

View file

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

View file

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

View file

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

View file

@ -5,12 +5,18 @@
#include "SingleImagePackModel.h" #include "SingleImagePackModel.h"
#include "Cache_p.h" #include "Cache_p.h"
#include "ChatPage.h"
#include "MatrixClient.h" #include "MatrixClient.h"
#include "timeline/Permissions.h"
#include "timeline/TimelineModel.h"
#include "Logging.h"
SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent) SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
, roomid_(std::move(pack_.source_room)) , roomid_(std::move(pack_.source_room))
, statekey_(std::move(pack_.state_key)) , statekey_(std::move(pack_.state_key))
, old_statekey_(statekey_)
, pack(std::move(pack_.pack)) , pack(std::move(pack_.pack))
{ {
if (!pack.pack) if (!pack.pack)
@ -61,6 +67,73 @@ SingleImagePackModel::data(const QModelIndex &index, int role) const
return {}; 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 bool
SingleImagePackModel::isGloballyEnabled() const SingleImagePackModel::isGloballyEnabled() const
{ {
@ -98,3 +171,111 @@ SingleImagePackModel::setGloballyEnabled(bool enabled)
// emit this->globallyEnabledChanged(); // 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, [this](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(),
[this](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)));
});
}
}

View file

@ -15,14 +15,18 @@ class SingleImagePackModel : public QAbstractListModel
Q_OBJECT Q_OBJECT
Q_PROPERTY(QString roomid READ roomid CONSTANT) Q_PROPERTY(QString roomid READ roomid CONSTANT)
Q_PROPERTY(QString statekey READ statekey CONSTANT) Q_PROPERTY(QString statekey READ statekey WRITE setStatekey NOTIFY statekeyChanged)
Q_PROPERTY(QString attribution READ statekey CONSTANT) Q_PROPERTY(
Q_PROPERTY(QString packname READ packname CONSTANT) QString attribution READ attribution WRITE setAttribution NOTIFY attributionChanged)
Q_PROPERTY(QString avatarUrl READ avatarUrl CONSTANT) Q_PROPERTY(QString packname READ packname WRITE setPackname NOTIFY packnameChanged)
Q_PROPERTY(bool isStickerPack READ isStickerPack CONSTANT) Q_PROPERTY(QString avatarUrl READ avatarUrl WRITE setAvatarUrl NOTIFY avatarUrlChanged)
Q_PROPERTY(bool isEmotePack READ isEmotePack CONSTANT) 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 Q_PROPERTY(bool isGloballyEnabled READ isGloballyEnabled WRITE setGloballyEnabled NOTIFY
globallyEnabledChanged) globallyEnabledChanged)
Q_PROPERTY(bool canEdit READ canEdit CONSTANT)
public: public:
enum Roles enum Roles
{ {
@ -32,11 +36,15 @@ public:
IsEmote, IsEmote,
IsSticker, IsSticker,
}; };
Q_ENUM(Roles);
SingleImagePackModel(ImagePackInfo pack_, QObject *parent = nullptr); SingleImagePackModel(ImagePackInfo pack_, QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) 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 roomid() const { return QString::fromStdString(roomid_); }
QString statekey() const { return QString::fromStdString(statekey_); } QString statekey() const { return QString::fromStdString(statekey_); }
@ -47,14 +55,30 @@ public:
bool isEmotePack() const { return pack.pack->is_emoji(); } bool isEmotePack() const { return pack.pack->is_emoji(); }
bool isGloballyEnabled() const; bool isGloballyEnabled() const;
bool canEdit() const;
void setGloballyEnabled(bool enabled); 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();
signals: signals:
void globallyEnabledChanged(); void globallyEnabledChanged();
void statekeyChanged();
void attributionChanged();
void packnameChanged();
void avatarUrlChanged();
void isEmotePackChanged();
void isStickerPackChanged();
private: private:
std::string roomid_; std::string roomid_;
std::string statekey_; std::string statekey_, old_statekey_;
mtx::events::msc2545::ImagePack pack; mtx::events::msc2545::ImagePack pack;
std::vector<std::string> shortcodes; 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::KeyVerificationDone:
case qml_mtx_events::KeyVerificationReady: case qml_mtx_events::KeyVerificationReady:
return mtx::events::EventType::RoomMessage; 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: default:
return mtx::events::EventType::Unsupported; return mtx::events::EventType::Unsupported;
}; };

View file

@ -107,7 +107,13 @@ enum EventType
KeyVerificationCancel, KeyVerificationCancel,
KeyVerificationKey, KeyVerificationKey,
KeyVerificationDone, 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) Q_ENUM_NS(EventType)
mtx::events::EventType fromRoomEventType(qml_mtx_events::EventType); mtx::events::EventType fromRoomEventType(qml_mtx_events::EventType);