mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Add custom image functionality for inline mod buttons. (#5369)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
parent
321d881bfe
commit
c3b84cb4b6
|
@ -3,6 +3,7 @@
|
||||||
## Unversioned
|
## Unversioned
|
||||||
|
|
||||||
- Major: Release plugins alpha. (#5288)
|
- Major: Release plugins alpha. (#5288)
|
||||||
|
- Minor: Add option to customise Moderation buttons with images. (#5369)
|
||||||
- Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378)
|
- Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378)
|
||||||
- Dev: Add doxygen build target. (#5377)
|
- Dev: Add doxygen build target. (#5377)
|
||||||
- Dev: Make printing of strings in tests easier. (#5379)
|
- Dev: Make printing of strings in tests easier. (#5379)
|
||||||
|
|
|
@ -500,6 +500,8 @@ set(SOURCE_FILES
|
||||||
util/IpcQueue.hpp
|
util/IpcQueue.hpp
|
||||||
util/LayoutHelper.cpp
|
util/LayoutHelper.cpp
|
||||||
util/LayoutHelper.hpp
|
util/LayoutHelper.hpp
|
||||||
|
util/LoadPixmap.cpp
|
||||||
|
util/LoadPixmap.hpp
|
||||||
util/RapidjsonHelpers.cpp
|
util/RapidjsonHelpers.cpp
|
||||||
util/RapidjsonHelpers.hpp
|
util/RapidjsonHelpers.hpp
|
||||||
util/RatelimitBucket.cpp
|
util/RatelimitBucket.cpp
|
||||||
|
@ -631,6 +633,8 @@ set(SOURCE_FILES
|
||||||
widgets/helper/EditableModelView.hpp
|
widgets/helper/EditableModelView.hpp
|
||||||
widgets/helper/EffectLabel.cpp
|
widgets/helper/EffectLabel.cpp
|
||||||
widgets/helper/EffectLabel.hpp
|
widgets/helper/EffectLabel.hpp
|
||||||
|
widgets/helper/IconDelegate.cpp
|
||||||
|
widgets/helper/IconDelegate.hpp
|
||||||
widgets/helper/InvisibleSizeGrip.cpp
|
widgets/helper/InvisibleSizeGrip.cpp
|
||||||
widgets/helper/InvisibleSizeGrip.hpp
|
widgets/helper/InvisibleSizeGrip.hpp
|
||||||
widgets/helper/NotebookButton.cpp
|
widgets/helper/NotebookButton.cpp
|
||||||
|
@ -639,8 +643,6 @@ set(SOURCE_FILES
|
||||||
widgets/helper/NotebookTab.hpp
|
widgets/helper/NotebookTab.hpp
|
||||||
widgets/helper/RegExpItemDelegate.cpp
|
widgets/helper/RegExpItemDelegate.cpp
|
||||||
widgets/helper/RegExpItemDelegate.hpp
|
widgets/helper/RegExpItemDelegate.hpp
|
||||||
widgets/helper/TrimRegExpValidator.cpp
|
|
||||||
widgets/helper/TrimRegExpValidator.hpp
|
|
||||||
widgets/helper/ResizingTextEdit.cpp
|
widgets/helper/ResizingTextEdit.cpp
|
||||||
widgets/helper/ResizingTextEdit.hpp
|
widgets/helper/ResizingTextEdit.hpp
|
||||||
widgets/helper/ScrollbarHighlight.cpp
|
widgets/helper/ScrollbarHighlight.cpp
|
||||||
|
@ -655,6 +657,8 @@ set(SOURCE_FILES
|
||||||
widgets/helper/TitlebarButton.hpp
|
widgets/helper/TitlebarButton.hpp
|
||||||
widgets/helper/TitlebarButtons.cpp
|
widgets/helper/TitlebarButtons.cpp
|
||||||
widgets/helper/TitlebarButtons.hpp
|
widgets/helper/TitlebarButtons.hpp
|
||||||
|
widgets/helper/TrimRegExpValidator.cpp
|
||||||
|
widgets/helper/TrimRegExpValidator.hpp
|
||||||
|
|
||||||
widgets/layout/FlowLayout.cpp
|
widgets/layout/FlowLayout.cpp
|
||||||
widgets/layout/FlowLayout.hpp
|
widgets/layout/FlowLayout.hpp
|
||||||
|
|
|
@ -6,28 +6,11 @@
|
||||||
#include "singletons/Resources.hpp"
|
#include "singletons/Resources.hpp"
|
||||||
|
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
// ModerationAction::ModerationAction(Image *_image, const QString &_action)
|
ModerationAction::ModerationAction(const QString &action, const QUrl &iconPath)
|
||||||
// : _isImage(true)
|
|
||||||
// , image(_image)
|
|
||||||
// , action(_action)
|
|
||||||
//{
|
|
||||||
//}
|
|
||||||
|
|
||||||
// ModerationAction::ModerationAction(const QString &_line1, const QString
|
|
||||||
// &_line2,
|
|
||||||
// const QString &_action)
|
|
||||||
// : _isImage(false)
|
|
||||||
// , image(nullptr)
|
|
||||||
// , line1(_line1)
|
|
||||||
// , line2(_line2)
|
|
||||||
// , action(_action)
|
|
||||||
//{
|
|
||||||
//}
|
|
||||||
|
|
||||||
ModerationAction::ModerationAction(const QString &action)
|
|
||||||
: action_(action)
|
: action_(action)
|
||||||
{
|
{
|
||||||
static QRegularExpression replaceRegex("[!/.]");
|
static QRegularExpression replaceRegex("[!/.]");
|
||||||
|
@ -37,6 +20,8 @@ ModerationAction::ModerationAction(const QString &action)
|
||||||
|
|
||||||
if (timeoutMatch.hasMatch())
|
if (timeoutMatch.hasMatch())
|
||||||
{
|
{
|
||||||
|
this->type_ = Type::Timeout;
|
||||||
|
|
||||||
// if (multipleTimeouts > 1) {
|
// if (multipleTimeouts > 1) {
|
||||||
// QString line1;
|
// QString line1;
|
||||||
// QString line2;
|
// QString line2;
|
||||||
|
@ -99,24 +84,19 @@ ModerationAction::ModerationAction(const QString &action)
|
||||||
}
|
}
|
||||||
this->line2_ = "w";
|
this->line2_ = "w";
|
||||||
}
|
}
|
||||||
|
|
||||||
// line1 = this->line1_;
|
|
||||||
// line2 = this->line2_;
|
|
||||||
// } else {
|
|
||||||
// this->_moderationActions.emplace_back(getResources().buttonTimeout,
|
|
||||||
// str);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
else if (action.startsWith("/ban "))
|
else if (action.startsWith("/ban "))
|
||||||
{
|
{
|
||||||
this->imageToLoad_ = 1;
|
this->type_ = Type::Ban;
|
||||||
}
|
}
|
||||||
else if (action.startsWith("/delete "))
|
else if (action.startsWith("/delete "))
|
||||||
{
|
{
|
||||||
this->imageToLoad_ = 2;
|
this->type_ = Type::Delete;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
this->type_ = Type::Custom;
|
||||||
|
|
||||||
QString xD = action;
|
QString xD = action;
|
||||||
|
|
||||||
xD.replace(replaceRegex, "");
|
xD.replace(replaceRegex, "");
|
||||||
|
@ -124,6 +104,11 @@ ModerationAction::ModerationAction(const QString &action)
|
||||||
this->line1_ = xD.mid(0, 2);
|
this->line1_ = xD.mid(0, 2);
|
||||||
this->line2_ = xD.mid(2, 2);
|
this->line2_ = xD.mid(2, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (iconPath.isValid())
|
||||||
|
{
|
||||||
|
this->iconPath_ = iconPath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ModerationAction::operator==(const ModerationAction &other) const
|
bool ModerationAction::operator==(const ModerationAction &other) const
|
||||||
|
@ -139,19 +124,23 @@ bool ModerationAction::isImage() const
|
||||||
const std::optional<ImagePtr> &ModerationAction::getImage() const
|
const std::optional<ImagePtr> &ModerationAction::getImage() const
|
||||||
{
|
{
|
||||||
assertInGuiThread();
|
assertInGuiThread();
|
||||||
|
if (this->image_.has_value())
|
||||||
if (this->imageToLoad_ != 0)
|
|
||||||
{
|
{
|
||||||
if (this->imageToLoad_ == 1)
|
return this->image_;
|
||||||
{
|
}
|
||||||
this->image_ =
|
|
||||||
Image::fromResourcePixmap(getResources().buttons.ban);
|
if (this->iconPath_.isValid())
|
||||||
}
|
{
|
||||||
else if (this->imageToLoad_ == 2)
|
this->image_ = Image::fromUrl({this->iconPath_.toString()});
|
||||||
{
|
}
|
||||||
this->image_ =
|
else if (this->type_ == Type::Ban)
|
||||||
Image::fromResourcePixmap(getResources().buttons.trashCan);
|
{
|
||||||
}
|
this->image_ = Image::fromResourcePixmap(getResources().buttons.ban);
|
||||||
|
}
|
||||||
|
else if (this->type_ == Type::Delete)
|
||||||
|
{
|
||||||
|
this->image_ =
|
||||||
|
Image::fromResourcePixmap(getResources().buttons.trashCan);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this->image_;
|
return this->image_;
|
||||||
|
@ -172,4 +161,14 @@ const QString &ModerationAction::getAction() const
|
||||||
return this->action_;
|
return this->action_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QUrl &ModerationAction::iconPath() const
|
||||||
|
{
|
||||||
|
return this->iconPath_;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModerationAction::Type ModerationAction::getType() const
|
||||||
|
{
|
||||||
|
return this->type_;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
#include <pajlada/serialize.hpp>
|
#include <pajlada/serialize.hpp>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
@ -16,7 +17,32 @@ using ImagePtr = std::shared_ptr<Image>;
|
||||||
class ModerationAction
|
class ModerationAction
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ModerationAction(const QString &action);
|
/**
|
||||||
|
* Type of the action, parsed from the input `action`
|
||||||
|
*/
|
||||||
|
enum class Type {
|
||||||
|
/**
|
||||||
|
* /ban <user>
|
||||||
|
*/
|
||||||
|
Ban,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /delete <msg-id>
|
||||||
|
*/
|
||||||
|
Delete,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /timeout <user> <duration>
|
||||||
|
*/
|
||||||
|
Timeout,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anything not matching the action types above
|
||||||
|
*/
|
||||||
|
Custom,
|
||||||
|
};
|
||||||
|
|
||||||
|
ModerationAction(const QString &action, const QUrl &iconPath = {});
|
||||||
|
|
||||||
bool operator==(const ModerationAction &other) const;
|
bool operator==(const ModerationAction &other) const;
|
||||||
|
|
||||||
|
@ -25,13 +51,18 @@ public:
|
||||||
const QString &getLine1() const;
|
const QString &getLine1() const;
|
||||||
const QString &getLine2() const;
|
const QString &getLine2() const;
|
||||||
const QString &getAction() const;
|
const QString &getAction() const;
|
||||||
|
const QUrl &iconPath() const;
|
||||||
|
Type getType() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
mutable std::optional<ImagePtr> image_;
|
mutable std::optional<ImagePtr> image_;
|
||||||
QString line1_;
|
QString line1_;
|
||||||
QString line2_;
|
QString line2_;
|
||||||
QString action_;
|
QString action_;
|
||||||
int imageToLoad_{};
|
|
||||||
|
Type type_{};
|
||||||
|
|
||||||
|
QUrl iconPath_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
@ -46,6 +77,7 @@ struct Serialize<chatterino::ModerationAction> {
|
||||||
rapidjson::Value ret(rapidjson::kObjectType);
|
rapidjson::Value ret(rapidjson::kObjectType);
|
||||||
|
|
||||||
chatterino::rj::set(ret, "pattern", value.getAction(), a);
|
chatterino::rj::set(ret, "pattern", value.getAction(), a);
|
||||||
|
chatterino::rj::set(ret, "icon", value.iconPath().toString(), a);
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
@ -63,10 +95,12 @@ struct Deserialize<chatterino::ModerationAction> {
|
||||||
}
|
}
|
||||||
|
|
||||||
QString pattern;
|
QString pattern;
|
||||||
|
|
||||||
chatterino::rj::getSafe(value, "pattern", pattern);
|
chatterino::rj::getSafe(value, "pattern", pattern);
|
||||||
|
|
||||||
return chatterino::ModerationAction(pattern);
|
QString icon;
|
||||||
|
chatterino::rj::getSafe(value, "icon", icon);
|
||||||
|
|
||||||
|
return chatterino::ModerationAction(pattern, QUrl(icon));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
#include "controllers/moderationactions/ModerationActionModel.hpp"
|
#include "controllers/moderationactions/ModerationActionModel.hpp"
|
||||||
|
|
||||||
#include "controllers/moderationactions/ModerationAction.hpp"
|
#include "controllers/moderationactions/ModerationAction.hpp"
|
||||||
|
#include "messages/Image.hpp"
|
||||||
|
#include "util/LoadPixmap.hpp"
|
||||||
|
#include "util/PostToThread.hpp"
|
||||||
#include "util/StandardItemHelper.hpp"
|
#include "util/StandardItemHelper.hpp"
|
||||||
|
|
||||||
|
#include <QIcon>
|
||||||
|
#include <QPixmap>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
// commandmodel
|
// commandmodel
|
||||||
ModerationActionModel ::ModerationActionModel(QObject *parent)
|
ModerationActionModel ::ModerationActionModel(QObject *parent)
|
||||||
: SignalVectorModel<ModerationAction>(1, parent)
|
: SignalVectorModel<ModerationAction>(2, parent)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,14 +21,31 @@ ModerationActionModel ::ModerationActionModel(QObject *parent)
|
||||||
ModerationAction ModerationActionModel::getItemFromRow(
|
ModerationAction ModerationActionModel::getItemFromRow(
|
||||||
std::vector<QStandardItem *> &row, const ModerationAction &original)
|
std::vector<QStandardItem *> &row, const ModerationAction &original)
|
||||||
{
|
{
|
||||||
return ModerationAction(row[0]->data(Qt::DisplayRole).toString());
|
return ModerationAction(
|
||||||
|
row[Column::Command]->data(Qt::DisplayRole).toString(),
|
||||||
|
row[Column::Icon]->data(Qt::UserRole).toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
// turns a row in the model into a vector item
|
// turns a row in the model into a vector item
|
||||||
void ModerationActionModel::getRowFromItem(const ModerationAction &item,
|
void ModerationActionModel::getRowFromItem(const ModerationAction &item,
|
||||||
std::vector<QStandardItem *> &row)
|
std::vector<QStandardItem *> &row)
|
||||||
{
|
{
|
||||||
setStringItem(row[0], item.getAction());
|
setStringItem(row[Column::Command], item.getAction());
|
||||||
|
setFilePathItem(row[Column::Icon], item.iconPath());
|
||||||
|
if (!item.iconPath().isEmpty())
|
||||||
|
{
|
||||||
|
auto oImage = item.getImage();
|
||||||
|
assert(oImage.has_value());
|
||||||
|
if (oImage.has_value())
|
||||||
|
{
|
||||||
|
auto url = oImage->get()->url();
|
||||||
|
loadPixmapFromUrl(url, [row](const QPixmap &pixmap) {
|
||||||
|
postToThread([row, pixmap]() {
|
||||||
|
row[Column::Icon]->setData(pixmap, Qt::DecorationRole);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -13,6 +13,11 @@ class ModerationActionModel : public SignalVectorModel<ModerationAction>
|
||||||
public:
|
public:
|
||||||
explicit ModerationActionModel(QObject *parent);
|
explicit ModerationActionModel(QObject *parent);
|
||||||
|
|
||||||
|
enum Column {
|
||||||
|
Command = 0,
|
||||||
|
Icon = 1,
|
||||||
|
};
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// turn a vector item into a model row
|
// turn a vector item into a model row
|
||||||
ModerationAction getItemFromRow(std::vector<QStandardItem *> &row,
|
ModerationAction getItemFromRow(std::vector<QStandardItem *> &row,
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#include "messages/Image.hpp"
|
#include "messages/Image.hpp"
|
||||||
#include "providers/twitch/api/Helix.hpp"
|
#include "providers/twitch/api/Helix.hpp"
|
||||||
#include "util/DisplayBadge.hpp"
|
#include "util/DisplayBadge.hpp"
|
||||||
|
#include "util/LoadPixmap.hpp"
|
||||||
|
|
||||||
#include <QBuffer>
|
#include <QBuffer>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
@ -239,48 +240,20 @@ void TwitchBadges::getBadgeIcons(const QList<DisplayBadge> &badges,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TwitchBadges::loadEmoteImage(const QString &name, ImagePtr image,
|
void TwitchBadges::loadEmoteImage(const QString &name, const ImagePtr &image,
|
||||||
BadgeIconCallback &&callback)
|
BadgeIconCallback &&callback)
|
||||||
{
|
{
|
||||||
auto url = image->url().string;
|
loadPixmapFromUrl(image->url(),
|
||||||
NetworkRequest(url)
|
[this, name, callback{std::move(callback)}](auto pixmap) {
|
||||||
.concurrent()
|
auto icon = std::make_shared<QIcon>(pixmap);
|
||||||
.cache()
|
|
||||||
.onSuccess([this, name, callback, url](auto result) {
|
|
||||||
auto data = result.getData();
|
|
||||||
|
|
||||||
// const cast since we are only reading from it
|
{
|
||||||
QBuffer buffer(const_cast<QByteArray *>(&data));
|
std::unique_lock lock(this->badgesMutex_);
|
||||||
buffer.open(QIODevice::ReadOnly);
|
this->badgesMap_[name] = icon;
|
||||||
QImageReader reader(&buffer);
|
}
|
||||||
|
|
||||||
if (!reader.canRead() || reader.size().isEmpty())
|
callback(name, icon);
|
||||||
{
|
});
|
||||||
qCWarning(chatterinoTwitch)
|
|
||||||
<< "Can't read badge image at" << url << "for" << name
|
|
||||||
<< reader.errorString();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QImage image = reader.read();
|
|
||||||
if (image.isNull())
|
|
||||||
{
|
|
||||||
qCWarning(chatterinoTwitch)
|
|
||||||
<< "Failed reading badge image at" << url << "for" << name
|
|
||||||
<< reader.errorString();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto icon = std::make_shared<QIcon>(QPixmap::fromImage(image));
|
|
||||||
|
|
||||||
{
|
|
||||||
std::unique_lock lock(this->badgesMutex_);
|
|
||||||
this->badgesMap_[name] = icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(name, icon);
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -48,7 +48,7 @@ public:
|
||||||
private:
|
private:
|
||||||
void parseTwitchBadges(QJsonObject root);
|
void parseTwitchBadges(QJsonObject root);
|
||||||
void loaded();
|
void loaded();
|
||||||
void loadEmoteImage(const QString &name, ImagePtr image,
|
void loadEmoteImage(const QString &name, const ImagePtr &image,
|
||||||
BadgeIconCallback &&callback);
|
BadgeIconCallback &&callback);
|
||||||
|
|
||||||
std::shared_mutex badgesMutex_;
|
std::shared_mutex badgesMutex_;
|
||||||
|
|
48
src/util/LoadPixmap.cpp
Normal file
48
src/util/LoadPixmap.cpp
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
#include "util/LoadPixmap.hpp"
|
||||||
|
|
||||||
|
#include "common/network/NetworkRequest.hpp"
|
||||||
|
#include "common/network/NetworkResult.hpp"
|
||||||
|
#include "common/QLogging.hpp"
|
||||||
|
|
||||||
|
#include <QBuffer>
|
||||||
|
#include <QImageReader>
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
#include <QPixmap>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
void loadPixmapFromUrl(const Url &url, std::function<void(QPixmap)> &&callback)
|
||||||
|
{
|
||||||
|
NetworkRequest(url.string)
|
||||||
|
.concurrent()
|
||||||
|
.cache()
|
||||||
|
.onSuccess(
|
||||||
|
[callback = std::move(callback), url](const NetworkResult &result) {
|
||||||
|
auto data = result.getData();
|
||||||
|
QBuffer buffer(&data);
|
||||||
|
buffer.open(QIODevice::ReadOnly);
|
||||||
|
QImageReader reader(&buffer);
|
||||||
|
|
||||||
|
if (!reader.canRead() || reader.size().isEmpty())
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoImage)
|
||||||
|
<< "Can't read image file at" << url.string << ":"
|
||||||
|
<< reader.errorString();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QImage image = reader.read();
|
||||||
|
if (image.isNull())
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoImage)
|
||||||
|
<< "Failed reading image at" << url.string << ":"
|
||||||
|
<< reader.errorString();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(QPixmap::fromImage(image));
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
15
src/util/LoadPixmap.hpp
Normal file
15
src/util/LoadPixmap.hpp
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
#pragma once
|
||||||
|
#include "common/Aliases.hpp"
|
||||||
|
|
||||||
|
#include <QPixmap>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads an image from url into a QPixmap. Allows for file:// protocol links. Uses cacheing.
|
||||||
|
*
|
||||||
|
* @param callback The callback you will get the pixmap by. It will be invoked concurrently with no guarantees on which thread.
|
||||||
|
*/
|
||||||
|
void loadPixmapFromUrl(const Url &url, std::function<void(QPixmap)> &&callback);
|
||||||
|
|
||||||
|
} // namespace chatterino
|
29
src/widgets/helper/IconDelegate.cpp
Normal file
29
src/widgets/helper/IconDelegate.cpp
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
#include "widgets/helper/IconDelegate.hpp"
|
||||||
|
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QVariant>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
IconDelegate::IconDelegate(QObject *parent)
|
||||||
|
: QStyledItemDelegate(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void IconDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
|
||||||
|
const QModelIndex &index) const
|
||||||
|
{
|
||||||
|
auto data = index.data(Qt::DecorationRole);
|
||||||
|
|
||||||
|
if (data.type() != QVariant::Pixmap)
|
||||||
|
{
|
||||||
|
return QStyledItemDelegate::paint(painter, option, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto scaledRect = option.rect;
|
||||||
|
scaledRect.setWidth(scaledRect.height());
|
||||||
|
|
||||||
|
painter->drawPixmap(scaledRect, data.value<QPixmap>());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
19
src/widgets/helper/IconDelegate.hpp
Normal file
19
src/widgets/helper/IconDelegate.hpp
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QStyledItemDelegate>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IconDelegate draws the decoration role pixmap scaled down to a square icon
|
||||||
|
*/
|
||||||
|
class IconDelegate : public QStyledItemDelegate
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit IconDelegate(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
void paint(QPainter *painter, const QStyleOptionViewItem &option,
|
||||||
|
const QModelIndex &index) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
|
@ -9,12 +9,16 @@
|
||||||
#include "singletons/Settings.hpp"
|
#include "singletons/Settings.hpp"
|
||||||
#include "util/Helpers.hpp"
|
#include "util/Helpers.hpp"
|
||||||
#include "util/LayoutCreator.hpp"
|
#include "util/LayoutCreator.hpp"
|
||||||
|
#include "util/LoadPixmap.hpp"
|
||||||
|
#include "util/PostToThread.hpp"
|
||||||
#include "widgets/helper/EditableModelView.hpp"
|
#include "widgets/helper/EditableModelView.hpp"
|
||||||
|
#include "widgets/helper/IconDelegate.hpp"
|
||||||
|
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QHeaderView>
|
#include <QHeaderView>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QPixmap>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QTableView>
|
#include <QTableView>
|
||||||
#include <QtConcurrent/QtConcurrent>
|
#include <QtConcurrent/QtConcurrent>
|
||||||
|
@ -207,11 +211,51 @@ ModerationPage::ModerationPage()
|
||||||
->initialized(&getSettings()->moderationActions))
|
->initialized(&getSettings()->moderationActions))
|
||||||
.getElement();
|
.getElement();
|
||||||
|
|
||||||
view->setTitles({"Actions"});
|
view->setTitles({"Action", "Icon"});
|
||||||
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
||||||
QHeaderView::Fixed);
|
QHeaderView::Fixed);
|
||||||
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
||||||
0, QHeaderView::Stretch);
|
0, QHeaderView::Stretch);
|
||||||
|
view->getTableView()->setItemDelegateForColumn(
|
||||||
|
ModerationActionModel::Column::Icon, new IconDelegate(view));
|
||||||
|
QObject::connect(
|
||||||
|
view->getTableView(), &QTableView::clicked,
|
||||||
|
[this, view](const QModelIndex &clicked) {
|
||||||
|
if (clicked.column() == ModerationActionModel::Column::Icon)
|
||||||
|
{
|
||||||
|
auto fileUrl = QFileDialog::getOpenFileUrl(
|
||||||
|
this, "Open Image", QUrl(),
|
||||||
|
"Image Files (*.png *.jpg *.jpeg)");
|
||||||
|
view->getModel()->setData(clicked, fileUrl, Qt::UserRole);
|
||||||
|
view->getModel()->setData(clicked, fileUrl.fileName(),
|
||||||
|
Qt::DisplayRole);
|
||||||
|
// Clear the icon if the user canceled the dialog
|
||||||
|
if (fileUrl.isEmpty())
|
||||||
|
{
|
||||||
|
view->getModel()->setData(clicked, QVariant(),
|
||||||
|
Qt::DecorationRole);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// QPointer will be cleared when view is destroyed
|
||||||
|
QPointer<EditableModelView> viewtemp = view;
|
||||||
|
|
||||||
|
loadPixmapFromUrl(
|
||||||
|
{fileUrl.toString()},
|
||||||
|
[clicked, view = viewtemp](const QPixmap &pixmap) {
|
||||||
|
postToThread([clicked, view, pixmap]() {
|
||||||
|
if (view.isNull())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
view->getModel()->setData(
|
||||||
|
clicked, pixmap, Qt::DecorationRole);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// We can safely ignore this signal connection since we own the view
|
// We can safely ignore this signal connection since we own the view
|
||||||
std::ignore = view->addButtonPressed.connect([] {
|
std::ignore = view->addButtonPressed.connect([] {
|
||||||
|
|
|
@ -42,6 +42,7 @@ set(test_SOURCES
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/LinkInfo.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/LinkInfo.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/MessageLayout.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/MessageLayout.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/QMagicEnum.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/QMagicEnum.cpp
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/src/ModerationAction.cpp
|
||||||
# Add your new file above this line!
|
# Add your new file above this line!
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
112
tests/src/ModerationAction.cpp
Normal file
112
tests/src/ModerationAction.cpp
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
#include "controllers/moderationactions/ModerationAction.hpp"
|
||||||
|
|
||||||
|
#include "messages/Image.hpp"
|
||||||
|
#include "mocks/EmptyApplication.hpp"
|
||||||
|
#include "singletons/Emotes.hpp"
|
||||||
|
#include "singletons/Resources.hpp"
|
||||||
|
#include "singletons/Settings.hpp"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
using namespace chatterino;
|
||||||
|
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class MockApplication : mock::EmptyApplication
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MockApplication()
|
||||||
|
: settings(this->settingsDir.filePath("settings.json"))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
IEmotes *getEmotes() override
|
||||||
|
{
|
||||||
|
return &this->emotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings settings;
|
||||||
|
Emotes emotes;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ModerationActionTest : public ::testing::Test
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MockApplication mockApplication;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_F(ModerationActionTest, Parse)
|
||||||
|
{
|
||||||
|
struct TestCase {
|
||||||
|
QString action;
|
||||||
|
QString iconPath;
|
||||||
|
|
||||||
|
QString expectedLine1;
|
||||||
|
QString expectedLine2;
|
||||||
|
|
||||||
|
std::optional<ImagePtr> expectedImage;
|
||||||
|
|
||||||
|
ModerationAction::Type expectedType;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<TestCase> tests{
|
||||||
|
{
|
||||||
|
.action = "/ban forsen",
|
||||||
|
.expectedImage =
|
||||||
|
Image::fromResourcePixmap(getResources().buttons.ban),
|
||||||
|
.expectedType = ModerationAction::Type::Ban,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.action = "/delete {message.id}",
|
||||||
|
.expectedImage =
|
||||||
|
Image::fromResourcePixmap(getResources().buttons.trashCan),
|
||||||
|
.expectedType = ModerationAction::Type::Delete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.action = "/timeout {user.name} 1d",
|
||||||
|
.expectedLine1 = "1",
|
||||||
|
.expectedLine2 = "d",
|
||||||
|
.expectedType = ModerationAction::Type::Timeout,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.action = ".timeout {user.name} 300",
|
||||||
|
.expectedLine1 = "5",
|
||||||
|
.expectedLine2 = "m",
|
||||||
|
.expectedType = ModerationAction::Type::Timeout,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.action = "forsen",
|
||||||
|
.expectedLine1 = "fo",
|
||||||
|
.expectedLine2 = "rs",
|
||||||
|
.expectedType = ModerationAction::Type::Custom,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.action = "forsen",
|
||||||
|
.iconPath = "file:///this-is-the-path-to-the-icon.png",
|
||||||
|
.expectedLine1 = "fo",
|
||||||
|
.expectedLine2 = "rs",
|
||||||
|
.expectedImage =
|
||||||
|
Image::fromUrl(Url{"file:///this-is-the-path-to-the-icon.png"}),
|
||||||
|
.expectedType = ModerationAction::Type::Custom,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto &test : tests)
|
||||||
|
{
|
||||||
|
ModerationAction moderationAction(test.action, test.iconPath);
|
||||||
|
|
||||||
|
EXPECT_EQ(moderationAction.getAction(), test.action);
|
||||||
|
|
||||||
|
EXPECT_EQ(moderationAction.getLine1(), test.expectedLine1);
|
||||||
|
EXPECT_EQ(moderationAction.getLine2(), test.expectedLine2);
|
||||||
|
|
||||||
|
EXPECT_EQ(moderationAction.getImage(), test.expectedImage);
|
||||||
|
|
||||||
|
EXPECT_EQ(moderationAction.getType(), test.expectedType);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue