refactored the managers

This commit is contained in:
2017-12-31 00:50:07 +01:00
parent 258288bad9
commit bfa2f1637e
80 changed files with 1023 additions and 964 deletions
+45
View File
@@ -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
+19
View File
@@ -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
+140
View File
@@ -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
+49
View File
@@ -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
+83
View File
@@ -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);
//}
}
+38
View File
@@ -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;
};
}
+28
View File
@@ -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
+24
View File
@@ -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
+537
View File
@@ -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
+172
View File
@@ -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
+91
View File
@@ -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
+135
View File
@@ -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();
}
};
}
+88
View File
@@ -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 + " ");
}
}
+41
View File
@@ -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;
};
}
+530
View File
@@ -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
+102
View File
@@ -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
+186
View File
@@ -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
+109
View File
@@ -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
+162
View File
@@ -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
+109
View File
@@ -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
+104
View File
@@ -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
+43
View File
@@ -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