diff --git a/.kde-ci.yml b/.kde-ci.yml index 73e054458..24f31c202 100644 --- a/.kde-ci.yml +++ b/.kde-ci.yml @@ -59,4 +59,4 @@ Dependencies: 'frameworks/kdbusaddons': '@latest-kf6' Options: - require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD', 'Windows/Qt5' ] + require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD' ] diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6a9b46680..87620174a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -15,6 +15,7 @@ add_library(neochat STATIC models/messagefiltermodel.cpp models/roomlistmodel.cpp models/sortfilterspacelistmodel.cpp + models/accountstickermodel.cpp spacehierarchycache.cpp roommanager.cpp neochatroom.cpp @@ -24,6 +25,7 @@ add_library(neochat STATIC models/publicroomlistmodel.cpp models/userdirectorylistmodel.cpp models/keywordnotificationrulemodel.cpp + models/emoticonfiltermodel.cpp notificationsmanager.cpp models/sortfilterroomlistmodel.cpp chatdocumenthandler.cpp diff --git a/src/events/imagepackevent.cpp b/src/events/imagepackevent.cpp index 609b6e8df..e95f81c90 100644 --- a/src/events/imagepackevent.cpp +++ b/src/events/imagepackevent.cpp @@ -51,5 +51,42 @@ ImagePackEventContent::ImagePackEventContent(const QJsonObject &json) void ImagePackEventContent::fillJson(QJsonObject *o) const { - // TODO + if (pack) { + QJsonObject packJson; + if (pack->displayName) { + packJson["display_name"] = *pack->displayName; + } + if (pack->usage) { + QJsonArray usageJson; + for (const auto &usage : *pack->usage) { + usageJson += usage; + } + packJson["usage"] = usageJson; + } + if (pack->avatarUrl) { + packJson["avatar_url"] = pack->avatarUrl->toString(); + } + if (pack->attribution) { + packJson["attribution"] = *pack->attribution; + } + (*o)["pack"_ls] = packJson; + } + + QJsonObject imagesJson; + for (const auto &image : images) { + QJsonObject imageJson; + imageJson["url"] = image.url.toString(); + if (image.body) { + imageJson["body"] = *image.body; + } + if (image.usage) { + QJsonArray usageJson; + for (const auto &usage : *image.usage) { + usageJson += usage; + } + imageJson["usage"] = usageJson; + } + imagesJson[image.shortcode] = imageJson; + } + (*o)["images"_ls] = imagesJson; } diff --git a/src/main.cpp b/src/main.cpp index c8b6a0dfc..358a83642 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -48,10 +48,12 @@ #include "logger.h" #include "login.h" #include "matriximageprovider.h" +#include "models/accountstickermodel.h" #include "models/collapsestateproxymodel.h" #include "models/customemojimodel.h" #include "models/devicesmodel.h" #include "models/emojimodel.h" +#include "models/emoticonfiltermodel.h" #include "models/imagepacksmodel.h" #include "models/keywordnotificationrulemodel.h" #include "models/messageeventmodel.h" @@ -248,6 +250,8 @@ int main(int argc, char *argv[]) qmlRegisterType("org.kde.neochat", 1, 0, "KeywordNotificationRuleModel"); qmlRegisterType("org.kde.neochat", 1, 0, "StickerModel"); qmlRegisterType("org.kde.neochat", 1, 0, "ImagePacksModel"); + qmlRegisterType("org.kde.neochat", 1, 0, "AccountStickerModel"); + qmlRegisterType("org.kde.neochat", 1, 0, "EmoticonFilterModel"); qmlRegisterUncreatableType("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM"); qmlRegisterUncreatableType("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM"); qmlRegisterUncreatableType("org.kde.neochat", 1, 0, "PushNotificationAction", "ENUM"); diff --git a/src/models/accountstickermodel.cpp b/src/models/accountstickermodel.cpp new file mode 100644 index 000000000..f97b9f9f4 --- /dev/null +++ b/src/models/accountstickermodel.cpp @@ -0,0 +1,178 @@ +// SPDX-FileCopyrightText: 2021-2023 Tobias Fella +// SPDX-License-Identifier: LGPL-2.0-or-later + +#include "accountstickermodel.h" + +#include +#include + +using namespace Quotient; + +AccountStickerModel::AccountStickerModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +int AccountStickerModel::rowCount(const QModelIndex &index) const +{ + Q_UNUSED(index); + if (!m_images) { + return 0; + } + return m_images->images.size(); +} + +QVariant AccountStickerModel::data(const QModelIndex &index, int role) const +{ + const auto &row = index.row(); + const auto &image = m_images->images[row]; + if (role == UrlRole) { +#ifdef QUOTIENT_07 + return m_connection->makeMediaUrl(image.url); +#else + return QUrl(); +#endif + } + if (role == BodyRole) { + if (image.body) { + return *image.body; + } + } + if (role == ShortCodeRole) { + return image.shortcode; + } + if (role == IsStickerRole) { + if (image.usage) { + return image.usage->isEmpty() || image.usage->contains("sticker"_ls); + } + if (m_images->pack && m_images->pack->usage) { + return m_images->pack->usage->isEmpty() || m_images->pack->usage->contains("sticker"_ls); + } + return true; + } + if (role == IsEmojiRole) { + if (image.usage) { + return image.usage->isEmpty() || image.usage->contains("emoticon"_ls); + } + if (m_images->pack && m_images->pack->usage) { + return m_images->pack->usage->isEmpty() || m_images->pack->usage->contains("emoticon"_ls); + } + return true; + } + return {}; +} + +QHash AccountStickerModel::roleNames() const +{ + return { + {AccountStickerModel::UrlRole, "url"}, + {AccountStickerModel::BodyRole, "body"}, + {AccountStickerModel::ShortCodeRole, "shortcode"}, + {AccountStickerModel::IsStickerRole, "isSticker"}, + {AccountStickerModel::IsEmojiRole, "isEmoji"}, + }; +} + +Connection *AccountStickerModel::connection() const +{ + return m_connection; +} + +void AccountStickerModel::setConnection(Connection *connection) +{ + if (m_connection) { + disconnect(m_connection, nullptr, this, nullptr); + } + + m_connection = connection; + Q_EMIT connectionChanged(); + connect(m_connection, &Connection::accountDataChanged, this, [this](QString type) { + if (type == QStringLiteral("im.ponies.user_emotes")) { + reloadStickers(); + } + }); + reloadStickers(); +} + +void AccountStickerModel::reloadStickers() +{ + if (!m_connection->hasAccountData("im.ponies.user_emotes"_ls)) { + return; + } + auto json = m_connection->accountData("im.ponies.user_emotes"_ls)->contentJson(); + const auto &content = ImagePackEventContent(json); + beginResetModel(); + m_images = content; + endResetModel(); +} + +void AccountStickerModel::deleteSticker(int index) +{ + QJsonObject data; + m_images->images.removeAt(index); + m_images->fillJson(&data); + m_connection->setAccountData("im.ponies.user_emotes"_ls, data); +} + +void AccountStickerModel::setStickerBody(int index, const QString &text) +{ + m_images->images[index].body = text; + QJsonObject data; + m_images->fillJson(&data); + m_connection->setAccountData("im.ponies.user_emotes"_ls, data); +} + +void AccountStickerModel::setStickerShortcode(int index, const QString &shortcode) +{ + m_images->images[index].shortcode = shortcode; + QJsonObject data; + m_images->fillJson(&data); + m_connection->setAccountData("im.ponies.user_emotes"_ls, data); +} + +void AccountStickerModel::setStickerImage(int index, const QUrl &source) +{ + doSetStickerImage(index, source); +} + +QCoro::Task AccountStickerModel::doSetStickerImage(int index, QUrl source) +{ + auto job = m_connection->uploadFile(source.isLocalFile() ? source.toLocalFile() : source.toString()); + co_await qCoro(job, &BaseJob::finished); + if (job->error() != BaseJob::NoError) { + co_return; + } +#ifdef QUOTIENT_07 + m_images->images[index].url = job->contentUri().toString(); +#else + m_images->images[index].url = job->contentUri(); +#endif + m_images->images[index].info = none; + QJsonObject data; + m_images->fillJson(&data); + m_connection->setAccountData("im.ponies.user_emotes"_ls, data); +} + +QCoro::Task AccountStickerModel::doAddSticker(QUrl source, QString shortcode, QString description) +{ + auto job = m_connection->uploadFile(source.isLocalFile() ? source.toLocalFile() : source.toString()); + co_await qCoro(job, &BaseJob::finished); + if (job->error() != BaseJob::NoError) { + co_return; + } + m_images->images.append(ImagePackEventContent::ImagePackImage{ + shortcode, + job->contentUri(), + description, + none, + QStringList{"sticker"_ls}, + }); + QJsonObject data; + m_images->fillJson(&data); + m_connection->setAccountData("im.ponies.user_emotes"_ls, data); +} + +void AccountStickerModel::addSticker(const QUrl &source, const QString &shortcode, const QString &description) +{ + doAddSticker(source, shortcode, description); +} diff --git a/src/models/accountstickermodel.h b/src/models/accountstickermodel.h new file mode 100644 index 000000000..57273c5c6 --- /dev/null +++ b/src/models/accountstickermodel.h @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// SPDX-License-Identifier: LGPL-2.0-or-later + +#pragma once + +#include "events/imagepackevent.h" +#include +#include +#include +#include +#include +#include + +class ImagePacksModel; + +/** + * @class AccountStickerModel + * + * This class defines the model for visualising the account stickers. + * + * This is based upon the im.ponies.user_emotes spec (MSC2545). + */ +class AccountStickerModel : public QAbstractListModel +{ + Q_OBJECT + /** + * @brief The connection to get stickers from. + */ + Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged) + +public: + enum Roles { + UrlRole = Qt::UserRole + 1, /**< The URL for the sticker. */ + ShortCodeRole, /**< The shortcode for the sticker. */ + BodyRole, //**< A textual description of the sticker */ + IsStickerRole, //**< Whether this emoticon is a sticker */ + IsEmojiRole, //**< Whether this emoticon is an emoji */ + }; + + explicit AccountStickerModel(QObject *parent = nullptr); + + /** + * @brief Number of rows in the model. + * + * @sa QAbstractItemModel::rowCount + */ + [[nodiscard]] int rowCount(const QModelIndex &index) const override; + + /** + * @brief Get the given role value at the given index. + * + * @sa QAbstractItemModel::data + */ + [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; + + /** + * @brief Returns a mapping from Role enum values to role names. + * + * @sa Roles, QAbstractItemModel::roleNames() + */ + [[nodiscard]] QHash roleNames() const override; + + [[nodiscard]] Quotient::Connection *connection() const; + void setConnection(Quotient::Connection *connection); + + /** + * @brief Deletes the sticker at the given index. + */ + Q_INVOKABLE void deleteSticker(int index); + + /** + * @brief Changes the description for the sticker at the given index. + */ + Q_INVOKABLE void setStickerBody(int index, const QString &text); + + /** + * @brief Changes the shortcode for the sticker at the given index. + */ + Q_INVOKABLE void setStickerShortcode(int index, const QString &shortCode); + + /** + * @brief Changes the image for the sticker at the given index. + */ + Q_INVOKABLE void setStickerImage(int index, const QUrl &source); + + /** + * @brief Adds a sticker with the given parameters. + */ + Q_INVOKABLE void addSticker(const QUrl &source, const QString &shortcode, const QString &description); + +Q_SIGNALS: + void connectionChanged(); + +private: + std::optional m_images; + QPointer m_connection; + QCoro::Task doSetStickerImage(int index, QUrl source); + QCoro::Task doAddSticker(QUrl source, QString shortcode, QString description); + + void reloadStickers(); +}; diff --git a/src/models/customemojimodel.cpp b/src/models/customemojimodel.cpp index 51ef0a103..d0e56d708 100644 --- a/src/models/customemojimodel.cpp +++ b/src/models/customemojimodel.cpp @@ -86,6 +86,7 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location) {QStringLiteral("url"), url}, {QStringLiteral("info"), imageInfo}, {QStringLiteral("body"), location.fileName()}, + {"usage"_ls, "emoticon"_ls}, }); json["images"] = emojiData; diff --git a/src/models/emoticonfiltermodel.cpp b/src/models/emoticonfiltermodel.cpp new file mode 100644 index 000000000..89060183b --- /dev/null +++ b/src/models/emoticonfiltermodel.cpp @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "emoticonfiltermodel.h" + +#include "accountstickermodel.h" + +EmoticonFilterModel::EmoticonFilterModel(QObject *parent) + : QSortFilterProxyModel(parent) +{ +} + +bool EmoticonFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + auto stickerUsage = sourceModel()->data(sourceModel()->index(sourceRow, 0), AccountStickerModel::IsStickerRole).toBool(); + auto emojiUsage = sourceModel()->data(sourceModel()->index(sourceRow, 0), AccountStickerModel::IsEmojiRole).toBool(); + return (stickerUsage && m_showStickers) || (emojiUsage && m_showEmojis); +} + +bool EmoticonFilterModel::showStickers() const +{ + return m_showStickers; +} + +void EmoticonFilterModel::setShowStickers(bool showStickers) +{ + m_showStickers = showStickers; + Q_EMIT showStickersChanged(); +} + +bool EmoticonFilterModel::showEmojis() const +{ + return m_showEmojis; +} + +void EmoticonFilterModel::setShowEmojis(bool showEmojis) +{ + m_showEmojis = showEmojis; + Q_EMIT showEmojisChanged(); +} diff --git a/src/models/emoticonfiltermodel.h b/src/models/emoticonfiltermodel.h new file mode 100644 index 000000000..70c6bc4d2 --- /dev/null +++ b/src/models/emoticonfiltermodel.h @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +/** + * @class EmoticonFilterModel + * + * This class creates a custom QSortFilterProxyModel for filtering a emoticon by type + * (Sticker or Emoji). + */ +class EmoticonFilterModel : public QSortFilterProxyModel +{ + Q_OBJECT + + /** + * @brief Whether stickers should be shown + */ + Q_PROPERTY(bool showStickers READ showStickers WRITE setShowStickers NOTIFY showStickersChanged) + + /** + * @brief Whether emojis show be shown + */ + Q_PROPERTY(bool showEmojis READ showEmojis WRITE setShowEmojis NOTIFY showEmojisChanged) + +public: + explicit EmoticonFilterModel(QObject *parent = nullptr); + + /** + * @brief Custom filter function checking the type of emoticon + * + * @note The filter cannot be modified and will always use the same filter properties. + */ + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + + [[nodiscard]] bool showStickers() const; + void setShowStickers(bool showStickers); + + [[nodiscard]] bool showEmojis() const; + void setShowEmojis(bool showEmojis); + +Q_SIGNALS: + void showStickersChanged(); + void showEmojisChanged(); + +private: + bool m_showStickers = false; + bool m_showEmojis = false; +}; diff --git a/src/models/imagepacksmodel.cpp b/src/models/imagepacksmodel.cpp index 36ca1ac1c..be4a3077f 100644 --- a/src/models/imagepacksmodel.cpp +++ b/src/models/imagepacksmodel.cpp @@ -20,7 +20,11 @@ int ImagePacksModel::rowCount(const QModelIndex &index) const QVariant ImagePacksModel::data(const QModelIndex &index, int role) const { - const auto &event = m_events[index.row()]; + const auto row = index.row(); + if (row < 0 || row >= m_events.size()) { + return {}; + } + const auto &event = m_events[row]; if (role == DisplayNameRole) { if (event.pack->displayName) { return *event.pack->displayName; @@ -59,14 +63,24 @@ void ImagePacksModel::setRoom(NeoChatRoom *room) { if (m_room) { disconnect(m_room, nullptr, this, nullptr); + disconnect(m_room->connection(), nullptr, this, nullptr); } m_room = room; + connect(m_room->connection(), &Connection::accountDataChanged, this, [this](const QString &type) { + if (type == "im.ponies.user_emotes"_ls) { + reloadImages(); + } + }); + // TODO listen to packs changing + reloadImages(); + Q_EMIT roomChanged(); +} + +void ImagePacksModel::reloadImages() +{ beginResetModel(); m_events.clear(); - - // TODO listen to account data changing - // TODO listen to packs changing if (m_room->connection()->hasAccountData("im.ponies.user_emotes"_ls)) { auto json = m_room->connection()->accountData("im.ponies.user_emotes"_ls)->contentJson(); json["pack"] = QJsonObject{ @@ -110,8 +124,8 @@ void ImagePacksModel::setRoom(NeoChatRoom *room) } } #endif + Q_EMIT imagesLoaded(); endResetModel(); - Q_EMIT roomChanged(); } bool ImagePacksModel::showStickers() const diff --git a/src/models/imagepacksmodel.h b/src/models/imagepacksmodel.h index ce1394c76..993e0c382 100644 --- a/src/models/imagepacksmodel.h +++ b/src/models/imagepacksmodel.h @@ -90,10 +90,12 @@ Q_SIGNALS: void roomChanged(); void showStickersChanged(); void showEmoticonsChanged(); + void imagesLoaded(); private: QPointer m_room; QVector m_events; bool m_showStickers = true; bool m_showEmoticons = true; + void reloadImages(); }; diff --git a/src/models/stickermodel.cpp b/src/models/stickermodel.cpp index 88f5e5ca8..e82c586bb 100644 --- a/src/models/stickermodel.cpp +++ b/src/models/stickermodel.cpp @@ -51,14 +51,11 @@ void StickerModel::setModel(ImagePacksModel *model) disconnect(m_model, nullptr, this, nullptr); } connect(model, &ImagePacksModel::roomChanged, this, [this]() { - beginResetModel(); - m_images = m_model->images(m_index); - endResetModel(); + reloadImages(); }); - beginResetModel(); + connect(model, &ImagePacksModel::imagesLoaded, this, &StickerModel::reloadImages); m_model = model; - m_images = m_model->images(m_index); - endResetModel(); + reloadImages(); Q_EMIT modelChanged(); } @@ -68,13 +65,18 @@ int StickerModel::packIndex() const } void StickerModel::setPackIndex(int index) { - beginResetModel(); m_index = index; + Q_EMIT packIndexChanged(); + reloadImages(); +} + +void StickerModel::reloadImages() +{ + beginResetModel(); if (m_model) { m_images = m_model->images(m_index); } endResetModel(); - Q_EMIT packIndexChanged(); } NeoChatRoom *StickerModel::room() const @@ -84,6 +86,9 @@ NeoChatRoom *StickerModel::room() const void StickerModel::setRoom(NeoChatRoom *room) { + if (room) { + disconnect(room->connection(), nullptr, this, nullptr); + } m_room = room; Q_EMIT roomChanged(); } diff --git a/src/models/stickermodel.h b/src/models/stickermodel.h index dec10882f..f83ab3c27 100644 --- a/src/models/stickermodel.h +++ b/src/models/stickermodel.h @@ -98,4 +98,5 @@ private: int m_index = 0; QVector m_images; NeoChatRoom *m_room; + void reloadImages(); }; diff --git a/src/qml/Settings/SettingsPage.qml b/src/qml/Settings/SettingsPage.qml index a1be11f83..c7db59c83 100644 --- a/src/qml/Settings/SettingsPage.qml +++ b/src/qml/Settings/SettingsPage.qml @@ -38,6 +38,12 @@ Kirigami.CategorizedSettings { icon.name: "preferences-desktop-emoticons" page: Qt.resolvedUrl("Emoticons.qml") }, + Kirigami.SettingAction { + actionName: "stickers" + text: i18n("Stickers") + icon.name: "stickers" + page: Qt.resolvedUrl("StickersPage.qml") + }, Kirigami.SettingAction { actionName: "spellChecking" text: i18n("Spell Checking") diff --git a/src/qml/Settings/StickerEditorPage.qml b/src/qml/Settings/StickerEditorPage.qml new file mode 100644 index 000000000..38e165e59 --- /dev/null +++ b/src/qml/Settings/StickerEditorPage.qml @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.15 +import Qt.labs.platform 1.1 +import QtQuick.Window 2.15 + +import org.kde.kirigami 2.19 as Kirigami +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm + +import org.kde.neochat 1.0 + +Kirigami.ScrollablePage { + id: root + + required property string description + required property string index + required property string url + required property string shortcode + required property var model + required property var proxyModel + property bool newSticker: false + + leftPadding: 0 + rightPadding: 0 + + title: newSticker ? i18nc("@title", "Add Sticker") : i18nc("@title", "Edit Sticker") + + ColumnLayout { + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + MobileForm.FormCardHeader { + title: i18n("Sticker") + } + MobileForm.AbstractFormDelegate { + Layout.fillWidth: true + background: Item {} + contentItem: RowLayout { + Item { + Layout.fillWidth: true + } + Image { + id: image + Layout.alignment: Qt.AlignRight + source: root.url + sourceSize.width: Kirigami.Units.gridUnit * 4 + sourceSize.height: Kirigami.Units.gridUnit * 4 + width: Kirigami.Units.gridUnit * 4 + height: Kirigami.Units.gridUnit * 4 + + Kirigami.Icon { + source: "stickers" + anchors.fill: parent + visible: parent.status !== Image.Ready + } + + QQC2.Button { + icon.name: "edit-entry" + anchors.right: parent.right + anchors.bottom: parent.bottom + onClicked: mouseArea.clicked() + text: image.source != "" ? i18n("Change Image") : i18n("Set Image") + QQC2.ToolTip.text: text + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + display: QQC2.Button.IconOnly + } + + MouseArea { + id: mouseArea + anchors.fill: parent + property var fileDialog: null; + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (fileDialog != null) { + return; + } + + fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.Overlay) + fileDialog.chosen.connect(function(receivedSource) { + mouseArea.fileDialog = null; + if (!receivedSource) { + return; + } + parent.source = receivedSource; + }); + fileDialog.onRejected.connect(function() { + mouseArea.fileDialog = null; + }); + fileDialog.open(); + } + } + } + Item { + Layout.fillWidth: true + } + } + } + MobileForm.FormTextFieldDelegate { + id: shortcode + label: i18n("Shortcode:") + text: root.shortcode + } + MobileForm.FormTextFieldDelegate { + id: description + label: i18n("Description:") + text: root.description + } + MobileForm.FormButtonDelegate { + id: save + text: i18n("Save") + icon.name: "document-save" + enabled: !root.newSticker || (image.source && shortcode.text && description.text) + onClicked: { + if (root.newSticker) { + model.addSticker(image.source, shortcode.text, description.text) + } else { + if (description.text !== root.description) { + root.model.setStickerBody(proxyModel.mapToSource(proxyModel.index(model.index, 0)).row, description.text) + } + if (shortcode.text !== root.shortcode) { + root.model.setStickerShortcode(proxyModel.mapToSource(proxyModel.index(model.index, 0)).row, shortcode.text) + } + if (image.source + "" !== root.url) { + root.model.setStickerImage(proxyModel.mapToSource(proxyModel.index(model.index, 0)).row, image.source) + } + } + root.closeDialog() + } + } + } + } + } + Component { + id: openFileDialog + + OpenFileDialog { + folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation) + parentWindow: root.Window.window + } + } +} diff --git a/src/qml/Settings/StickersPage.qml b/src/qml/Settings/StickersPage.qml new file mode 100644 index 000000000..16607339d --- /dev/null +++ b/src/qml/Settings/StickersPage.qml @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.15 + +import org.kde.kirigami 2.19 as Kirigami +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm + +import org.kde.neochat 1.0 + +Kirigami.ScrollablePage { + title: i18n("Stickers") + leftPadding: 0 + rightPadding: 0 + + ColumnLayout { + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + MobileForm.FormCardHeader { + title: i18n("Stickers") + } + Flow { + id: stickerFlow + Layout.fillWidth: true + Repeater { + model: EmoticonFilterModel { + id: emoticonFilterModel + sourceModel: AccountStickerModel { + id: stickerModel + connection: Controller.activeConnection + } + showStickers: true + showEmojis: false + } + + delegate: MobileForm.AbstractFormDelegate { + id: stickerDelegate + + width: stickerFlow.width / 4 + height: width + + onClicked: pageSettingStack.pushDialogLayer(stickerEditorPage, { + description: model.body ?? "", + index: model.index, + url: model.url, + shortcode: model.shortcode, + model: stickerModel, + proxyModel: emoticonFilterModel + }, { + title: i18nc("@title", "Edit Sticker") + }); + + contentItem: ColumnLayout { + Image { + source: model.url + Layout.fillWidth: true + sourceSize.height: parent.width * 0.8 + fillMode: Image.PreserveAspectFit + autoTransform: true + Kirigami.Icon { + source: "stickers" + anchors.fill: parent + visible: parent.status !== Image.Ready + } + } + QQC2.Label { + id: descriptionLabel + text: model.body ?? i18nc("As in 'This sticker has no description'", "No Description") + horizontalAlignment: Qt.AlignHCenter + Layout.fillWidth: true + wrapMode: Text.Wrap + maximumLineCount: 2 + elide: Text.ElideRight + } + } + QQC2.Button { + icon.name: "edit-delete" + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Kirigami.Units.smallSpacing + z: 2 + onClicked: stickerModel.deleteSticker(emoticonFilterModel.mapToSource(emoticonFilterModel.index(model.index, 0)).row) + } + } + } + MobileForm.AbstractFormDelegate { + width: stickerFlow.width / 4 + height: width + + onClicked: pageSettingStack.pushDialogLayer(stickerEditorPage, { + description: "", + index: -1, + url: "", + shortcode: "", + model: stickerModel, + proxyModel: emoticonFilterModel, + newSticker: true + }, { + title: i18nc("@title", "Add Sticker") + }); + contentItem: ColumnLayout { + spacing: 0 + Kirigami.Icon { + source: "list-add" + Layout.fillWidth: true + } + QQC2.Label { + text: i18n("Add Sticker") + horizontalAlignment: Qt.AlignHCenter + Layout.fillWidth: true + } + } + } + } + } + } + } + + Component { + id: stickerEditorPage + StickerEditorPage {} + } +} diff --git a/src/res.qrc b/src/res.qrc index 3e53e4cee..851330f3b 100644 --- a/src/res.qrc +++ b/src/res.qrc @@ -98,6 +98,8 @@ qml/Settings/ColorScheme.qml qml/Settings/GeneralSettingsPage.qml qml/Settings/Emoticons.qml + qml/Settings/StickersPage.qml + qml/Settings/StickerEditorPage.qml qml/Settings/GlobalNotificationsPage.qml qml/Settings/NotificationRuleItem.qml qml/Settings/AppearanceSettingsPage.qml