mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-21 22:24:07 +01:00
Estimate size of images to avoid layout shifts (#5192)
This commit is contained in:
parent
0cfd25ce8e
commit
f285ada36c
13 changed files with 145 additions and 70 deletions
|
@ -162,6 +162,7 @@
|
|||
- Dev: Specialize `Atomic<std::shared_ptr<T>>` if underlying standard library supports it. (#5133)
|
||||
- Dev: Added the `developer_name` field to the Linux AppData specification. (#5138)
|
||||
- Dev: Twitch messages can be sent using Twitch's Helix API instead of IRC (disabled by default). (#5200)
|
||||
- Dev: Added estimation for image sizes to avoid layout shifts. (#5192)
|
||||
|
||||
## 2.4.6
|
||||
|
||||
|
|
|
@ -318,7 +318,7 @@ Image::~Image()
|
|||
}
|
||||
}
|
||||
|
||||
ImagePtr Image::fromUrl(const Url &url, qreal scale)
|
||||
ImagePtr Image::fromUrl(const Url &url, qreal scale, QSize expectedSize)
|
||||
{
|
||||
static std::unordered_map<Url, std::weak_ptr<Image>> cache;
|
||||
static std::mutex mutex;
|
||||
|
@ -329,7 +329,7 @@ ImagePtr Image::fromUrl(const Url &url, qreal scale)
|
|||
|
||||
if (!shared)
|
||||
{
|
||||
cache[url] = shared = ImagePtr(new Image(url, scale));
|
||||
cache[url] = shared = ImagePtr(new Image(url, scale, expectedSize));
|
||||
}
|
||||
|
||||
return shared;
|
||||
|
@ -382,9 +382,11 @@ Image::Image()
|
|||
{
|
||||
}
|
||||
|
||||
Image::Image(const Url &url, qreal scale)
|
||||
Image::Image(const Url &url, qreal scale, QSize expectedSize)
|
||||
: url_(url)
|
||||
, scale_(scale)
|
||||
, expectedSize_(expectedSize.isValid() ? expectedSize
|
||||
: (QSize(16, 16) * scale))
|
||||
, shouldLoad_(true)
|
||||
, frames_(std::make_unique<detail::Frames>())
|
||||
{
|
||||
|
@ -477,11 +479,11 @@ int Image::width() const
|
|||
|
||||
if (auto pixmap = this->frames_->first())
|
||||
{
|
||||
return int(pixmap->width() * this->scale_);
|
||||
return static_cast<int>(pixmap->width() * this->scale_);
|
||||
}
|
||||
|
||||
// No frames loaded, use our default magic width 16
|
||||
return 16;
|
||||
// No frames loaded, use the expected size
|
||||
return static_cast<int>(this->expectedSize_.width() * this->scale_);
|
||||
}
|
||||
|
||||
int Image::height() const
|
||||
|
@ -490,11 +492,11 @@ int Image::height() const
|
|||
|
||||
if (auto pixmap = this->frames_->first())
|
||||
{
|
||||
return int(pixmap->height() * this->scale_);
|
||||
return static_cast<int>(pixmap->height() * this->scale_);
|
||||
}
|
||||
|
||||
// No frames loaded, use our default magic height 16
|
||||
return 16;
|
||||
// No frames loaded, use the expected size
|
||||
return static_cast<int>(this->expectedSize_.height() * this->scale_);
|
||||
}
|
||||
|
||||
void Image::actuallyLoad()
|
||||
|
|
|
@ -73,7 +73,8 @@ public:
|
|||
Image(Image &&) = delete;
|
||||
Image &operator=(Image &&) = delete;
|
||||
|
||||
static ImagePtr fromUrl(const Url &url, qreal scale = 1);
|
||||
static ImagePtr fromUrl(const Url &url, qreal scale = 1,
|
||||
QSize expectedSize = {});
|
||||
static ImagePtr fromResourcePixmap(const QPixmap &pixmap, qreal scale = 1);
|
||||
static ImagePtr getEmpty();
|
||||
|
||||
|
@ -93,7 +94,7 @@ public:
|
|||
|
||||
private:
|
||||
Image();
|
||||
Image(const Url &url, qreal scale);
|
||||
Image(const Url &url, qreal scale, QSize expectedSize);
|
||||
Image(qreal scale);
|
||||
|
||||
void setPixmap(const QPixmap &pixmap);
|
||||
|
@ -102,12 +103,18 @@ private:
|
|||
|
||||
const Url url_{};
|
||||
const qreal scale_{1};
|
||||
/// @brief The expected size of this image once its loaded.
|
||||
///
|
||||
/// This doesn't represent the actual size (it can be different) - it's
|
||||
/// just an estimation and provided to avoid (large) layout shifts when
|
||||
/// loading images.
|
||||
const QSize expectedSize_{16, 16};
|
||||
std::atomic_bool empty_{false};
|
||||
|
||||
mutable std::chrono::time_point<std::chrono::steady_clock> lastUsed_;
|
||||
|
||||
bool shouldLoad_{false};
|
||||
|
||||
mutable std::chrono::time_point<std::chrono::steady_clock> lastUsed_;
|
||||
|
||||
// gui thread only
|
||||
std::unique_ptr<detail::Frames> frames_{};
|
||||
|
||||
|
|
|
@ -22,6 +22,8 @@ namespace {
|
|||
"This channel has no BetterTTV channel emotes.");
|
||||
|
||||
QString emoteLinkFormat("https://betterttv.com/emotes/%1");
|
||||
// BTTV doesn't provide any data on the size, so we assume an emote is 28x28
|
||||
constexpr QSize EMOTE_BASE_SIZE(28, 28);
|
||||
|
||||
struct CreateEmoteResult {
|
||||
EmoteId id;
|
||||
|
@ -65,9 +67,12 @@ namespace {
|
|||
|
||||
auto emote = Emote({
|
||||
name,
|
||||
ImageSet{Image::fromUrl(getEmoteLinkV3(id, "1x"), 1),
|
||||
Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5),
|
||||
Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25)},
|
||||
ImageSet{Image::fromUrl(getEmoteLinkV3(id, "1x"), 1,
|
||||
EMOTE_BASE_SIZE),
|
||||
Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5,
|
||||
EMOTE_BASE_SIZE * 2),
|
||||
Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25,
|
||||
EMOTE_BASE_SIZE * 4)},
|
||||
Tooltip{name.string + "<br>Global BetterTTV Emote"},
|
||||
Url{emoteLinkFormat.arg(id.string)},
|
||||
});
|
||||
|
@ -90,9 +95,11 @@ namespace {
|
|||
auto emote = Emote({
|
||||
name,
|
||||
ImageSet{
|
||||
Image::fromUrl(getEmoteLinkV3(id, "1x"), 1),
|
||||
Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5),
|
||||
Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25),
|
||||
Image::fromUrl(getEmoteLinkV3(id, "1x"), 1, EMOTE_BASE_SIZE),
|
||||
Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5,
|
||||
EMOTE_BASE_SIZE * 2),
|
||||
Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25,
|
||||
EMOTE_BASE_SIZE * 4),
|
||||
},
|
||||
Tooltip{
|
||||
QString("%1<br>%2 BetterTTV Emote<br>By: %3")
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#include "common/network/NetworkRequest.hpp"
|
||||
#include "common/network/NetworkResult.hpp"
|
||||
#include "messages/Emote.hpp"
|
||||
#include "messages/Image.hpp"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
@ -44,12 +45,23 @@ void ChatterinoBadges::loadChatterinoBadges()
|
|||
jsonRoot.value("badges").toArray())
|
||||
{
|
||||
auto jsonBadge = jsonBadgeValue.toObject();
|
||||
// The sizes for the images are only an estimation, there might
|
||||
// be badges with different sizes.
|
||||
constexpr QSize baseSize(18, 18);
|
||||
auto emote = Emote{
|
||||
.name = EmoteName{},
|
||||
.images =
|
||||
ImageSet{Url{jsonBadge.value("image1").toString()},
|
||||
Url{jsonBadge.value("image2").toString()},
|
||||
Url{jsonBadge.value("image3").toString()}},
|
||||
ImageSet{
|
||||
Image::fromUrl(
|
||||
Url{jsonBadge.value("image1").toString()}, 1.0,
|
||||
baseSize),
|
||||
Image::fromUrl(
|
||||
Url{jsonBadge.value("image2").toString()}, 0.5,
|
||||
baseSize * 2),
|
||||
Image::fromUrl(
|
||||
Url{jsonBadge.value("image3").toString()}, 0.25,
|
||||
baseSize * 4),
|
||||
},
|
||||
.tooltip = Tooltip{jsonBadge.value("tooltip").toString()},
|
||||
.homePage = Url{},
|
||||
};
|
||||
|
|
|
@ -285,7 +285,8 @@ void Emojis::loadEmojiSet()
|
|||
}
|
||||
QString url = urlPrefix + code + ".png";
|
||||
emoji->emote = std::make_shared<Emote>(Emote{
|
||||
EmoteName{emoji->value}, ImageSet{Image::fromUrl({url}, 0.35)},
|
||||
EmoteName{emoji->value},
|
||||
ImageSet{Image::fromUrl({url}, 0.35, {64, 64})},
|
||||
Tooltip{":" + emoji->shortCodes[0] + ":<br/>Emoji"}, Url{}});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#include "common/network/NetworkRequest.hpp"
|
||||
#include "common/network/NetworkResult.hpp"
|
||||
#include "messages/Emote.hpp"
|
||||
#include "messages/Image.hpp"
|
||||
#include "providers/ffz/FfzUtil.hpp"
|
||||
|
||||
#include <QJsonArray>
|
||||
|
@ -68,13 +69,21 @@ void FfzBadges::load()
|
|||
{
|
||||
auto jsonBadge = jsonBadge_.toObject();
|
||||
auto jsonUrls = jsonBadge.value("urls").toObject();
|
||||
QSize baseSize(jsonBadge["width"].toInt(18),
|
||||
jsonBadge["height"].toInt(18));
|
||||
|
||||
auto emote =
|
||||
Emote{EmoteName{},
|
||||
ImageSet{parseFfzUrl(jsonUrls.value("1").toString()),
|
||||
parseFfzUrl(jsonUrls.value("2").toString()),
|
||||
parseFfzUrl(jsonUrls.value("4").toString())},
|
||||
Tooltip{jsonBadge.value("title").toString()}, Url{}};
|
||||
auto emote = Emote{
|
||||
EmoteName{},
|
||||
ImageSet{Image::fromUrl(
|
||||
parseFfzUrl(jsonUrls.value("1").toString()),
|
||||
1.0, baseSize),
|
||||
Image::fromUrl(
|
||||
parseFfzUrl(jsonUrls.value("2").toString()),
|
||||
0.5, baseSize * 2),
|
||||
Image::fromUrl(
|
||||
parseFfzUrl(jsonUrls.value("4").toString()),
|
||||
0.25, baseSize * 4)},
|
||||
Tooltip{jsonBadge.value("title").toString()}, Url{}};
|
||||
|
||||
Badge badge;
|
||||
|
||||
|
|
|
@ -20,6 +20,10 @@ const auto &LOG = chatterinoFfzemotes;
|
|||
const QString CHANNEL_HAS_NO_EMOTES(
|
||||
"This channel has no FrankerFaceZ channel emotes.");
|
||||
|
||||
// FFZ doesn't provide any data on the size for room badges,
|
||||
// so we assume 18x18 (same as a Twitch badge)
|
||||
constexpr QSize BASE_BADGE_SIZE(18, 18);
|
||||
|
||||
Url getEmoteLink(const QJsonObject &urls, const QString &emoteScale)
|
||||
{
|
||||
auto emote = urls[emoteScale];
|
||||
|
@ -33,20 +37,23 @@ Url getEmoteLink(const QJsonObject &urls, const QString &emoteScale)
|
|||
return parseFfzUrl(emote.toString());
|
||||
}
|
||||
|
||||
void fillInEmoteData(const QJsonObject &urls, const EmoteName &name,
|
||||
const QString &tooltip, Emote &emoteData)
|
||||
void fillInEmoteData(const QJsonObject &emote, const QJsonObject &urls,
|
||||
const EmoteName &name, const QString &tooltip,
|
||||
Emote &emoteData)
|
||||
{
|
||||
auto url1x = getEmoteLink(urls, "1");
|
||||
auto url2x = getEmoteLink(urls, "2");
|
||||
auto url3x = getEmoteLink(urls, "4");
|
||||
QSize baseSize(emote["width"].toInt(28), emote["height"].toInt(28));
|
||||
|
||||
//, code, tooltip
|
||||
emoteData.name = name;
|
||||
emoteData.images = ImageSet{
|
||||
Image::fromUrl(url1x, 1),
|
||||
url2x.string.isEmpty() ? Image::getEmpty() : Image::fromUrl(url2x, 0.5),
|
||||
Image::fromUrl(url1x, 1, baseSize),
|
||||
url2x.string.isEmpty() ? Image::getEmpty()
|
||||
: Image::fromUrl(url2x, 0.5, baseSize * 2),
|
||||
url3x.string.isEmpty() ? Image::getEmpty()
|
||||
: Image::fromUrl(url3x, 0.25)};
|
||||
: Image::fromUrl(url3x, 0.25, baseSize * 4)};
|
||||
emoteData.tooltip = {tooltip};
|
||||
}
|
||||
|
||||
|
@ -78,7 +85,7 @@ void parseEmoteSetInto(const QJsonObject &emoteSet, const QString &kind,
|
|||
}
|
||||
|
||||
Emote emote;
|
||||
fillInEmoteData(urls, name,
|
||||
fillInEmoteData(emoteJson, urls, name,
|
||||
QString("%1<br>%2 FFZ Emote<br>By: %3")
|
||||
.arg(name.string, kind, author.string),
|
||||
emote);
|
||||
|
@ -132,13 +139,13 @@ std::optional<EmotePtr> parseAuthorityBadge(const QJsonObject &badgeUrls,
|
|||
auto authorityBadge3x = getEmoteLink(badgeUrls, "4");
|
||||
|
||||
auto authorityBadgeImageSet = ImageSet{
|
||||
Image::fromUrl(authorityBadge1x, 1),
|
||||
Image::fromUrl(authorityBadge1x, 1, BASE_BADGE_SIZE),
|
||||
authorityBadge2x.string.isEmpty()
|
||||
? Image::getEmpty()
|
||||
: Image::fromUrl(authorityBadge2x, 0.5),
|
||||
: Image::fromUrl(authorityBadge2x, 0.5, BASE_BADGE_SIZE * 2),
|
||||
authorityBadge3x.string.isEmpty()
|
||||
? Image::getEmpty()
|
||||
: Image::fromUrl(authorityBadge3x, 0.25),
|
||||
: Image::fromUrl(authorityBadge3x, 0.25, BASE_BADGE_SIZE * 4),
|
||||
};
|
||||
|
||||
authorityBadge = std::make_shared<Emote>(Emote{
|
||||
|
|
|
@ -463,7 +463,7 @@ ImageSet SeventvEmotes::createImageSet(const QJsonObject &emoteData)
|
|||
|
||||
auto image = Image::fromUrl(
|
||||
{QString("https:%1/%2").arg(baseUrl, file["name"].toString())},
|
||||
scale);
|
||||
scale, {static_cast<int>(width), file["height"].toInt(16)});
|
||||
|
||||
sizes.at(nextSize) = image;
|
||||
nextSize++;
|
||||
|
|
|
@ -27,22 +27,31 @@ ChannelPointReward::ChannelPointReward(const QJsonObject &redemption)
|
|||
}
|
||||
|
||||
auto imageValue = reward.value("image");
|
||||
// From Twitch docs
|
||||
// The size is only an estimation, the actual size might vary.
|
||||
constexpr QSize baseSize(28, 28);
|
||||
|
||||
if (imageValue.isObject())
|
||||
{
|
||||
auto imageObject = imageValue.toObject();
|
||||
this->image = ImageSet{
|
||||
Image::fromUrl({imageObject.value("url_1x").toString()}, 1),
|
||||
Image::fromUrl({imageObject.value("url_2x").toString()}, 0.5),
|
||||
Image::fromUrl({imageObject.value("url_4x").toString()}, 0.25),
|
||||
Image::fromUrl({imageObject.value("url_1x").toString()}, 1,
|
||||
baseSize),
|
||||
Image::fromUrl({imageObject.value("url_2x").toString()}, 0.5,
|
||||
baseSize * 2),
|
||||
Image::fromUrl({imageObject.value("url_4x").toString()}, 0.25,
|
||||
baseSize * 4),
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
static const ImageSet defaultImage{
|
||||
Image::fromUrl({TWITCH_CHANNEL_POINT_REWARD_URL("1.png")}, 1),
|
||||
Image::fromUrl({TWITCH_CHANNEL_POINT_REWARD_URL("2.png")}, 0.5),
|
||||
Image::fromUrl({TWITCH_CHANNEL_POINT_REWARD_URL("4.png")}, 0.25)};
|
||||
Image::fromUrl({TWITCH_CHANNEL_POINT_REWARD_URL("1.png")}, 1,
|
||||
baseSize),
|
||||
Image::fromUrl({TWITCH_CHANNEL_POINT_REWARD_URL("2.png")}, 0.5,
|
||||
baseSize * 2),
|
||||
Image::fromUrl({TWITCH_CHANNEL_POINT_REWARD_URL("4.png")}, 0.25,
|
||||
baseSize * 4)};
|
||||
this->image = defaultImage;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,13 @@
|
|||
#include <QThread>
|
||||
#include <QUrlQuery>
|
||||
|
||||
namespace {
|
||||
|
||||
// From Twitch docs - expected size for a badge (1x)
|
||||
constexpr QSize BADGE_BASE_SIZE(18, 18);
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
void TwitchBadges::loadTwitchBadges()
|
||||
|
@ -37,9 +44,12 @@ void TwitchBadges::loadTwitchBadges()
|
|||
.name = EmoteName{},
|
||||
.images =
|
||||
ImageSet{
|
||||
Image::fromUrl(version.imageURL1x, 1),
|
||||
Image::fromUrl(version.imageURL2x, .5),
|
||||
Image::fromUrl(version.imageURL4x, .25),
|
||||
Image::fromUrl(version.imageURL1x, 1,
|
||||
BADGE_BASE_SIZE),
|
||||
Image::fromUrl(version.imageURL2x, .5,
|
||||
BADGE_BASE_SIZE * 2),
|
||||
Image::fromUrl(version.imageURL4x, .25,
|
||||
BADGE_BASE_SIZE * 4),
|
||||
},
|
||||
.tooltip = Tooltip{version.title},
|
||||
.homePage = version.clickURL,
|
||||
|
@ -100,17 +110,19 @@ void TwitchBadges::parseTwitchBadges(QJsonObject root)
|
|||
for (auto vIt = versions.begin(); vIt != versions.end(); ++vIt)
|
||||
{
|
||||
auto versionObj = vIt.value().toObject();
|
||||
|
||||
auto emote = Emote{
|
||||
.name = {""},
|
||||
.images =
|
||||
ImageSet{
|
||||
Image::fromUrl(
|
||||
{versionObj.value("image_url_1x").toString()}, 1),
|
||||
{versionObj.value("image_url_1x").toString()}, 1,
|
||||
BADGE_BASE_SIZE),
|
||||
Image::fromUrl(
|
||||
{versionObj.value("image_url_2x").toString()}, .5),
|
||||
{versionObj.value("image_url_2x").toString()}, .5,
|
||||
BADGE_BASE_SIZE * 2),
|
||||
Image::fromUrl(
|
||||
{versionObj.value("image_url_4x").toString()}, .25),
|
||||
{versionObj.value("image_url_4x").toString()}, .25,
|
||||
BADGE_BASE_SIZE * 4),
|
||||
},
|
||||
.tooltip = Tooltip{versionObj.value("title").toString()},
|
||||
.homePage = Url{versionObj.value("click_url").toString()},
|
||||
|
|
|
@ -72,6 +72,9 @@ namespace {
|
|||
|
||||
// Maximum number of chatters to fetch when refreshing chatters
|
||||
constexpr auto MAX_CHATTERS_TO_FETCH = 5000;
|
||||
|
||||
// From Twitch docs - expected size for a badge (1x)
|
||||
constexpr QSize BASE_BADGE_SIZE(18, 18);
|
||||
} // namespace
|
||||
|
||||
TwitchChannel::TwitchChannel(const QString &name)
|
||||
|
@ -1466,9 +1469,12 @@ void TwitchChannel::refreshBadges()
|
|||
.name = EmoteName{},
|
||||
.images =
|
||||
ImageSet{
|
||||
Image::fromUrl(version.imageURL1x, 1),
|
||||
Image::fromUrl(version.imageURL2x, .5),
|
||||
Image::fromUrl(version.imageURL4x, .25),
|
||||
Image::fromUrl(version.imageURL1x, 1,
|
||||
BASE_BADGE_SIZE),
|
||||
Image::fromUrl(version.imageURL2x, .5,
|
||||
BASE_BADGE_SIZE * 2),
|
||||
Image::fromUrl(version.imageURL4x, .25,
|
||||
BASE_BADGE_SIZE * 4),
|
||||
},
|
||||
.tooltip = Tooltip{version.title},
|
||||
.homePage = version.clickURL,
|
||||
|
@ -1543,25 +1549,25 @@ void TwitchChannel::refreshCheerEmotes()
|
|||
// Combine the prefix (e.g. BibleThump) with the tier (1, 100 etc.)
|
||||
auto emoteTooltip =
|
||||
set.prefix + tier.id + "<br>Twitch Cheer Emote";
|
||||
auto makeImageSet = [](const HelixCheermoteImage &image) {
|
||||
return ImageSet{
|
||||
Image::fromUrl(image.imageURL1x, 1.0,
|
||||
BASE_BADGE_SIZE),
|
||||
Image::fromUrl(image.imageURL2x, 0.5,
|
||||
BASE_BADGE_SIZE * 2),
|
||||
Image::fromUrl(image.imageURL4x, 0.25,
|
||||
BASE_BADGE_SIZE * 4),
|
||||
};
|
||||
};
|
||||
cheerEmote.animatedEmote = std::make_shared<Emote>(Emote{
|
||||
.name = EmoteName{"cheer emote"},
|
||||
.images =
|
||||
ImageSet{
|
||||
tier.darkAnimated.imageURL1x,
|
||||
tier.darkAnimated.imageURL2x,
|
||||
tier.darkAnimated.imageURL4x,
|
||||
},
|
||||
.images = makeImageSet(tier.darkAnimated),
|
||||
.tooltip = Tooltip{emoteTooltip},
|
||||
.homePage = Url{},
|
||||
});
|
||||
cheerEmote.staticEmote = std::make_shared<Emote>(Emote{
|
||||
.name = EmoteName{"cheer emote"},
|
||||
.images =
|
||||
ImageSet{
|
||||
tier.darkStatic.imageURL1x,
|
||||
tier.darkStatic.imageURL2x,
|
||||
tier.darkStatic.imageURL4x,
|
||||
},
|
||||
.images = makeImageSet(tier.darkStatic),
|
||||
.tooltip = Tooltip{emoteTooltip},
|
||||
.homePage = Url{},
|
||||
});
|
||||
|
|
|
@ -44,12 +44,14 @@ EmotePtr TwitchEmotes::getOrCreateEmote(const EmoteId &id,
|
|||
|
||||
if (!shared)
|
||||
{
|
||||
// From Twitch docs - expected size for an emote (1x)
|
||||
constexpr QSize baseSize(28, 28);
|
||||
(*cache)[id] = shared = std::make_shared<Emote>(Emote{
|
||||
EmoteName{name},
|
||||
ImageSet{
|
||||
Image::fromUrl(getEmoteLink(id, "1.0"), 1),
|
||||
Image::fromUrl(getEmoteLink(id, "2.0"), 0.5),
|
||||
Image::fromUrl(getEmoteLink(id, "3.0"), 0.25),
|
||||
Image::fromUrl(getEmoteLink(id, "1.0"), 1, baseSize),
|
||||
Image::fromUrl(getEmoteLink(id, "2.0"), 0.5, baseSize * 2),
|
||||
Image::fromUrl(getEmoteLink(id, "3.0"), 0.25, baseSize * 4),
|
||||
},
|
||||
Tooltip{name.toHtmlEscaped() + "<br>Twitch Emote"},
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue