refactored the managers
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
#include "singletons/accountmanager.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
namespace {
|
||||
|
||||
inline QString getEnvString(const char *target)
|
||||
{
|
||||
char *val = std::getenv(target);
|
||||
if (val == nullptr) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
return QString(val);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AccountManager::AccountManager()
|
||||
{
|
||||
}
|
||||
|
||||
AccountManager &AccountManager::getInstance()
|
||||
{
|
||||
static AccountManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void AccountManager::load()
|
||||
{
|
||||
this->Twitch.reloadUsers();
|
||||
|
||||
auto currentUser = this->Twitch.findUserByUsername(
|
||||
QString::fromStdString(this->Twitch.currentUsername.getValue()));
|
||||
|
||||
if (currentUser) {
|
||||
this->Twitch.currentUser = currentUser;
|
||||
} else {
|
||||
this->Twitch.currentUser = this->Twitch.anonymousUser;
|
||||
}
|
||||
|
||||
this->Twitch.userChanged.invoke();
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "twitch/twitchaccountmanager.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class AccountManager
|
||||
{
|
||||
AccountManager();
|
||||
|
||||
public:
|
||||
static AccountManager &getInstance();
|
||||
|
||||
void load();
|
||||
|
||||
twitch::TwitchAccountManager Twitch;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,140 @@
|
||||
#include "singletons/channelmanager.hpp"
|
||||
#include "singletons/ircmanager.hpp"
|
||||
|
||||
using namespace chatterino::twitch;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
ChannelManager &ChannelManager::getInstance()
|
||||
{
|
||||
static ChannelManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
ChannelManager::ChannelManager()
|
||||
: whispersChannel(new Channel("/whispers"))
|
||||
, mentionsChannel(new Channel("/mentions"))
|
||||
, emptyChannel(new Channel(""))
|
||||
{
|
||||
}
|
||||
|
||||
const std::vector<std::shared_ptr<Channel>> ChannelManager::getItems()
|
||||
{
|
||||
QMutexLocker locker(&this->channelsMutex);
|
||||
|
||||
std::vector<std::shared_ptr<Channel>> items;
|
||||
|
||||
for (auto &item : this->twitchChannels.values()) {
|
||||
items.push_back(std::get<0>(item));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
std::shared_ptr<Channel> ChannelManager::addTwitchChannel(const QString &rawChannelName)
|
||||
{
|
||||
QString channelName = rawChannelName.toLower();
|
||||
|
||||
if (channelName.length() > 1 && channelName.at(0) == '/') {
|
||||
return this->getTwitchChannel(channelName);
|
||||
}
|
||||
|
||||
if (channelName.length() > 0 && channelName.at(0) == '#') {
|
||||
channelName = channelName.mid(1);
|
||||
}
|
||||
|
||||
QMutexLocker locker(&this->channelsMutex);
|
||||
|
||||
auto it = this->twitchChannels.find(channelName);
|
||||
|
||||
if (it == this->twitchChannels.end()) {
|
||||
auto channel = std::make_shared<TwitchChannel>(channelName);
|
||||
|
||||
this->twitchChannels.insert(channelName, std::make_tuple(channel, 1));
|
||||
|
||||
this->ircJoin.invoke(channelName);
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
std::get<1>(it.value())++;
|
||||
|
||||
return std::get<0>(it.value());
|
||||
}
|
||||
|
||||
std::shared_ptr<Channel> ChannelManager::getTwitchChannel(const QString &channel)
|
||||
{
|
||||
QMutexLocker locker(&this->channelsMutex);
|
||||
|
||||
QString c = channel.toLower();
|
||||
|
||||
if (channel.length() > 1 && channel.at(0) == '/') {
|
||||
if (c == "/whispers") {
|
||||
return whispersChannel;
|
||||
}
|
||||
|
||||
if (c == "/mentions") {
|
||||
return mentionsChannel;
|
||||
}
|
||||
|
||||
return emptyChannel;
|
||||
}
|
||||
|
||||
auto a = this->twitchChannels.find(c);
|
||||
|
||||
if (a == this->twitchChannels.end()) {
|
||||
return emptyChannel;
|
||||
}
|
||||
|
||||
return std::get<0>(a.value());
|
||||
}
|
||||
|
||||
void ChannelManager::removeTwitchChannel(const QString &channel)
|
||||
{
|
||||
QMutexLocker locker(&this->channelsMutex);
|
||||
|
||||
if (channel.length() > 1 && channel.at(0) == '/') {
|
||||
return;
|
||||
}
|
||||
|
||||
QString c = channel.toLower();
|
||||
|
||||
auto a = this->twitchChannels.find(c);
|
||||
|
||||
if (a == this->twitchChannels.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::get<1>(a.value())--;
|
||||
|
||||
if (std::get<1>(a.value()) == 0) {
|
||||
this->ircPart.invoke(c);
|
||||
this->twitchChannels.remove(c);
|
||||
}
|
||||
}
|
||||
|
||||
const std::string &ChannelManager::getUserID(const std::string &username)
|
||||
{
|
||||
/* TODO: Implement
|
||||
auto it = this->usernameToID.find(username);
|
||||
|
||||
if (it != std::end(this->usernameToID)) {
|
||||
return *it;
|
||||
}
|
||||
*/
|
||||
|
||||
static std::string temporary = "xd";
|
||||
return temporary;
|
||||
}
|
||||
|
||||
void ChannelManager::doOnAll(std::function<void(std::shared_ptr<Channel>)> func)
|
||||
{
|
||||
for (const auto &channel : this->twitchChannels) {
|
||||
func(std::get<0>(channel));
|
||||
}
|
||||
|
||||
func(this->whispersChannel);
|
||||
func(this->mentionsChannel);
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include "channel.hpp"
|
||||
#include "channeldata.hpp"
|
||||
#include "twitch/twitchchannel.hpp"
|
||||
|
||||
#include <map>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class WindowManager;
|
||||
class IrcManager;
|
||||
|
||||
class ChannelManager
|
||||
{
|
||||
explicit ChannelManager();
|
||||
|
||||
public:
|
||||
static ChannelManager &getInstance();
|
||||
|
||||
const std::vector<std::shared_ptr<Channel>> getItems();
|
||||
|
||||
std::shared_ptr<Channel> addTwitchChannel(const QString &channel);
|
||||
std::shared_ptr<Channel> getTwitchChannel(const QString &channel);
|
||||
void removeTwitchChannel(const QString &channel);
|
||||
|
||||
const std::string &getUserID(const std::string &username);
|
||||
|
||||
void doOnAll(std::function<void(std::shared_ptr<Channel>)> func);
|
||||
|
||||
// Special channels
|
||||
const std::shared_ptr<Channel> whispersChannel;
|
||||
const std::shared_ptr<Channel> mentionsChannel;
|
||||
const std::shared_ptr<Channel> emptyChannel;
|
||||
|
||||
private:
|
||||
std::map<std::string, std::string> usernameToID;
|
||||
std::map<std::string, ChannelData> channelDatas;
|
||||
|
||||
QMutex channelsMutex;
|
||||
QMap<QString, std::tuple<std::shared_ptr<twitch::TwitchChannel>, int>> twitchChannels;
|
||||
|
||||
pajlada::Signals::Signal<const QString &> ircJoin;
|
||||
pajlada::Signals::Signal<const QString &> ircPart;
|
||||
|
||||
friend class IrcManager;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,83 @@
|
||||
#include "singletons/commandmanager.hpp"
|
||||
|
||||
#include <QRegularExpression>
|
||||
|
||||
namespace chatterino {
|
||||
CommandManager &CommandManager::getInstance()
|
||||
{
|
||||
static CommandManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
// QString CommandManager::execCommand(QString text)
|
||||
//{
|
||||
// QStringList words = text.split(' ', QString::SkipEmptyParts);
|
||||
|
||||
// if (words.length() == 0) {
|
||||
// return text;
|
||||
// }
|
||||
|
||||
// QString commandName = words[0];
|
||||
// if (commandName[0] == "/") {
|
||||
// commandName = commandName.mid(1);
|
||||
// }
|
||||
|
||||
// Command *command = nullptr;
|
||||
// for (Command &command : this->commands) {
|
||||
// if (command.name == commandName) {
|
||||
// command = &command;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (command == nullptr) {
|
||||
// return text;
|
||||
// }
|
||||
|
||||
// QString result;
|
||||
|
||||
// static QRegularExpression parseCommand("[^{]({{)*{(\d+\+?)}");
|
||||
// for (QRegularExpressionMatch &match : parseCommand.globalMatch(command->text)) {
|
||||
// result += text.mid(match.capturedStart(), match.capturedLength());
|
||||
|
||||
// QString wordIndexMatch = match.captured(2);
|
||||
|
||||
// bool ok;
|
||||
// int wordIndex = wordIndexMatch.replace("=", "").toInt(ok);
|
||||
// if (!ok) {
|
||||
// result += match.captured();
|
||||
// continue;
|
||||
// }
|
||||
// if (words.length() <= wordIndex) {
|
||||
// // alternatively return text because the operation failed
|
||||
// result += "";
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (wordIndexMatch[wordIndexMatch.length() - 1] == '+') {
|
||||
// for (int i = wordIndex; i < words.length(); i++) {
|
||||
// result += words[i];
|
||||
// }
|
||||
// } else {
|
||||
// result += words[wordIndex];
|
||||
// }
|
||||
// }
|
||||
|
||||
// result += text.mid(match.capturedStart(), match.capturedLength());
|
||||
|
||||
// return result;
|
||||
//}
|
||||
|
||||
// CommandManager::Command::Command(QString _text)
|
||||
//{
|
||||
// int index = _text.indexOf(' ');
|
||||
|
||||
// if (index == -1) {
|
||||
// this->name == _text;
|
||||
// return;
|
||||
// }
|
||||
|
||||
// this->name = _text.mid(0, index);
|
||||
// this->text = _text.mid(index + 1);
|
||||
//}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <vector>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class CommandManager
|
||||
{
|
||||
CommandManager() = default;
|
||||
|
||||
public:
|
||||
static CommandManager &getInstance();
|
||||
|
||||
// CommandManager() = delete;
|
||||
|
||||
// QString execCommand(QString text);
|
||||
// void addCommand ?
|
||||
// void loadCommands(QString) taking all commands as a \n seperated list ?
|
||||
|
||||
// static CommandManager *getInstance()
|
||||
// {
|
||||
// static CommandManager manager;
|
||||
|
||||
// return manager;
|
||||
// }
|
||||
|
||||
// private:
|
||||
// struct Command {
|
||||
// QString name;
|
||||
// QString text;
|
||||
|
||||
// Command(QString text);
|
||||
// };
|
||||
|
||||
// std::vector<Command> commands;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#include "singletons/completionmanager.hpp"
|
||||
#include "common.hpp"
|
||||
#include "debug/log.hpp"
|
||||
#include "singletons/channelmanager.hpp"
|
||||
#include "singletons/emotemanager.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
CompletionManager &CompletionManager::getInstance()
|
||||
{
|
||||
static CompletionManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
CompletionModel *CompletionManager::createModel(const std::string &channelName)
|
||||
{
|
||||
auto it = this->models.find(channelName);
|
||||
if (it != this->models.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
CompletionModel *ret = new CompletionModel(qS(channelName));
|
||||
this->models[channelName] = ret;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QVector>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include "helper/completionmodel.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
class CompletionManager
|
||||
{
|
||||
CompletionManager() = default;
|
||||
|
||||
public:
|
||||
static CompletionManager &getInstance();
|
||||
|
||||
CompletionModel *createModel(const std::string &channelName);
|
||||
|
||||
private:
|
||||
std::map<std::string, CompletionModel *> models;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,537 @@
|
||||
#include "emotemanager.hpp"
|
||||
#include "common.hpp"
|
||||
#include "singletons/settingsmanager.hpp"
|
||||
#include "singletons/windowmanager.hpp"
|
||||
#include "util/urlfetch.hpp"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#define TWITCH_EMOTE_TEMPLATE "https://static-cdn.jtvnw.net/emoticons/v1/{id}/{scale}.0"
|
||||
|
||||
using namespace chatterino::messages;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
EmoteManager::EmoteManager(SettingsManager &_settingsManager, WindowManager &_windowManager)
|
||||
: settingsManager(_settingsManager)
|
||||
, windowManager(_windowManager)
|
||||
, findShortCodesRegex(":([-+\\w]+):")
|
||||
{
|
||||
auto &accountManager = AccountManager::getInstance();
|
||||
|
||||
accountManager.Twitch.userChanged.connect([this] {
|
||||
auto currentUser = AccountManager::getInstance().Twitch.getCurrent();
|
||||
assert(currentUser);
|
||||
this->refreshTwitchEmotes(currentUser);
|
||||
});
|
||||
}
|
||||
|
||||
EmoteManager &EmoteManager::getInstance()
|
||||
{
|
||||
static EmoteManager instance(SettingsManager::getInstance(), WindowManager::getInstance());
|
||||
return instance;
|
||||
}
|
||||
|
||||
void EmoteManager::loadGlobalEmotes()
|
||||
{
|
||||
this->loadEmojis();
|
||||
this->loadBTTVEmotes();
|
||||
this->loadFFZEmotes();
|
||||
}
|
||||
|
||||
void EmoteManager::reloadBTTVChannelEmotes(const QString &channelName, std::weak_ptr<EmoteMap> _map)
|
||||
{
|
||||
printf("[EmoteManager] Reload BTTV Channel Emotes for channel %s\n", qPrintable(channelName));
|
||||
|
||||
QString url("https://api.betterttv.net/2/channels/" + channelName);
|
||||
|
||||
debug::Log("Request bttv channel emotes for {}", channelName);
|
||||
|
||||
util::NetworkRequest req(url);
|
||||
req.setCaller(QThread::currentThread());
|
||||
req.setTimeout(3000);
|
||||
req.getJSON([this, channelName, _map](QJsonObject &rootNode) {
|
||||
debug::Log("Got bttv channel emotes for {}", channelName);
|
||||
auto map = _map.lock();
|
||||
|
||||
if (_map.expired()) {
|
||||
return;
|
||||
}
|
||||
|
||||
map->clear();
|
||||
|
||||
auto emotesNode = rootNode.value("emotes").toArray();
|
||||
|
||||
QString linkTemplate = "https:" + rootNode.value("urlTemplate").toString();
|
||||
|
||||
std::vector<std::string> codes;
|
||||
for (const QJsonValue &emoteNode : emotesNode) {
|
||||
QJsonObject emoteObject = emoteNode.toObject();
|
||||
|
||||
QString id = emoteObject.value("id").toString();
|
||||
QString code = emoteObject.value("code").toString();
|
||||
// emoteObject.value("imageType").toString();
|
||||
|
||||
QString link = linkTemplate;
|
||||
link.detach();
|
||||
|
||||
link = link.replace("{{id}}", id).replace("{{image}}", "1x");
|
||||
|
||||
auto emote = this->getBTTVChannelEmoteFromCaches().getOrAdd(id, [this, &code, &link] {
|
||||
return EmoteData(
|
||||
new LazyLoadedImage(link, 1, code, code + "<br/>Channel BTTV Emote"));
|
||||
});
|
||||
|
||||
this->bttvChannelEmotes.insert(code, emote);
|
||||
map->insert(code, emote);
|
||||
codes.push_back(code.toStdString());
|
||||
}
|
||||
|
||||
this->bttvChannelEmoteCodes[channelName.toStdString()] = codes;
|
||||
});
|
||||
}
|
||||
|
||||
void EmoteManager::reloadFFZChannelEmotes(const QString &channelName, std::weak_ptr<EmoteMap> _map)
|
||||
{
|
||||
printf("[EmoteManager] Reload FFZ Channel Emotes for channel %s\n", qPrintable(channelName));
|
||||
|
||||
QString url("http://api.frankerfacez.com/v1/room/" + channelName);
|
||||
|
||||
util::NetworkRequest req(url);
|
||||
req.setCaller(QThread::currentThread());
|
||||
req.setTimeout(3000);
|
||||
req.getJSON([this, channelName, _map](QJsonObject &rootNode) {
|
||||
auto map = _map.lock();
|
||||
|
||||
if (_map.expired()) {
|
||||
return;
|
||||
}
|
||||
|
||||
map->clear();
|
||||
|
||||
auto setsNode = rootNode.value("sets").toObject();
|
||||
|
||||
std::vector<std::string> codes;
|
||||
for (const QJsonValue &setNode : setsNode) {
|
||||
auto emotesNode = setNode.toObject().value("emoticons").toArray();
|
||||
|
||||
for (const QJsonValue &emoteNode : emotesNode) {
|
||||
QJsonObject emoteObject = emoteNode.toObject();
|
||||
|
||||
// margins
|
||||
int id = emoteObject.value("id").toInt();
|
||||
QString code = emoteObject.value("name").toString();
|
||||
|
||||
QJsonObject urls = emoteObject.value("urls").toObject();
|
||||
QString url1 = "http:" + urls.value("1").toString();
|
||||
|
||||
auto emote =
|
||||
this->getFFZChannelEmoteFromCaches().getOrAdd(id, [this, &code, &url1] {
|
||||
return EmoteData(
|
||||
new LazyLoadedImage(url1, 1, code, code + "<br/>Channel FFZ Emote"));
|
||||
});
|
||||
|
||||
this->ffzChannelEmotes.insert(code, emote);
|
||||
map->insert(code, emote);
|
||||
codes.push_back(code.toStdString());
|
||||
}
|
||||
|
||||
this->ffzChannelEmoteCodes[channelName.toStdString()] = codes;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ConcurrentMap<QString, twitch::EmoteValue *> &EmoteManager::getTwitchEmotes()
|
||||
{
|
||||
return _twitchEmotes;
|
||||
}
|
||||
|
||||
EmoteMap &EmoteManager::getFFZEmotes()
|
||||
{
|
||||
return ffzGlobalEmotes;
|
||||
}
|
||||
|
||||
EmoteMap &EmoteManager::getChatterinoEmotes()
|
||||
{
|
||||
return _chatterinoEmotes;
|
||||
}
|
||||
|
||||
EmoteMap &EmoteManager::getBTTVChannelEmoteFromCaches()
|
||||
{
|
||||
return _bttvChannelEmoteFromCaches;
|
||||
}
|
||||
|
||||
EmoteMap &EmoteManager::getEmojis()
|
||||
{
|
||||
return this->emojis;
|
||||
}
|
||||
|
||||
ConcurrentMap<int, EmoteData> &EmoteManager::getFFZChannelEmoteFromCaches()
|
||||
{
|
||||
return _ffzChannelEmoteFromCaches;
|
||||
}
|
||||
|
||||
ConcurrentMap<long, EmoteData> &EmoteManager::getTwitchEmoteFromCache()
|
||||
{
|
||||
return _twitchEmoteFromCache;
|
||||
}
|
||||
|
||||
void EmoteManager::loadEmojis()
|
||||
{
|
||||
QFile file(":/emojidata.txt");
|
||||
file.open(QFile::ReadOnly);
|
||||
QTextStream in(&file);
|
||||
|
||||
uint unicodeBytes[4];
|
||||
|
||||
while (!in.atEnd()) {
|
||||
// Line example: sunglasses 1f60e
|
||||
QString line = in.readLine();
|
||||
|
||||
if (line.at(0) == '#') {
|
||||
// Ignore lines starting with # (comments)
|
||||
continue;
|
||||
}
|
||||
|
||||
QStringList parts = line.split(' ');
|
||||
if (parts.length() < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString shortCode = parts[0];
|
||||
QString code = parts[1];
|
||||
|
||||
QStringList unicodeCharacters = code.split('-');
|
||||
if (unicodeCharacters.length() < 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int numUnicodeBytes = 0;
|
||||
|
||||
for (const QString &unicodeCharacter : unicodeCharacters) {
|
||||
unicodeBytes[numUnicodeBytes++] = QString(unicodeCharacter).toUInt(nullptr, 16);
|
||||
}
|
||||
|
||||
EmojiData emojiData{
|
||||
QString::fromUcs4(unicodeBytes, numUnicodeBytes), //
|
||||
code, //
|
||||
shortCode, //
|
||||
};
|
||||
|
||||
this->emojiShortCodeToEmoji.insert(shortCode, emojiData);
|
||||
this->emojiShortCodes.push_back(shortCode.toStdString());
|
||||
|
||||
this->emojiFirstByte[emojiData.value.at(0)].append(emojiData);
|
||||
|
||||
QString url = "https://cdnjs.cloudflare.com/ajax/libs/"
|
||||
"emojione/2.2.6/assets/png/" +
|
||||
code + ".png";
|
||||
|
||||
this->emojis.insert(code, EmoteData(new LazyLoadedImage(url, 0.35, ":" + shortCode + ":",
|
||||
":" + shortCode + ":<br/>Emoji")));
|
||||
|
||||
// TODO(pajlada): The vectors in emojiFirstByte need to be sorted by
|
||||
// emojiData.code.length()
|
||||
}
|
||||
}
|
||||
|
||||
void EmoteManager::parseEmojis(std::vector<std::tuple<EmoteData, QString>> &parsedWords,
|
||||
const QString &text)
|
||||
{
|
||||
int lastParsedEmojiEndIndex = 0;
|
||||
|
||||
for (auto i = 0; i < text.length() - 1; i++) {
|
||||
const QChar character = text.at(i);
|
||||
|
||||
if (character.isLowSurrogate()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto it = this->emojiFirstByte.find(character);
|
||||
if (it == this->emojiFirstByte.end()) {
|
||||
// No emoji starts with this character
|
||||
continue;
|
||||
}
|
||||
|
||||
const QVector<EmojiData> possibleEmojis = it.value();
|
||||
|
||||
int remainingCharacters = text.length() - i;
|
||||
|
||||
EmojiData matchedEmoji;
|
||||
|
||||
int matchedEmojiLength = 0;
|
||||
|
||||
for (const EmojiData &emoji : possibleEmojis) {
|
||||
if (remainingCharacters < emoji.value.length()) {
|
||||
// It cannot be this emoji, there's not enough space for it
|
||||
continue;
|
||||
}
|
||||
|
||||
bool match = true;
|
||||
|
||||
for (int j = 1; j < emoji.value.length(); ++j) {
|
||||
if (text.at(i + j) != emoji.value.at(j)) {
|
||||
match = false;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (match) {
|
||||
matchedEmoji = emoji;
|
||||
matchedEmojiLength = emoji.value.length();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedEmojiLength == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int currentParsedEmojiFirstIndex = i;
|
||||
int currentParsedEmojiEndIndex = i + (matchedEmojiLength);
|
||||
|
||||
int charactersFromLastParsedEmoji = currentParsedEmojiFirstIndex - lastParsedEmojiEndIndex;
|
||||
|
||||
if (charactersFromLastParsedEmoji > 0) {
|
||||
// Add characters inbetween emojis
|
||||
parsedWords.push_back(std::tuple<messages::LazyLoadedImage *, QString>(
|
||||
nullptr, text.mid(lastParsedEmojiEndIndex, charactersFromLastParsedEmoji)));
|
||||
}
|
||||
|
||||
QString url = "https://cdnjs.cloudflare.com/ajax/libs/"
|
||||
"emojione/2.2.6/assets/png/" +
|
||||
matchedEmoji.code + ".png";
|
||||
|
||||
// Create or fetch cached emoji image
|
||||
auto emojiImage = this->emojis.getOrAdd(matchedEmoji.code, [this, &url] {
|
||||
return EmoteData(new LazyLoadedImage(url, 0.35, "?????????", "???????????????")); //
|
||||
});
|
||||
|
||||
// Push the emoji as a word to parsedWords
|
||||
parsedWords.push_back(std::tuple<EmoteData, QString>(emojiImage, QString()));
|
||||
|
||||
lastParsedEmojiEndIndex = currentParsedEmojiEndIndex;
|
||||
|
||||
i += matchedEmojiLength - 1;
|
||||
}
|
||||
|
||||
if (lastParsedEmojiEndIndex < text.length()) {
|
||||
// Add remaining characters
|
||||
parsedWords.push_back(std::tuple<messages::LazyLoadedImage *, QString>(
|
||||
nullptr, text.mid(lastParsedEmojiEndIndex)));
|
||||
}
|
||||
}
|
||||
|
||||
QString EmoteManager::replaceShortCodes(const QString &text)
|
||||
{
|
||||
QString ret(text);
|
||||
auto it = this->findShortCodesRegex.globalMatch(text);
|
||||
|
||||
int32_t offset = 0;
|
||||
|
||||
while (it.hasNext()) {
|
||||
auto match = it.next();
|
||||
|
||||
auto capturedString = match.captured();
|
||||
|
||||
QString matchString = capturedString.toLower().mid(1, capturedString.size() - 2);
|
||||
|
||||
auto emojiIt = this->emojiShortCodeToEmoji.constFind(matchString);
|
||||
|
||||
if (emojiIt == this->emojiShortCodeToEmoji.constEnd()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto emojiData = emojiIt.value();
|
||||
|
||||
ret.replace(offset + match.capturedStart(), match.capturedLength(), emojiData.value);
|
||||
|
||||
offset += emojiData.value.size() - match.capturedLength();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void EmoteManager::refreshTwitchEmotes(const std::shared_ptr<twitch::TwitchUser> &user)
|
||||
{
|
||||
debug::Log("Loading Twitch emotes for user {}", user->getNickName());
|
||||
|
||||
const auto &roomID = user->getUserId();
|
||||
const auto &clientID = user->getOAuthClient();
|
||||
const auto &oauthToken = user->getOAuthToken();
|
||||
|
||||
if (clientID.isEmpty() || oauthToken.isEmpty()) {
|
||||
debug::Log("Missing Client ID or OAuth token");
|
||||
return;
|
||||
}
|
||||
|
||||
TwitchAccountEmoteData &emoteData = this->twitchAccountEmotes[roomID.toStdString()];
|
||||
|
||||
if (emoteData.filled) {
|
||||
qDebug() << "Already loaded for room id " << roomID;
|
||||
return;
|
||||
}
|
||||
|
||||
QString url("https://api.twitch.tv/kraken/users/" + roomID + "/emotes");
|
||||
|
||||
util::twitch::getAuthorized(
|
||||
url, clientID, oauthToken, QThread::currentThread(),
|
||||
[=, &emoteData](QJsonObject &root) {
|
||||
emoteData.emoteSets.clear();
|
||||
emoteData.emoteCodes.clear();
|
||||
auto emoticonSets = root.value("emoticon_sets").toObject();
|
||||
for (QJsonObject::iterator it = emoticonSets.begin(); it != emoticonSets.end(); ++it) {
|
||||
std::string emoteSetString = it.key().toStdString();
|
||||
QJsonArray emoteSetList = it.value().toArray();
|
||||
|
||||
for (QJsonValue emoteValue : emoteSetList) {
|
||||
QJsonObject emoticon = emoteValue.toObject();
|
||||
std::string id = emoticon["id"].toString().toStdString();
|
||||
std::string code = emoticon["code"].toString().toStdString();
|
||||
emoteData.emoteSets[emoteSetString].push_back({id, code});
|
||||
emoteData.emoteCodes.push_back(code);
|
||||
}
|
||||
}
|
||||
|
||||
emoteData.filled = true;
|
||||
}
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
void EmoteManager::loadBTTVEmotes()
|
||||
{
|
||||
QString url("https://api.betterttv.net/2/emotes");
|
||||
|
||||
util::NetworkRequest req(url);
|
||||
req.setCaller(QThread::currentThread());
|
||||
req.setTimeout(30000);
|
||||
req.getJSON([this](QJsonObject &root) {
|
||||
debug::Log("Got global bttv emotes");
|
||||
auto emotes = root.value("emotes").toArray();
|
||||
|
||||
QString linkTemplate = "https:" + root.value("urlTemplate").toString();
|
||||
|
||||
std::vector<std::string> codes;
|
||||
for (const QJsonValue &emote : emotes) {
|
||||
QString id = emote.toObject().value("id").toString();
|
||||
QString code = emote.toObject().value("code").toString();
|
||||
// emote.value("imageType").toString();
|
||||
|
||||
QString tmp = linkTemplate;
|
||||
tmp.detach();
|
||||
QString url = tmp.replace("{{id}}", id).replace("{{image}}", "1x");
|
||||
|
||||
this->bttvGlobalEmotes.insert(
|
||||
code, new LazyLoadedImage(url, 1, code, code + "<br/>Global BTTV Emote"));
|
||||
codes.push_back(code.toStdString());
|
||||
}
|
||||
|
||||
this->bttvGlobalEmoteCodes = codes;
|
||||
});
|
||||
}
|
||||
|
||||
void EmoteManager::loadFFZEmotes()
|
||||
{
|
||||
QString url("https://api.frankerfacez.com/v1/set/global");
|
||||
|
||||
util::NetworkRequest req(url);
|
||||
req.setCaller(QThread::currentThread());
|
||||
req.setTimeout(30000);
|
||||
req.getJSON([this](QJsonObject &root) {
|
||||
debug::Log("Got global ffz emotes");
|
||||
|
||||
auto sets = root.value("sets").toObject();
|
||||
|
||||
std::vector<std::string> codes;
|
||||
for (const QJsonValue &set : sets) {
|
||||
auto emoticons = set.toObject().value("emoticons").toArray();
|
||||
|
||||
for (const QJsonValue &emote : emoticons) {
|
||||
QJsonObject object = emote.toObject();
|
||||
|
||||
// margins
|
||||
|
||||
// int id = object.value("id").toInt();
|
||||
QString code = object.value("name").toString();
|
||||
|
||||
QJsonObject urls = object.value("urls").toObject();
|
||||
QString url1 = "http:" + urls.value("1").toString();
|
||||
|
||||
this->ffzGlobalEmotes.insert(
|
||||
code, new LazyLoadedImage(url1, 1, code, code + "<br/>Global FFZ Emote"));
|
||||
codes.push_back(code.toStdString());
|
||||
}
|
||||
|
||||
this->ffzGlobalEmoteCodes = codes;
|
||||
}
|
||||
});
|
||||
} // namespace chatterino
|
||||
|
||||
// id is used for lookup
|
||||
// emoteName is used for giving a name to the emote in case it doesn't exist
|
||||
EmoteData EmoteManager::getTwitchEmoteById(long id, const QString &emoteName)
|
||||
{
|
||||
return _twitchEmoteFromCache.getOrAdd(id, [this, &emoteName, &id] {
|
||||
qreal scale;
|
||||
QString url = getTwitchEmoteLink(id, scale);
|
||||
return new LazyLoadedImage(url, scale, emoteName, emoteName + "<br/>Twitch Emote");
|
||||
});
|
||||
}
|
||||
|
||||
QString EmoteManager::getTwitchEmoteLink(long id, qreal &scale)
|
||||
{
|
||||
scale = .5;
|
||||
|
||||
QString value = TWITCH_EMOTE_TEMPLATE;
|
||||
|
||||
value.detach();
|
||||
|
||||
return value.replace("{id}", QString::number(id)).replace("{scale}", "2");
|
||||
}
|
||||
|
||||
EmoteData EmoteManager::getCheerImage(long long amount, bool animated)
|
||||
{
|
||||
// TODO: fix this xD
|
||||
return EmoteData();
|
||||
}
|
||||
|
||||
boost::signals2::signal<void()> &EmoteManager::getGifUpdateSignal()
|
||||
{
|
||||
if (!this->gifUpdateTimerInitiated) {
|
||||
this->gifUpdateTimerInitiated = true;
|
||||
|
||||
this->gifUpdateTimer.setInterval(30);
|
||||
this->gifUpdateTimer.start();
|
||||
|
||||
this->settingsManager.enableGifAnimations.connect([this](bool enabled, auto) {
|
||||
if (enabled) {
|
||||
this->gifUpdateTimer.start();
|
||||
} else {
|
||||
this->gifUpdateTimer.stop();
|
||||
}
|
||||
});
|
||||
|
||||
QObject::connect(&this->gifUpdateTimer, &QTimer::timeout, [this] {
|
||||
this->gifUpdateTimerSignal();
|
||||
// fourtf:
|
||||
this->windowManager.repaintGifEmotes();
|
||||
});
|
||||
}
|
||||
|
||||
return this->gifUpdateTimerSignal;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,172 @@
|
||||
#pragma once
|
||||
|
||||
#define GIF_FRAME_LENGTH 33
|
||||
|
||||
#include "concurrentmap.hpp"
|
||||
#include "emojis.hpp"
|
||||
#include "messages/lazyloadedimage.hpp"
|
||||
#include "signalvector.hpp"
|
||||
#include "twitch/emotevalue.hpp"
|
||||
#include "twitch/twitchuser.hpp"
|
||||
|
||||
#include <QMap>
|
||||
#include <QMutex>
|
||||
#include <QRegularExpression>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <boost/signals2.hpp>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class SettingsManager;
|
||||
class WindowManager;
|
||||
|
||||
struct EmoteData {
|
||||
EmoteData()
|
||||
{
|
||||
}
|
||||
|
||||
EmoteData(messages::LazyLoadedImage *_image)
|
||||
: image(_image)
|
||||
{
|
||||
}
|
||||
|
||||
messages::LazyLoadedImage *image = nullptr;
|
||||
};
|
||||
|
||||
typedef ConcurrentMap<QString, EmoteData> EmoteMap;
|
||||
|
||||
class EmoteManager
|
||||
{
|
||||
explicit EmoteManager(SettingsManager &manager, WindowManager &windowManager);
|
||||
|
||||
public:
|
||||
static EmoteManager &getInstance();
|
||||
|
||||
void loadGlobalEmotes();
|
||||
|
||||
void reloadBTTVChannelEmotes(const QString &channelName,
|
||||
std::weak_ptr<EmoteMap> channelEmoteMap);
|
||||
void reloadFFZChannelEmotes(const QString &channelName,
|
||||
std::weak_ptr<EmoteMap> channelEmoteMap);
|
||||
|
||||
ConcurrentMap<QString, twitch::EmoteValue *> &getTwitchEmotes();
|
||||
EmoteMap &getFFZEmotes();
|
||||
EmoteMap &getChatterinoEmotes();
|
||||
EmoteMap &getBTTVChannelEmoteFromCaches();
|
||||
EmoteMap &getEmojis();
|
||||
ConcurrentMap<int, EmoteData> &getFFZChannelEmoteFromCaches();
|
||||
ConcurrentMap<long, EmoteData> &getTwitchEmoteFromCache();
|
||||
|
||||
EmoteData getCheerImage(long long int amount, bool animated);
|
||||
|
||||
EmoteData getTwitchEmoteById(long int id, const QString &emoteName);
|
||||
|
||||
int getGeneration()
|
||||
{
|
||||
return _generation;
|
||||
}
|
||||
|
||||
void incGeneration()
|
||||
{
|
||||
_generation++;
|
||||
}
|
||||
|
||||
boost::signals2::signal<void()> &getGifUpdateSignal();
|
||||
|
||||
// Bit badge/emotes?
|
||||
ConcurrentMap<QString, messages::LazyLoadedImage *> miscImageCache;
|
||||
|
||||
private:
|
||||
SettingsManager &settingsManager;
|
||||
WindowManager &windowManager;
|
||||
|
||||
/// Emojis
|
||||
QRegularExpression findShortCodesRegex;
|
||||
|
||||
// shortCodeToEmoji maps strings like "sunglasses" to its emoji
|
||||
QMap<QString, EmojiData> emojiShortCodeToEmoji;
|
||||
|
||||
// Maps the first character of the emoji unicode string to a vector of possible emojis
|
||||
QMap<QChar, QVector<EmojiData>> emojiFirstByte;
|
||||
|
||||
// url Emoji-one image
|
||||
EmoteMap emojis;
|
||||
|
||||
void loadEmojis();
|
||||
|
||||
public:
|
||||
void parseEmojis(std::vector<std::tuple<EmoteData, QString>> &parsedWords, const QString &text);
|
||||
|
||||
QString replaceShortCodes(const QString &text);
|
||||
|
||||
std::vector<std::string> emojiShortCodes;
|
||||
|
||||
/// Twitch emotes
|
||||
void refreshTwitchEmotes(const std::shared_ptr<twitch::TwitchUser> &user);
|
||||
|
||||
struct TwitchAccountEmoteData {
|
||||
struct TwitchEmote {
|
||||
std::string id;
|
||||
std::string code;
|
||||
};
|
||||
|
||||
// emote set
|
||||
std::map<std::string, std::vector<TwitchEmote>> emoteSets;
|
||||
|
||||
std::vector<std::string> emoteCodes;
|
||||
|
||||
bool filled = false;
|
||||
};
|
||||
|
||||
std::map<std::string, TwitchAccountEmoteData> twitchAccountEmotes;
|
||||
|
||||
private:
|
||||
// emote code
|
||||
ConcurrentMap<QString, twitch::EmoteValue *> _twitchEmotes;
|
||||
|
||||
// emote id
|
||||
ConcurrentMap<long, EmoteData> _twitchEmoteFromCache;
|
||||
|
||||
/// BTTV emotes
|
||||
EmoteMap bttvChannelEmotes;
|
||||
|
||||
public:
|
||||
ConcurrentMap<QString, EmoteMap> bttvChannels;
|
||||
EmoteMap bttvGlobalEmotes;
|
||||
SignalVector<std::string> bttvGlobalEmoteCodes;
|
||||
// roomID
|
||||
std::map<std::string, SignalVector<std::string>> bttvChannelEmoteCodes;
|
||||
EmoteMap _bttvChannelEmoteFromCaches;
|
||||
|
||||
private:
|
||||
void loadBTTVEmotes();
|
||||
|
||||
/// FFZ emotes
|
||||
EmoteMap ffzChannelEmotes;
|
||||
|
||||
public:
|
||||
ConcurrentMap<QString, EmoteMap> ffzChannels;
|
||||
EmoteMap ffzGlobalEmotes;
|
||||
SignalVector<std::string> ffzGlobalEmoteCodes;
|
||||
std::map<std::string, SignalVector<std::string>> ffzChannelEmoteCodes;
|
||||
|
||||
private:
|
||||
ConcurrentMap<int, EmoteData> _ffzChannelEmoteFromCaches;
|
||||
|
||||
void loadFFZEmotes();
|
||||
|
||||
/// Chatterino emotes
|
||||
EmoteMap _chatterinoEmotes;
|
||||
|
||||
boost::signals2::signal<void()> gifUpdateTimerSignal;
|
||||
QTimer gifUpdateTimer;
|
||||
bool gifUpdateTimerInitiated = false;
|
||||
|
||||
int _generation = 0;
|
||||
|
||||
// methods
|
||||
static QString getTwitchEmoteLink(long id, qreal &scale);
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,91 @@
|
||||
#include "singletons/fontmanager.hpp"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
FontManager::FontManager()
|
||||
: currentFontFamily("/appearance/currentFontFamily", "Arial")
|
||||
, currentFontSize("/appearance/currentFontSize", 12)
|
||||
// , currentFont(this->currentFontFamily.getValue().c_str(), currentFontSize.getValue())
|
||||
{
|
||||
this->currentFontFamily.connect([this](const std::string &newValue, auto) {
|
||||
this->incGeneration();
|
||||
// this->currentFont.setFamily(newValue.c_str());
|
||||
this->currentFontByDpi.clear();
|
||||
this->fontChanged.invoke();
|
||||
});
|
||||
this->currentFontSize.connect([this](const int &newValue, auto) {
|
||||
this->incGeneration();
|
||||
// this->currentFont.setSize(newValue);
|
||||
this->currentFontByDpi.clear();
|
||||
this->fontChanged.invoke();
|
||||
});
|
||||
}
|
||||
|
||||
FontManager &FontManager::getInstance()
|
||||
{
|
||||
static FontManager instance;
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
QFont &FontManager::getFont(Type type, float dpi)
|
||||
{
|
||||
// return this->currentFont.getFont(type);
|
||||
return this->getCurrentFont(dpi).getFont(type);
|
||||
}
|
||||
|
||||
QFontMetrics &FontManager::getFontMetrics(Type type, float dpi)
|
||||
{
|
||||
// return this->currentFont.getFontMetrics(type);
|
||||
return this->getCurrentFont(dpi).getFontMetrics(type);
|
||||
}
|
||||
|
||||
FontManager::FontData &FontManager::Font::getFontData(Type type)
|
||||
{
|
||||
switch (type) {
|
||||
case Small:
|
||||
return this->small;
|
||||
case MediumSmall:
|
||||
return this->mediumSmall;
|
||||
case Medium:
|
||||
return this->medium;
|
||||
case MediumBold:
|
||||
return this->mediumBold;
|
||||
case MediumItalic:
|
||||
return this->mediumItalic;
|
||||
case Large:
|
||||
return this->large;
|
||||
case VeryLarge:
|
||||
return this->veryLarge;
|
||||
default:
|
||||
qDebug() << "Unknown font type:" << type << ", defaulting to medium";
|
||||
return this->medium;
|
||||
}
|
||||
}
|
||||
|
||||
QFont &FontManager::Font::getFont(Type type)
|
||||
{
|
||||
return this->getFontData(type).font;
|
||||
}
|
||||
|
||||
QFontMetrics &FontManager::Font::getFontMetrics(Type type)
|
||||
{
|
||||
return this->getFontData(type).metrics;
|
||||
}
|
||||
|
||||
FontManager::Font &FontManager::getCurrentFont(float dpi)
|
||||
{
|
||||
for (auto it = this->currentFontByDpi.begin(); it != this->currentFontByDpi.end(); it++) {
|
||||
if (it->first == dpi) {
|
||||
return it->second;
|
||||
}
|
||||
}
|
||||
this->currentFontByDpi.push_back(std::make_pair(
|
||||
dpi,
|
||||
Font(this->currentFontFamily.getValue().c_str(), this->currentFontSize.getValue() * dpi)));
|
||||
|
||||
return this->currentFontByDpi.back().second;
|
||||
}
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,135 @@
|
||||
#pragma once
|
||||
|
||||
#include <QFont>
|
||||
#include <QFontMetrics>
|
||||
#include <pajlada/settings/setting.hpp>
|
||||
#include <pajlada/signals/signal.hpp>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class FontManager
|
||||
{
|
||||
FontManager(const FontManager &) = delete;
|
||||
FontManager(FontManager &&) = delete;
|
||||
FontManager();
|
||||
|
||||
public:
|
||||
enum Type : uint8_t {
|
||||
Small,
|
||||
MediumSmall,
|
||||
Medium,
|
||||
MediumBold,
|
||||
MediumItalic,
|
||||
Large,
|
||||
VeryLarge,
|
||||
};
|
||||
|
||||
// FontManager is initialized only once, on first use
|
||||
static FontManager &getInstance();
|
||||
|
||||
QFont &getFont(Type type, float dpi);
|
||||
QFontMetrics &getFontMetrics(Type type, float dpi);
|
||||
|
||||
int getGeneration() const
|
||||
{
|
||||
return this->generation;
|
||||
}
|
||||
|
||||
void incGeneration()
|
||||
{
|
||||
this->generation++;
|
||||
}
|
||||
|
||||
pajlada::Settings::Setting<std::string> currentFontFamily;
|
||||
pajlada::Settings::Setting<int> currentFontSize;
|
||||
|
||||
pajlada::Signals::NoArgSignal fontChanged;
|
||||
|
||||
private:
|
||||
struct FontData {
|
||||
FontData(QFont &&_font)
|
||||
: font(_font)
|
||||
, metrics(this->font)
|
||||
{
|
||||
}
|
||||
|
||||
QFont font;
|
||||
QFontMetrics metrics;
|
||||
};
|
||||
|
||||
struct Font {
|
||||
Font() = delete;
|
||||
|
||||
explicit Font(const char *fontFamilyName, int mediumSize)
|
||||
: small(QFont(fontFamilyName, mediumSize - 4))
|
||||
, mediumSmall(QFont(fontFamilyName, mediumSize - 2))
|
||||
, medium(QFont(fontFamilyName, mediumSize))
|
||||
, mediumBold(QFont(fontFamilyName, mediumSize, QFont::DemiBold))
|
||||
, mediumItalic(QFont(fontFamilyName, mediumSize, -1, true))
|
||||
, large(QFont(fontFamilyName, mediumSize))
|
||||
, veryLarge(QFont(fontFamilyName, mediumSize))
|
||||
{
|
||||
}
|
||||
|
||||
void setFamily(const char *newFamily)
|
||||
{
|
||||
this->small.font.setFamily(newFamily);
|
||||
this->mediumSmall.font.setFamily(newFamily);
|
||||
this->medium.font.setFamily(newFamily);
|
||||
this->mediumBold.font.setFamily(newFamily);
|
||||
this->mediumItalic.font.setFamily(newFamily);
|
||||
this->large.font.setFamily(newFamily);
|
||||
this->veryLarge.font.setFamily(newFamily);
|
||||
|
||||
this->updateMetrics();
|
||||
}
|
||||
|
||||
void setSize(int newMediumSize)
|
||||
{
|
||||
this->small.font.setPointSize(newMediumSize - 4);
|
||||
this->mediumSmall.font.setPointSize(newMediumSize - 2);
|
||||
this->medium.font.setPointSize(newMediumSize);
|
||||
this->mediumBold.font.setPointSize(newMediumSize);
|
||||
this->mediumItalic.font.setPointSize(newMediumSize);
|
||||
this->large.font.setPointSize(newMediumSize + 2);
|
||||
this->veryLarge.font.setPointSize(newMediumSize + 4);
|
||||
|
||||
this->updateMetrics();
|
||||
}
|
||||
|
||||
void updateMetrics()
|
||||
{
|
||||
this->small.metrics = QFontMetrics(this->small.font);
|
||||
this->mediumSmall.metrics = QFontMetrics(this->mediumSmall.font);
|
||||
this->medium.metrics = QFontMetrics(this->medium.font);
|
||||
this->mediumBold.metrics = QFontMetrics(this->mediumBold.font);
|
||||
this->mediumItalic.metrics = QFontMetrics(this->mediumItalic.font);
|
||||
this->large.metrics = QFontMetrics(this->large.font);
|
||||
this->veryLarge.metrics = QFontMetrics(this->veryLarge.font);
|
||||
}
|
||||
|
||||
FontData &getFontData(Type type);
|
||||
|
||||
QFont &getFont(Type type);
|
||||
QFontMetrics &getFontMetrics(Type type);
|
||||
|
||||
FontData small;
|
||||
FontData mediumSmall;
|
||||
FontData medium;
|
||||
FontData mediumBold;
|
||||
FontData mediumItalic;
|
||||
FontData large;
|
||||
FontData veryLarge;
|
||||
};
|
||||
|
||||
Font &getCurrentFont(float dpi);
|
||||
|
||||
// Future plans:
|
||||
// Could have multiple fonts in here, such as "Menu font", "Application font", "Chat font"
|
||||
|
||||
std::list<std::pair<float, Font>> currentFontByDpi;
|
||||
|
||||
int generation = 0;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
namespace chatterino {
|
||||
static void _registerSetting(std::weak_ptr<pajlada::Settings::ISettingData> setting);
|
||||
|
||||
template <typename Type>
|
||||
class ChatterinoSetting : public pajlada::Settings::Setting<Type>
|
||||
{
|
||||
public:
|
||||
ChatterinoSetting(const std::string &_path, const Type &_defaultValue)
|
||||
: pajlada::Settings::Setting<Type>(_path, _defaultValue)
|
||||
{
|
||||
_registerSetting(this->data);
|
||||
}
|
||||
|
||||
void saveRecall();
|
||||
|
||||
ChatterinoSetting &operator=(const Type &newValue)
|
||||
{
|
||||
assert(this->data != nullptr);
|
||||
|
||||
this->setValue(newValue);
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
template <typename T2>
|
||||
ChatterinoSetting &operator=(const T2 &newValue)
|
||||
{
|
||||
assert(this->data != nullptr);
|
||||
|
||||
this->setValue(newValue);
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
ChatterinoSetting &operator=(Type &&newValue) noexcept
|
||||
{
|
||||
assert(this->data != nullptr);
|
||||
|
||||
this->setValue(std::move(newValue));
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool operator==(const Type &rhs) const
|
||||
{
|
||||
assert(this->data != nullptr);
|
||||
|
||||
return this->getValue() == rhs;
|
||||
}
|
||||
|
||||
bool operator!=(const Type &rhs) const
|
||||
{
|
||||
assert(this->data != nullptr);
|
||||
|
||||
return this->getValue() != rhs;
|
||||
}
|
||||
|
||||
operator const Type() const
|
||||
{
|
||||
assert(this->data != nullptr);
|
||||
|
||||
return this->getValue();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
#include "completionmodel.hpp"
|
||||
|
||||
#include "common.hpp"
|
||||
#include "debug/log.hpp"
|
||||
#include "singletons/channelmanager.hpp"
|
||||
#include "singletons/completionmanager.hpp"
|
||||
#include "singletons/emotemanager.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
CompletionModel::CompletionModel(const QString &_channelName)
|
||||
: channelName(_channelName)
|
||||
{
|
||||
}
|
||||
|
||||
void CompletionModel::refresh()
|
||||
{
|
||||
// debug::Log("[CompletionModel:{}] Refreshing...]", this->channelName);
|
||||
|
||||
auto &emoteManager = EmoteManager::getInstance();
|
||||
this->emotes.clear();
|
||||
|
||||
// User-specific: Twitch Emotes
|
||||
// TODO: Fix this so it properly updates with the proper api. oauth token needs proper scope
|
||||
for (const auto &m : emoteManager.twitchAccountEmotes) {
|
||||
for (const auto &emoteName : m.second.emoteCodes) {
|
||||
this->addString(emoteName);
|
||||
}
|
||||
}
|
||||
|
||||
// Global: BTTV Global Emotes
|
||||
std::vector<std::string> &bttvGlobalEmoteCodes = emoteManager.bttvGlobalEmoteCodes;
|
||||
for (const auto &m : bttvGlobalEmoteCodes) {
|
||||
this->addString(m);
|
||||
}
|
||||
|
||||
// Global: FFZ Global Emotes
|
||||
std::vector<std::string> &ffzGlobalEmoteCodes = emoteManager.ffzGlobalEmoteCodes;
|
||||
for (const auto &m : ffzGlobalEmoteCodes) {
|
||||
this->addString(m);
|
||||
}
|
||||
|
||||
// Channel-specific: BTTV Channel Emotes
|
||||
std::vector<std::string> &bttvChannelEmoteCodes =
|
||||
emoteManager.bttvChannelEmoteCodes[this->channelName.toStdString()];
|
||||
for (const auto &m : bttvChannelEmoteCodes) {
|
||||
this->addString(m);
|
||||
}
|
||||
|
||||
// Channel-specific: FFZ Channel Emotes
|
||||
std::vector<std::string> &ffzChannelEmoteCodes =
|
||||
emoteManager.ffzChannelEmoteCodes[this->channelName.toStdString()];
|
||||
for (const auto &m : ffzChannelEmoteCodes) {
|
||||
this->addString(m);
|
||||
}
|
||||
|
||||
// Global: Emojis
|
||||
const auto &emojiShortCodes = emoteManager.emojiShortCodes;
|
||||
for (const auto &m : emojiShortCodes) {
|
||||
this->addString(":" + m + ":");
|
||||
}
|
||||
|
||||
// Channel-specific: Usernames
|
||||
auto c = ChannelManager::getInstance().getTwitchChannel(this->channelName);
|
||||
auto usernames = c->getUsernamesForCompletions();
|
||||
for (const auto &name : usernames) {
|
||||
assert(!name.displayName.isEmpty());
|
||||
this->addString(name.displayName);
|
||||
this->addString('@' + name.displayName);
|
||||
|
||||
if (!name.localizedName.isEmpty()) {
|
||||
this->addString(name.localizedName);
|
||||
this->addString('@' + name.localizedName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CompletionModel::addString(const std::string &str)
|
||||
{
|
||||
// Always add a space at the end of completions
|
||||
this->emotes.push_back(qS(str) + " ");
|
||||
}
|
||||
|
||||
void CompletionModel::addString(const QString &str)
|
||||
{
|
||||
// Always add a space at the end of completions
|
||||
this->emotes.push_back(str + " ");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QVector>
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace chatterino {
|
||||
class CompletionModel : public QAbstractListModel
|
||||
{
|
||||
public:
|
||||
CompletionModel(const QString &_channelName);
|
||||
|
||||
virtual int columnCount(const QModelIndex &) const override
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
virtual QVariant data(const QModelIndex &index, int) const override
|
||||
{
|
||||
// TODO: Implement more safely
|
||||
return QVariant(this->emotes.at(index.row()));
|
||||
}
|
||||
|
||||
virtual int rowCount(const QModelIndex &) const override
|
||||
{
|
||||
return this->emotes.size();
|
||||
}
|
||||
|
||||
void refresh();
|
||||
|
||||
private:
|
||||
void addString(const std::string &str);
|
||||
void addString(const QString &str);
|
||||
|
||||
QVector<QString> emotes;
|
||||
|
||||
QString channelName;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
#include "singletons/ircmanager.hpp"
|
||||
#include "asyncexec.hpp"
|
||||
#include "channel.hpp"
|
||||
#include "debug/log.hpp"
|
||||
#include "messages/messageparseargs.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "singletons/accountmanager.hpp"
|
||||
#include "singletons/channelmanager.hpp"
|
||||
#include "singletons/emotemanager.hpp"
|
||||
#include "singletons/settingsmanager.hpp"
|
||||
#include "singletons/windowmanager.hpp"
|
||||
#include "twitch/twitchmessagebuilder.hpp"
|
||||
#include "twitch/twitchparsemessage.hpp"
|
||||
#include "twitch/twitchuser.hpp"
|
||||
#include "util/urlfetch.hpp"
|
||||
|
||||
#include <irccommand.h>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
|
||||
#include <future>
|
||||
|
||||
using namespace chatterino::messages;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
IrcManager::IrcManager(ChannelManager &_channelManager, Resources &_resources,
|
||||
AccountManager &_accountManager)
|
||||
: channelManager(_channelManager)
|
||||
, resources(_resources)
|
||||
, accountManager(_accountManager)
|
||||
{
|
||||
this->messageSuffix.append(' ');
|
||||
this->messageSuffix.append(QChar(0x206D));
|
||||
|
||||
this->account = accountManager.Twitch.getCurrent();
|
||||
accountManager.Twitch.userChanged.connect([this]() {
|
||||
this->setUser(accountManager.Twitch.getCurrent());
|
||||
|
||||
debug::Log("[IrcManager] Reconnecting to Twitch IRC as new user {}",
|
||||
this->account->getUserName());
|
||||
|
||||
postToThread([this] { this->connect(); });
|
||||
});
|
||||
|
||||
// Initialize the connections
|
||||
this->writeConnection.reset(new Communi::IrcConnection);
|
||||
this->writeConnection->moveToThread(QCoreApplication::instance()->thread());
|
||||
|
||||
QObject::connect(this->writeConnection.get(), &Communi::IrcConnection::messageReceived, this,
|
||||
&IrcManager::writeConnectionMessageReceived);
|
||||
|
||||
this->readConnection.reset(new Communi::IrcConnection);
|
||||
this->readConnection->moveToThread(QCoreApplication::instance()->thread());
|
||||
|
||||
// Listen to read connection message signals
|
||||
QObject::connect(this->readConnection.get(), &Communi::IrcConnection::messageReceived, this,
|
||||
&IrcManager::messageReceived);
|
||||
QObject::connect(this->readConnection.get(), &Communi::IrcConnection::privateMessageReceived,
|
||||
this, &IrcManager::privateMessageReceived);
|
||||
|
||||
QObject::connect(this->readConnection.get(), &Communi::IrcConnection::connected, this,
|
||||
&IrcManager::onConnected);
|
||||
QObject::connect(this->readConnection.get(), &Communi::IrcConnection::disconnected, this,
|
||||
&IrcManager::onDisconnected);
|
||||
}
|
||||
|
||||
IrcManager &IrcManager::getInstance()
|
||||
{
|
||||
static IrcManager instance(ChannelManager::getInstance(), Resources::getInstance(),
|
||||
AccountManager::getInstance());
|
||||
return instance;
|
||||
}
|
||||
|
||||
void IrcManager::setUser(std::shared_ptr<twitch::TwitchUser> newAccount)
|
||||
{
|
||||
this->account = newAccount;
|
||||
}
|
||||
|
||||
void IrcManager::connect()
|
||||
{
|
||||
this->disconnect();
|
||||
|
||||
this->initializeConnection(this->writeConnection, false);
|
||||
this->initializeConnection(this->readConnection, true);
|
||||
|
||||
// XXX(pajlada): Disabled the async_exec for now, because if we happen to run the
|
||||
// `beginConnecting` function in a different thread than last time, we won't be able to connect
|
||||
// because we can't clean up the previous connection properly
|
||||
// async_exec([this] { beginConnecting(); });
|
||||
this->beginConnecting();
|
||||
}
|
||||
|
||||
void IrcManager::initializeConnection(const std::unique_ptr<Communi::IrcConnection> &connection,
|
||||
bool isReadConnection)
|
||||
{
|
||||
assert(this->account);
|
||||
|
||||
QString username = this->account->getUserName();
|
||||
QString oauthClient = this->account->getOAuthClient();
|
||||
QString oauthToken = this->account->getOAuthToken();
|
||||
if (!oauthToken.startsWith("oauth:")) {
|
||||
oauthToken.prepend("oauth:");
|
||||
}
|
||||
|
||||
connection->setUserName(username);
|
||||
connection->setNickName(username);
|
||||
connection->setRealName(username);
|
||||
|
||||
if (!this->account->isAnon()) {
|
||||
connection->setPassword(oauthToken);
|
||||
|
||||
this->refreshIgnoredUsers(username, oauthClient, oauthToken);
|
||||
}
|
||||
|
||||
if (isReadConnection) {
|
||||
connection->sendCommand(
|
||||
Communi::IrcCommand::createCapability("REQ", "twitch.tv/membership"));
|
||||
connection->sendCommand(Communi::IrcCommand::createCapability("REQ", "twitch.tv/commands"));
|
||||
connection->sendCommand(Communi::IrcCommand::createCapability("REQ", "twitch.tv/tags"));
|
||||
} else {
|
||||
connection->sendCommand(Communi::IrcCommand::createCapability("REQ", "twitch.tv/tags"));
|
||||
|
||||
connection->sendCommand(
|
||||
Communi::IrcCommand::createCapability("REQ", "twitch.tv/membership"));
|
||||
connection->sendCommand(Communi::IrcCommand::createCapability("REQ", "twitch.tv/commands"));
|
||||
}
|
||||
|
||||
connection->setHost("irc.chat.twitch.tv");
|
||||
connection->setPort(6667);
|
||||
}
|
||||
|
||||
void IrcManager::refreshIgnoredUsers(const QString &username, const QString &oauthClient,
|
||||
const QString &oauthToken)
|
||||
{
|
||||
QString nextLink = "https://api.twitch.tv/kraken/users/" + username + "/blocks?limit=" + 100 +
|
||||
"&client_id=" + oauthClient;
|
||||
|
||||
QNetworkAccessManager *manager = new QNetworkAccessManager();
|
||||
QNetworkRequest req(QUrl(nextLink + "&oauth_token=" + oauthToken));
|
||||
QNetworkReply *reply = manager->get(req);
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished, [=] {
|
||||
this->twitchBlockedUsersMutex.lock();
|
||||
this->twitchBlockedUsers.clear();
|
||||
this->twitchBlockedUsersMutex.unlock();
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
QJsonDocument jsonDoc(QJsonDocument::fromJson(data));
|
||||
QJsonObject root = jsonDoc.object();
|
||||
|
||||
// nextLink =
|
||||
// root.value("this->links").toObject().value("next").toString();
|
||||
|
||||
auto blocks = root.value("blocks").toArray();
|
||||
|
||||
this->twitchBlockedUsersMutex.lock();
|
||||
for (QJsonValue block : blocks) {
|
||||
QJsonObject user = block.toObject().value("user").toObject();
|
||||
// displaythis->name
|
||||
this->twitchBlockedUsers.insert(user.value("name").toString().toLower(), true);
|
||||
}
|
||||
this->twitchBlockedUsersMutex.unlock();
|
||||
|
||||
manager->deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
void IrcManager::beginConnecting()
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(this->connectionMutex);
|
||||
|
||||
for (auto &channel : this->channelManager.getItems()) {
|
||||
this->writeConnection->sendRaw("JOIN #" + channel->name);
|
||||
this->readConnection->sendRaw("JOIN #" + channel->name);
|
||||
}
|
||||
|
||||
this->writeConnection->open();
|
||||
this->readConnection->open();
|
||||
}
|
||||
|
||||
void IrcManager::disconnect()
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(this->connectionMutex);
|
||||
|
||||
this->readConnection->close();
|
||||
this->writeConnection->close();
|
||||
}
|
||||
|
||||
void IrcManager::sendMessage(const QString &channelName, QString message)
|
||||
{
|
||||
this->connectionMutex.lock();
|
||||
static int i = 0;
|
||||
|
||||
if (this->writeConnection) {
|
||||
if (SettingsManager::getInstance().allowDuplicateMessages && (++i % 2) == 0) {
|
||||
message.append(this->messageSuffix);
|
||||
}
|
||||
this->writeConnection->sendRaw("PRIVMSG #" + channelName + " :" + message);
|
||||
}
|
||||
|
||||
this->connectionMutex.unlock();
|
||||
}
|
||||
|
||||
void IrcManager::joinChannel(const QString &channelName)
|
||||
{
|
||||
this->connectionMutex.lock();
|
||||
|
||||
if (this->readConnection && this->writeConnection) {
|
||||
this->readConnection->sendRaw("JOIN #" + channelName);
|
||||
this->writeConnection->sendRaw("JOIN #" + channelName);
|
||||
}
|
||||
|
||||
this->connectionMutex.unlock();
|
||||
}
|
||||
|
||||
void IrcManager::partChannel(const QString &channelName)
|
||||
{
|
||||
this->connectionMutex.lock();
|
||||
|
||||
if (this->readConnection && this->writeConnection) {
|
||||
this->readConnection->sendRaw("PART #" + channelName);
|
||||
this->writeConnection->sendRaw("PART #" + channelName);
|
||||
}
|
||||
|
||||
this->connectionMutex.unlock();
|
||||
}
|
||||
|
||||
void IrcManager::privateMessageReceived(Communi::IrcPrivateMessage *message)
|
||||
{
|
||||
this->onPrivateMessage.invoke(message);
|
||||
auto c = this->channelManager.getTwitchChannel(message->target().mid(1));
|
||||
|
||||
if (!c) {
|
||||
return;
|
||||
}
|
||||
|
||||
messages::MessageParseArgs args;
|
||||
|
||||
twitch::TwitchMessageBuilder builder(c.get(), message, args);
|
||||
|
||||
c->addMessage(builder.parse());
|
||||
}
|
||||
|
||||
void IrcManager::messageReceived(Communi::IrcMessage *message)
|
||||
{
|
||||
if (message->type() == Communi::IrcMessage::Type::Private) {
|
||||
// We already have a handler for private messages
|
||||
return;
|
||||
}
|
||||
|
||||
const QString &command = message->command();
|
||||
|
||||
if (command == "ROOMSTATE") {
|
||||
this->handleRoomStateMessage(message);
|
||||
} else if (command == "CLEARCHAT") {
|
||||
this->handleClearChatMessage(message);
|
||||
} else if (command == "USERSTATE") {
|
||||
this->handleUserStateMessage(message);
|
||||
} else if (command == "WHISPER") {
|
||||
this->handleWhisperMessage(message);
|
||||
} else if (command == "USERNOTICE") {
|
||||
this->handleUserNoticeMessage(message);
|
||||
} else if (command == "MODE") {
|
||||
this->handleModeMessage(message);
|
||||
} else if (command == "NOTICE") {
|
||||
this->handleNoticeMessage(static_cast<Communi::IrcNoticeMessage *>(message));
|
||||
}
|
||||
}
|
||||
|
||||
void IrcManager::writeConnectionMessageReceived(Communi::IrcMessage *message)
|
||||
{
|
||||
switch (message->type()) {
|
||||
case Communi::IrcMessage::Type::Notice: {
|
||||
this->handleWriteConnectionNoticeMessage(
|
||||
static_cast<Communi::IrcNoticeMessage *>(message));
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
void IrcManager::handleRoomStateMessage(Communi::IrcMessage *message)
|
||||
{
|
||||
const auto &tags = message->tags();
|
||||
|
||||
auto iterator = tags.find("room-id");
|
||||
|
||||
if (iterator != tags.end()) {
|
||||
auto roomID = iterator.value().toString();
|
||||
|
||||
auto channel = channelManager.getTwitchChannel(QString(message->toData()).split("#").at(1));
|
||||
auto twitchChannel = dynamic_cast<twitch::TwitchChannel *>(channel.get());
|
||||
if (twitchChannel != nullptr) {
|
||||
twitchChannel->setRoomID(roomID);
|
||||
}
|
||||
|
||||
this->resources.loadChannelData(roomID);
|
||||
}
|
||||
}
|
||||
|
||||
void IrcManager::handleClearChatMessage(Communi::IrcMessage *message)
|
||||
{
|
||||
assert(message->parameters().length() >= 1);
|
||||
|
||||
auto rawChannelName = message->parameter(0);
|
||||
|
||||
assert(rawChannelName.length() >= 2);
|
||||
|
||||
auto trimmedChannelName = rawChannelName.mid(1);
|
||||
|
||||
auto c = this->channelManager.getTwitchChannel(trimmedChannelName);
|
||||
|
||||
if (!c) {
|
||||
debug::Log("[IrcManager:handleClearChatMessage] Channel {} not found in channel manager",
|
||||
trimmedChannelName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message->parameters().length() == 1) {
|
||||
std::shared_ptr<Message> msg(
|
||||
Message::createSystemMessage("Chat has been cleared by a moderator."));
|
||||
|
||||
c->addMessage(msg);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
assert(message->parameters().length() >= 2);
|
||||
|
||||
QString username = message->parameter(1);
|
||||
QString durationInSeconds, reason;
|
||||
QVariant v = message->tag("ban-duration");
|
||||
if (v.isValid()) {
|
||||
durationInSeconds = v.toString();
|
||||
}
|
||||
|
||||
v = message->tag("ban-reason");
|
||||
if (v.isValid()) {
|
||||
reason = v.toString();
|
||||
}
|
||||
|
||||
std::shared_ptr<Message> msg(
|
||||
Message::createTimeoutMessage(username, durationInSeconds, reason));
|
||||
|
||||
c->addMessage(msg);
|
||||
}
|
||||
|
||||
void IrcManager::handleUserStateMessage(Communi::IrcMessage *message)
|
||||
{
|
||||
// TODO: Implement
|
||||
}
|
||||
|
||||
void IrcManager::handleWhisperMessage(Communi::IrcMessage *message)
|
||||
{
|
||||
// TODO: Implement
|
||||
}
|
||||
|
||||
void IrcManager::handleUserNoticeMessage(Communi::IrcMessage *message)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
void IrcManager::handleModeMessage(Communi::IrcMessage *message)
|
||||
{
|
||||
auto channel = channelManager.getTwitchChannel(message->parameter(0).remove(0, 1));
|
||||
if (message->parameter(1) == "+o") {
|
||||
channel->modList.append(message->parameter(2));
|
||||
} else if (message->parameter(1) == "-o") {
|
||||
channel->modList.append(message->parameter(2));
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: This does not fit in IrcManager
|
||||
bool IrcManager::isTwitchBlockedUser(QString const &username)
|
||||
{
|
||||
QMutexLocker locker(&this->twitchBlockedUsersMutex);
|
||||
|
||||
auto iterator = this->twitchBlockedUsers.find(username);
|
||||
|
||||
return iterator != this->twitchBlockedUsers.end();
|
||||
}
|
||||
|
||||
// XXX: This does not fit in IrcManager
|
||||
bool IrcManager::tryAddIgnoredUser(QString const &username, QString &errorMessage)
|
||||
{
|
||||
assert(this->account);
|
||||
|
||||
QUrl url("https://api.twitch.tv/kraken/users/" + this->account->getUserName() + "/blocks/" +
|
||||
username + "?oauth_token=" + this->account->getOAuthToken() +
|
||||
"&client_id=" + this->account->getOAuthClient());
|
||||
|
||||
QNetworkRequest request(url);
|
||||
auto reply = this->networkAccessManager.put(request, QByteArray());
|
||||
reply->waitForReadyRead(10000);
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
this->twitchBlockedUsersMutex.lock();
|
||||
this->twitchBlockedUsers.insert(username, true);
|
||||
this->twitchBlockedUsersMutex.unlock();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
errorMessage = "Error while ignoring user \"" + username + "\": " + reply->errorString();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// XXX: This does not fit in IrcManager
|
||||
void IrcManager::addIgnoredUser(QString const &username)
|
||||
{
|
||||
QString errorMessage;
|
||||
if (!tryAddIgnoredUser(username, errorMessage)) {
|
||||
// TODO: Implement IrcManager::addIgnoredUser
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: This does not fit in IrcManager
|
||||
bool IrcManager::tryRemoveIgnoredUser(QString const &username, QString &errorMessage)
|
||||
{
|
||||
assert(this->account);
|
||||
|
||||
QUrl url("https://api.twitch.tv/kraken/users/" + this->account->getUserName() + "/blocks/" +
|
||||
username + "?oauth_token=" + this->account->getOAuthToken() +
|
||||
"&client_id=" + this->account->getOAuthClient());
|
||||
|
||||
QNetworkRequest request(url);
|
||||
auto reply = this->networkAccessManager.deleteResource(request);
|
||||
reply->waitForReadyRead(10000);
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
this->twitchBlockedUsersMutex.lock();
|
||||
this->twitchBlockedUsers.remove(username);
|
||||
this->twitchBlockedUsersMutex.unlock();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
errorMessage = "Error while unignoring user \"" + username + "\": " + reply->errorString();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// XXX: This does not fit in IrcManager
|
||||
void IrcManager::removeIgnoredUser(QString const &username)
|
||||
{
|
||||
QString errorMessage;
|
||||
if (!tryRemoveIgnoredUser(username, errorMessage)) {
|
||||
// TODO: Implement IrcManager::removeIgnoredUser
|
||||
}
|
||||
}
|
||||
|
||||
void IrcManager::handleNoticeMessage(Communi::IrcNoticeMessage *message)
|
||||
{
|
||||
auto rawChannelName = message->target();
|
||||
|
||||
bool broadcast = rawChannelName.length() < 2;
|
||||
std::shared_ptr<Message> msg(Message::createSystemMessage(message->content()));
|
||||
|
||||
if (broadcast) {
|
||||
this->channelManager.doOnAll([msg](const auto &c) {
|
||||
c->addMessage(msg); //
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto trimmedChannelName = rawChannelName.mid(1);
|
||||
|
||||
auto c = this->channelManager.getTwitchChannel(trimmedChannelName);
|
||||
|
||||
if (!c) {
|
||||
debug::Log("[IrcManager:handleNoticeMessage] Channel {} not found in channel manager",
|
||||
trimmedChannelName);
|
||||
return;
|
||||
}
|
||||
|
||||
c->addMessage(msg);
|
||||
}
|
||||
|
||||
void IrcManager::handleWriteConnectionNoticeMessage(Communi::IrcNoticeMessage *message)
|
||||
{
|
||||
QVariant v = message->tag("msg-id");
|
||||
if (!v.isValid()) {
|
||||
return;
|
||||
}
|
||||
QString msg_id = v.toString();
|
||||
|
||||
static QList<QString> idsToSkip = {"timeout_success", "ban_success"};
|
||||
|
||||
if (idsToSkip.contains(msg_id)) {
|
||||
// Already handled in the read-connection
|
||||
return;
|
||||
}
|
||||
|
||||
this->handleNoticeMessage(message);
|
||||
}
|
||||
|
||||
void IrcManager::onConnected()
|
||||
{
|
||||
std::shared_ptr<Message> msg(Message::createSystemMessage("connected to chat"));
|
||||
|
||||
this->channelManager.doOnAll([msg](std::shared_ptr<Channel> channel) {
|
||||
assert(channel);
|
||||
channel->addMessage(msg);
|
||||
});
|
||||
}
|
||||
|
||||
void IrcManager::onDisconnected()
|
||||
{
|
||||
std::shared_ptr<Message> msg(Message::createSystemMessage("disconnected from chat"));
|
||||
|
||||
this->channelManager.doOnAll([msg](std::shared_ptr<Channel> channel) {
|
||||
assert(channel);
|
||||
channel->addMessage(msg);
|
||||
});
|
||||
}
|
||||
|
||||
Communi::IrcConnection *IrcManager::getReadConnection()
|
||||
{
|
||||
return this->readConnection.get();
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
|
||||
#define TWITCH_MAX_MESSAGELENGTH 500
|
||||
|
||||
#include "messages/message.hpp"
|
||||
#include "twitch/twitchuser.hpp"
|
||||
|
||||
#include <ircconnection.h>
|
||||
#include <IrcMessage>
|
||||
#include <QMap>
|
||||
#include <QMutex>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QString>
|
||||
#include <pajlada/signals/signal.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class ChannelManager;
|
||||
class Resources;
|
||||
class AccountManager;
|
||||
|
||||
class IrcManager : public QObject
|
||||
{
|
||||
// Q_OBJECT
|
||||
|
||||
IrcManager(ChannelManager &channelManager, Resources &resources,
|
||||
AccountManager &accountManager);
|
||||
|
||||
public:
|
||||
static IrcManager &getInstance();
|
||||
|
||||
void connect();
|
||||
void disconnect();
|
||||
|
||||
bool isTwitchBlockedUser(QString const &username);
|
||||
bool tryAddIgnoredUser(QString const &username, QString &errorMessage);
|
||||
void addIgnoredUser(QString const &username);
|
||||
bool tryRemoveIgnoredUser(QString const &username, QString &errorMessage);
|
||||
void removeIgnoredUser(QString const &username);
|
||||
|
||||
void sendMessage(const QString &channelName, QString message);
|
||||
|
||||
void joinChannel(const QString &channelName);
|
||||
void partChannel(const QString &channelName);
|
||||
|
||||
void setUser(std::shared_ptr<twitch::TwitchUser> newAccount);
|
||||
|
||||
pajlada::Signals::Signal<Communi::IrcPrivateMessage *> onPrivateMessage;
|
||||
void privateMessageReceived(Communi::IrcPrivateMessage *message);
|
||||
|
||||
Communi::IrcConnection *getReadConnection();
|
||||
|
||||
private:
|
||||
ChannelManager &channelManager;
|
||||
Resources &resources;
|
||||
AccountManager &accountManager;
|
||||
|
||||
// variables
|
||||
std::shared_ptr<twitch::TwitchUser> account = nullptr;
|
||||
|
||||
std::unique_ptr<Communi::IrcConnection> writeConnection = nullptr;
|
||||
std::unique_ptr<Communi::IrcConnection> readConnection = nullptr;
|
||||
|
||||
std::mutex connectionMutex;
|
||||
|
||||
QMap<QString, bool> twitchBlockedUsers;
|
||||
QMutex twitchBlockedUsersMutex;
|
||||
|
||||
QNetworkAccessManager networkAccessManager;
|
||||
|
||||
void initializeConnection(const std::unique_ptr<Communi::IrcConnection> &connection,
|
||||
bool isReadConnection);
|
||||
|
||||
void refreshIgnoredUsers(const QString &username, const QString &oauthClient,
|
||||
const QString &oauthToken);
|
||||
|
||||
void beginConnecting();
|
||||
|
||||
void messageReceived(Communi::IrcMessage *message);
|
||||
|
||||
void writeConnectionMessageReceived(Communi::IrcMessage *message);
|
||||
|
||||
void handleRoomStateMessage(Communi::IrcMessage *message);
|
||||
void handleClearChatMessage(Communi::IrcMessage *message);
|
||||
void handleUserStateMessage(Communi::IrcMessage *message);
|
||||
void handleWhisperMessage(Communi::IrcMessage *message);
|
||||
void handleUserNoticeMessage(Communi::IrcMessage *message);
|
||||
void handleModeMessage(Communi::IrcMessage *message);
|
||||
void handleNoticeMessage(Communi::IrcNoticeMessage *message);
|
||||
void handleWriteConnectionNoticeMessage(Communi::IrcNoticeMessage *message);
|
||||
|
||||
void onConnected();
|
||||
void onDisconnected();
|
||||
|
||||
private:
|
||||
QByteArray messageSuffix;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,186 @@
|
||||
#include "singletons/settingsmanager.hpp"
|
||||
#include "appdatapath.hpp"
|
||||
#include "debug/log.hpp"
|
||||
|
||||
#include <QDir>
|
||||
#include <QStandardPaths>
|
||||
|
||||
using namespace chatterino::messages;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
std::vector<std::weak_ptr<pajlada::Settings::ISettingData>> _settings;
|
||||
|
||||
void _registerSetting(std::weak_ptr<pajlada::Settings::ISettingData> setting)
|
||||
{
|
||||
_settings.push_back(setting);
|
||||
}
|
||||
|
||||
SettingsManager::SettingsManager()
|
||||
: streamlinkPath("/behaviour/streamlink/path", "")
|
||||
, preferredQuality("/behaviour/streamlink/quality", "Choose")
|
||||
, emoteScale(this->settingsItems, "emoteScale", 1.0)
|
||||
, pathHighlightSound(this->settingsItems, "pathHighlightSound", "qrc:/sounds/ping2.wav")
|
||||
, highlightProperties(this->settingsItems, "highlightProperties",
|
||||
QMap<QString, QPair<bool, bool>>())
|
||||
, highlightUserBlacklist(this->settingsItems, "highlightUserBlacklist", "")
|
||||
, snapshot(nullptr)
|
||||
, settings(Path::getAppdataPath() + "settings.ini", QSettings::IniFormat)
|
||||
{
|
||||
this->wordMaskListener.addSetting(this->showTimestamps);
|
||||
this->wordMaskListener.addSetting(this->showTimestampSeconds);
|
||||
this->wordMaskListener.addSetting(this->showBadges);
|
||||
this->wordMaskListener.addSetting(this->enableBttvEmotes);
|
||||
this->wordMaskListener.addSetting(this->enableEmojis);
|
||||
this->wordMaskListener.addSetting(this->enableFfzEmotes);
|
||||
this->wordMaskListener.addSetting(this->enableTwitchEmotes);
|
||||
this->wordMaskListener.cb = [this](auto) {
|
||||
this->updateWordTypeMask(); //
|
||||
};
|
||||
}
|
||||
|
||||
void SettingsManager::save()
|
||||
{
|
||||
for (auto &item : this->settingsItems) {
|
||||
if (item.get().getName() != "highlightProperties") {
|
||||
this->settings.setValue(item.get().getName(), item.get().getVariant());
|
||||
} else {
|
||||
this->settings.beginGroup("Highlights");
|
||||
QStringList list = highlightProperties.get().keys();
|
||||
list.removeAll("");
|
||||
this->settings.remove("");
|
||||
for (auto string : list) {
|
||||
this->settings.beginGroup(string);
|
||||
this->settings.setValue("highlightSound",
|
||||
highlightProperties.get().value(string).first);
|
||||
this->settings.setValue("highlightTask",
|
||||
highlightProperties.get().value(string).second);
|
||||
this->settings.endGroup();
|
||||
}
|
||||
this->settings.endGroup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsManager::load()
|
||||
{
|
||||
for (auto &item : this->settingsItems) {
|
||||
if (item.get().getName() != "highlightProperties") {
|
||||
item.get().setVariant(this->settings.value(item.get().getName()));
|
||||
} else {
|
||||
this->settings.beginGroup("Highlights");
|
||||
QStringList list = this->settings.childGroups();
|
||||
for (auto string : list) {
|
||||
this->settings.beginGroup(string);
|
||||
highlightProperties.insertMap(string,
|
||||
this->settings.value("highlightSound").toBool(),
|
||||
this->settings.value("highlightTask").toBool());
|
||||
this->settings.endGroup();
|
||||
}
|
||||
this->settings.endGroup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Word::Flags SettingsManager::getWordTypeMask()
|
||||
{
|
||||
return this->wordTypeMask;
|
||||
}
|
||||
|
||||
bool SettingsManager::isIgnoredEmote(const QString &)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QSettings &SettingsManager::getQSettings()
|
||||
{
|
||||
return this->settings;
|
||||
}
|
||||
|
||||
void SettingsManager::updateWordTypeMask()
|
||||
{
|
||||
uint32_t newMaskUint = Word::Text;
|
||||
|
||||
if (this->showTimestamps) {
|
||||
if (this->showTimestampSeconds) {
|
||||
newMaskUint |= Word::TimestampWithSeconds;
|
||||
} else {
|
||||
newMaskUint |= Word::TimestampNoSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
newMaskUint |= enableTwitchEmotes ? Word::TwitchEmoteImage : Word::TwitchEmoteText;
|
||||
newMaskUint |= enableFfzEmotes ? Word::FfzEmoteImage : Word::FfzEmoteText;
|
||||
newMaskUint |= enableBttvEmotes ? Word::BttvEmoteImage : Word::BttvEmoteText;
|
||||
newMaskUint |=
|
||||
(enableBttvEmotes && enableGifAnimations) ? Word::BttvEmoteImage : Word::BttvEmoteText;
|
||||
newMaskUint |= enableEmojis ? Word::EmojiImage : Word::EmojiText;
|
||||
|
||||
newMaskUint |= Word::BitsAmount;
|
||||
newMaskUint |= enableGifAnimations ? Word::BitsAnimated : Word::BitsStatic;
|
||||
|
||||
if (this->showBadges) {
|
||||
newMaskUint |= Word::Badges;
|
||||
}
|
||||
|
||||
newMaskUint |= Word::Username;
|
||||
|
||||
newMaskUint |= Word::AlwaysShow;
|
||||
|
||||
Word::Flags newMask = static_cast<Word::Flags>(newMaskUint);
|
||||
|
||||
if (newMask != this->wordTypeMask) {
|
||||
this->wordTypeMask = newMask;
|
||||
|
||||
emit wordTypeMaskChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsManager::saveSnapshot()
|
||||
{
|
||||
rapidjson::Document *d = new rapidjson::Document(rapidjson::kObjectType);
|
||||
rapidjson::Document::AllocatorType &a = d->GetAllocator();
|
||||
|
||||
for (const auto &weakSetting : _settings) {
|
||||
auto setting = weakSetting.lock();
|
||||
if (!setting) {
|
||||
continue;
|
||||
}
|
||||
|
||||
rapidjson::Value key(setting->getPath().c_str(), a);
|
||||
rapidjson::Value val = setting->marshalInto(*d);
|
||||
d->AddMember(key.Move(), val.Move(), a);
|
||||
}
|
||||
|
||||
this->snapshot.reset(d);
|
||||
|
||||
debug::Log("hehe: {}", pajlada::Settings::SettingManager::stringify(*d));
|
||||
}
|
||||
|
||||
void SettingsManager::recallSnapshot()
|
||||
{
|
||||
if (!this->snapshot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &snapshotObject = this->snapshot->GetObject();
|
||||
|
||||
for (const auto &weakSetting : _settings) {
|
||||
auto setting = weakSetting.lock();
|
||||
if (!setting) {
|
||||
debug::Log("Error stage 1 of loading");
|
||||
continue;
|
||||
}
|
||||
|
||||
const char *path = setting->getPath().c_str();
|
||||
|
||||
if (!snapshotObject.HasMember(path)) {
|
||||
debug::Log("Error stage 2 of loading");
|
||||
continue;
|
||||
}
|
||||
|
||||
setting->unmarshalValue(snapshotObject[path]);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,109 @@
|
||||
#pragma once
|
||||
|
||||
#include "messages/word.hpp"
|
||||
#include "setting.hpp"
|
||||
#include "singletons/helper/chatterinosetting.hpp"
|
||||
|
||||
#include <QSettings>
|
||||
#include <pajlada/settings/setting.hpp>
|
||||
#include <pajlada/settings/settinglistener.hpp>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
static void _registerSetting(std::weak_ptr<pajlada::Settings::ISettingData> setting);
|
||||
|
||||
class SettingsManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
using BoolSetting = ChatterinoSetting<bool>;
|
||||
using FloatSetting = ChatterinoSetting<float>;
|
||||
|
||||
public:
|
||||
void load();
|
||||
void save();
|
||||
|
||||
messages::Word::Flags getWordTypeMask();
|
||||
bool isIgnoredEmote(const QString &emote);
|
||||
QSettings &getQSettings();
|
||||
|
||||
/// Appearance
|
||||
BoolSetting showTimestamps = {"/appearance/messages/showTimestamps", true};
|
||||
BoolSetting showTimestampSeconds = {"/appearance/messages/showTimestampSeconds", true};
|
||||
BoolSetting showBadges = {"/appearance/messages/showBadges", true};
|
||||
BoolSetting showLastMessageIndicator = {"/appearance/messages/showLastMessageIndicator", false};
|
||||
BoolSetting hideEmptyInput = {"/appearance/hideEmptyInputBox", false};
|
||||
BoolSetting showMessageLength = {"/appearance/messages/showMessageLength", false};
|
||||
BoolSetting seperateMessages = {"/appearance/messages/separateMessages", false};
|
||||
BoolSetting windowTopMost = {"/appearance/windowAlwaysOnTop", false};
|
||||
BoolSetting hideTabX = {"/appearance/hideTabX", false};
|
||||
BoolSetting hidePreferencesButton = {"/appearance/hidePreferencesButton", false};
|
||||
BoolSetting hideUserButton = {"/appearance/hideUserButton", false};
|
||||
BoolSetting enableSmoothScrolling = {"/appearance/smoothScrolling", true};
|
||||
// BoolSetting useCustomWindowFrame = {"/appearance/useCustomWindowFrame", false};
|
||||
|
||||
/// Behaviour
|
||||
BoolSetting allowDuplicateMessages = {"/behaviour/allowDuplicateMessages", true};
|
||||
BoolSetting mentionUsersWithAt = {"/behaviour/mentionUsersWithAt", false};
|
||||
FloatSetting mouseScrollMultiplier = {"/behaviour/mouseScrollMultiplier", 1.0};
|
||||
|
||||
/// Commands
|
||||
BoolSetting allowCommandsAtEnd = {"/commands/allowCommandsAtEnd", false};
|
||||
|
||||
/// Emotes
|
||||
BoolSetting scaleEmotesByLineHeight = {"/emotes/scaleEmotesByLineHeight", false};
|
||||
BoolSetting enableTwitchEmotes = {"/emotes/enableTwitchEmotes", true};
|
||||
BoolSetting enableBttvEmotes = {"/emotes/enableBTTVEmotes", true};
|
||||
BoolSetting enableFfzEmotes = {"/emotes/enableFFZEmotes", true};
|
||||
BoolSetting enableEmojis = {"/emotes/enableEmojis", true};
|
||||
BoolSetting enableGifAnimations = {"/emotes/enableGifAnimations", true};
|
||||
|
||||
/// Links
|
||||
BoolSetting linksDoubleClickOnly = {"/links/doubleClickToOpen", false};
|
||||
|
||||
/// Highlighting
|
||||
BoolSetting enableHighlights = {"/highlighting/enabled", true};
|
||||
BoolSetting enableHighlightsSelf = {"/highlighting/nameIsHighlightKeyword", true};
|
||||
BoolSetting enableHighlightSound = {"/highlighting/enableSound", true};
|
||||
BoolSetting enableHighlightTaskbar = {"/highlighting/enableTaskbarFlashing", true};
|
||||
BoolSetting customHighlightSound = {"/highlighting/useCustomSound", false};
|
||||
|
||||
pajlada::Settings::Setting<std::string> streamlinkPath;
|
||||
pajlada::Settings::Setting<std::string> preferredQuality;
|
||||
|
||||
Setting<float> emoteScale;
|
||||
|
||||
Setting<QString> pathHighlightSound;
|
||||
Setting<QMap<QString, QPair<bool, bool>>> highlightProperties;
|
||||
Setting<QString> highlightUserBlacklist;
|
||||
|
||||
BoolSetting highlightAlwaysPlaySound = {"/highlighting/alwaysPlaySound", false};
|
||||
|
||||
BoolSetting inlineWhispers = {"/whispers/enableInlineWhispers", true};
|
||||
|
||||
static SettingsManager &getInstance()
|
||||
{
|
||||
static SettingsManager instance;
|
||||
return instance;
|
||||
}
|
||||
void updateWordTypeMask();
|
||||
|
||||
void saveSnapshot();
|
||||
void recallSnapshot();
|
||||
|
||||
signals:
|
||||
void wordTypeMaskChanged();
|
||||
|
||||
private:
|
||||
std::unique_ptr<rapidjson::Document> snapshot;
|
||||
|
||||
SettingsManager();
|
||||
|
||||
QSettings settings;
|
||||
std::vector<std::reference_wrapper<BaseSetting>> settingsItems;
|
||||
messages::Word::Flags wordTypeMask = messages::Word::Default;
|
||||
|
||||
pajlada::Settings::SettingListener wordMaskListener;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,162 @@
|
||||
#define LOOKUP_COLOR_COUNT 360
|
||||
|
||||
#include "thememanager.hpp"
|
||||
|
||||
#include <QColor>
|
||||
|
||||
#include <math.h>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
namespace detail {
|
||||
|
||||
double getMultiplierByTheme(const std::string &themeName)
|
||||
{
|
||||
if (themeName == "Light") {
|
||||
return 0.8;
|
||||
} else if (themeName == "White") {
|
||||
return 1.0;
|
||||
} else if (themeName == "Black") {
|
||||
return -1.0;
|
||||
} else if (themeName == "Dark") {
|
||||
return -0.8;
|
||||
}
|
||||
|
||||
return -0.8;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
ThemeManager &ThemeManager::getInstance()
|
||||
{
|
||||
static ThemeManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
ThemeManager::ThemeManager()
|
||||
: themeName("/appearance/theme/name", "Dark")
|
||||
, themeHue("/appearance/theme/hue", 0.0)
|
||||
{
|
||||
this->update();
|
||||
|
||||
this->themeName.connectSimple([this](auto) { this->update(); });
|
||||
this->themeHue.connectSimple([this](auto) { this->update(); });
|
||||
}
|
||||
|
||||
void ThemeManager::update()
|
||||
{
|
||||
this->actuallyUpdate(this->themeHue, detail::getMultiplierByTheme(this->themeName));
|
||||
}
|
||||
|
||||
// hue: theme color (0 - 1)
|
||||
// multiplier: 1 = white, 0.8 = light, -0.8 dark, -1 black
|
||||
void ThemeManager::actuallyUpdate(double hue, double multiplier)
|
||||
{
|
||||
lightTheme = multiplier > 0;
|
||||
|
||||
qreal sat = 0.05;
|
||||
|
||||
SystemMessageColor = QColor(140, 127, 127);
|
||||
|
||||
auto getColor = [multiplier](double h, double s, double l, double a = 1.0) {
|
||||
return QColor::fromHslF(h, s, ((l - 0.5) * multiplier) + 0.5, a);
|
||||
};
|
||||
|
||||
DropPreviewBackground = getColor(hue, 0.5, 0.5, 0.6);
|
||||
|
||||
Text = TextCaret = lightTheme ? QColor(0, 0, 0) : QColor(255, 255, 255);
|
||||
TextLink = lightTheme ? QColor(66, 134, 244) : QColor(66, 134, 244);
|
||||
|
||||
// tab
|
||||
if (true) {
|
||||
TabText = QColor(0, 0, 0);
|
||||
TabBackground = QColor(255, 255, 255);
|
||||
|
||||
TabHoverText = QColor(0, 0, 0);
|
||||
TabHoverBackground = QColor::fromHslF(hue, 0, 0.95);
|
||||
} else {
|
||||
// Ubuntu style
|
||||
// TODO: add setting for this
|
||||
TabText = QColor(210, 210, 210);
|
||||
TabBackground = QColor(61, 60, 56);
|
||||
|
||||
TabHoverText = QColor(210, 210, 210);
|
||||
TabHoverBackground = QColor(73, 72, 68);
|
||||
}
|
||||
|
||||
TabSelectedText = QColor(255, 255, 255);
|
||||
TabSelectedBackground = QColor::fromHslF(hue, 0.5, 0.5);
|
||||
|
||||
TabSelectedUnfocusedText = QColor(255, 255, 255);
|
||||
TabSelectedUnfocusedBackground = QColor::fromHslF(hue, 0, 0.5);
|
||||
|
||||
TabHighlightedText = QColor(0, 0, 0);
|
||||
TabHighlightedBackground = QColor::fromHslF(hue, 0.5, 0.8);
|
||||
|
||||
TabNewMessageBackground = QBrush(QColor::fromHslF(hue, 0.5, 0.8), Qt::DiagCrossPattern);
|
||||
|
||||
// Chat
|
||||
bool flat = lightTheme;
|
||||
|
||||
ChatBackground = getColor(0, sat, 1);
|
||||
ChatBackgroundHighlighted = blendColors(TabSelectedBackground, ChatBackground, 0.8);
|
||||
ChatHeaderBackground = getColor(0, sat, flat ? 1 : 0.9);
|
||||
ChatHeaderBorder = getColor(0, sat, flat ? 1 : 0.85);
|
||||
ChatInputBackground = getColor(0, sat, flat ? 0.95 : 0.95);
|
||||
ChatInputBorder = getColor(0, sat, flat ? 1 : 1);
|
||||
ChatSeperator = lightTheme ? QColor(127, 127, 127) : QColor(80, 80, 80);
|
||||
|
||||
// Scrollbar
|
||||
ScrollbarBG = getColor(0, sat, 0.94);
|
||||
ScrollbarThumb = getColor(0, sat, 0.80);
|
||||
ScrollbarThumbSelected = getColor(0, sat, 0.7);
|
||||
ScrollbarArrow = getColor(0, sat, 0.9);
|
||||
|
||||
// stylesheet
|
||||
InputStyleSheet = "background:" + ChatInputBackground.name() + ";" +
|
||||
"border:" + TabSelectedBackground.name() + ";" + "color:" + Text.name() +
|
||||
";" + "selection-background-color:" + TabSelectedBackground.name();
|
||||
|
||||
// Selection
|
||||
Selection = isLightTheme() ? QColor(0, 0, 0, 64) : QColor(255, 255, 255, 64);
|
||||
|
||||
this->updated();
|
||||
}
|
||||
|
||||
QColor ThemeManager::blendColors(const QColor &color1, const QColor &color2, qreal ratio)
|
||||
{
|
||||
int r = color1.red() * (1 - ratio) + color2.red() * ratio;
|
||||
int g = color1.green() * (1 - ratio) + color2.green() * ratio;
|
||||
int b = color1.blue() * (1 - ratio) + color2.blue() * ratio;
|
||||
|
||||
return QColor(r, g, b, 255);
|
||||
}
|
||||
|
||||
void ThemeManager::normalizeColor(QColor &color)
|
||||
{
|
||||
if (this->lightTheme) {
|
||||
if (color.lightnessF() > 0.5f) {
|
||||
color.setHslF(color.hueF(), color.saturationF(), 0.5f);
|
||||
}
|
||||
|
||||
if (color.lightnessF() > 0.4f && color.hueF() > 0.1 && color.hueF() < 0.33333) {
|
||||
color.setHslF(color.hueF(), color.saturationF(),
|
||||
color.lightnessF() -
|
||||
sin((color.hueF() - 0.1) / (0.3333 - 0.1) * 3.14159) *
|
||||
color.saturationF() * 0.2);
|
||||
}
|
||||
} else {
|
||||
if (color.lightnessF() < 0.5f) {
|
||||
color.setHslF(color.hueF(), color.saturationF(), 0.5f);
|
||||
}
|
||||
|
||||
if (color.lightnessF() < 0.6f && color.hueF() > 0.54444 && color.hueF() < 0.83333) {
|
||||
color.setHslF(color.hueF(), color.saturationF(),
|
||||
color.lightnessF() +
|
||||
sin((color.hueF() - 0.54444) / (0.8333 - 0.54444) * 3.14159) *
|
||||
color.saturationF() * 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,109 @@
|
||||
#pragma once
|
||||
|
||||
#include <QBrush>
|
||||
#include <QColor>
|
||||
#include <boost/signals2.hpp>
|
||||
#include <pajlada/settings/setting.hpp>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class WindowManager;
|
||||
|
||||
class ThemeManager
|
||||
{
|
||||
ThemeManager();
|
||||
|
||||
public:
|
||||
static ThemeManager &getInstance();
|
||||
|
||||
inline bool isLightTheme() const
|
||||
{
|
||||
return this->lightTheme;
|
||||
}
|
||||
|
||||
QString InputStyleSheet;
|
||||
|
||||
QColor SystemMessageColor;
|
||||
|
||||
QColor DropPreviewBackground;
|
||||
|
||||
QColor TooltipBackground;
|
||||
QColor TooltipText;
|
||||
|
||||
QColor ChatSeperator;
|
||||
QColor ChatBackground;
|
||||
QColor ChatBackgroundHighlighted;
|
||||
QColor ChatBackgroundResub;
|
||||
QColor ChatBackgroundWhisper;
|
||||
|
||||
QColor ChatHeaderBorder;
|
||||
QColor ChatHeaderBackground;
|
||||
|
||||
QColor ChatInputBackground;
|
||||
QColor ChatInputBorder;
|
||||
|
||||
QColor ChatMessageSeperatorBorder;
|
||||
QColor ChatMessageSeperatorBorderInner;
|
||||
QColor ChatBorder;
|
||||
QColor ChatBorderFocused;
|
||||
QColor Text;
|
||||
QColor TextCaret;
|
||||
QColor TextLink;
|
||||
QColor TextFocused;
|
||||
QColor Menu;
|
||||
QColor MenuBorder;
|
||||
|
||||
QColor ScrollbarBG;
|
||||
QColor ScrollbarThumb;
|
||||
QColor ScrollbarThumbSelected;
|
||||
QColor ScrollbarArrow;
|
||||
|
||||
QColor TabText;
|
||||
QColor TabBackground;
|
||||
|
||||
QColor TabHoverText;
|
||||
QColor TabHoverBackground;
|
||||
|
||||
QColor TabSelectedText;
|
||||
QColor TabSelectedBackground;
|
||||
|
||||
QColor TabHighlightedText;
|
||||
QColor TabHighlightedBackground;
|
||||
|
||||
QColor TabSelectedUnfocusedText;
|
||||
QColor TabSelectedUnfocusedBackground;
|
||||
|
||||
QBrush TabNewMessageBackground;
|
||||
|
||||
QColor Selection;
|
||||
|
||||
const int HighlightColorCount = 3;
|
||||
QColor HighlightColors[3];
|
||||
|
||||
void normalizeColor(QColor &color);
|
||||
|
||||
void update();
|
||||
|
||||
boost::signals2::signal<void()> updated;
|
||||
|
||||
private:
|
||||
pajlada::Settings::Setting<std::string> themeName;
|
||||
pajlada::Settings::Setting<double> themeHue;
|
||||
|
||||
void actuallyUpdate(double hue, double multiplier);
|
||||
QColor blendColors(const QColor &color1, const QColor &color2, qreal ratio);
|
||||
|
||||
double middleLookupTable[360] = {};
|
||||
double minLookupTable[360] = {};
|
||||
|
||||
void fillLookupTableValues(double (&array)[360], double from, double to, double fromValue,
|
||||
double toValue);
|
||||
|
||||
bool lightTheme = false;
|
||||
|
||||
pajlada::Signals::NoArgSignal repaintVisibleChatWidgets;
|
||||
|
||||
friend class WindowManager;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,104 @@
|
||||
#include "windowmanager.hpp"
|
||||
#include "appdatapath.hpp"
|
||||
#include "singletons/thememanager.hpp"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QStandardPaths>
|
||||
#include <boost/foreach.hpp>
|
||||
|
||||
namespace chatterino {
|
||||
WindowManager &WindowManager::getInstance()
|
||||
{
|
||||
static WindowManager instance(ThemeManager::getInstance());
|
||||
return instance;
|
||||
}
|
||||
|
||||
WindowManager::WindowManager(ThemeManager &_themeManager)
|
||||
: themeManager(_themeManager)
|
||||
{
|
||||
_themeManager.repaintVisibleChatWidgets.connect([this] { this->repaintVisibleChatWidgets(); });
|
||||
}
|
||||
|
||||
void WindowManager::initMainWindow()
|
||||
{
|
||||
this->selectedWindow = this->mainWindow = new widgets::Window("main", this->themeManager, true);
|
||||
}
|
||||
|
||||
static const std::string &getSettingsPath()
|
||||
{
|
||||
static std::string path = (Path::getAppdataPath() + "uilayout.json").toStdString();
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
void WindowManager::layoutVisibleChatWidgets(Channel *channel)
|
||||
{
|
||||
this->layout();
|
||||
}
|
||||
|
||||
void WindowManager::repaintVisibleChatWidgets(Channel *channel)
|
||||
{
|
||||
if (this->mainWindow != nullptr) {
|
||||
this->mainWindow->repaintVisibleChatWidgets(channel);
|
||||
}
|
||||
}
|
||||
|
||||
void WindowManager::repaintGifEmotes()
|
||||
{
|
||||
this->repaintGifs();
|
||||
}
|
||||
|
||||
// void WindowManager::updateAll()
|
||||
//{
|
||||
// if (this->mainWindow != nullptr) {
|
||||
// this->mainWindow->update();
|
||||
// }
|
||||
//}
|
||||
|
||||
widgets::Window &WindowManager::getMainWindow()
|
||||
{
|
||||
return *this->mainWindow;
|
||||
}
|
||||
|
||||
widgets::Window &WindowManager::getSelectedWindow()
|
||||
{
|
||||
return *this->selectedWindow;
|
||||
}
|
||||
|
||||
widgets::Window &WindowManager::createWindow()
|
||||
{
|
||||
auto *window = new widgets::Window("external", this->themeManager, false);
|
||||
window->getNotebook().addNewPage();
|
||||
|
||||
this->windows.push_back(window);
|
||||
|
||||
return *window;
|
||||
}
|
||||
|
||||
int WindowManager::windowCount()
|
||||
{
|
||||
return this->windows.size();
|
||||
}
|
||||
|
||||
widgets::Window *WindowManager::windowAt(int index)
|
||||
{
|
||||
if (index < 0 || (size_t)index >= this->windows.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
qDebug() << "getting window at bad index" << index;
|
||||
|
||||
return this->windows.at(index);
|
||||
}
|
||||
|
||||
void WindowManager::save()
|
||||
{
|
||||
assert(this->mainWindow);
|
||||
|
||||
this->mainWindow->save();
|
||||
|
||||
for (widgets::Window *window : this->windows) {
|
||||
window->save();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "widgets/window.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class ThemeManager;
|
||||
|
||||
class WindowManager
|
||||
{
|
||||
explicit WindowManager(ThemeManager &_themeManager);
|
||||
|
||||
public:
|
||||
static WindowManager &getInstance();
|
||||
|
||||
void layoutVisibleChatWidgets(Channel *channel = nullptr);
|
||||
void repaintVisibleChatWidgets(Channel *channel = nullptr);
|
||||
void repaintGifEmotes();
|
||||
void initMainWindow();
|
||||
// void updateAll();
|
||||
|
||||
widgets::Window &getMainWindow();
|
||||
widgets::Window &getSelectedWindow();
|
||||
widgets::Window &createWindow();
|
||||
|
||||
int windowCount();
|
||||
widgets::Window *windowAt(int index);
|
||||
|
||||
void save();
|
||||
|
||||
boost::signals2::signal<void()> repaintGifs;
|
||||
boost::signals2::signal<void()> layout;
|
||||
|
||||
private:
|
||||
ThemeManager &themeManager;
|
||||
|
||||
std::vector<widgets::Window *> windows;
|
||||
|
||||
widgets::Window *mainWindow = nullptr;
|
||||
widgets::Window *selectedWindow = nullptr;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
Reference in New Issue
Block a user