Add ability to highlight messages based on user badges (#1704)
Co-authored-by: Paweł <zneix@zneix.eu> Co-authored-by: 23rd <23rd@vivaldi.net> Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
|
@ -2,6 +2,7 @@
|
|||
|
||||
## Unversioned
|
||||
|
||||
- Major: Added the ability to highlight messages based on user badges. (#1704)
|
||||
- Minor: Added visual indicator to message length if over 500 characters long (#2659)
|
||||
- Minor: Added `is:<flags>` search filter to find messages of specific types. (#2653, #2671)
|
||||
- Minor: Added image links to the badge context menu. (#2667)
|
||||
|
|
|
@ -149,6 +149,8 @@ SOURCES += \
|
|||
src/controllers/commands/Command.cpp \
|
||||
src/controllers/commands/CommandController.cpp \
|
||||
src/controllers/commands/CommandModel.cpp \
|
||||
src/controllers/highlights/BadgeHighlightModel.cpp \
|
||||
src/controllers/highlights/HighlightBadge.cpp \
|
||||
src/controllers/filters/FilterModel.cpp \
|
||||
src/controllers/filters/parser/FilterParser.cpp \
|
||||
src/controllers/filters/parser/Tokenizer.cpp \
|
||||
|
@ -239,6 +241,7 @@ SOURCES += \
|
|||
src/util/AttachToConsole.cpp \
|
||||
src/util/Clipboard.cpp \
|
||||
src/util/DebugCount.cpp \
|
||||
src/util/DisplayBadge.cpp \
|
||||
src/util/FormatTime.cpp \
|
||||
src/util/FunctionEventFilter.cpp \
|
||||
src/util/FuzzyConvert.cpp \
|
||||
|
@ -261,6 +264,7 @@ SOURCES += \
|
|||
src/widgets/BaseWidget.cpp \
|
||||
src/widgets/BaseWindow.cpp \
|
||||
src/widgets/FramelessEmbedWindow.cpp \
|
||||
src/widgets/dialogs/BadgePickerDialog.cpp \
|
||||
src/widgets/dialogs/ChannelFilterEditorDialog.cpp \
|
||||
src/widgets/dialogs/ColorPickerDialog.cpp \
|
||||
src/widgets/dialogs/EmotePopup.cpp \
|
||||
|
@ -369,6 +373,8 @@ HEADERS += \
|
|||
src/controllers/commands/Command.hpp \
|
||||
src/controllers/commands/CommandController.hpp \
|
||||
src/controllers/commands/CommandModel.hpp \
|
||||
src/controllers/highlights/BadgeHighlightModel.hpp \
|
||||
src/controllers/highlights/HighlightBadge.hpp \
|
||||
src/controllers/filters/FilterModel.hpp \
|
||||
src/controllers/filters/FilterRecord.hpp \
|
||||
src/controllers/filters/FilterSet.hpp \
|
||||
|
@ -477,6 +483,7 @@ HEADERS += \
|
|||
src/util/CombinePath.hpp \
|
||||
src/util/ConcurrentMap.hpp \
|
||||
src/util/DebugCount.hpp \
|
||||
src/util/DisplayBadge.hpp \
|
||||
src/util/DistanceBetweenPoints.hpp \
|
||||
src/util/FormatTime.hpp \
|
||||
src/util/FunctionEventFilter.hpp \
|
||||
|
@ -515,6 +522,7 @@ HEADERS += \
|
|||
src/widgets/BaseWidget.hpp \
|
||||
src/widgets/BaseWindow.hpp \
|
||||
src/widgets/FramelessEmbedWindow.hpp \
|
||||
src/widgets/dialogs/BadgePickerDialog.hpp \
|
||||
src/widgets/dialogs/ChannelFilterEditorDialog.hpp \
|
||||
src/widgets/dialogs/ColorPickerDialog.hpp \
|
||||
src/widgets/dialogs/EmotePopup.hpp \
|
||||
|
|
Before Width: | Height: | Size: 439 B After Width: | Height: | Size: 525 B |
Before Width: | Height: | Size: 305 B After Width: | Height: | Size: 402 B |
Before Width: | Height: | Size: 191 B After Width: | Height: | Size: 320 B |
Before Width: | Height: | Size: 397 B After Width: | Height: | Size: 396 B |
Before Width: | Height: | Size: 376 B After Width: | Height: | Size: 420 B |
Before Width: | Height: | Size: 116 B After Width: | Height: | Size: 317 B |
Before Width: | Height: | Size: 269 B After Width: | Height: | Size: 290 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 442 B |
Before Width: | Height: | Size: 257 B After Width: | Height: | Size: 367 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 442 B |
Before Width: | Height: | Size: 476 B After Width: | Height: | Size: 326 B |
|
@ -72,6 +72,10 @@ set(SOURCE_FILES main.cpp
|
|||
controllers/filters/parser/Types.cpp
|
||||
controllers/filters/parser/Types.hpp
|
||||
|
||||
controllers/highlights/BadgeHighlightModel.cpp
|
||||
controllers/highlights/BadgeHighlightModel.hpp
|
||||
controllers/highlights/HighlightBadge.cpp
|
||||
controllers/highlights/HighlightBadge.hpp
|
||||
controllers/highlights/HighlightBlacklistModel.cpp
|
||||
controllers/highlights/HighlightBlacklistModel.hpp
|
||||
controllers/highlights/HighlightModel.cpp
|
||||
|
@ -261,6 +265,8 @@ set(SOURCE_FILES main.cpp
|
|||
util/Clipboard.hpp
|
||||
util/DebugCount.cpp
|
||||
util/DebugCount.hpp
|
||||
util/DisplayBadge.cpp
|
||||
util/DisplayBadge.hpp
|
||||
util/FormatTime.cpp
|
||||
util/FormatTime.hpp
|
||||
util/FunctionEventFilter.cpp
|
||||
|
@ -319,6 +325,8 @@ set(SOURCE_FILES main.cpp
|
|||
widgets/Window.cpp
|
||||
widgets/Window.hpp
|
||||
|
||||
widgets/dialogs/BadgePickerDialog.cpp
|
||||
widgets/dialogs/BadgePickerDialog.hpp
|
||||
widgets/dialogs/ChannelFilterEditorDialog.cpp
|
||||
widgets/dialogs/ChannelFilterEditorDialog.hpp
|
||||
widgets/dialogs/ColorPickerDialog.cpp
|
||||
|
|
56
src/controllers/highlights/BadgeHighlightModel.cpp
Normal file
|
@ -0,0 +1,56 @@
|
|||
#include "BadgeHighlightModel.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "messages/Emote.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "util/StandardItemHelper.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
// commandmodel
|
||||
BadgeHighlightModel::BadgeHighlightModel(QObject *parent)
|
||||
: SignalVectorModel<HighlightBadge>(5, parent)
|
||||
{
|
||||
}
|
||||
|
||||
// turn vector item into model row
|
||||
HighlightBadge BadgeHighlightModel::getItemFromRow(
|
||||
std::vector<QStandardItem *> &row, const HighlightBadge &original)
|
||||
{
|
||||
using Column = BadgeHighlightModel::Column;
|
||||
|
||||
// In order for old messages to update their highlight color, we need to
|
||||
// update the highlight color here.
|
||||
auto highlightColor = original.getColor();
|
||||
*highlightColor =
|
||||
row[Column::Color]->data(Qt::DecorationRole).value<QColor>();
|
||||
|
||||
return HighlightBadge{
|
||||
original.badgeName(),
|
||||
row[Column::Badge]->data(Qt::DisplayRole).toString(),
|
||||
row[Column::FlashTaskbar]->data(Qt::CheckStateRole).toBool(),
|
||||
row[Column::PlaySound]->data(Qt::CheckStateRole).toBool(),
|
||||
row[Column::SoundPath]->data(Qt::UserRole).toString(),
|
||||
highlightColor};
|
||||
}
|
||||
|
||||
// row into vector item
|
||||
void BadgeHighlightModel::getRowFromItem(const HighlightBadge &item,
|
||||
std::vector<QStandardItem *> &row)
|
||||
{
|
||||
using QIconPtr = std::shared_ptr<QIcon>;
|
||||
using Column = BadgeHighlightModel::Column;
|
||||
|
||||
setStringItem(row[Column::Badge], item.displayName(), false, true);
|
||||
setBoolItem(row[Column::FlashTaskbar], item.hasAlert());
|
||||
setBoolItem(row[Column::PlaySound], item.hasSound());
|
||||
setFilePathItem(row[Column::SoundPath], item.getSoundUrl());
|
||||
setColorItem(row[Column::Color], *item.getColor());
|
||||
|
||||
TwitchBadges::instance()->getBadgeIcon(
|
||||
item.badgeName(), [item, row](QString /*name*/, const QIconPtr pixmap) {
|
||||
row[Column::Badge]->setData(QVariant(*pixmap), Qt::DecorationRole);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
36
src/controllers/highlights/BadgeHighlightModel.hpp
Normal file
|
@ -0,0 +1,36 @@
|
|||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "common/SignalVectorModel.hpp"
|
||||
#include "controllers/highlights/HighlightBadge.hpp"
|
||||
#include "providers/twitch/TwitchBadges.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class HighlightController;
|
||||
|
||||
class BadgeHighlightModel : public SignalVectorModel<HighlightBadge>
|
||||
{
|
||||
public:
|
||||
explicit BadgeHighlightModel(QObject *parent);
|
||||
|
||||
enum Column {
|
||||
Badge = 0,
|
||||
FlashTaskbar = 1,
|
||||
PlaySound = 2,
|
||||
SoundPath = 3,
|
||||
Color = 4
|
||||
};
|
||||
|
||||
protected:
|
||||
// vector into model row
|
||||
virtual HighlightBadge getItemFromRow(
|
||||
std::vector<QStandardItem *> &row,
|
||||
const HighlightBadge &original) override;
|
||||
|
||||
virtual void getRowFromItem(const HighlightBadge &item,
|
||||
std::vector<QStandardItem *> &row) override;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
121
src/controllers/highlights/HighlightBadge.cpp
Normal file
|
@ -0,0 +1,121 @@
|
|||
#include "HighlightBadge.hpp"
|
||||
|
||||
#include "singletons/Resources.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
QColor HighlightBadge::FALLBACK_HIGHLIGHT_COLOR = QColor(127, 63, 73, 127);
|
||||
|
||||
bool HighlightBadge::operator==(const HighlightBadge &other) const
|
||||
{
|
||||
return std::tie(this->badgeName_, this->displayName_, this->hasSound_,
|
||||
this->hasAlert_, this->soundUrl_, this->color_) ==
|
||||
std::tie(other.badgeName_, other.displayName_, other.hasSound_,
|
||||
other.hasAlert_, other.soundUrl_, other.color_);
|
||||
}
|
||||
|
||||
HighlightBadge::HighlightBadge(const QString &badgeName,
|
||||
const QString &displayName, bool hasAlert,
|
||||
bool hasSound, const QString &soundUrl,
|
||||
QColor color)
|
||||
: HighlightBadge(badgeName, displayName, hasAlert, hasSound, soundUrl,
|
||||
std::make_shared<QColor>(color))
|
||||
{
|
||||
}
|
||||
|
||||
HighlightBadge::HighlightBadge(const QString &badgeName,
|
||||
const QString &displayName, bool hasAlert,
|
||||
bool hasSound, const QString &soundUrl,
|
||||
std::shared_ptr<QColor> color)
|
||||
: badgeName_(badgeName)
|
||||
, displayName_(displayName)
|
||||
, hasAlert_(hasAlert)
|
||||
, hasSound_(hasSound)
|
||||
, soundUrl_(soundUrl)
|
||||
, color_(color)
|
||||
{
|
||||
// check badgeName at initialization to reduce cost per isMatch call
|
||||
this->hasVersions_ = badgeName.contains("/");
|
||||
this->isMulti_ = badgeName.contains(",");
|
||||
if (this->isMulti_)
|
||||
{
|
||||
this->badges_ = badgeName.split(",");
|
||||
}
|
||||
}
|
||||
|
||||
const QString &HighlightBadge::badgeName() const
|
||||
{
|
||||
return this->badgeName_;
|
||||
}
|
||||
|
||||
const QString &HighlightBadge::displayName() const
|
||||
{
|
||||
return this->displayName_;
|
||||
}
|
||||
|
||||
bool HighlightBadge::hasAlert() const
|
||||
{
|
||||
return this->hasAlert_;
|
||||
}
|
||||
|
||||
bool HighlightBadge::hasSound() const
|
||||
{
|
||||
return this->hasSound_;
|
||||
}
|
||||
|
||||
bool HighlightBadge::isMatch(const Badge &badge) const
|
||||
{
|
||||
if (this->isMulti_)
|
||||
{
|
||||
for (const auto &id : this->badges_)
|
||||
{
|
||||
if (this->compare(id, badge))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
return this->compare(this->badgeName_, badge);
|
||||
}
|
||||
}
|
||||
|
||||
bool HighlightBadge::compare(const QString &id, const Badge &badge) const
|
||||
{
|
||||
if (this->hasVersions_)
|
||||
{
|
||||
auto parts = id.split("/");
|
||||
if (parts.size() == 2)
|
||||
{
|
||||
return parts.at(0).compare(badge.key_, Qt::CaseInsensitive) == 0 &&
|
||||
parts.at(1).compare(badge.value_, Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
return parts.at(0).compare(badge.key_, Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return id.compare(badge.key_, Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool HighlightBadge::hasCustomSound() const
|
||||
{
|
||||
return !this->soundUrl_.isEmpty();
|
||||
}
|
||||
|
||||
const QUrl &HighlightBadge::getSoundUrl() const
|
||||
{
|
||||
return this->soundUrl_;
|
||||
}
|
||||
|
||||
const std::shared_ptr<QColor> HighlightBadge::getColor() const
|
||||
{
|
||||
return this->color_;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
123
src/controllers/highlights/HighlightBadge.hpp
Normal file
|
@ -0,0 +1,123 @@
|
|||
#pragma once
|
||||
|
||||
#include "providers/twitch/TwitchBadge.hpp"
|
||||
#include "util/RapidJsonSerializeQString.hpp"
|
||||
#include "util/RapidjsonHelpers.hpp"
|
||||
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <pajlada/serialize.hpp>
|
||||
|
||||
namespace chatterino {
|
||||
class HighlightBadge
|
||||
{
|
||||
public:
|
||||
bool operator==(const HighlightBadge &other) const;
|
||||
|
||||
HighlightBadge(const QString &badgeName, const QString &displayName,
|
||||
bool hasAlert, bool hasSound, const QString &soundUrl,
|
||||
QColor color);
|
||||
|
||||
HighlightBadge(const QString &badgeName, const QString &displayName,
|
||||
bool hasAlert, bool hasSound, const QString &soundUrl,
|
||||
std::shared_ptr<QColor> color);
|
||||
|
||||
const QString &badgeName() const;
|
||||
const QString &displayName() const;
|
||||
bool hasAlert() const;
|
||||
bool hasSound() const;
|
||||
bool isMatch(const Badge &badge) const;
|
||||
|
||||
/**
|
||||
* @brief Check if this highlight phrase has a custom sound set.
|
||||
*
|
||||
* Note that this method only checks whether the path to the custom sound
|
||||
* is not empty. It does not check whether the file still exists, is a
|
||||
* sound file, or anything else.
|
||||
*
|
||||
* @return true, if the custom sound file path is not empty, false otherwise
|
||||
*/
|
||||
bool hasCustomSound() const;
|
||||
|
||||
const QUrl &getSoundUrl() const;
|
||||
const std::shared_ptr<QColor> getColor() const;
|
||||
|
||||
/*
|
||||
* XXX: Use the constexpr constructor here once we are building with
|
||||
* Qt>=5.13.
|
||||
*/
|
||||
static QColor FALLBACK_HIGHLIGHT_COLOR;
|
||||
|
||||
private:
|
||||
bool compare(const QString &id, const Badge &badge) const;
|
||||
|
||||
QString badgeName_;
|
||||
QString displayName_;
|
||||
bool hasAlert_;
|
||||
bool hasSound_;
|
||||
QUrl soundUrl_;
|
||||
std::shared_ptr<QColor> color_;
|
||||
|
||||
bool isMulti_;
|
||||
bool hasVersions_;
|
||||
QStringList badges_;
|
||||
};
|
||||
}; // namespace chatterino
|
||||
|
||||
namespace pajlada {
|
||||
|
||||
template <>
|
||||
struct Serialize<chatterino::HighlightBadge> {
|
||||
static rapidjson::Value get(const chatterino::HighlightBadge &value,
|
||||
rapidjson::Document::AllocatorType &a)
|
||||
{
|
||||
rapidjson::Value ret(rapidjson::kObjectType);
|
||||
|
||||
chatterino::rj::set(ret, "name", value.badgeName(), a);
|
||||
chatterino::rj::set(ret, "displayName", value.displayName(), a);
|
||||
chatterino::rj::set(ret, "alert", value.hasAlert(), a);
|
||||
chatterino::rj::set(ret, "sound", value.hasSound(), a);
|
||||
chatterino::rj::set(ret, "soundUrl", value.getSoundUrl().toString(), a);
|
||||
chatterino::rj::set(ret, "color",
|
||||
value.getColor()->name(QColor::HexArgb), a);
|
||||
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct Deserialize<chatterino::HighlightBadge> {
|
||||
static chatterino::HighlightBadge get(const rapidjson::Value &value,
|
||||
bool *error)
|
||||
{
|
||||
if (!value.IsObject())
|
||||
{
|
||||
PAJLADA_REPORT_ERROR(error);
|
||||
return chatterino::HighlightBadge(QString(), QString(), false,
|
||||
false, "", QColor());
|
||||
}
|
||||
|
||||
QString _name;
|
||||
QString _displayName;
|
||||
bool _hasAlert = true;
|
||||
bool _hasSound = false;
|
||||
QString _soundUrl;
|
||||
QString encodedColor;
|
||||
|
||||
chatterino::rj::getSafe(value, "name", _name);
|
||||
chatterino::rj::getSafe(value, "displayName", _displayName);
|
||||
chatterino::rj::getSafe(value, "alert", _hasAlert);
|
||||
chatterino::rj::getSafe(value, "sound", _hasSound);
|
||||
chatterino::rj::getSafe(value, "soundUrl", _soundUrl);
|
||||
chatterino::rj::getSafe(value, "color", encodedColor);
|
||||
|
||||
auto _color = QColor(encodedColor);
|
||||
if (!_color.isValid())
|
||||
_color = chatterino::HighlightBadge::FALLBACK_HIGHLIGHT_COLOR;
|
||||
|
||||
return chatterino::HighlightBadge(_name, _displayName, _hasAlert,
|
||||
_hasSound, _soundUrl, _color);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace pajlada
|
|
@ -31,6 +31,34 @@ namespace {
|
|||
}
|
||||
}
|
||||
|
||||
QStringList parseTagList(const QVariantMap &tags, const QString &key)
|
||||
{
|
||||
auto iterator = tags.find(key);
|
||||
if (iterator == tags.end())
|
||||
return QStringList{};
|
||||
|
||||
return iterator.value().toString().split(
|
||||
',', QString::SplitBehavior::SkipEmptyParts);
|
||||
}
|
||||
|
||||
std::vector<Badge> parseBadges(const QVariantMap &tags)
|
||||
{
|
||||
std::vector<Badge> badges;
|
||||
|
||||
for (QString badge : parseTagList(tags, "badges"))
|
||||
{
|
||||
QStringList parts = badge.split('/');
|
||||
if (parts.size() != 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
badges.emplace_back(parts[0], parts[1]);
|
||||
}
|
||||
|
||||
return badges;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SharedMessageBuilder::SharedMessageBuilder(
|
||||
|
@ -316,6 +344,53 @@ void SharedMessageBuilder::parseHighlights()
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight because of badge
|
||||
auto badges = parseBadges(this->tags);
|
||||
auto badgeHighlights = getCSettings().highlightedBadges.readOnly();
|
||||
bool badgeHighlightSet = false;
|
||||
for (const HighlightBadge &highlight : *badgeHighlights)
|
||||
{
|
||||
for (const Badge &badge : badges)
|
||||
{
|
||||
if (!highlight.isMatch(badge))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!badgeHighlightSet)
|
||||
{
|
||||
this->message().flags.set(MessageFlag::Highlighted);
|
||||
this->message().highlightColor = highlight.getColor();
|
||||
badgeHighlightSet = true;
|
||||
}
|
||||
|
||||
if (highlight.hasAlert())
|
||||
{
|
||||
this->highlightAlert_ = true;
|
||||
}
|
||||
|
||||
// Only set highlightSound_ if it hasn't been set by badge
|
||||
// highlights already.
|
||||
if (highlight.hasSound() && !this->highlightSound_)
|
||||
{
|
||||
this->highlightSound_ = true;
|
||||
// Use custom sound if set, otherwise use fallback sound
|
||||
this->highlightSoundUrl_ = highlight.hasCustomSound()
|
||||
? highlight.getSoundUrl()
|
||||
: getFallbackHighlightSound();
|
||||
}
|
||||
|
||||
if (this->highlightAlert_ && this->highlightSound_)
|
||||
{
|
||||
/*
|
||||
* Break once no further attributes (taskbar, sound) can be
|
||||
* applied.
|
||||
*/
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SharedMessageBuilder::addTextOrEmoji(EmotePtr emote)
|
||||
|
|
|
@ -7,60 +7,95 @@
|
|||
|
||||
#include "common/NetworkRequest.hpp"
|
||||
#include "common/Outcome.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "messages/Emote.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
TwitchBadges::TwitchBadges()
|
||||
{
|
||||
this->loadTwitchBadges();
|
||||
}
|
||||
|
||||
void TwitchBadges::loadTwitchBadges()
|
||||
{
|
||||
assert(this->loaded_ == false);
|
||||
|
||||
static QString url(
|
||||
"https://badges.twitch.tv/v1/badges/global/display?language=en");
|
||||
|
||||
NetworkRequest(url)
|
||||
|
||||
.onSuccess([this](auto result) -> Outcome {
|
||||
auto root = result.parseJson();
|
||||
auto badgeSets = this->badgeSets_.access();
|
||||
|
||||
auto jsonSets = root.value("badge_sets").toObject();
|
||||
for (auto sIt = jsonSets.begin(); sIt != jsonSets.end(); ++sIt)
|
||||
{
|
||||
auto key = sIt.key();
|
||||
auto versions =
|
||||
sIt.value().toObject().value("versions").toObject();
|
||||
auto root = result.parseJson();
|
||||
auto badgeSets = this->badgeSets_.access();
|
||||
|
||||
for (auto vIt = versions.begin(); vIt != versions.end(); ++vIt)
|
||||
auto jsonSets = root.value("badge_sets").toObject();
|
||||
for (auto sIt = jsonSets.begin(); sIt != jsonSets.end(); ++sIt)
|
||||
{
|
||||
auto versionObj = vIt.value().toObject();
|
||||
auto key = sIt.key();
|
||||
auto versions =
|
||||
sIt.value().toObject().value("versions").toObject();
|
||||
|
||||
auto emote = Emote{
|
||||
{""},
|
||||
ImageSet{
|
||||
Image::fromUrl(
|
||||
{versionObj.value("image_url_1x").toString()},
|
||||
1),
|
||||
Image::fromUrl(
|
||||
{versionObj.value("image_url_2x").toString()},
|
||||
.5),
|
||||
Image::fromUrl(
|
||||
{versionObj.value("image_url_4x").toString()},
|
||||
.25),
|
||||
},
|
||||
Tooltip{versionObj.value("title").toString()},
|
||||
Url{versionObj.value("click_url").toString()}};
|
||||
// "title"
|
||||
// "clickAction"
|
||||
for (auto vIt = versions.begin(); vIt != versions.end();
|
||||
++vIt)
|
||||
{
|
||||
auto versionObj = vIt.value().toObject();
|
||||
|
||||
(*badgeSets)[key][vIt.key()] =
|
||||
std::make_shared<Emote>(emote);
|
||||
auto emote = Emote{
|
||||
{""},
|
||||
ImageSet{
|
||||
Image::fromUrl({versionObj.value("image_url_1x")
|
||||
.toString()},
|
||||
1),
|
||||
Image::fromUrl({versionObj.value("image_url_2x")
|
||||
.toString()},
|
||||
.5),
|
||||
Image::fromUrl({versionObj.value("image_url_4x")
|
||||
.toString()},
|
||||
.25),
|
||||
},
|
||||
Tooltip{versionObj.value("title").toString()},
|
||||
Url{versionObj.value("click_url").toString()}};
|
||||
// "title"
|
||||
// "clickAction"
|
||||
|
||||
(*badgeSets)[key][vIt.key()] =
|
||||
std::make_shared<Emote>(emote);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this->loaded();
|
||||
return Success;
|
||||
})
|
||||
.onError([this](auto res) {
|
||||
qCDebug(chatterinoTwitch)
|
||||
<< "Error loading Twitch Badges:" << res.status();
|
||||
// Despite erroring out, we still want to reach the same point
|
||||
// Loaded should still be set to true to not build up an endless queue, and the quuee should still be flushed.
|
||||
this->loaded();
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
void TwitchBadges::loaded()
|
||||
{
|
||||
std::unique_lock loadedLock(this->loadedMutex_);
|
||||
|
||||
assert(this->loaded_ == false);
|
||||
|
||||
this->loaded_ = true;
|
||||
|
||||
// Flush callback queue
|
||||
std::unique_lock queueLock(this->queueMutex_);
|
||||
while (!this->callbackQueue_.empty())
|
||||
{
|
||||
auto callback = this->callbackQueue_.front();
|
||||
this->callbackQueue_.pop();
|
||||
this->getBadgeIcon(callback.first, callback.second);
|
||||
}
|
||||
}
|
||||
|
||||
boost::optional<EmotePtr> TwitchBadges::badge(const QString &set,
|
||||
const QString &version) const
|
||||
{
|
||||
|
@ -77,4 +112,117 @@ boost::optional<EmotePtr> TwitchBadges::badge(const QString &set,
|
|||
return boost::none;
|
||||
}
|
||||
|
||||
boost::optional<EmotePtr> TwitchBadges::badge(const QString &set) const
|
||||
{
|
||||
auto badgeSets = this->badgeSets_.access();
|
||||
auto it = badgeSets->find(set);
|
||||
if (it != badgeSets->end())
|
||||
{
|
||||
if (it->second.size() > 0)
|
||||
{
|
||||
return it->second.begin()->second;
|
||||
}
|
||||
}
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
void TwitchBadges::getBadgeIcon(const QString &name, BadgeIconCallback callback)
|
||||
{
|
||||
{
|
||||
std::shared_lock loadedLock(this->loadedMutex_);
|
||||
|
||||
if (!this->loaded_)
|
||||
{
|
||||
// Badges have not been loaded yet, store callback in a queue
|
||||
std::unique_lock queueLock(this->queueMutex_);
|
||||
this->callbackQueue_.push({name, std::move(callback)});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::shared_lock badgeLock(this->badgesMutex_);
|
||||
if (this->badgesMap_.contains(name))
|
||||
{
|
||||
callback(name, this->badgesMap_[name]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Split string in format "name1/version1,name2/version2" to "name1", "version1"
|
||||
// If not in list+version form, name will remain the same
|
||||
auto targetBadge = name.split(",").at(0).split("/");
|
||||
|
||||
const auto badge = targetBadge.size() == 2
|
||||
? this->badge(targetBadge.at(0), targetBadge.at(1))
|
||||
: this->badge(targetBadge.at(0));
|
||||
|
||||
if (badge)
|
||||
{
|
||||
this->loadEmoteImage(name, (*badge)->images.getImage3(),
|
||||
std::move(callback));
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchBadges::getBadgeIcon(const DisplayBadge &badge,
|
||||
BadgeIconCallback callback)
|
||||
{
|
||||
this->getBadgeIcon(badge.badgeName(), std::move(callback));
|
||||
}
|
||||
|
||||
void TwitchBadges::getBadgeIcons(const QList<DisplayBadge> &badges,
|
||||
BadgeIconCallback callback)
|
||||
{
|
||||
for (const auto &item : badges)
|
||||
{
|
||||
this->getBadgeIcon(item, callback);
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchBadges::loadEmoteImage(const QString &name, ImagePtr image,
|
||||
BadgeIconCallback &&callback)
|
||||
{
|
||||
NetworkRequest(image->url().string)
|
||||
.concurrent()
|
||||
.cache()
|
||||
.onSuccess([this, name, callback](auto result) -> Outcome {
|
||||
auto data = result.getData();
|
||||
|
||||
// const cast since we are only reading from it
|
||||
QBuffer buffer(const_cast<QByteArray *>(&data));
|
||||
buffer.open(QIODevice::ReadOnly);
|
||||
QImageReader reader(&buffer);
|
||||
|
||||
QImage image;
|
||||
if (reader.imageCount() == 0 || !reader.read(&image))
|
||||
{
|
||||
return Failure;
|
||||
}
|
||||
|
||||
auto icon = std::make_shared<QIcon>(QPixmap::fromImage(image));
|
||||
|
||||
{
|
||||
std::unique_lock lock(this->badgesMutex_);
|
||||
this->badgesMap_[name] = icon;
|
||||
}
|
||||
|
||||
callback(name, icon);
|
||||
|
||||
return Success;
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
TwitchBadges *TwitchBadges::instance_;
|
||||
|
||||
TwitchBadges *TwitchBadges::instance()
|
||||
{
|
||||
if (TwitchBadges::instance_ == nullptr)
|
||||
{
|
||||
TwitchBadges::instance_ = new TwitchBadges();
|
||||
}
|
||||
|
||||
return TwitchBadges::instance_;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -5,8 +5,14 @@
|
|||
#include <unordered_map>
|
||||
|
||||
#include "common/UniqueAccess.hpp"
|
||||
#include "messages/Image.hpp"
|
||||
#include "util/DisplayBadge.hpp"
|
||||
#include "util/QStringHash.hpp"
|
||||
|
||||
#include "pajlada/signals/signal.hpp"
|
||||
|
||||
#include <shared_mutex>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
struct Emote;
|
||||
|
@ -17,13 +23,42 @@ class Paths;
|
|||
|
||||
class TwitchBadges
|
||||
{
|
||||
public:
|
||||
void loadTwitchBadges();
|
||||
using QIconPtr = std::shared_ptr<QIcon>;
|
||||
using ImagePtr = std::shared_ptr<Image>;
|
||||
using BadgeIconCallback = std::function<void(QString, const QIconPtr)>;
|
||||
|
||||
public:
|
||||
static TwitchBadges *instance();
|
||||
|
||||
// Get badge from name and version
|
||||
boost::optional<EmotePtr> badge(const QString &set,
|
||||
const QString &version) const;
|
||||
// Get first matching badge with name, regardless of version
|
||||
boost::optional<EmotePtr> badge(const QString &set) const;
|
||||
|
||||
void getBadgeIcon(const QString &name, BadgeIconCallback callback);
|
||||
void getBadgeIcon(const DisplayBadge &badge, BadgeIconCallback callback);
|
||||
void getBadgeIcons(const QList<DisplayBadge> &badges,
|
||||
BadgeIconCallback callback);
|
||||
|
||||
private:
|
||||
static TwitchBadges *instance_;
|
||||
|
||||
TwitchBadges();
|
||||
void loadTwitchBadges();
|
||||
void loaded();
|
||||
void loadEmoteImage(const QString &name, ImagePtr image,
|
||||
BadgeIconCallback &&callback);
|
||||
|
||||
std::shared_mutex badgesMutex_;
|
||||
QMap<QString, QIconPtr> badgesMap_;
|
||||
|
||||
std::mutex queueMutex_;
|
||||
std::queue<QPair<QString, BadgeIconCallback>> callbackQueue_;
|
||||
|
||||
std::shared_mutex loadedMutex_;
|
||||
bool loaded_ = false;
|
||||
|
||||
UniqueAccess<
|
||||
std::unordered_map<QString, std::unordered_map<QString, EmotePtr>>>
|
||||
badgeSets_; // "bits": { "100": ... "500": ...
|
||||
|
|
|
@ -143,8 +143,7 @@ namespace {
|
|||
}
|
||||
} // namespace
|
||||
|
||||
TwitchChannel::TwitchChannel(const QString &name,
|
||||
TwitchBadges &globalTwitchBadges, BttvEmotes &bttv,
|
||||
TwitchChannel::TwitchChannel(const QString &name, BttvEmotes &bttv,
|
||||
FfzEmotes &ffz)
|
||||
: Channel(name, Channel::Type::Twitch)
|
||||
, ChannelChatters(*static_cast<Channel *>(this))
|
||||
|
@ -153,7 +152,6 @@ TwitchChannel::TwitchChannel(const QString &name,
|
|||
, channelUrl_("https://twitch.tv/" + name)
|
||||
, popoutPlayerUrl_("https://player.twitch.tv/?parent=twitch.tv&channel=" +
|
||||
name)
|
||||
, globalTwitchBadges_(globalTwitchBadges)
|
||||
, globalBttv_(bttv)
|
||||
, globalFfz_(ffz)
|
||||
, bttvEmotes_(std::make_shared<EmoteMap>())
|
||||
|
@ -478,11 +476,6 @@ SharedAccessGuard<const TwitchChannel::StreamStatus>
|
|||
return this->streamStatus_.accessConst();
|
||||
}
|
||||
|
||||
const TwitchBadges &TwitchChannel::globalTwitchBadges() const
|
||||
{
|
||||
return this->globalTwitchBadges_;
|
||||
}
|
||||
|
||||
const BttvEmotes &TwitchChannel::globalBttv() const
|
||||
{
|
||||
return this->globalBttv_;
|
||||
|
|
|
@ -87,7 +87,6 @@ public:
|
|||
SharedAccessGuard<const StreamStatus> accessStreamStatus() const;
|
||||
|
||||
// Emotes
|
||||
const TwitchBadges &globalTwitchBadges() const;
|
||||
const BttvEmotes &globalBttv() const;
|
||||
const FfzEmotes &globalFfz() const;
|
||||
boost::optional<EmotePtr> bttvEmote(const EmoteName &name) const;
|
||||
|
@ -128,9 +127,8 @@ private:
|
|||
} nameOptions;
|
||||
|
||||
protected:
|
||||
explicit TwitchChannel(const QString &channelName,
|
||||
TwitchBadges &globalTwitchBadges,
|
||||
BttvEmotes &globalBttv, FfzEmotes &globalFfz);
|
||||
explicit TwitchChannel(const QString &channelName, BttvEmotes &globalBttv,
|
||||
FfzEmotes &globalFfz);
|
||||
|
||||
private:
|
||||
// Methods
|
||||
|
@ -163,9 +161,6 @@ private:
|
|||
UniqueAccess<StreamStatus> streamStatus_;
|
||||
UniqueAccess<RoomModes> roomModes_;
|
||||
|
||||
// Emotes
|
||||
TwitchBadges &globalTwitchBadges_;
|
||||
|
||||
protected:
|
||||
BttvEmotes &globalBttv_;
|
||||
FfzEmotes &globalFfz_;
|
||||
|
|
|
@ -45,7 +45,6 @@ void TwitchIrcServer::initialize(Settings &settings, Paths &paths)
|
|||
});
|
||||
});
|
||||
|
||||
this->twitchBadges.loadTwitchBadges();
|
||||
this->bttv.loadEmotes();
|
||||
this->ffz.loadEmotes();
|
||||
}
|
||||
|
@ -88,8 +87,8 @@ void TwitchIrcServer::initializeConnection(IrcConnection *connection,
|
|||
std::shared_ptr<Channel> TwitchIrcServer::createChannel(
|
||||
const QString &channelName)
|
||||
{
|
||||
auto channel = std::shared_ptr<TwitchChannel>(new TwitchChannel(
|
||||
channelName, this->twitchBadges, this->bttv, this->ffz));
|
||||
auto channel = std::shared_ptr<TwitchChannel>(
|
||||
new TwitchChannel(channelName, this->bttv, this->ffz));
|
||||
channel->initialize();
|
||||
|
||||
channel->sendMessageSignal.connect(
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
#include "providers/bttv/BttvEmotes.hpp"
|
||||
#include "providers/ffz/FfzEmotes.hpp"
|
||||
#include "providers/irc/AbstractIrcServer.hpp"
|
||||
#include "providers/twitch/TwitchBadges.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
|
@ -75,7 +74,6 @@ private:
|
|||
std::chrono::steady_clock::time_point lastErrorTimeSpeed_;
|
||||
std::chrono::steady_clock::time_point lastErrorTimeAmount_;
|
||||
|
||||
TwitchBadges twitchBadges;
|
||||
BttvEmotes bttv;
|
||||
FfzEmotes ffz;
|
||||
|
||||
|
|
|
@ -1083,8 +1083,8 @@ boost::optional<EmotePtr> TwitchMessageBuilder::getTwitchBadge(
|
|||
return channelBadge;
|
||||
}
|
||||
|
||||
if (auto globalBadge = this->twitchChannel->globalTwitchBadges().badge(
|
||||
badge.key_, badge.value_))
|
||||
if (auto globalBadge =
|
||||
TwitchBadges::instance()->badge(badge.key_, badge.value_))
|
||||
{
|
||||
return globalBadge;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ ConcurrentSettings::ConcurrentSettings()
|
|||
// NOTE: these do not get deleted
|
||||
: highlightedMessages(*new SignalVector<HighlightPhrase>())
|
||||
, highlightedUsers(*new SignalVector<HighlightPhrase>())
|
||||
, highlightedBadges(*new SignalVector<HighlightBadge>())
|
||||
, blacklistedUsers(*new SignalVector<HighlightBlacklistUser>())
|
||||
, ignoredMessages(*new SignalVector<IgnorePhrase>())
|
||||
, mutedChannels(*new SignalVector<QString>())
|
||||
|
@ -26,6 +27,7 @@ ConcurrentSettings::ConcurrentSettings()
|
|||
{
|
||||
persist(this->highlightedMessages, "/highlighting/highlights");
|
||||
persist(this->blacklistedUsers, "/highlighting/blacklist");
|
||||
persist(this->highlightedBadges, "/highlighting/badges");
|
||||
persist(this->highlightedUsers, "/highlighting/users");
|
||||
persist(this->ignoredMessages, "/ignore/phrases");
|
||||
persist(this->mutedChannels, "/pings/muted");
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#include "common/Channel.hpp"
|
||||
#include "common/SignalVector.hpp"
|
||||
#include "controllers/filters/FilterRecord.hpp"
|
||||
#include "controllers/highlights/HighlightBadge.hpp"
|
||||
#include "controllers/highlights/HighlightPhrase.hpp"
|
||||
#include "controllers/moderationactions/ModerationAction.hpp"
|
||||
#include "singletons/Toasts.hpp"
|
||||
|
@ -31,6 +32,7 @@ public:
|
|||
|
||||
SignalVector<HighlightPhrase> &highlightedMessages;
|
||||
SignalVector<HighlightPhrase> &highlightedUsers;
|
||||
SignalVector<HighlightBadge> &highlightedBadges;
|
||||
SignalVector<HighlightBlacklistUser> &blacklistedUsers;
|
||||
SignalVector<IgnorePhrase> &ignoredMessages;
|
||||
SignalVector<QString> &mutedChannels;
|
||||
|
|
20
src/util/DisplayBadge.cpp
Normal file
|
@ -0,0 +1,20 @@
|
|||
#include "DisplayBadge.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
DisplayBadge::DisplayBadge(QString displayName, QString badgeName)
|
||||
: displayName_(displayName)
|
||||
, badgeName_(badgeName)
|
||||
{
|
||||
}
|
||||
|
||||
QString DisplayBadge::displayName() const
|
||||
{
|
||||
return this->displayName_;
|
||||
}
|
||||
|
||||
QString DisplayBadge::badgeName() const
|
||||
{
|
||||
return this->badgeName_;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
17
src/util/DisplayBadge.hpp
Normal file
|
@ -0,0 +1,17 @@
|
|||
#pragma once
|
||||
|
||||
namespace chatterino {
|
||||
class DisplayBadge
|
||||
{
|
||||
public:
|
||||
DisplayBadge(QString displayName, QString badgeName);
|
||||
|
||||
QString displayName() const;
|
||||
QString badgeName() const;
|
||||
|
||||
private:
|
||||
QString displayName_;
|
||||
QString badgeName_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
71
src/widgets/dialogs/BadgePickerDialog.cpp
Normal file
|
@ -0,0 +1,71 @@
|
|||
#include "BadgePickerDialog.hpp"
|
||||
#include <QSizePolicy>
|
||||
#include "singletons/Resources.hpp"
|
||||
|
||||
#include "providers/twitch/TwitchBadges.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
BadgePickerDialog::BadgePickerDialog(QList<DisplayBadge> badges,
|
||||
QWidget *parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
this->dropdown_ = new QComboBox;
|
||||
auto vbox = new QVBoxLayout(this);
|
||||
auto buttonBox =
|
||||
new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
|
||||
vbox->addWidget(this->dropdown_);
|
||||
vbox->addWidget(buttonBox);
|
||||
|
||||
QObject::connect(buttonBox, &QDialogButtonBox::accepted, [this] {
|
||||
this->accept();
|
||||
this->close();
|
||||
});
|
||||
QObject::connect(buttonBox, &QDialogButtonBox::rejected, [this] {
|
||||
this->reject();
|
||||
this->close();
|
||||
});
|
||||
|
||||
this->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
|
||||
this->setWindowFlags(
|
||||
(this->windowFlags() & ~(Qt::WindowContextHelpButtonHint)) |
|
||||
Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint);
|
||||
|
||||
// Add items.
|
||||
for (const auto &item : badges)
|
||||
{
|
||||
this->dropdown_->addItem(item.displayName(), item.badgeName());
|
||||
}
|
||||
|
||||
const auto updateBadge = [=](int index) {
|
||||
BadgeOpt badge;
|
||||
if (index >= 0 && index < badges.size())
|
||||
{
|
||||
badge = badges[index];
|
||||
}
|
||||
this->currentBadge_ = badge;
|
||||
};
|
||||
|
||||
QObject::connect(this->dropdown_,
|
||||
QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
updateBadge);
|
||||
updateBadge(0);
|
||||
|
||||
// Set icons.
|
||||
TwitchBadges::instance()->getBadgeIcons(
|
||||
badges,
|
||||
[&dropdown = this->dropdown_](QString identifier, const QIconPtr icon) {
|
||||
if (!dropdown)
|
||||
return;
|
||||
|
||||
int index = dropdown->findData(identifier);
|
||||
if (index != -1)
|
||||
{
|
||||
dropdown->setItemIcon(index, *icon);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
29
src/widgets/dialogs/BadgePickerDialog.hpp
Normal file
|
@ -0,0 +1,29 @@
|
|||
#pragma once
|
||||
|
||||
#include "util/DisplayBadge.hpp"
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class BadgePickerDialog : public QDialog,
|
||||
public std::enable_shared_from_this<BadgePickerDialog>
|
||||
{
|
||||
using QIconPtr = std::shared_ptr<QIcon>;
|
||||
using BadgeOpt = boost::optional<DisplayBadge>;
|
||||
|
||||
public:
|
||||
BadgePickerDialog(QList<DisplayBadge> badges, QWidget *parent = nullptr);
|
||||
|
||||
BadgeOpt getSelection() const
|
||||
{
|
||||
return this->currentBadge_;
|
||||
}
|
||||
|
||||
private:
|
||||
QComboBox *dropdown_;
|
||||
|
||||
BadgeOpt currentBadge_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
|
@ -1,6 +1,7 @@
|
|||
#include "HighlightingPage.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "controllers/highlights/BadgeHighlightModel.hpp"
|
||||
#include "controllers/highlights/HighlightBlacklistModel.hpp"
|
||||
#include "controllers/highlights/HighlightModel.hpp"
|
||||
#include "controllers/highlights/UserHighlightModel.hpp"
|
||||
|
@ -8,6 +9,7 @@
|
|||
#include "singletons/Theme.hpp"
|
||||
#include "util/LayoutCreator.hpp"
|
||||
#include "util/StandardItemHelper.hpp"
|
||||
#include "widgets/dialogs/BadgePickerDialog.hpp"
|
||||
#include "widgets/dialogs/ColorPickerDialog.hpp"
|
||||
|
||||
#include <QFileDialog>
|
||||
|
@ -27,6 +29,20 @@
|
|||
|
||||
namespace chatterino {
|
||||
|
||||
namespace {
|
||||
// Add additional badges for highlights here
|
||||
QList<DisplayBadge> availableBadges = {
|
||||
{"Broadcaster", "broadcaster"},
|
||||
{"Admin", "admin"},
|
||||
{"Staff", "staff"},
|
||||
{"Moderator", "moderator"},
|
||||
{"Verified", "partner"},
|
||||
{"VIP", "vip"},
|
||||
{"Predicted Blue", "predictions/blue-1,predictions/blue-2"},
|
||||
{"Predicted Pink", "predictions/pink-2,predictions/pink-1"},
|
||||
};
|
||||
} // namespace
|
||||
|
||||
HighlightingPage::HighlightingPage()
|
||||
{
|
||||
LayoutCreator<HighlightingPage> layoutCreator(this);
|
||||
|
@ -45,7 +61,9 @@ HighlightingPage::HighlightingPage()
|
|||
{
|
||||
highlights.emplace<QLabel>(
|
||||
"Play notification sounds and highlight messages based on "
|
||||
"certain patterns.");
|
||||
"certain patterns.\n"
|
||||
"Message highlights are prioritized over badge highlights, "
|
||||
"but under user highlights");
|
||||
|
||||
auto view =
|
||||
highlights
|
||||
|
@ -80,7 +98,8 @@ HighlightingPage::HighlightingPage()
|
|||
|
||||
QObject::connect(view->getTableView(), &QTableView::clicked,
|
||||
[this, view](const QModelIndex &clicked) {
|
||||
this->tableCellClicked(clicked, view);
|
||||
this->tableCellClicked(
|
||||
clicked, view, HighlightTab::Messages);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -89,7 +108,7 @@ HighlightingPage::HighlightingPage()
|
|||
pingUsers.emplace<QLabel>(
|
||||
"Play notification sounds and highlight messages from "
|
||||
"certain users.\n"
|
||||
"User highlights are prioritized over message "
|
||||
"User highlights are prioritized over message and badge "
|
||||
"highlights.");
|
||||
EditableModelView *view =
|
||||
pingUsers
|
||||
|
@ -130,7 +149,61 @@ HighlightingPage::HighlightingPage()
|
|||
|
||||
QObject::connect(view->getTableView(), &QTableView::clicked,
|
||||
[this, view](const QModelIndex &clicked) {
|
||||
this->tableCellClicked(clicked, view);
|
||||
this->tableCellClicked(
|
||||
clicked, view, HighlightTab::Users);
|
||||
});
|
||||
}
|
||||
|
||||
auto badgeHighlights = tabs.appendTab(new QVBoxLayout, "Badges");
|
||||
{
|
||||
badgeHighlights.emplace<QLabel>(
|
||||
"Play notification sounds and highlight messages based on "
|
||||
"user badges.\n"
|
||||
"Badge highlights are prioritzed under user and message "
|
||||
"highlights.");
|
||||
auto view = badgeHighlights
|
||||
.emplace<EditableModelView>(
|
||||
(new BadgeHighlightModel(nullptr))
|
||||
->initialized(
|
||||
&getSettings()->highlightedBadges))
|
||||
.getElement();
|
||||
view->setTitles({"Name", "Flash\ntaskbar", "Play\nsound",
|
||||
"Custom\nsound", "Color"});
|
||||
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
||||
QHeaderView::Fixed);
|
||||
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
||||
0, QHeaderView::Stretch);
|
||||
|
||||
// fourtf: make class extrend BaseWidget and add this to
|
||||
// dpiChanged
|
||||
QTimer::singleShot(1, [view] {
|
||||
view->getTableView()->resizeColumnsToContents();
|
||||
view->getTableView()->setColumnWidth(0, 200);
|
||||
});
|
||||
|
||||
view->addButtonPressed.connect([this] {
|
||||
auto d = std::make_shared<BadgePickerDialog>(
|
||||
availableBadges, this);
|
||||
|
||||
d->setWindowTitle("Choose badge");
|
||||
if (d->exec() == QDialog::Accepted)
|
||||
{
|
||||
auto s = d->getSelection();
|
||||
if (!s)
|
||||
{
|
||||
return;
|
||||
}
|
||||
getSettings()->highlightedBadges.append(HighlightBadge{
|
||||
s->badgeName(), s->displayName(), false, false, "",
|
||||
ColorProvider::instance().color(
|
||||
ColorType::SelfHighlight)});
|
||||
}
|
||||
});
|
||||
|
||||
QObject::connect(view->getTableView(), &QTableView::clicked,
|
||||
[this, view](const QModelIndex &clicked) {
|
||||
this->tableCellClicked(
|
||||
clicked, view, HighlightTab::Badges);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -209,73 +282,97 @@ HighlightingPage::HighlightingPage()
|
|||
|
||||
// ---- misc
|
||||
this->disabledUsersChangedTimer_.setSingleShot(true);
|
||||
} // namespace chatterino
|
||||
}
|
||||
|
||||
void HighlightingPage::tableCellClicked(const QModelIndex &clicked,
|
||||
EditableModelView *view)
|
||||
void HighlightingPage::openSoundDialog(const QModelIndex &clicked,
|
||||
EditableModelView *view, int soundColumn)
|
||||
{
|
||||
using Column = HighlightModel::Column;
|
||||
auto fileUrl = QFileDialog::getOpenFileUrl(this, tr("Open Sound"), QUrl(),
|
||||
tr("Audio Files (*.mp3 *.wav)"));
|
||||
view->getModel()->setData(clicked, fileUrl, Qt::UserRole);
|
||||
view->getModel()->setData(clicked, fileUrl.fileName(), Qt::DisplayRole);
|
||||
|
||||
if (clicked.column() == Column::SoundPath)
|
||||
// Enable custom sound check box if user set a sound
|
||||
if (!fileUrl.isEmpty())
|
||||
{
|
||||
auto fileUrl = QFileDialog::getOpenFileUrl(
|
||||
this, tr("Open Sound"), QUrl(), tr("Audio Files (*.mp3 *.wav)"));
|
||||
view->getModel()->setData(clicked, fileUrl, Qt::UserRole);
|
||||
view->getModel()->setData(clicked, fileUrl.fileName(), Qt::DisplayRole);
|
||||
|
||||
// Enable custom sound check box if user set a sound
|
||||
if (!fileUrl.isEmpty())
|
||||
{
|
||||
QModelIndex checkBox = clicked.siblingAtColumn(Column::PlaySound);
|
||||
view->getModel()->setData(checkBox, Qt::Checked,
|
||||
Qt::CheckStateRole);
|
||||
}
|
||||
QModelIndex checkBox = clicked.siblingAtColumn(soundColumn);
|
||||
view->getModel()->setData(checkBox, Qt::Checked, Qt::CheckStateRole);
|
||||
}
|
||||
else if (clicked.column() == Column::Color)
|
||||
{
|
||||
// Hacky (?) way to figure out what tab the cell was clicked in
|
||||
const bool fromMessagesTab =
|
||||
(dynamic_cast<HighlightModel *>(view->getModel()) != nullptr);
|
||||
}
|
||||
|
||||
if (fromMessagesTab && clicked.row() == HighlightModel::WHISPER_ROW)
|
||||
return;
|
||||
void HighlightingPage::openColorDialog(const QModelIndex &clicked,
|
||||
EditableModelView *view,
|
||||
HighlightTab tab)
|
||||
{
|
||||
auto initial =
|
||||
view->getModel()->data(clicked, Qt::DecorationRole).value<QColor>();
|
||||
|
||||
auto initial =
|
||||
view->getModel()->data(clicked, Qt::DecorationRole).value<QColor>();
|
||||
auto dialog = new ColorPickerDialog(initial, this);
|
||||
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dialog->show();
|
||||
dialog->closed.connect([=](auto selected) {
|
||||
if (selected.isValid())
|
||||
{
|
||||
view->getModel()->setData(clicked, selected, Qt::DecorationRole);
|
||||
|
||||
auto dialog = new ColorPickerDialog(initial, this);
|
||||
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dialog->show();
|
||||
dialog->closed.connect([=](QColor selected) {
|
||||
if (selected.isValid())
|
||||
if (tab == HighlightTab::Messages)
|
||||
{
|
||||
view->getModel()->setData(clicked, selected,
|
||||
Qt::DecorationRole);
|
||||
|
||||
if (fromMessagesTab)
|
||||
/*
|
||||
* For preset highlights in the "Messages" tab, we need to
|
||||
* manually update the color map.
|
||||
*/
|
||||
auto instance = ColorProvider::instance();
|
||||
switch (clicked.row())
|
||||
{
|
||||
/*
|
||||
* For preset highlights in the "Messages" tab, we need to
|
||||
* manually update the color map.
|
||||
*/
|
||||
auto instance = ColorProvider::instance();
|
||||
switch (clicked.row())
|
||||
{
|
||||
case 0:
|
||||
instance.updateColor(ColorType::SelfHighlight,
|
||||
selected);
|
||||
break;
|
||||
case 1:
|
||||
instance.updateColor(ColorType::Whisper, selected);
|
||||
break;
|
||||
case 2:
|
||||
instance.updateColor(ColorType::Subscription,
|
||||
selected);
|
||||
break;
|
||||
}
|
||||
case 0:
|
||||
instance.updateColor(ColorType::SelfHighlight,
|
||||
selected);
|
||||
break;
|
||||
case 1:
|
||||
instance.updateColor(ColorType::Whisper, selected);
|
||||
break;
|
||||
case 2:
|
||||
instance.updateColor(ColorType::Subscription, selected);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void HighlightingPage::tableCellClicked(const QModelIndex &clicked,
|
||||
EditableModelView *view,
|
||||
HighlightTab tab)
|
||||
{
|
||||
switch (tab)
|
||||
{
|
||||
case HighlightTab::Messages:
|
||||
case HighlightTab::Users: {
|
||||
using Column = HighlightModel::Column;
|
||||
if (clicked.column() == Column::SoundPath)
|
||||
{
|
||||
this->openSoundDialog(clicked, view, Column::SoundPath);
|
||||
}
|
||||
else if (clicked.column() == Column::Color &&
|
||||
clicked.row() != HighlightModel::WHISPER_ROW)
|
||||
{
|
||||
this->openColorDialog(clicked, view, tab);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case HighlightTab::Badges: {
|
||||
using Column = BadgeHighlightModel::Column;
|
||||
if (clicked.column() == Column::SoundPath)
|
||||
{
|
||||
this->openSoundDialog(clicked, view, Column::SoundPath);
|
||||
}
|
||||
else if (clicked.column() == Column::Color)
|
||||
{
|
||||
this->openColorDialog(clicked, view, tab);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,9 +17,16 @@ public:
|
|||
HighlightingPage();
|
||||
|
||||
private:
|
||||
enum HighlightTab { Messages = 0, Users = 1, Badges = 2, Blacklist = 3 };
|
||||
|
||||
QTimer disabledUsersChangedTimer_;
|
||||
|
||||
void tableCellClicked(const QModelIndex &clicked, EditableModelView *view);
|
||||
void tableCellClicked(const QModelIndex &clicked, EditableModelView *view,
|
||||
HighlightTab tab);
|
||||
void openSoundDialog(const QModelIndex &clicked, EditableModelView *view,
|
||||
int soundColumn);
|
||||
void openColorDialog(const QModelIndex &clicked, EditableModelView *view,
|
||||
HighlightTab tab);
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|