Merge branch 'master' into git_is_pepega

This commit is contained in:
Mm2PL
2020-01-01 21:06:29 +01:00
committed by GitHub
174 changed files with 2445 additions and 1112 deletions
+3 -3
View File
@@ -1,6 +1,5 @@
Language: Cpp
AccessModifierOffset: -1
AccessModifierOffset: -4
AlignEscapedNewlinesLeft: true
AllowShortFunctionsOnASingleLine: false
@@ -10,12 +9,12 @@ AlwaysBreakAfterDefinitionReturnType: false
AlwaysBreakBeforeMultilineStrings: false
BasedOnStyle: Google
BraceWrapping: {
AfterNamespace: 'false'
AfterClass: 'true'
BeforeElse: 'true'
AfterControlStatement: 'true'
AfterFunction: 'true'
AfterNamespace: 'false'
BeforeCatch: 'true'
BeforeElse: 'true'
}
BreakBeforeBraces: Custom
BreakConstructorInitializersBeforeComma: true
@@ -27,6 +26,7 @@ IndentCaseLabels: true
IndentWidth: 4
IndentWrappedFunctionNames: true
IndentPPDirectives: AfterHash
IncludeBlocks: Preserve
NamespaceIndentation: Inner
PointerBindsToType: false
SpacesBeforeTrailingComments: 2
+33 -7
View File
@@ -1,5 +1,8 @@
#include "Application.hpp"
#include <atomic>
#include "common/Args.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "controllers/commands/CommandController.hpp"
#include "controllers/highlights/HighlightController.hpp"
@@ -29,9 +32,9 @@
#include "singletons/WindowManager.hpp"
#include "util/IsBigEndian.hpp"
#include "util/PostToThread.hpp"
#include "widgets/Notebook.hpp"
#include "widgets/Window.hpp"
#include <atomic>
#include "widgets/splits/Split.hpp"
namespace chatterino {
@@ -44,9 +47,7 @@ Application *Application::instance = nullptr;
// to each other
Application::Application(Settings &_settings, Paths &_paths)
: resources(&this->emplace<Resources2>())
, themes(&this->emplace<Theme>())
: themes(&this->emplace<Theme>())
, fonts(&this->emplace<Fonts>())
, emotes(&this->emplace<Emotes>())
, windows(&this->emplace<WindowManager>())
@@ -79,13 +80,38 @@ void Application::initialize(Settings &settings, Paths &paths)
assert(isAppInitialized == false);
isAppInitialized = true;
Irc::getInstance().load();
if (getSettings()->enableExperimentalIrc)
{
Irc::instance().load();
}
for (auto &singleton : this->singletons_)
{
singleton->initialize(settings, paths);
}
// add crash message
if (getArgs().crashRecovery)
{
if (auto selected =
this->windows->getMainWindow().getNotebook().getSelectedPage())
{
if (auto container = dynamic_cast<SplitContainer *>(selected))
{
for (auto &&split : container->getSplits())
{
if (auto channel = split->getChannel(); !channel->isEmpty())
{
channel->addMessage(makeSystemMessage(
"Chatterino unexpectedly crashed and restarted. "
"You can disable automatic restarts in the "
"settings."));
}
}
}
}
}
this->windows->updateWordTypeMask();
this->initNm(paths);
@@ -104,7 +130,7 @@ int Application::run(QApplication &qtApp)
this->windows->getMainWindow().show();
getSettings()->betaUpdates.connect(
[] { Updates::getInstance().checkForUpdates(); }, false);
[] { Updates::instance().checkForUpdates(); }, false);
return qtApp.exec();
}
+3 -6
View File
@@ -1,11 +1,11 @@
#pragma once
#include "common/Singleton.hpp"
#include "singletons/NativeMessaging.hpp"
#include <QApplication>
#include <memory>
#include "common/Singleton.hpp"
#include "singletons/NativeMessaging.hpp"
namespace chatterino {
class TwitchIrcServer;
@@ -28,7 +28,6 @@ class AccountManager;
class Emotes;
class Settings;
class Fonts;
class Resources2;
class Toasts;
class ChatterinoBadges;
@@ -51,8 +50,6 @@ public:
friend void test();
Resources2 *const resources;
Theme *const themes{};
Fonts *const fonts{};
Emotes *const emotes{};
+128
View File
@@ -0,0 +1,128 @@
#include "BaseSettings.hpp"
#include <QDebug>
#include "util/Clamp.hpp"
namespace chatterino {
std::vector<std::weak_ptr<pajlada::Settings::SettingData>> _settings;
AB_SETTINGS_CLASS *AB_SETTINGS_CLASS::instance = nullptr;
void _actuallyRegisterSetting(
std::weak_ptr<pajlada::Settings::SettingData> setting)
{
_settings.push_back(setting);
}
AB_SETTINGS_CLASS::AB_SETTINGS_CLASS(const QString &settingsDirectory)
{
AB_SETTINGS_CLASS::instance = this;
QString settingsPath = settingsDirectory + "/settings.json";
// get global instance of the settings library
auto settingsInstance = pajlada::Settings::SettingManager::getInstance();
settingsInstance->load(qPrintable(settingsPath));
settingsInstance->setBackupEnabled(true);
settingsInstance->setBackupSlots(9);
settingsInstance->saveMethod =
pajlada::Settings::SettingManager::SaveMethod::SaveOnExit;
}
void AB_SETTINGS_CLASS::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);
auto curVal = setting->unmarshalJSON();
if (curVal == nullptr)
{
continue;
}
rapidjson::Value val;
val.CopyFrom(*curVal, a);
d->AddMember(key.Move(), val.Move(), a);
}
// log("Snapshot state: {}", rj::stringify(*d));
this->snapshot_.reset(d);
}
void AB_SETTINGS_CLASS::restoreSnapshot()
{
if (!this->snapshot_)
{
return;
}
const auto &snapshot = *(this->snapshot_.get());
if (!snapshot.IsObject())
{
return;
}
for (const auto &weakSetting : _settings)
{
auto setting = weakSetting.lock();
if (!setting)
{
continue;
}
const char *path = setting->getPath().c_str();
if (!snapshot.HasMember(path))
{
continue;
}
setting->marshalJSON(snapshot[path]);
}
}
float AB_SETTINGS_CLASS::getClampedUiScale() const
{
return clamp<float>(this->uiScale.getValue(), 0.2f, 10);
}
void AB_SETTINGS_CLASS::setClampedUiScale(float value)
{
this->uiScale.setValue(clamp<float>(value, 0.2f, 10));
}
#ifndef AB_CUSTOM_SETTINGS
Settings *getSettings()
{
static_assert(std::is_same_v<AB_SETTINGS_CLASS, Settings>,
"`AB_SETTINGS_CLASS` must be the same as `Settings`");
assert(AB_SETTINGS_CLASS::instance);
return AB_SETTINGS_CLASS::instance;
}
#endif
AB_SETTINGS_CLASS *getABSettings()
{
assert(AB_SETTINGS_CLASS::instance);
return AB_SETTINGS_CLASS::instance;
}
} // namespace chatterino
+52
View File
@@ -0,0 +1,52 @@
#ifndef AB_SETTINGS_H
#define AB_SETTINGS_H
#include <rapidjson/document.h>
#include <QString>
#include <memory>
#include <pajlada/settings/settingdata.hpp>
#include "common/ChatterinoSetting.hpp"
#ifdef AB_CUSTOM_SETTINGS
# define AB_SETTINGS_CLASS ABSettings
#else
# define AB_SETTINGS_CLASS Settings
#endif
namespace chatterino {
class Settings;
void _actuallyRegisterSetting(
std::weak_ptr<pajlada::Settings::SettingData> setting);
class AB_SETTINGS_CLASS
{
public:
AB_SETTINGS_CLASS(const QString &settingsDirectory);
void saveSnapshot();
void restoreSnapshot();
static AB_SETTINGS_CLASS *instance;
FloatSetting uiScale = {"/appearance/uiScale2", 1};
BoolSetting windowTopMost = {"/appearance/windowAlwaysOnTop", false};
float getClampedUiScale() const;
void setClampedUiScale(float value);
private:
std::unique_ptr<rapidjson::Document> snapshot_;
};
Settings *getSettings();
AB_SETTINGS_CLASS *getABSettings();
} // namespace chatterino
#ifdef CHATTERINO
# include "singletons/Settings.hpp"
#endif
#endif
+222
View File
@@ -0,0 +1,222 @@
#include "BaseTheme.hpp"
namespace chatterino {
namespace {
double getMultiplierByTheme(const QString &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;
}
/*
else if (themeName == "Custom")
{
return getSettings()->customThemeMultiplier.getValue();
}
*/
return -0.8;
}
} // namespace
bool AB_THEME_CLASS::isLightTheme() const
{
return this->isLight_;
}
void AB_THEME_CLASS::update()
{
this->actuallyUpdate(this->themeHue,
getMultiplierByTheme(this->themeName.getValue()));
this->updated.invoke();
}
void AB_THEME_CLASS::actuallyUpdate(double hue, double multiplier)
{
this->isLight_ = multiplier > 0;
bool lightWin = isLight_;
// QColor themeColor = QColor::fromHslF(hue, 0.43, 0.5);
QColor themeColor = QColor::fromHslF(hue, 0.8, 0.5);
QColor themeColorNoSat = QColor::fromHslF(hue, 0, 0.5);
qreal sat = 0;
// 0.05;
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);
};
/// WINDOW
{
#ifdef Q_OS_LINUX
this->window.background = lightWin ? "#fff" : QColor(61, 60, 56);
#else
this->window.background = lightWin ? "#fff" : "#111";
#endif
QColor fg = this->window.text = lightWin ? "#000" : "#eee";
this->window.borderFocused = lightWin ? "#ccc" : themeColor;
this->window.borderUnfocused = lightWin ? "#ccc" : themeColorNoSat;
// 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);
// message (referenced later)
this->messages.textColors.caret = //
this->messages.textColors.regular = isLight_ ? "#000" : "#fff";
QColor highlighted = lightWin ? QColor("#ff0000") : QColor("#ee6166");
/// TABS
if (lightWin)
{
this->tabs.regular = {
QColor("#444"),
{QColor("#fff"), QColor("#eee"), QColor("#fff")},
{QColor("#fff"), QColor("#fff"), QColor("#fff")}};
this->tabs.newMessage = {
QColor("#222"),
{QColor("#fff"), QColor("#eee"), QColor("#fff")},
{QColor("#bbb"), QColor("#bbb"), QColor("#bbb")}};
this->tabs.highlighted = {
fg,
{QColor("#fff"), QColor("#eee"), QColor("#fff")},
{highlighted, highlighted, highlighted}};
this->tabs.selected = {
QColor("#000"),
{QColor("#b4d7ff"), QColor("#b4d7ff"), QColor("#b4d7ff")},
{QColor("#00aeef"), QColor("#00aeef"), QColor("#00aeef")}};
}
else
{
this->tabs.regular = {
QColor("#aaa"),
{QColor("#252525"), QColor("#252525"), QColor("#252525")},
{QColor("#444"), QColor("#444"), QColor("#444")}};
this->tabs.newMessage = {
fg,
{QColor("#252525"), QColor("#252525"), QColor("#252525")},
{QColor("#888"), QColor("#888"), QColor("#888")}};
this->tabs.highlighted = {
fg,
{QColor("#252525"), QColor("#252525"), QColor("#252525")},
{highlighted, highlighted, highlighted}};
this->tabs.selected = {
QColor("#fff"),
{QColor("#555555"), QColor("#555555"), QColor("#555555")},
{QColor("#00aeef"), QColor("#00aeef"), QColor("#00aeef")}};
}
// scrollbar
this->scrollbars.highlights.highlight = QColor("#ee6166");
this->scrollbars.highlights.subscription = QColor("#C466FF");
// this->tabs.newMessage = {
// fg,
// {QBrush(blendColors(themeColor, "#ccc", 0.9), Qt::FDiagPattern),
// QBrush(blendColors(themeColor, "#ccc", 0.9), Qt::FDiagPattern),
// QBrush(blendColors(themeColorNoSat, "#ccc", 0.9),
// Qt::FDiagPattern)}};
// this->tabs.newMessage = {
// fg,
// {QBrush(blendColors(themeColor, "#666", 0.7),
// Qt::FDiagPattern),
// QBrush(blendColors(themeColor, "#666", 0.5),
// Qt::FDiagPattern),
// QBrush(blendColors(themeColorNoSat, "#666", 0.7),
// Qt::FDiagPattern)}};
// this->tabs.highlighted = {fg, {QColor("#777"),
// QColor("#777"), QColor("#666")}};
this->tabs.bottomLine = this->tabs.selected.backgrounds.regular.color();
}
// Message
this->messages.textColors.link =
isLight_ ? QColor(66, 134, 244) : QColor(66, 134, 244);
this->messages.textColors.system = QColor(140, 127, 127);
this->messages.backgrounds.regular = getColor(0, sat, 1);
this->messages.backgrounds.alternate = getColor(0, sat, 0.96);
if (isLight_)
{
this->messages.backgrounds.highlighted =
blendColors(themeColor, this->messages.backgrounds.regular, 0.8);
}
else
{
// REMOVED
// this->messages.backgrounds.highlighted =
// QColor(getSettings()->highlightColor);
}
this->messages.backgrounds.subscription =
blendColors(QColor("#C466FF"), this->messages.backgrounds.regular, 0.7);
// this->messages.backgrounds.resub
// this->messages.backgrounds.whisper
this->messages.disabled = getColor(0, sat, 1, 0.6);
// this->messages.seperator =
// this->messages.seperatorInner =
// Scrollbar
this->scrollbars.background = QColor(0, 0, 0, 0);
// this->scrollbars.background = splits.background;
// this->scrollbars.background.setAlphaF(qreal(0.2));
this->scrollbars.thumb = getColor(0, sat, 0.70);
this->scrollbars.thumbSelected = getColor(0, sat, 0.65);
// tooltip
this->tooltip.background = QColor(0, 0, 0);
this->tooltip.text = QColor(255, 255, 255);
// Selection
this->messages.selection =
isLightTheme() ? QColor(0, 0, 0, 64) : QColor(255, 255, 255, 64);
}
QColor AB_THEME_CLASS::blendColors(const QColor &color1, const QColor &color2,
qreal ratio)
{
int r = int(color1.red() * (1 - ratio) + color2.red() * ratio);
int g = int(color1.green() * (1 - ratio) + color2.green() * ratio);
int b = int(color1.blue() * (1 - ratio) + color2.blue() * ratio);
return QColor(r, g, b, 255);
}
#ifndef AB_CUSTOM_THEME
Theme *getTheme()
{
static auto theme = [] {
auto theme = new Theme();
theme->update();
return theme;
}();
return theme;
}
#endif
} // namespace chatterino
+117
View File
@@ -0,0 +1,117 @@
#ifndef AB_THEME_H
#define AB_THEME_H
#include <QBrush>
#include <QColor>
#include <common/ChatterinoSetting.hpp>
#ifdef AB_CUSTOM_THEME
# define AB_THEME_CLASS BaseTheme
#else
# define AB_THEME_CLASS Theme
#endif
namespace chatterino {
class Theme;
class AB_THEME_CLASS
{
public:
bool isLightTheme() const;
struct TabColors {
QColor text;
struct {
QBrush regular;
QBrush hover;
QBrush unfocused;
} backgrounds;
struct {
QColor regular;
QColor hover;
QColor unfocused;
} line;
};
/// WINDOW
struct {
QColor background;
QColor text;
QColor borderUnfocused;
QColor borderFocused;
} window;
/// TABS
struct {
TabColors regular;
TabColors newMessage;
TabColors highlighted;
TabColors selected;
QColor border;
QColor bottomLine;
} tabs;
/// MESSAGES
struct {
struct {
QColor regular;
QColor caret;
QColor link;
QColor system;
} textColors;
struct {
QColor regular;
QColor alternate;
QColor highlighted;
QColor subscription;
// QColor whisper;
} backgrounds;
QColor disabled;
// QColor seperator;
// QColor seperatorInner;
QColor selection;
} messages;
/// SCROLLBAR
struct {
QColor background;
QColor thumb;
QColor thumbSelected;
struct {
QColor highlight;
QColor subscription;
} highlights;
} scrollbars;
/// TOOLTIP
struct {
QColor text;
QColor background;
} tooltip;
void update();
virtual void actuallyUpdate(double hue, double multiplier);
QColor blendColors(const QColor &color1, const QColor &color2, qreal ratio);
pajlada::Signals::NoArgSignal updated;
QStringSetting themeName{"/appearance/theme/name", "Dark"};
DoubleSetting themeHue{"/appearance/theme/hue", 0.0};
private:
bool isLight_ = false;
};
// Implemented in parent project if AB_CUSTOM_THEME is set.
// Otherwise implemented in BaseThemecpp
Theme *getTheme();
} // namespace chatterino
#ifdef CHATTERINO
# include "singletons/Theme.hpp"
#endif
#endif
+69 -7
View File
@@ -4,10 +4,16 @@
#include <QFile>
#include <QPalette>
#include <QStyleFactory>
#include <Qt>
#include <QtConcurrent>
#include <csignal>
#include "Application.hpp"
#include "common/Modes.hpp"
#include "common/NetworkManager.hpp"
#include "singletons/Paths.hpp"
#include "singletons/Resources.hpp"
#include "singletons/Settings.hpp"
#include "singletons/Updates.hpp"
#include "util/CombinePath.hpp"
#include "widgets/dialogs/LastRunCrashDialog.hpp"
@@ -20,12 +26,6 @@
# include <QBreakpadHandler.h>
#endif
// void initQt();
// void installCustomPalette();
// void showLastCrashDialog();
// void createRunningFile(const QString &path);
// void removeRunningFile(const QString &path);
namespace chatterino {
namespace {
void installCustomPalette()
@@ -108,11 +108,68 @@ namespace {
{
QFile::remove(path);
}
std::chrono::steady_clock::time_point signalsInitTime;
bool restartOnSignal = false;
[[noreturn]] void handleSignal(int signum)
{
using namespace std::chrono_literals;
if (restartOnSignal &&
std::chrono::steady_clock::now() - signalsInitTime > 30s)
{
QProcess proc;
proc.setProgram(QApplication::applicationFilePath());
proc.setArguments({"--crash-recovery"});
proc.startDetached();
}
_exit(signum);
}
// We want to restart chatterino when it crashes and the setting is set to
// true.
void initSignalHandler()
{
#ifndef C_DEBUG
signalsInitTime = std::chrono::steady_clock::now();
signal(SIGSEGV, handleSignal);
#endif
}
// We delete cache files that haven't been modified in 14 days. This strategy may be
// improved in the future.
void clearCache(const QDir &dir)
{
qDebug() << "[Cache] cleared cache";
QStringList toBeRemoved;
for (auto &&info : dir.entryInfoList(QDir::Files))
{
if (info.lastModified().addDays(14) < QDateTime::currentDateTime())
{
toBeRemoved << info.absoluteFilePath();
}
}
for (auto &&path : toBeRemoved)
{
qDebug() << path << QFile(path).remove();
}
}
} // namespace
void runGui(QApplication &a, Paths &paths, Settings &settings)
{
initQt();
initResources();
initSignalHandler();
settings.restartOnCrash.connect(
[](const bool &value) { restartOnSignal = value; });
auto thread = std::thread([dir = paths.miscDirectory] {
{
@@ -131,8 +188,13 @@ void runGui(QApplication &a, Paths &paths, Settings &settings)
}
});
// Clear the cache 1 minute after start.
QTimer::singleShot(60 * 1000, [cachePath = paths.cacheDirectory()] {
QtConcurrent::run([cachePath]() { clearCache(cachePath); });
});
chatterino::NetworkManager::init();
chatterino::Updates::getInstance().checkForUpdates();
chatterino::Updates::instance().checkForUpdates();
#ifdef C_USE_BREAKPAD
QBreakpadInstance.setDumpPath(getPaths()->settingsFolderPath + "/Crashes");
+1 -1
View File
@@ -50,4 +50,4 @@ Resources2::Resources2()
this->twitch.vip = QPixmap(":/twitch/vip.png");
}
} // namespace chatterino
} // namespace chatterino
+2 -1
View File
@@ -1,4 +1,5 @@
#include <QPixmap>
#include "common/Singleton.hpp"
namespace chatterino {
@@ -64,4 +65,4 @@ public:
} twitch;
};
} // namespace chatterino
} // namespace chatterino
+34
View File
@@ -0,0 +1,34 @@
#include "Args.hpp"
namespace chatterino {
Args::Args(const QStringList &args)
{
for (auto &&arg : args)
{
if (arg == "--crash-recovery")
{
this->crashRecovery = true;
}
else if (arg == "--version")
{
this->printVersion = true;
}
}
}
static Args *instance = nullptr;
void initArgs(const QStringList &args)
{
instance = new Args(args);
}
const Args &getArgs()
{
assert(instance);
return *instance;
}
} // namespace chatterino
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <QStringList>
namespace chatterino {
/// Command line arguments passed to Chatterino.
class Args
{
public:
Args(const QStringList &args);
bool printVersion{};
bool crashRecovery{};
};
void initArgs(const QStringList &args);
const Args &getArgs();
} // namespace chatterino
+1 -1
View File
@@ -178,7 +178,7 @@ void Channel::addOrReplaceTimeout(MessagePtr message)
}
// XXX: Might need the following line
// WindowManager::getInstance().repaintVisibleChatWidgets(this);
// WindowManager::instance().repaintVisibleChatWidgets(this);
}
void Channel::disableAllMessages()
+12
View File
@@ -0,0 +1,12 @@
#include "common/ChatterinoSetting.hpp"
#include "BaseSettings.hpp"
namespace chatterino {
void _registerSetting(std::weak_ptr<pajlada::Settings::SettingData> setting)
{
_actuallyRegisterSetting(setting);
}
} // namespace chatterino
+88
View File
@@ -0,0 +1,88 @@
#pragma once
#include <QString>
#include <pajlada/settings.hpp>
namespace chatterino {
void _registerSetting(std::weak_ptr<pajlada::Settings::SettingData> setting);
template <typename Type>
class ChatterinoSetting : public pajlada::Settings::Setting<Type>
{
public:
ChatterinoSetting(const std::string &path)
: pajlada::Settings::Setting<Type>(path)
{
_registerSetting(this->getData());
}
ChatterinoSetting(const std::string &path, const Type &defaultValue)
: pajlada::Settings::Setting<Type>(path, defaultValue)
{
_registerSetting(this->getData());
}
template <typename T2>
ChatterinoSetting &operator=(const T2 &newValue)
{
this->setValue(newValue);
return *this;
}
ChatterinoSetting &operator=(Type &&newValue) noexcept
{
pajlada::Settings::Setting<Type>::operator=(newValue);
return *this;
}
using pajlada::Settings::Setting<Type>::operator==;
using pajlada::Settings::Setting<Type>::operator!=;
using pajlada::Settings::Setting<Type>::operator Type;
};
using BoolSetting = ChatterinoSetting<bool>;
using FloatSetting = ChatterinoSetting<float>;
using DoubleSetting = ChatterinoSetting<double>;
using IntSetting = ChatterinoSetting<int>;
using StringSetting = ChatterinoSetting<std::string>;
using QStringSetting = ChatterinoSetting<QString>;
template <typename Enum>
class EnumSetting
: public ChatterinoSetting<typename std::underlying_type<Enum>::type>
{
using Underlying = typename std::underlying_type<Enum>::type;
public:
using ChatterinoSetting<Underlying>::ChatterinoSetting;
EnumSetting(const std::string &path, const Enum &defaultValue)
: ChatterinoSetting<Underlying>(path, Underlying(defaultValue))
{
_registerSetting(this->getData());
}
template <typename T2>
EnumSetting<Enum> &operator=(Enum newValue)
{
this->setValue(Underlying(newValue));
return *this;
}
operator Enum()
{
return Enum(this->getValue());
}
Enum getEnum()
{
return Enum(this->getValue());
}
};
} // namespace chatterino
+14 -5
View File
@@ -1,16 +1,15 @@
#pragma once
#include "common/Aliases.hpp"
#include "common/Outcome.hpp"
#include "common/ProviderId.hpp"
#include <QString>
#include <QWidget>
#include <boost/optional.hpp>
#include <boost/preprocessor.hpp>
#include <string>
#include "common/Aliases.hpp"
#include "common/Outcome.hpp"
#include "common/ProviderId.hpp"
namespace chatterino {
enum class HighlightState {
@@ -46,4 +45,14 @@ enum class CopyMode {
OnlyTextAndEmotes,
};
struct DeleteLater {
void operator()(QObject *obj)
{
obj->deleteLater();
}
};
template <typename T>
using QObjectPtr = std::unique_ptr<T, DeleteLater>;
} // namespace chatterino
+10 -7
View File
@@ -143,7 +143,7 @@ namespace {
}
} // namespace
Credentials &Credentials::getInstance()
Credentials &Credentials::instance()
{
static Credentials creds;
return creds;
@@ -166,11 +166,12 @@ void Credentials::get(const QString &provider, const QString &name_,
auto job = new QKeychain::ReadPasswordJob("chatterino");
job->setAutoDelete(true);
job->setKey(name);
QObject::connect(job, &QKeychain::Job::finished, receiver,
[job, onLoaded = std::move(onLoaded)](auto) mutable {
onLoaded(job->textData());
},
Qt::DirectConnection);
QObject::connect(
job, &QKeychain::Job::finished, receiver,
[job, onLoaded = std::move(onLoaded)](auto) mutable {
onLoaded(job->textData());
},
Qt::DirectConnection);
job->start();
}
else
@@ -199,7 +200,9 @@ void Credentials::set(const QString &provider, const QString &name_,
{
auto &instance = insecureInstance();
instance.object()[name] = credential;
auto obj = instance.object();
obj[name] = credential;
instance.setObject(obj);
queueInsecureSave();
}
+1 -1
View File
@@ -8,7 +8,7 @@ namespace chatterino {
class Credentials
{
public:
static Credentials &getInstance();
static Credentials &instance();
void get(const QString &provider, const QString &name, QObject *receiver,
std::function<void(const QString &)> &&onLoaded);
+2 -4
View File
@@ -48,13 +48,11 @@ void DownloadManager::onFinished(QNetworkReply *reply)
{
switch (reply->error())
{
case QNetworkReply::NoError:
{
case QNetworkReply::NoError: {
qDebug("file is downloaded successfully.");
}
break;
default:
{
default: {
qDebug() << reply->errorString().toLatin1();
};
}
+33
View File
@@ -1,5 +1,7 @@
#include "common/Env.hpp"
#include <QVariant>
namespace chatterino {
namespace {
@@ -15,6 +17,33 @@ namespace {
return defaultValue;
}
uint16_t readPortEnv(const char *envName, uint16_t defaultValue)
{
auto envString = std::getenv(envName);
if (envString != nullptr)
{
bool ok;
auto val = QString(envString).toUShort(&ok);
if (ok)
{
return val;
}
}
return defaultValue;
}
uint16_t readBoolEnv(const char *envName, bool defaultValue)
{
auto envString = std::getenv(envName);
if (envString != nullptr)
{
return QVariant(QString(envString)).toBool();
}
return defaultValue;
}
} // namespace
Env::Env()
@@ -30,6 +59,10 @@ Env::Env()
"https://braize.pajlada.com/chatterino/twitchemotes/set/%1/"))
, imageUploaderUrl(readStringEnv("CHATTERINO2_IMAGE_PASTE_SITE_URL",
"https://i.nuuls.com/upload"))
, twitchServerHost(
readStringEnv("CHATTERINO2_TWITCH_SERVER_HOST", "irc.chat.twitch.tv"))
, twitchServerPort(readPortEnv("CHATTERINO2_TWITCH_SERVER_PORT", 6697))
, twitchServerSecure(readBoolEnv("CHATTERINO2_TWITCH_SERVER_SECURE", true))
{
}
+3
View File
@@ -15,6 +15,9 @@ public:
const QString linkResolverUrl;
const QString twitchEmoteSetResolverUrl;
const QString imageUploaderUrl;
const QString twitchServerHost;
const uint16_t twitchServerPort;
const bool twitchServerSecure;
};
} // namespace chatterino
+82
View File
@@ -0,0 +1,82 @@
#pragma once
#include <type_traits>
namespace chatterino {
template <typename T, typename Q = typename std::underlying_type<T>::type>
class FlagsEnum
{
public:
FlagsEnum()
: value_(static_cast<T>(0))
{
}
FlagsEnum(T value)
: value_(value)
{
}
FlagsEnum(std::initializer_list<T> flags)
{
for (auto flag : flags)
{
this->set(flag);
}
}
bool operator==(const FlagsEnum<T> &other)
{
return this->value_ == other.value_;
}
bool operator!=(const FlagsEnum &other)
{
return this->value_ != other.value_;
}
void set(T flag)
{
reinterpret_cast<Q &>(this->value_) |= static_cast<Q>(flag);
}
void unset(T flag)
{
reinterpret_cast<Q &>(this->value_) &= ~static_cast<Q>(flag);
}
void set(T flag, bool value)
{
if (value)
this->set(flag);
else
this->unset(flag);
}
bool has(T flag) const
{
return static_cast<Q>(this->value_) & static_cast<Q>(flag);
}
bool hasAny(FlagsEnum flags) const
{
return static_cast<Q>(this->value_) & static_cast<Q>(flags.value_);
}
bool hasAll(FlagsEnum<T> flags) const
{
return (static_cast<Q>(this->value_) & static_cast<Q>(flags.value_)) &&
static_cast<Q>(flags->value);
}
bool hasNone(std::initializer_list<T> flags) const
{
return !this->hasAny(flags);
}
private:
T value_{};
};
} // namespace chatterino
+176 -42
View File
@@ -1,67 +1,201 @@
#include "common/LinkParser.hpp"
#include <QFile>
#include <QMap>
#include <QRegularExpression>
#include <QString>
#include <QStringRef>
#include <QTextStream>
// ip 0.0.0.0 - 224.0.0.0
#define IP \
"(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" \
"(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" \
"(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))"
#define PORT "(?::\\d{2,5})"
#define WEB_CHAR1 "[_a-z\\x{00a1}-\\x{ffff}0-9]"
#define WEB_CHAR2 "[a-z\\x{00a1}-\\x{ffff}0-9]"
#define SPOTIFY_1 "(?:artist|album|track|user:[^:]+:playlist):[a-zA-Z0-9]+"
#define SPOTIFY_2 "user:[^:]+"
#define SPOTIFY_3 "search:(?:[-\\w$\\.+!*'(),]+|%[a-fA-F0-9]{2})+"
#define SPOTIFY_PARAMS "(?:" SPOTIFY_1 "|" SPOTIFY_2 "|" SPOTIFY_3 ")"
#define SPOTIFY_LINK "(?x-mi:(spotify:" SPOTIFY_PARAMS "))"
#define WEB_PROTOCOL "(?:(?:https?|ftps?)://)?"
#define WEB_USER "(?:\\S+(?::\\S*)?@)?"
#define WEB_HOST "(?:(?:" WEB_CHAR1 "-*)*" WEB_CHAR2 "+)"
#define WEB_DOMAIN "(?:\\.(?:" WEB_CHAR2 "-*)*" WEB_CHAR2 "+)*"
#define WEB_TLD "(?:" + tldData + ")"
#define WEB_RESOURCE_PATH "(?:[/?#]\\S*)"
#define WEB_LINK \
WEB_PROTOCOL WEB_USER "(?:" IP "|" WEB_HOST WEB_DOMAIN "\\." WEB_TLD PORT \
"?" WEB_RESOURCE_PATH "?)"
#define LINK "^(?:" SPOTIFY_LINK "|" WEB_LINK ")$"
namespace chatterino {
namespace {
QSet<QString> &tlds()
{
static QSet<QString> tlds = [] {
QFile file(":/tlds.txt");
file.open(QFile::ReadOnly);
QTextStream stream(&file);
stream.setCodec("UTF-8");
int safetyMax = 20000;
QSet<QString> set;
while (!stream.atEnd())
{
auto line = stream.readLine();
set.insert(line);
if (safetyMax-- == 0)
break;
}
return set;
}();
return tlds;
}
bool isValidHostname(QStringRef &host)
{
int index = host.lastIndexOf('.');
return index != -1 &&
tlds().contains(host.mid(index + 1).toString().toLower());
}
bool isValidIpv4(QStringRef &host)
{
static auto exp = QRegularExpression("^\\d{1,3}(?:\\.\\d{1,3}){3}$");
return exp.match(host).hasMatch();
}
#ifdef C_MATCH_IPV6_LINK
bool isValidIpv6(QStringRef &host)
{
static auto exp = QRegularExpression("^\\[[a-fA-F0-9:%]+\\]$");
return exp.match(host).hasMatch();
}
#endif
} // namespace
LinkParser::LinkParser(const QString &unparsedString)
{
static QRegularExpression linkRegex = [] {
static QRegularExpression newLineRegex("\r?\n");
QFile file(":/tlds.txt");
file.open(QFile::ReadOnly);
QTextStream tlds(&file);
tlds.setCodec("UTF-8");
this->match_ = unparsedString;
// tldData gets injected into the LINK macro
auto tldData = tlds.readAll().replace(newLineRegex, "|");
(void)tldData;
// This is not implemented with a regex to increase performance.
// We keep removing parts of the url until there's either nothing left or we fail.
QStringRef l(&unparsedString);
return QRegularExpression(LINK,
QRegularExpression::CaseInsensitiveOption);
}();
bool hasHttp = false;
this->match_ = linkRegex.match(unparsedString);
// Protocol `https?://`
if (l.startsWith("https://", Qt::CaseInsensitive))
{
hasHttp = true;
l = l.mid(8);
}
else if (l.startsWith("http://", Qt::CaseInsensitive))
{
hasHttp = true;
l = l.mid(7);
}
// Http basic auth `user:password`.
// Not supported for security reasons (misleading links)
// Host `a.b.c.com`
QStringRef host = l;
bool lastWasDot = true;
bool inIpv6 = false;
for (int i = 0; i < l.size(); i++)
{
if (l[i] == '.')
{
if (lastWasDot == true) // no double dots ..
goto error;
lastWasDot = true;
}
else
{
lastWasDot = false;
}
if (l[i] == ':' && !inIpv6)
{
host = l.mid(0, i);
l = l.mid(i + 1);
goto parsePort;
}
else if (l[i] == '/')
{
host = l.mid(0, i);
l = l.mid(i + 1);
goto parsePath;
}
else if (l[i] == '?')
{
host = l.mid(0, i);
l = l.mid(i + 1);
goto parseQuery;
}
else if (l[i] == '#')
{
host = l.mid(0, i);
l = l.mid(i + 1);
goto parseAnchor;
}
// ipv6
if (l[i] == '[')
{
if (i == 0)
inIpv6 = true;
else
goto error;
}
else if (l[i] == ']')
{
inIpv6 = false;
}
}
if (lastWasDot)
goto error;
else
goto done;
parsePort:
// Port `:12345`
for (int i = 0; i < std::min<int>(5, l.size()); i++)
{
if (l[i] == '/')
goto parsePath;
else if (l[i] == '?')
goto parseQuery;
else if (l[i] == '#')
goto parseAnchor;
if (!l[i].isDigit())
goto error;
}
goto done;
parsePath:
parseQuery:
parseAnchor:
// we accept everything in the path/query/anchor
done:
// check host
this->hasMatch_ = isValidHostname(host) || isValidIpv4(host)
#ifdef C_MATCH_IPV6_LINK
|| (hasHttp && isValidIpv6(host))
#endif
;
if (this->hasMatch_)
{
this->match_ = unparsedString;
}
return;
error:
hasMatch_ = false;
}
bool LinkParser::hasMatch() const
{
return this->match_.hasMatch();
return this->hasMatch_;
}
QString LinkParser::getCaptured() const
{
return this->match_.captured();
return this->match_;
}
} // namespace chatterino
+2 -1
View File
@@ -14,7 +14,8 @@ public:
QString getCaptured() const;
private:
QRegularExpressionMatch match_;
bool hasMatch_{false};
QString match_;
};
} // namespace chatterino
+1 -1
View File
@@ -27,7 +27,7 @@ Modes::Modes()
}
}
const Modes &Modes::getInstance()
const Modes &Modes::instance()
{
static Modes instance;
return instance;
+1 -1
View File
@@ -7,7 +7,7 @@ class Modes
public:
Modes();
static const Modes &getInstance();
static const Modes &instance();
bool isNightly{};
bool isPortable{};
+51
View File
@@ -0,0 +1,51 @@
#pragma once
namespace chatterino {
struct SuccessTag {
};
struct FailureTag {
};
const SuccessTag Success{};
const FailureTag Failure{};
class Outcome
{
public:
Outcome(SuccessTag)
: success_(true)
{
}
Outcome(FailureTag)
: success_(false)
{
}
explicit operator bool() const
{
return this->success_;
}
bool operator!() const
{
return !this->success_;
}
bool operator==(const Outcome &other) const
{
return this->success_ == other.success_;
}
bool operator!=(const Outcome &other) const
{
return !this->operator==(other);
}
private:
bool success_;
};
} // namespace chatterino
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <boost/noncopyable.hpp>
namespace chatterino {
class Settings;
class Paths;
class Singleton : boost::noncopyable
{
public:
virtual ~Singleton() = default;
virtual void initialize(Settings &settings, Paths &paths)
{
(void)(settings);
(void)(paths);
}
virtual void save()
{
}
};
} // namespace chatterino
+9 -13
View File
@@ -16,50 +16,46 @@ Version::Version()
this->commitHash_ =
QString(FROM_EXTERNAL_DEFINE(CHATTERINO_GIT_HASH)).remove('"');
// Date of build
// Date of build, this is depended on the format not changing
#ifdef CHATTERINO_NIGHTLY_VERSION_STRING
this->dateOfBuild_ =
QString(FROM_EXTERNAL_DEFINE(CHATTERINO_NIGHTLY_VERSION_STRING))
.remove('"');
.remove('"')
.split(' ')[0];
#endif
// "Full" version string, as displayed in window title
this->fullVersion_ = "Chatterino ";
if (Modes::getInstance().isNightly)
if (Modes::instance().isNightly)
{
this->fullVersion_ += "Nightly ";
}
this->fullVersion_ += this->version_;
if (Modes::getInstance().isNightly)
{
this->fullVersion_ += this->dateOfBuild_;
}
}
const Version &Version::getInstance()
const Version &Version::instance()
{
static Version instance;
return instance;
}
const QString &Version::getVersion() const
const QString &Version::version() const
{
return this->version_;
}
const QString &Version::getFullVersion() const
const QString &Version::fullVersion() const
{
return this->fullVersion_;
}
const QString &Version::getCommitHash() const
const QString &Version::commitHash() const
{
return this->commitHash_;
}
const QString &Version::getDateOfBuild() const
const QString &Version::dateOfBuild() const
{
return this->dateOfBuild_;
}
+8 -7
View File
@@ -1,8 +1,9 @@
#pragma once
#include <QString>
#include <QtGlobal>
#define CHATTERINO_VERSION "2.1.4"
#define CHATTERINO_VERSION "2.1.7"
#if defined(Q_OS_WIN)
# define CHATTERINO_OS "win"
@@ -19,12 +20,12 @@ namespace chatterino {
class Version
{
public:
static const Version &getInstance();
static const Version &instance();
const QString &getVersion() const;
const QString &getCommitHash() const;
const QString &getDateOfBuild() const;
const QString &getFullVersion() const;
const QString &version() const;
const QString &commitHash() const;
const QString &dateOfBuild() const;
const QString &fullVersion() const;
private:
Version();
@@ -35,4 +36,4 @@ private:
QString fullVersion_;
};
};
}; // namespace chatterino
@@ -27,8 +27,7 @@ AccountController::AccountController()
this->accounts_.itemRemoved.connect([this](const auto &args) {
switch (args.item->getProviderId())
{
case ProviderId::Twitch:
{
case ProviderId::Twitch: {
if (args.caller != this)
{
auto accs = this->twitch.accounts.cloneVector();
@@ -70,8 +70,7 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
{
switch (column)
{
case 0:
{
case 0: {
if (role == Qt::CheckStateRole)
{
if (rowIndex == 0)
@@ -86,8 +85,7 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
}
}
break;
case 1:
{
case 1: {
if (role == Qt::CheckStateRole)
{
if (rowIndex == 0)
@@ -103,8 +101,7 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
}
}
break;
case 2:
{
case 2: {
if (role == Qt::CheckStateRole)
{
if (rowIndex == 0)
@@ -120,8 +117,7 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
}
}
break;
case 3:
{
case 3: {
// empty element
}
break;
@@ -119,8 +119,8 @@ struct Deserialize<chatterino::HighlightPhrase> {
chatterino::rj::getSafe(value, "regex", _isRegex);
chatterino::rj::getSafe(value, "case", _caseSensitive);
return chatterino::HighlightPhrase(_pattern, _alert, _sound,
_isRegex, _caseSensitive);
return chatterino::HighlightPhrase(_pattern, _alert, _sound, _isRegex,
_caseSensitive);
}
};
@@ -65,17 +65,17 @@ ModerationAction::ModerationAction(const QString &action)
// line1 = this->line1_;
// line2 = this->line2_;
// } else {
// this->_moderationActions.emplace_back(app->resources->buttonTimeout,
// this->_moderationActions.emplace_back(getResources().buttonTimeout,
// str);
// }
}
else if (action.startsWith("/ban "))
{
this->image_ = Image::fromPixmap(getApp()->resources->buttons.ban);
this->image_ = Image::fromPixmap(getResources().buttons.ban);
}
else if (action.startsWith("/delete "))
{
this->image_ = Image::fromPixmap(getApp()->resources->buttons.trashCan);
this->image_ = Image::fromPixmap(getResources().buttons.trashCan);
}
else
{
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include <QCoreApplication>
#include <QThread>
#include <cassert>
namespace chatterino {
static bool isGuiThread()
{
return QCoreApplication::instance()->thread() == QThread::currentThread();
}
static void assertInGuiThread()
{
#ifdef _DEBUG
assert(isGuiThread());
#endif
}
} // namespace chatterino
+21
View File
@@ -0,0 +1,21 @@
#include "Benchmark.hpp"
namespace chatterino {
BenchmarkGuard::BenchmarkGuard(const QString &_name)
: name_(_name)
{
timer_.start();
}
BenchmarkGuard::~BenchmarkGuard()
{
log("{} {} ms", this->name_, float(timer_.nsecsElapsed()) / 1000000.0f);
}
qreal BenchmarkGuard::getElapsedMs()
{
return qreal(timer_.nsecsElapsed()) / 1000000.0;
}
} // namespace chatterino
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include "debug/Log.hpp"
#include <QElapsedTimer>
#include <boost/noncopyable.hpp>
namespace chatterino {
class BenchmarkGuard : boost::noncopyable
{
public:
BenchmarkGuard(const QString &_name);
~BenchmarkGuard();
qreal getElapsedMs();
private:
QElapsedTimer timer_;
QString name_;
};
} // namespace chatterino
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include "util/Helpers.hpp"
#include <QDebug>
#include <QTime>
namespace chatterino {
template <typename... Args>
inline void log(const std::string &formatString, Args &&... args)
{
qDebug().noquote() << QTime::currentTime().toString("hh:mm:ss.zzz")
<< fS(formatString, std::forward<Args>(args)...).c_str();
}
template <typename... Args>
inline void log(const char *formatString, Args &&... args)
{
log(std::string(formatString), std::forward<Args>(args)...);
}
template <typename... Args>
inline void log(const QString &formatString, Args &&... args)
{
log(formatString.toStdString(), std::forward<Args>(args)...);
}
} // namespace chatterino
+44 -7
View File
@@ -1,13 +1,18 @@
#include <QApplication>
#include <QDebug>
#include <QMessageBox>
#include <QStringList>
#include <memory>
#include "BrowserExtension.hpp"
#include "RunGui.hpp"
#include "common/Args.hpp"
#include "common/Modes.hpp"
#include "common/Version.hpp"
#include "singletons/Paths.hpp"
#include "singletons/Settings.hpp"
#include "util/IncognitoBrowser.hpp"
#include <QApplication>
#include <QStringList>
#include <memory>
using namespace chatterino;
int main(int argc, char **argv)
@@ -18,17 +23,49 @@ int main(int argc, char **argv)
auto args = QStringList();
std::transform(argv + 1, argv + argc, std::back_inserter(args),
[&](auto s) { return s; });
initArgs(args);
// run in gui mode or browser extension host mode
if (shouldRunBrowserExtensionHost(args))
{
runBrowserExtensionHost();
}
else if (getArgs().printVersion)
{
qInfo().noquote() << Version::instance().fullVersion();
}
else
{
Paths paths;
Settings settings(paths.settingsDirectory);
Paths *paths{};
runGui(a, paths, settings);
try
{
paths = new Paths;
}
catch (std::runtime_error &error)
{
QMessageBox box;
if (Modes::instance().isPortable)
{
box.setText(
error.what() +
QStringLiteral(
"\n\nInfo: Portable mode requires the application to "
"be in a writeable location. If you don't want "
"portable mode reinstall the application. "
"https://chatterino.com."));
}
else
{
box.setText(error.what());
}
box.exec();
return 1;
}
Settings settings(paths->settingsDirectory);
runGui(a, *paths, settings);
}
return 0;
}
+20 -10
View File
@@ -1,5 +1,14 @@
#include "messages/Image.hpp"
#include <QBuffer>
#include <QImageReader>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QTimer>
#include <functional>
#include <thread>
#include "Application.hpp"
#include "common/Common.hpp"
#include "common/NetworkRequest.hpp"
@@ -11,15 +20,6 @@
#include "util/DebugCount.hpp"
#include "util/PostToThread.hpp"
#include <QBuffer>
#include <QImageReader>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QTimer>
#include <functional>
#include <thread>
namespace chatterino {
namespace detail {
// Frames
@@ -198,6 +198,15 @@ namespace detail {
} // namespace detail
// IMAGE2
Image::~Image()
{
// run destructor of Frames in gui thread
if (!isGuiThread())
{
postToThread([frames = this->frames_.release()]() { delete frames; });
}
}
ImagePtr Image::fromUrl(const Url &url, qreal scale)
{
static std::unordered_map<Url, std::weak_ptr<Image>> cache;
@@ -324,7 +333,7 @@ int Image::width() const
assertInGuiThread();
if (auto pixmap = this->frames_->first())
return pixmap->width() * this->scale_;
return int(pixmap->width() * this->scale_);
else
return 16;
}
@@ -369,6 +378,7 @@ void Image::actuallyLoad()
if (!shared)
return false;
// fourtf: is this the right thing to do?
shared->empty_ = true;
return true;
+9 -6
View File
@@ -13,7 +13,7 @@
#include <pajlada/signals/signal.hpp>
#include "common/Aliases.hpp"
#include "common/NullablePtr.hpp"
#include "common/Common.hpp"
namespace chatterino {
namespace detail {
@@ -45,9 +45,12 @@ namespace detail {
class Image;
using ImagePtr = std::shared_ptr<Image>;
/// This class is thread safe.
class Image : public std::enable_shared_from_this<Image>, boost::noncopyable
{
public:
~Image();
static ImagePtr fromUrl(const Url &url, qreal scale = 1);
static ImagePtr fromPixmap(const QPixmap &pixmap, qreal scale = 1);
static ImagePtr getEmpty();
@@ -72,14 +75,14 @@ private:
Image(qreal scale);
void setPixmap(const QPixmap &pixmap);
void actuallyLoad();
Url url_{};
qreal scale_{1};
bool empty_{false};
const Url url_{};
const qreal scale_{1};
std::atomic_bool empty_{false};
// gui thread only
bool shouldLoad_{false};
std::unique_ptr<detail::Frames> frames_{};
QObject object_{};
};
} // namespace chatterino
+7 -13
View File
@@ -32,9 +32,8 @@ std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
builder.message().flags.set(MessageFlag::PubSub);
builder
.emplace<ImageElement>(
Image::fromPixmap(getApp()->resources->twitch.automod),
MessageElementFlag::BadgeChannelAuthority)
.emplace<ImageElement>(Image::fromPixmap(getResources().twitch.automod),
MessageElementFlag::BadgeChannelAuthority)
->setTooltip("AutoMod");
builder.emplace<TextElement>("AutoMod:", MessageElementFlag::BoldUsername,
MessageColor(QColor("blue")),
@@ -258,40 +257,35 @@ MessageBuilder::MessageBuilder(const AutomodUserAction &action)
QString text;
switch (action.type)
{
case AutomodUserAction::AddPermitted:
{
case AutomodUserAction::AddPermitted: {
text = QString("%1 added %2 as a permitted term on AutoMod.")
.arg(action.source.name)
.arg(action.message);
}
break;
case AutomodUserAction::AddBlocked:
{
case AutomodUserAction::AddBlocked: {
text = QString("%1 added %2 as a blocked term on AutoMod.")
.arg(action.source.name)
.arg(action.message);
}
break;
case AutomodUserAction::RemovePermitted:
{
case AutomodUserAction::RemovePermitted: {
text = QString("%1 removed %2 as a permitted term term on AutoMod.")
.arg(action.source.name)
.arg(action.message);
}
break;
case AutomodUserAction::RemoveBlocked:
{
case AutomodUserAction::RemoveBlocked: {
text = QString("%1 removed %2 as a blocked term on AutoMod.")
.arg(action.source.name)
.arg(action.message);
}
break;
case AutomodUserAction::Properties:
{
case AutomodUserAction::Properties: {
text = QString("%1 modified the AutoMod properties.")
.arg(action.source.name);
}
+4
View File
@@ -103,6 +103,10 @@ enum class MessageElementFlag {
LowercaseLink = (1 << 29),
OriginalLink = (1 << 30),
// ZeroWidthEmotes are emotes that are supposed to overlay over any pre-existing emotes
// e.g. BTTV's SoSnowy during christmas season
ZeroWidthEmote = (1 << 31),
Default = Timestamp | Badges | Username | BitsStatic | FfzEmoteImage |
BttvEmoteImage | TwitchEmoteImage | BitsAmount | Text |
AlwaysShow,
+1 -1
View File
@@ -158,7 +158,7 @@ void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex,
// create new buffer if required
if (!pixmap)
{
#ifdef Q_OS_MACOS
#if defined(Q_OS_MACOS) || defined(Q_OS_LINUX)
pixmap = new QPixmap(int(width * painter.device()->devicePixelRatioF()),
int(container_->getHeight() *
painter.device()->devicePixelRatioF()));
@@ -115,15 +115,27 @@ void MessageLayoutContainer::_addElement(MessageLayoutElement *element,
// update line height
this->lineHeight_ = std::max(this->lineHeight_, newLineHeight);
auto xOffset = 0;
if (element->getCreator().getFlags().has(
MessageElementFlag::ZeroWidthEmote))
{
xOffset -= element->getRect().width() + this->spaceWidth_;
}
// set move element
element->setPosition(
QPoint(this->currentX_, this->currentY_ - element->getRect().height()));
element->setPosition(QPoint(this->currentX_ + xOffset,
this->currentY_ - element->getRect().height()));
// add element
this->elements_.push_back(std::unique_ptr<MessageLayoutElement>(element));
// set current x
this->currentX_ += element->getRect().width();
if (!element->getCreator().getFlags().has(
MessageElementFlag::ZeroWidthEmote))
{
this->currentX_ += element->getRect().width();
}
if (element->hasTrailingSpace())
{
@@ -137,7 +149,9 @@ void MessageLayoutContainer::breakLine()
if (this->flags_.has(MessageFlag::Centered) && this->elements_.size() > 0)
{
xOffset = (width_ - this->elements_.at(0)->getRect().left() -
const int marginOffset = int(this->margin.left * this->scale_) +
int(this->margin.right * this->scale_);
xOffset = (width_ - marginOffset -
this->elements_.at(this->elements_.size() - 1)
->getRect()
.right()) /
+24 -19
View File
@@ -1,5 +1,8 @@
#include "providers/bttv/BttvEmotes.hpp"
#include <QJsonArray>
#include <QThread>
#include "common/Common.hpp"
#include "common/NetworkRequest.hpp"
#include "debug/Log.hpp"
@@ -8,11 +11,11 @@
#include "messages/ImageSet.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include <QJsonArray>
#include <QThread>
namespace chatterino {
namespace {
QString emoteLinkFormat("https://betterttv.com/emotes/%1");
Url getEmoteLink(QString urlTemplate, const EmoteId &id,
const QString &emoteScale)
{
@@ -47,13 +50,14 @@ namespace {
auto name =
EmoteName{jsonEmote.toObject().value("code").toString()};
auto emote = Emote(
{name,
ImageSet{Image::fromUrl(getEmoteLinkV3(id, "1x"), 1),
Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5),
Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25)},
Tooltip{name.string + "<br />Global BetterTTV Emote"},
Url{"https://manage.betterttv.net/emotes/" + id.string}});
auto emote = Emote({
name,
ImageSet{Image::fromUrl(getEmoteLinkV3(id, "1x"), 1),
Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5),
Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25)},
Tooltip{name.string + "<br />Global BetterTTV Emote"},
Url{emoteLinkFormat.arg(id.string)},
});
emotes[name] =
cachedOrMakeEmotePtr(std::move(emote), currentEmotes);
@@ -75,15 +79,16 @@ namespace {
auto name = EmoteName{jsonEmote.value("code").toString()};
// emoteObject.value("imageType").toString();
auto emote = Emote(
{name,
ImageSet{
Image::fromUrl(getEmoteLinkV3(id, "1x"), 1),
Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5),
Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25),
},
Tooltip{name.string + "<br />Channel BetterTTV Emote"},
Url{"https://manage.betterttv.net/emotes/" + id.string}});
auto emote = Emote({
name,
ImageSet{
Image::fromUrl(getEmoteLinkV3(id, "1x"), 1),
Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5),
Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25),
},
Tooltip{name.string + "<br />Channel BetterTTV Emote"},
Url{emoteLinkFormat.arg(id.string)},
});
emotes[name] = cachedOrMake(std::move(emote), id);
}
+6 -13
View File
@@ -1,13 +1,13 @@
#pragma once
#include "providers/irc/IrcConnection2.hpp"
#include <IrcMessage>
#include <functional>
#include <mutex>
#include <pajlada/signals/signal.hpp>
#include <pajlada/signals/signalholder.hpp>
#include <functional>
#include <mutex>
#include "common/Common.hpp"
#include "providers/irc/IrcConnection2.hpp"
namespace chatterino {
@@ -73,15 +73,8 @@ protected:
private:
void initConnection();
struct Deleter {
void operator()(IrcConnection *conn)
{
conn->deleteLater();
}
};
std::unique_ptr<IrcConnection, Deleter> writeConnection_ = nullptr;
std::unique_ptr<IrcConnection, Deleter> readConnection_ = nullptr;
QObjectPtr<IrcConnection> writeConnection_ = nullptr;
QObjectPtr<IrcConnection> readConnection_ = nullptr;
QTimer reconnectTimer_;
int falloffCounter_ = 1;
+5 -6
View File
@@ -73,13 +73,13 @@ inline QString getCredentialName(const IrcServerData &data)
void IrcServerData::getPassword(
QObject *receiver, std::function<void(const QString &)> &&onLoaded) const
{
Credentials::getInstance().get("irc", getCredentialName(*this), receiver,
std::move(onLoaded));
Credentials::instance().get("irc", getCredentialName(*this), receiver,
std::move(onLoaded));
}
void IrcServerData::setPassword(const QString &password)
{
Credentials::getInstance().set("irc", getCredentialName(*this), password);
Credentials::instance().set("irc", getCredentialName(*this), password);
}
Irc::Irc()
@@ -133,8 +133,7 @@ Irc::Irc()
if (args.caller != Irc::noEraseCredentialCaller)
{
Credentials::getInstance().erase("irc",
getCredentialName(args.item));
Credentials::instance().erase("irc", getCredentialName(args.item));
}
});
@@ -164,7 +163,7 @@ ChannelPtr Irc::getOrAddChannel(int id, QString name)
}
}
Irc &Irc::getInstance()
Irc &Irc::instance()
{
static Irc irc;
return irc;
+1 -1
View File
@@ -36,7 +36,7 @@ class Irc
public:
Irc();
static Irc &getInstance();
static Irc &instance();
static inline void *const noEraseCredentialCaller =
reinterpret_cast<void *>(1);
+23 -22
View File
@@ -66,26 +66,29 @@ void IrcServer::initializeConnection(IrcConnection *connection,
connection->setRealName(this->data_->real.isEmpty() ? this->data_->user
: this->data_->nick);
switch (this->data_->authType)
if (getSettings()->enableExperimentalIrc)
{
case IrcAuthType::Sasl:
connection->setSaslMechanism("PLAIN");
[[fallthrough]];
case IrcAuthType::Pass:
this->data_->getPassword(
this, [conn = new QObjectRef(connection) /* can't copy */,
this](const QString &password) mutable {
if (*conn)
{
(*conn)->setPassword(password);
this->open(Both);
}
switch (this->data_->authType)
{
case IrcAuthType::Sasl:
connection->setSaslMechanism("PLAIN");
[[fallthrough]];
case IrcAuthType::Pass:
this->data_->getPassword(
this, [conn = new QObjectRef(connection) /* can't copy */,
this](const QString &password) mutable {
if (*conn)
{
(*conn)->setPassword(password);
this->open(Both);
}
delete conn;
});
break;
default:
this->open(Both);
delete conn;
});
break;
default:
this->open(Both);
}
}
QObject::connect(
@@ -179,8 +182,7 @@ void IrcServer::readConnectionMessageReceived(Communi::IrcMessage *message)
switch (message->type())
{
case Communi::IrcMessage::Join:
{
case Communi::IrcMessage::Join: {
auto x = static_cast<Communi::IrcJoinMessage *>(message);
if (auto it =
@@ -205,8 +207,7 @@ void IrcServer::readConnectionMessageReceived(Communi::IrcMessage *message)
return;
}
case Communi::IrcMessage::Part:
{
case Communi::IrcMessage::Part: {
auto x = static_cast<Communi::IrcPartMessage *>(message);
if (auto it =
+2 -2
View File
@@ -40,7 +40,7 @@ static QMap<QString, QString> parseBadges(QString badgesString)
return badges;
}
IrcMessageHandler &IrcMessageHandler::getInstance()
IrcMessageHandler &IrcMessageHandler::instance()
{
static IrcMessageHandler instance;
return instance;
@@ -618,7 +618,7 @@ void IrcMessageHandler::handlePartMessage(Communi::IrcMessage *message)
{
if (message->nick() !=
getApp()->accounts->twitch.getCurrent()->getUserName() &&
getSettings()->showJoins.getValue())
getSettings()->showParts.getValue())
{
twitchChannel->addPartedUser(message->nick());
}
+1 -1
View File
@@ -13,7 +13,7 @@ class IrcMessageHandler
IrcMessageHandler() = default;
public:
static IrcMessageHandler &getInstance();
static IrcMessageHandler &instance();
// parseMessage parses a single IRC message into 0+ Chatterino messages
std::vector<MessagePtr> parseMessage(Channel *channel,
+1 -1
View File
@@ -156,7 +156,7 @@ namespace detail {
if (self->awaitingPong_)
{
log("No pong respnose, disconnect!");
log("No pong response, disconnect!");
// TODO(pajlada): Label this connection as "disconnect
// me"
}
@@ -102,14 +102,12 @@ void TwitchAccountManager::reloadUsers()
switch (this->addUser(userData))
{
case AddUserResponse::UserAlreadyExists:
{
case AddUserResponse::UserAlreadyExists: {
log("User {} already exists", userData.username);
// Do nothing
}
break;
case AddUserResponse::UserValuesUpdated:
{
case AddUserResponse::UserValuesUpdated: {
log("User {} already exists, and values updated!",
userData.username);
if (userData.username == this->getCurrent()->getUserName())
@@ -120,8 +118,7 @@ void TwitchAccountManager::reloadUsers()
}
}
break;
case AddUserResponse::UserAdded:
{
case AddUserResponse::UserAdded: {
log("Added user {}", userData.username);
listUpdated = true;
}
+31
View File
@@ -0,0 +1,31 @@
#include "providers/twitch/TwitchBadge.hpp"
#include <QSet>
namespace chatterino {
// set of badge IDs that should be given specific flags.
// vanity flag is left out on purpose as it is our default flag
const QSet<QString> globalAuthority{"staff", "admin", "global_mod"};
const QSet<QString> channelAuthority{"moderator", "vip", "broadcaster"};
const QSet<QString> subBadges{"subscriber", "founder"};
Badge::Badge(QString key, QString value)
: key_(std::move(key))
, value_(std::move(value))
{
if (globalAuthority.contains(this->key_))
{
this->flag_ = MessageElementFlag::BadgeGlobalAuthority;
}
else if (channelAuthority.contains(this->key_))
{
this->flag_ = MessageElementFlag::BadgeChannelAuthority;
}
else if (subBadges.contains(this->key_))
{
this->flag_ = MessageElementFlag::BadgeSubscription;
}
}
} // namespace chatterino
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "messages/MessageElement.hpp"
#include <QString>
namespace chatterino {
class Badge
{
public:
Badge(QString key, QString value);
QString key_; // e.g. bits
QString value_; // e.g. 100
QString extraValue_{}; // e.g. 5 (the number of months subscribed)
MessageElementFlag flag_{
MessageElementFlag::BadgeVanity}; // badge slot it takes up
};
} // namespace chatterino
+1 -1
View File
@@ -47,7 +47,7 @@ void TwitchBadges::loadTwitchBadges()
{versionObj.value("image_url_4x").toString()},
.25),
},
Tooltip{versionObj.value("description").toString()},
Tooltip{versionObj.value("title").toString()},
Url{versionObj.value("click_url").toString()}};
// "title"
// "clickAction"
+52 -3
View File
@@ -31,6 +31,7 @@
namespace chatterino {
namespace {
constexpr int TITLE_REFRESH_PERIOD = 10;
constexpr char MAGIC_MESSAGE_SUFFIX[] = u8" \U000E0000";
// parseRecentMessages takes a json object and returns a vector of
@@ -89,6 +90,7 @@ TwitchChannel::TwitchChannel(const QString &name,
, bttvEmotes_(std::make_shared<EmoteMap>())
, ffzEmotes_(std::make_shared<EmoteMap>())
, mod_(false)
, titleRefreshedTime_(QTime::currentTime().addSecs(-TITLE_REFRESH_PERIOD))
{
log("[TwitchChannel:{}] Opened", name);
@@ -110,6 +112,7 @@ TwitchChannel::TwitchChannel(const QString &name,
// room id loaded -> refresh live status
this->roomIdChanged.connect([this]() {
this->refreshPubsub();
this->refreshTitle();
this->refreshLiveStatus();
this->refreshBadges();
this->refreshCheerEmotes();
@@ -229,7 +232,7 @@ bool TwitchChannel::isMod() const
return this->mod_;
}
bool TwitchChannel::isVIP() const
bool TwitchChannel::isVip() const
{
return this->vip_;
}
@@ -278,7 +281,7 @@ bool TwitchChannel::isBroadcaster() const
bool TwitchChannel::hasHighRateLimit() const
{
return this->isMod() || this->isBroadcaster() || this->isVIP();
return this->isMod() || this->isBroadcaster() || this->isVip();
}
bool TwitchChannel::canReconnect() const
@@ -437,6 +440,51 @@ void TwitchChannel::setLive(bool newLiveStatus)
}
}
void TwitchChannel::refreshTitle()
{
auto roomID = this->roomId();
if (roomID.isEmpty())
{
return;
}
if (this->titleRefreshedTime_.elapsed() < TITLE_REFRESH_PERIOD * 1000)
{
return;
}
this->titleRefreshedTime_ = QTime::currentTime();
QString url("https://api.twitch.tv/kraken/channels/" + roomID);
NetworkRequest::twitchRequest(url)
.onSuccess(
[this, weak = weakOf<Channel>(this)](auto result) -> Outcome {
ChannelPtr shared = weak.lock();
if (!shared)
return Failure;
const auto document = result.parseRapidJson();
auto statusIt = document.FindMember("status");
if (statusIt == document.MemberEnd())
{
return Failure;
}
{
auto status = this->streamStatus_.access();
if (!rj::getSafe(statusIt->value, status->title))
{
return Failure;
}
}
this->liveStatusChanged.invoke();
return Success;
})
.execute();
}
void TwitchChannel::refreshLiveStatus()
{
auto roomID = this->roomId();
@@ -566,7 +614,7 @@ void TwitchChannel::loadRecentMessages()
auto messages = parseRecentMessages(result.parseJson(), shared);
auto &handler = IrcMessageHandler::getInstance();
auto &handler = IrcMessageHandler::instance();
std::vector<MessagePtr> allBuiltMessages;
@@ -711,6 +759,7 @@ void TwitchChannel::refreshCheerEmotes()
cheerEmote.color = QColor(tier.color);
cheerEmote.minBits = tier.minBits;
cheerEmote.regex = cheerEmoteSet.regex;
// TODO(pajlada): We currently hardcode dark here :|
// We will continue to do so for now since we haven't had to
+3 -1
View File
@@ -63,12 +63,13 @@ public:
virtual bool canSendMessage() const override;
virtual void sendMessage(const QString &message) override;
virtual bool isMod() const override;
bool isVIP() const;
bool isVip() const;
bool isStaff() const;
virtual bool isBroadcaster() const override;
virtual bool hasHighRateLimit() const override;
virtual bool canReconnect() const override;
virtual void reconnect() override;
void refreshTitle();
// Data
const QString &subscriptionUrl();
@@ -166,6 +167,7 @@ private:
QObject lifetimeGuard_;
QTimer liveStatusTimer_;
QTimer chattersListTimer_;
QTime titleRefreshedTime_;
friend class TwitchIrcServer;
friend class TwitchMessageBuilder;
+1
View File
@@ -18,6 +18,7 @@ using EmotePtr = std::shared_ptr<const Emote>;
struct CheerEmote {
QColor color;
int minBits;
QRegularExpression regex;
EmotePtr animatedEmote;
EmotePtr staticEmote;
+40 -13
View File
@@ -1,7 +1,11 @@
#include "TwitchIrcServer.hpp"
#include <IrcCommand>
#include <cassert>
#include "Application.hpp"
#include "common/Common.hpp"
#include "common/Env.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "controllers/highlights/HighlightController.hpp"
#include "messages/Message.hpp"
@@ -15,9 +19,6 @@
#include "providers/twitch/TwitchMessageBuilder.hpp"
#include "util/PostToThread.hpp"
#include <IrcCommand>
#include <cassert>
// using namespace Communi;
using namespace std::chrono_literals;
@@ -86,13 +87,12 @@ void TwitchIrcServer::initializeConnection(IrcConnection *connection,
connection->setPassword(oauthToken);
}
connection->setSecure(true);
// https://dev.twitch.tv/docs/irc/guide/#connecting-to-twitch-irc
// SSL disabled: irc://irc.chat.twitch.tv:6667
// SSL enabled: irc://irc.chat.twitch.tv:6697
connection->setHost("irc.chat.twitch.tv");
connection->setPort(6697);
// SSL disabled: irc://irc.chat.twitch.tv:6667 (or port 80)
// SSL enabled: irc://irc.chat.twitch.tv:6697 (or port 443)
connection->setHost(Env::get().twitchServerHost);
connection->setPort(Env::get().twitchServerPort);
connection->setSecure(Env::get().twitchServerSecure);
this->open(type);
}
@@ -125,7 +125,7 @@ std::shared_ptr<Channel> TwitchIrcServer::createChannel(
void TwitchIrcServer::privateMessageReceived(
Communi::IrcPrivateMessage *message)
{
IrcMessageHandler::getInstance().handlePrivMessage(message, *this);
IrcMessageHandler::instance().handlePrivMessage(message, *this);
}
void TwitchIrcServer::readConnectionMessageReceived(
@@ -141,7 +141,7 @@ void TwitchIrcServer::readConnectionMessageReceived(
const QString &command = message->command();
auto &handler = IrcMessageHandler::getInstance();
auto &handler = IrcMessageHandler::instance();
// Below commands enabled through the twitch.tv/membership CAP REQ
if (command == "MODE")
@@ -194,14 +194,41 @@ void TwitchIrcServer::writeConnectionMessageReceived(
{
const QString &command = message->command();
auto &handler = IrcMessageHandler::getInstance();
auto &handler = IrcMessageHandler::instance();
// Below commands enabled through the twitch.tv/commands CAP REQ
if (command == "USERSTATE")
{
// Received USERSTATE upon PRIVMSGing
handler.handleUserStateMessage(message);
}
else if (command == "NOTICE")
{
static std::unordered_set<std::string> readConnectionOnlyIDs{
"host_on",
"host_off",
"host_target_went_offline",
"emote_only_on",
"emote_only_off",
"slow_on",
"slow_off",
"subs_on",
"subs_off",
"r9k_on",
"r9k_off",
// Display for user who times someone out. This implies you're a
// moderator, at which point you will be connected to PubSub and receive
// a better message from there.
"timeout_success",
"ban_success",
// Channel suspended notices
"msg_channel_suspended",
};
handler.handleNoticeMessage(
static_cast<Communi::IrcNoticeMessage *>(message));
}
}
void TwitchIrcServer::onReadConnected(IrcConnection *connection)
+165 -155
View File
@@ -28,6 +28,10 @@
namespace {
const QSet<QString> zeroWidthEmotes{
"SoSnowy", "IceCold", "SantaHat", "TopHat", "ReinDeer", "CandyCane",
};
QColor getRandomColor(const QVariant &userId)
{
static const std::vector<QColor> twitchUsernameColors = {
@@ -65,6 +69,59 @@ QColor getRandomColor(const QVariant &userId)
namespace chatterino {
namespace {
QStringList parseTagList(const QVariantMap &tags, const QString &key)
{
auto iterator = tags.find(key);
if (iterator == tags.end())
return QStringList{};
return iterator.value().toString().split(
',', QString::SplitBehavior::SkipEmptyParts);
}
std::map<QString, QString> parseBadgeInfos(const QVariantMap &tags)
{
std::map<QString, QString> badgeInfos;
for (QString badgeInfo : parseTagList(tags, "badge-info"))
{
QStringList parts = badgeInfo.split('/');
if (parts.size() != 2)
{
log("Skipping badge-info because it split weird: {}",
badgeInfo);
continue;
}
badgeInfos.emplace(parts[0], parts[1]);
}
return badgeInfos;
}
std::vector<Badge> parseBadges(const QVariantMap &tags)
{
std::vector<Badge> badges;
for (QString badge : parseTagList(tags, "badges"))
{
QStringList parts = badge.split('/');
if (parts.size() != 2)
{
log("Skipping badge because it split weird: {}", badge);
continue;
}
badges.emplace_back(parts[0], parts[1]);
}
return badges;
}
} // namespace
TwitchMessageBuilder::TwitchMessageBuilder(
Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage,
const MessageParseArgs &_args)
@@ -289,6 +346,7 @@ MessagePtr TwitchMessageBuilder::build()
if (iterator != this->tags.end())
{
this->hasBits_ = true;
this->bitsLeft = iterator.value().toInt();
this->bits = iterator.value().toString();
}
@@ -617,14 +675,12 @@ void TwitchMessageBuilder::appendUsername()
switch (usernameDisplayMode.getValue())
{
case UsernameDisplayMode::Username:
{
case UsernameDisplayMode::Username: {
usernameText = username;
}
break;
case UsernameDisplayMode::LocalizedName:
{
case UsernameDisplayMode::LocalizedName: {
if (hasLocalizedName)
{
usernameText = localizedName;
@@ -637,8 +693,7 @@ void TwitchMessageBuilder::appendUsername()
break;
default:
case UsernameDisplayMode::UsernameAndLocalizedName:
{
case UsernameDisplayMode::UsernameAndLocalizedName: {
if (hasLocalizedName)
{
usernameText = username + "(" + localizedName + ")";
@@ -655,7 +710,7 @@ void TwitchMessageBuilder::appendUsername()
{
// TODO(pajlada): Re-implement
// userDisplayString +=
// IrcManager::getInstance().getUser().getUserName();
// IrcManager::instance().getUser().getUserName();
}
else if (this->args.isReceivedWhisper)
{
@@ -1112,6 +1167,11 @@ Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name)
else if ((emote = globalBttvEmotes.emote(name)))
{
flags = MessageElementFlag::BttvEmote;
if (zeroWidthEmotes.contains(name.string))
{
flags.set(MessageElementFlag::ZeroWidthEmote);
}
}
if (emote)
@@ -1123,7 +1183,24 @@ Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name)
return Failure;
}
// fourtf: this is ugly
boost::optional<EmotePtr> TwitchMessageBuilder::getTwitchBadge(
const Badge &badge)
{
if (auto channelBadge =
this->twitchChannel->twitchBadge(badge.key_, badge.value_))
{
return channelBadge;
}
if (auto globalBadge = this->twitchChannel->globalTwitchBadges().badge(
badge.key_, badge.value_))
{
return globalBadge;
}
return boost::none;
}
void TwitchMessageBuilder::appendTwitchBadges()
{
if (this->twitchChannel == nullptr)
@@ -1131,68 +1208,25 @@ void TwitchMessageBuilder::appendTwitchBadges()
return;
}
auto app = getApp();
auto badgeInfos = parseBadgeInfos(this->tags);
auto badges = parseBadges(this->tags);
auto iterator = this->tags.find("badges");
if (iterator == this->tags.end())
return;
for (QString badge : iterator.value().toString().split(','))
for (const auto &badge : badges)
{
if (badge.startsWith("bits/"))
auto badgeEmote = this->getTwitchBadge(badge);
if (!badgeEmote)
{
QString cheerAmount = badge.mid(5);
QString tooltip = QString("Twitch cheer ") + cheerAmount;
log("No channel/global variant found {}", badge.key_);
continue;
}
auto tooltip = (*badgeEmote)->tooltip.string;
// Try to fetch channel-specific bit badge
try
{
if (twitchChannel)
if (const auto &_badge = this->twitchChannel->twitchBadge(
"bits", cheerAmount))
{
this->emplace<BadgeElement>(
_badge.get(), MessageElementFlag::BadgeVanity)
->setTooltip(tooltip);
continue;
}
}
catch (const std::out_of_range &)
{
// Channel does not contain a special bit badge for this version
}
// Use default bit badge
if (auto _badge = this->twitchChannel->globalTwitchBadges().badge(
"bits", cheerAmount))
{
this->emplace<BadgeElement>(_badge.get(),
MessageElementFlag::BadgeVanity)
->setTooltip(tooltip);
}
}
else if (badge == "staff/1")
if (badge.key_ == "bits")
{
this->emplace<ImageElement>(
Image::fromPixmap(app->resources->twitch.staff),
MessageElementFlag::BadgeGlobalAuthority)
->setTooltip("Twitch Staff");
const auto &cheerAmount = badge.value_;
tooltip = QString("Twitch cheer %0").arg(cheerAmount);
}
else if (badge == "admin/1")
{
this->emplace<ImageElement>(
Image::fromPixmap(app->resources->twitch.admin),
MessageElementFlag::BadgeGlobalAuthority)
->setTooltip("Twitch Admin");
}
else if (badge == "global_mod/1")
{
this->emplace<ImageElement>(
Image::fromPixmap(app->resources->twitch.globalmod),
MessageElementFlag::BadgeGlobalAuthority)
->setTooltip("Twitch Global Moderator");
}
else if (badge == "moderator/1")
else if (badge.key_ == "moderator")
{
if (auto customModBadge = this->twitchChannel->ffzCustomModBadge())
{
@@ -1200,104 +1234,22 @@ void TwitchMessageBuilder::appendTwitchBadges()
customModBadge.get(),
MessageElementFlag::BadgeChannelAuthority)
->setTooltip((*customModBadge)->tooltip.string);
// early out, since we have to add a custom badge element here
continue;
}
this->emplace<ImageElement>(
Image::fromPixmap(app->resources->twitch.moderator),
MessageElementFlag::BadgeChannelAuthority)
->setTooltip("Twitch Channel Moderator");
}
else if (badge == "vip/1")
else if (badge.flag_ == MessageElementFlag::BadgeSubscription)
{
this->emplace<ImageElement>(
Image::fromPixmap(app->resources->twitch.vip),
MessageElementFlag::BadgeChannelAuthority)
->setTooltip("VIP");
}
else if (badge == "broadcaster/1")
{
this->emplace<ImageElement>(
Image::fromPixmap(app->resources->twitch.broadcaster),
MessageElementFlag::BadgeChannelAuthority)
->setTooltip("Twitch Broadcaster");
}
else if (badge == "turbo/1")
{
this->emplace<ImageElement>(
Image::fromPixmap(app->resources->twitch.turbo),
MessageElementFlag::BadgeVanity)
->setTooltip("Twitch Turbo Subscriber");
}
else if (badge == "premium/1")
{
this->emplace<ImageElement>(
Image::fromPixmap(app->resources->twitch.prime),
MessageElementFlag::BadgeVanity)
->setTooltip("Twitch Prime Subscriber");
}
else if (badge.startsWith("partner/"))
{
int index = badge.midRef(8).toInt();
switch (index)
auto badgeInfoIt = badgeInfos.find(badge.key_);
if (badgeInfoIt != badgeInfos.end())
{
case 1:
{
this->emplace<ImageElement>(
Image::fromPixmap(app->resources->twitch.verified,
0.25),
MessageElementFlag::BadgeVanity)
->setTooltip("Twitch Verified");
}
break;
default:
{
printf("[TwitchMessageBuilder] Unhandled partner badge "
"index: %d\n",
index);
}
break;
const auto &subMonths = badgeInfoIt->second;
tooltip += QString(" (%0 months)").arg(subMonths);
}
}
else if (badge.startsWith("subscriber/"))
{
if (auto badgeEmote = this->twitchChannel->twitchBadge(
"subscriber", badge.mid(11)))
{
this->emplace<BadgeElement>(
badgeEmote.get(), MessageElementFlag::BadgeSubscription)
->setTooltip((*badgeEmote)->tooltip.string);
continue;
}
// use default subscriber badge if custom one not found
this->emplace<ImageElement>(
Image::fromPixmap(app->resources->twitch.subscriber, 0.25),
MessageElementFlag::BadgeSubscription)
->setTooltip("Twitch Subscriber");
}
else
{
auto splits = badge.split('/');
if (splits.size() != 2)
continue;
if (auto badgeEmote =
this->twitchChannel->twitchBadge(splits[0], splits[1]))
{
this->emplace<BadgeElement>(badgeEmote.get(),
MessageElementFlag::BadgeVanity)
->setTooltip((*badgeEmote)->tooltip.string);
continue;
}
if (auto _badge = this->twitchChannel->globalTwitchBadges().badge(
splits[0], splits[1]))
{
this->emplace<BadgeElement>(_badge.get(),
MessageElementFlag::BadgeVanity)
->setTooltip((*_badge)->tooltip.string);
continue;
}
}
this->emplace<BadgeElement>(badgeEmote.get(), badge.flag_)
->setTooltip(tooltip);
}
}
@@ -1312,12 +1264,67 @@ void TwitchMessageBuilder::appendChatterinoBadges()
Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string)
{
if (this->bitsLeft == 0)
{
return Failure;
}
auto cheerOpt = this->twitchChannel->cheerEmote(string);
if (!cheerOpt)
{
return Failure;
}
auto &cheerEmote = *cheerOpt;
auto match = cheerEmote.regex.match(string);
if (!match.hasMatch())
{
return Failure;
}
int cheerValue = match.captured(1).toInt();
if (getSettings()->stackBits)
{
if (this->bitsStacked)
{
return Success;
}
if (cheerEmote.staticEmote)
{
this->emplace<EmoteElement>(cheerEmote.staticEmote,
MessageElementFlag::BitsStatic);
}
if (cheerEmote.animatedEmote)
{
this->emplace<EmoteElement>(cheerEmote.animatedEmote,
MessageElementFlag::BitsAnimated);
}
if (cheerEmote.color != QColor())
{
this->emplace<TextElement>(QString::number(this->bitsLeft),
MessageElementFlag::BitsAmount,
cheerEmote.color);
}
this->bitsStacked = true;
return Success;
}
if (this->bitsLeft >= cheerValue)
{
this->bitsLeft -= cheerValue;
}
else
{
QString newString = string;
newString.chop(QString::number(cheerValue).length());
newString += QString::number(cheerValue - this->bitsLeft);
return tryParseCheermote(newString);
}
if (cheerEmote.staticEmote)
{
this->emplace<EmoteElement>(cheerEmote.staticEmote,
@@ -1330,9 +1337,12 @@ Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string)
}
if (cheerEmote.color != QColor())
{
this->emplace<TextElement>(this->bits, MessageElementFlag::BitsAmount,
this->emplace<TextElement>(match.captured(1),
MessageElementFlag::BitsAmount,
cheerEmote.color);
}
return Success;
}
} // namespace chatterino
@@ -3,6 +3,7 @@
#include "common/Aliases.hpp"
#include "common/Outcome.hpp"
#include "messages/MessageBuilder.hpp"
#include "providers/twitch/TwitchBadge.hpp"
#include <IrcMessage>
#include <QString>
@@ -60,6 +61,7 @@ private:
// parseHighlights only updates the visual state of the message, but leaves the playing of alerts and sounds to the triggerHighlights function
void parseHighlights();
boost::optional<EmotePtr> getTwitchBadge(const Badge &badge);
void appendTwitchEmote(
const QString &emote,
std::vector<std::tuple<int, EmotePtr, EmoteName>> &vec,
@@ -79,6 +81,8 @@ private:
QString roomID_;
bool hasBits_ = false;
QString bits;
int bitsLeft;
bool bitsStacked = false;
bool historicalMessage_ = false;
QString userId_;
+181
View File
@@ -0,0 +1,181 @@
#include "singletons/Fonts.hpp"
#include "BaseSettings.hpp"
#include "debug/AssertInGuiThread.hpp"
#include <QDebug>
#include <QtGlobal>
#ifdef CHATTERINO
# include "Application.hpp"
# include "singletons/WindowManager.hpp"
#endif
#ifdef Q_OS_WIN32
# define DEFAULT_FONT_FAMILY "Segoe UI"
# define DEFAULT_FONT_SIZE 10
#else
# ifdef Q_OS_MACOS
# define DEFAULT_FONT_FAMILY "Helvetica Neue"
# define DEFAULT_FONT_SIZE 12
# else
# define DEFAULT_FONT_FAMILY "Arial"
# define DEFAULT_FONT_SIZE 11
# endif
#endif
namespace chatterino {
namespace {
int getBoldness()
{
#ifdef CHATTERINO
return getSettings()->boldScale.getValue();
#else
return QFont::Bold;
#endif
}
} // namespace
Fonts *Fonts::instance = nullptr;
Fonts::Fonts()
: chatFontFamily("/appearance/currentFontFamily", DEFAULT_FONT_FAMILY)
, chatFontSize("/appearance/currentFontSize", DEFAULT_FONT_SIZE)
{
Fonts::instance = this;
this->fontsByType_.resize(size_t(FontStyle::EndType));
}
void Fonts::initialize(Settings &, Paths &)
{
this->chatFontFamily.connect(
[this]() {
assertInGuiThread();
for (auto &map : this->fontsByType_)
{
map.clear();
}
this->fontChanged.invoke();
},
false);
this->chatFontSize.connect(
[this]() {
assertInGuiThread();
for (auto &map : this->fontsByType_)
{
map.clear();
}
this->fontChanged.invoke();
},
false);
#ifdef CHATTERINO
getSettings()->boldScale.connect(
[this]() {
assertInGuiThread();
// REMOVED
getApp()->windows->incGeneration();
for (auto &map : this->fontsByType_)
{
map.clear();
}
this->fontChanged.invoke();
},
false);
#endif
}
QFont Fonts::getFont(FontStyle type, float scale)
{
return this->getOrCreateFontData(type, scale).font;
}
QFontMetrics Fonts::getFontMetrics(FontStyle type, float scale)
{
return this->getOrCreateFontData(type, scale).metrics;
}
Fonts::FontData &Fonts::getOrCreateFontData(FontStyle type, float scale)
{
assertInGuiThread();
assert(type < FontStyle::EndType);
auto &map = this->fontsByType_[size_t(type)];
// find element
auto it = map.find(scale);
if (it != map.end())
{
// return if found
return it->second;
}
// emplace new element
auto result = map.emplace(scale, this->createFontData(type, scale));
assert(result.second);
return result.first->second;
}
Fonts::FontData Fonts::createFontData(FontStyle type, float scale)
{
// check if it's a chat (scale the setting)
if (type >= FontStyle::ChatStart && type <= FontStyle::ChatEnd)
{
static std::unordered_map<FontStyle, ChatFontData> sizeScale{
{FontStyle::ChatSmall, {0.6f, false, QFont::Normal}},
{FontStyle::ChatMediumSmall, {0.8f, false, QFont::Normal}},
{FontStyle::ChatMedium, {1, false, QFont::Normal}},
{FontStyle::ChatMediumBold,
{1, false, QFont::Weight(getBoldness())}},
{FontStyle::ChatMediumItalic, {1, true, QFont::Normal}},
{FontStyle::ChatLarge, {1.2f, false, QFont::Normal}},
{FontStyle::ChatVeryLarge, {1.4f, false, QFont::Normal}},
};
sizeScale[FontStyle::ChatMediumBold] = {1, false,
QFont::Weight(getBoldness())};
auto data = sizeScale[type];
return FontData(
QFont(this->chatFontFamily.getValue(),
int(this->chatFontSize.getValue() * data.scale * scale),
data.weight, data.italic));
}
// normal Ui font (use pt size)
{
#ifdef Q_OS_MAC
constexpr float multiplier = 0.8f;
#else
constexpr float multiplier = 1.f;
#endif
static std::unordered_map<FontStyle, UiFontData> defaultSize{
{FontStyle::Tiny, {8, "Monospace", false, QFont::Normal}},
{FontStyle::UiMedium,
{int(9 * multiplier), DEFAULT_FONT_FAMILY, false, QFont::Normal}},
{FontStyle::UiMediumBold,
{int(9 * multiplier), DEFAULT_FONT_FAMILY, false, QFont::Bold}},
{FontStyle::UiTabs,
{int(9 * multiplier), DEFAULT_FONT_FAMILY, false, QFont::Normal}},
};
UiFontData &data = defaultSize[type];
QFont font(data.name, int(data.size * scale), data.weight, data.italic);
return FontData(font);
}
}
Fonts *getFonts()
{
return Fonts::instance;
}
} // namespace chatterino
+93
View File
@@ -0,0 +1,93 @@
#pragma once
#include "common/ChatterinoSetting.hpp"
#include "common/Singleton.hpp"
#include <QFont>
#include <QFontDatabase>
#include <QFontMetrics>
#include <boost/noncopyable.hpp>
#include <pajlada/signals/signal.hpp>
#include <array>
#include <unordered_map>
namespace chatterino {
class Settings;
class Paths;
enum class FontStyle : uint8_t {
Tiny,
ChatSmall,
ChatMediumSmall,
ChatMedium,
ChatMediumBold,
ChatMediumItalic,
ChatLarge,
ChatVeryLarge,
UiMedium,
UiMediumBold,
UiTabs,
// don't remove this value
EndType,
// make sure to update these values accordingly!
ChatStart = ChatSmall,
ChatEnd = ChatVeryLarge,
};
class Fonts final : public Singleton
{
public:
Fonts();
virtual void initialize(Settings &settings, Paths &paths) override;
// font data gets set in createFontData(...)
QFont getFont(FontStyle type, float scale);
QFontMetrics getFontMetrics(FontStyle type, float scale);
QStringSetting chatFontFamily;
IntSetting chatFontSize;
pajlada::Signals::NoArgSignal fontChanged;
static Fonts *instance;
private:
struct FontData {
FontData(const QFont &_font)
: font(_font)
, metrics(_font)
{
}
const QFont font;
const QFontMetrics metrics;
};
struct ChatFontData {
float scale;
bool italic;
QFont::Weight weight;
};
struct UiFontData {
float size;
const char *name;
bool italic;
QFont::Weight weight;
};
FontData &getOrCreateFontData(FontStyle type, float scale);
FontData createFontData(FontStyle type, float scale);
std::vector<std::unordered_map<float, FontData>> fontsByType_;
};
Fonts *getFonts();
} // namespace chatterino
+9 -7
View File
@@ -11,6 +11,8 @@
#include "common/Modes.hpp"
#include "util/CombinePath.hpp"
using namespace std::literals;
namespace chatterino {
Paths *Paths::instance = nullptr;
@@ -22,7 +24,7 @@ Paths::Paths()
this->initAppFilePathHash();
this->initCheckPortable();
this->initAppDataDirectory();
this->initRootDirectory();
this->initSubDirectories();
}
@@ -33,7 +35,7 @@ bool Paths::createFolder(const QString &folderPath)
bool Paths::isPortable()
{
return Modes::getInstance().isPortable;
return Modes::instance().isPortable;
}
QString Paths::cacheDirectory()
@@ -76,7 +78,7 @@ void Paths::initCheckPortable()
combinePath(QCoreApplication::applicationDirPath(), "portable"));
}
void Paths::initAppDataDirectory()
void Paths::initRootDirectory()
{
assert(this->portable_.is_initialized());
@@ -95,8 +97,8 @@ void Paths::initAppDataDirectory()
QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
if (path.isEmpty())
{
throw std::runtime_error(
"Error finding writable location for settings");
throw std::runtime_error("Could not create directory \""s +
path.toStdString() + "\"");
}
// create directory Chatterino2 instead of chatterino on windows because the
@@ -123,8 +125,8 @@ void Paths::initSubDirectories()
if (!QDir().mkpath(path))
{
throw std::runtime_error(
"Error creating appdata path %appdata%/chatterino/" + name);
throw std::runtime_error("Could not create directory \""s +
path.toStdString() + "\"");
}
return path;
+1 -1
View File
@@ -39,7 +39,7 @@ public:
private:
void initAppFilePathHash();
void initCheckPortable();
void initAppDataDirectory();
void initRootDirectory();
void initSubDirectories();
boost::optional<bool> portable_;
+23
View File
@@ -1 +1,24 @@
#include "singletons/Resources.hpp"
#include "debug/AssertInGuiThread.hpp"
namespace chatterino {
namespace {
static Resources2 *resources = nullptr;
}
Resources2 &getResources()
{
assert(resources);
return *resources;
}
void initResources()
{
assertInGuiThread();
resources = new Resources2;
}
} // namespace chatterino
+9
View File
@@ -1,3 +1,12 @@
#pragma once
#include "autogenerated/ResourcesAutogen.hpp"
namespace chatterino {
/// This class in thread safe but needs to be initialized from the gui thread
/// first.
Resources2 &getResources();
void initResources();
} // namespace chatterino
+5 -5
View File
@@ -9,12 +9,12 @@
namespace chatterino {
Settings *Settings::instance = nullptr;
Settings *Settings::instance_ = nullptr;
Settings::Settings(const QString &settingsDirectory)
: ABSettings(settingsDirectory)
{
instance = this;
instance_ = this;
#ifdef USEWINSDK
this->autorun = isRegisteredForStartup();
@@ -23,14 +23,14 @@ Settings::Settings(const QString &settingsDirectory)
#endif
}
Settings &Settings::getInstance()
Settings &Settings::instance()
{
return *instance;
return *instance_;
}
Settings *getSettings()
{
return &Settings::getInstance();
return &Settings::instance();
}
} // namespace chatterino
+11 -6
View File
@@ -1,25 +1,24 @@
#pragma once
#include "BaseSettings.hpp"
#include <pajlada/settings/setting.hpp>
#include <pajlada/settings/settinglistener.hpp>
#include "BaseSettings.hpp"
#include "common/Channel.hpp"
#include "controllers/highlights/HighlightPhrase.hpp"
#include "controllers/moderationactions/ModerationAction.hpp"
#include "singletons/Toasts.hpp"
#include <pajlada/settings/setting.hpp>
#include <pajlada/settings/settinglistener.hpp>
namespace chatterino {
class Settings : public ABSettings
{
static Settings *instance;
static Settings *instance_;
public:
Settings(const QString &settingsDirectory);
static Settings &getInstance();
static Settings &instance();
/// Appearance
BoolSetting showTimestamps = {"/appearance/messages/showTimestamps", true};
@@ -121,6 +120,8 @@ public:
QStringSetting emojiSet = {"/emotes/emojiSet", "EmojiOne 2"};
BoolSetting stackBits = {"/emotes/stackBits", false};
/// Links
BoolSetting linksDoubleClickOnly = {"/links/doubleClickToOpen", false};
BoolSetting linkInfoTooltip = {"/links/linkInfoTooltip", false};
@@ -204,6 +205,7 @@ public:
#ifdef Q_OS_LINUX
BoolSetting useKeyring = {"/misc/useKeyring", true};
#endif
BoolSetting enableExperimentalIrc = {"/misc/experimentalIrc", false};
IntSetting startUpNotification = {"/misc/startUpNotification", 0};
QStringSetting currentVersion = {"/misc/currentVersion", ""};
@@ -213,6 +215,9 @@ public:
BoolSetting openLinksIncognito = {"/misc/openLinksIncognito", 0};
QStringSetting cachePath = {"/cache/path", ""};
BoolSetting restartOnCrash = {"/misc/restartOnCrash", false};
BoolSetting attachExtensionToAnyProcess = {
"/misc/attachExtensionToAnyProcess", false};
/// Debug
BoolSetting showUnhandledIrcMessages = {"/debug/showUnhandledIrcMessages",
+1 -1
View File
@@ -85,7 +85,7 @@ void Theme::actuallyUpdate(double hue, double multiplier)
if (getSettings()->highlightColor != "")
{
this->messages.backgrounds.highlighted =
QColor(getSettings()->highlightColor);
QColor(getSettings()->highlightColor.getValue());
}
}
+1 -2
View File
@@ -134,8 +134,7 @@ public:
}
QDesktopServices::openUrl(QUrl(link));
break;
case ToastReaction::OpenInStreamlink:
{
case ToastReaction::OpenInStreamlink: {
openStreamlinkForChannel(channelName_);
break;
}
+2 -2
View File
@@ -5,7 +5,7 @@
#include "widgets/TooltipWidget.hpp"
namespace chatterino {
TooltipPreviewImage &TooltipPreviewImage::getInstance()
TooltipPreviewImage &TooltipPreviewImage::instance()
{
static TooltipPreviewImage *instance = new TooltipPreviewImage();
return *instance;
@@ -14,7 +14,7 @@ TooltipPreviewImage &TooltipPreviewImage::getInstance()
TooltipPreviewImage::TooltipPreviewImage()
{
connections_.push_back(getApp()->windows->gifRepaintRequested.connect([&] {
auto tooltipWidget = TooltipWidget::getInstance();
auto tooltipWidget = TooltipWidget::instance();
if (this->image_ && !tooltipWidget->isHidden())
{
auto pixmap = this->image_->pixmapOrLoad();
+1 -1
View File
@@ -6,7 +6,7 @@ namespace chatterino {
class TooltipPreviewImage
{
public:
static TooltipPreviewImage &getInstance();
static TooltipPreviewImage &instance();
void setImage(ImagePtr image);
TooltipPreviewImage(const TooltipPreviewImage &) = delete;
+42 -2
View File
@@ -13,6 +13,7 @@
#include <QDesktopServices>
#include <QMessageBox>
#include <QProcess>
#include <QRegularExpression>
namespace chatterino {
namespace {
@@ -20,6 +21,38 @@ namespace {
{
return getSettings()->betaUpdates ? "beta" : "stable";
}
/// Checks if the online version is newer or older than the current version.
bool isDowngradeOf(const QString &online, const QString &current)
{
static auto matchVersion =
QRegularExpression(R"((\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?)");
// Versions are just strings, they don't need to follow a specific
// format so we can only assume if one version is newer than another
// one.
// We match x.x.x.x with each version level being optional.
auto onlineMatch = matchVersion.match(online);
auto currentMatch = matchVersion.match(current);
for (int i = 1; i <= 4; i++)
{
if (onlineMatch.captured(i).toInt() <
currentMatch.captured(i).toInt())
{
return true;
}
if (onlineMatch.captured(i).toInt() >
currentMatch.captured(i).toInt())
{
break;
}
}
return false;
}
} // namespace
Updates::Updates()
@@ -29,7 +62,7 @@ Updates::Updates()
qDebug() << "init UpdateManager";
}
Updates &Updates::getInstance()
Updates &Updates::instance()
{
// fourtf: don't add this class to the application class
static Updates instance;
@@ -201,7 +234,7 @@ void Updates::checkForUpdates()
{
// Disable updates if on nightly and windows.
#ifdef Q_OS_WIN
if (Modes::getInstance().isNightly)
if (Modes::instance().isNightly)
{
return;
}
@@ -263,6 +296,8 @@ void Updates::checkForUpdates()
if (this->currentVersion_ != this->onlineVersion_)
{
this->setStatus_(UpdateAvailable);
this->isDowngrade_ =
isDowngradeOf(this->onlineVersion_, this->currentVersion_);
}
else
{
@@ -309,6 +344,11 @@ bool Updates::isError() const
}
}
bool Updates::isDowngrade() const
{
return this->isDowngrade_;
}
void Updates::setStatus_(Status status)
{
if (this->status_ != status)
+3 -1
View File
@@ -22,7 +22,7 @@ public:
};
// fourtf: don't add this class to the application class
static Updates &getInstance();
static Updates &instance();
void checkForUpdates();
const QString &getCurrentVersion() const;
@@ -32,6 +32,7 @@ public:
bool shouldShowUpdateButton() const;
bool isError() const;
bool isDowngrade() const;
pajlada::Signals::Signal<Status> statusUpdated;
@@ -39,6 +40,7 @@ private:
QString currentVersion_;
QString onlineVersion_;
Status status_ = None;
bool isDowngrade_{};
QString updateExe_;
QString updatePortable_;
+20 -28
View File
@@ -1,5 +1,16 @@
#include "singletons/WindowManager.hpp"
#include <QDebug>
#include <QDesktopWidget>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMessageBox>
#include <QSaveFile>
#include <QScreen>
#include <boost/optional.hpp>
#include <chrono>
#include "Application.hpp"
#include "debug/AssertInGuiThread.hpp"
#include "debug/Log.hpp"
@@ -21,18 +32,6 @@
#include "widgets/splits/Split.hpp"
#include "widgets/splits/SplitContainer.hpp"
#include <QDebug>
#include <QDesktopWidget>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMessageBox>
#include <QSaveFile>
#include <QScreen>
#include <boost/optional.hpp>
#include <chrono>
#define SETTINGS_FILENAME "/window-layout.json"
namespace chatterino {
@@ -556,8 +555,7 @@ void WindowManager::encodeNodeRecusively(SplitNode *node, QJsonObject &obj)
{
switch (node->getType())
{
case SplitNode::_Split:
{
case SplitNode::_Split: {
obj.insert("type", "split");
obj.insert("moderationMode", node->getSplit()->getModerationMode());
QJsonObject split;
@@ -568,8 +566,7 @@ void WindowManager::encodeNodeRecusively(SplitNode *node, QJsonObject &obj)
}
break;
case SplitNode::HorizontalContainer:
case SplitNode::VerticalContainer:
{
case SplitNode::VerticalContainer: {
obj.insert("type", node->getType() == SplitNode::HorizontalContainer
? "horizontal"
: "vertical");
@@ -593,29 +590,24 @@ void WindowManager::encodeChannel(IndirectChannel channel, QJsonObject &obj)
switch (channel.getType())
{
case Channel::Type::Twitch:
{
case Channel::Type::Twitch: {
obj.insert("type", "twitch");
obj.insert("name", channel.get()->getName());
}
break;
case Channel::Type::TwitchMentions:
{
case Channel::Type::TwitchMentions: {
obj.insert("type", "mentions");
}
break;
case Channel::Type::TwitchWatching:
{
case Channel::Type::TwitchWatching: {
obj.insert("type", "watching");
}
break;
case Channel::Type::TwitchWhispers:
{
case Channel::Type::TwitchWhispers: {
obj.insert("type", "whispers");
}
break;
case Channel::Type::Irc:
{
case Channel::Type::Irc: {
if (auto ircChannel =
dynamic_cast<IrcChannel *>(channel.get().get()))
{
@@ -657,8 +649,8 @@ IndirectChannel WindowManager::decodeChannel(const QJsonObject &obj)
}
else if (type == "irc")
{
return Irc::getInstance().getOrAddChannel(
obj.value("server").toInt(-1), obj.value("channel").toString());
return Irc::instance().getOrAddChannel(obj.value("server").toInt(-1),
obj.value("channel").toString());
}
return Channel::getEmpty();
+13
View File
@@ -0,0 +1,13 @@
#pragma once
namespace chatterino {
// http://en.cppreference.com/w/cpp/algorithm/clamp
template <class T>
constexpr const T &clamp(const T &v, const T &lo, const T &hi)
{
return assert(!(hi < lo)), (v < lo) ? lo : (hi < v) ? hi : v;
}
} // namespace chatterino
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <QDir>
#include <QString>
namespace chatterino {
// https://stackoverflow.com/a/13014491
inline QString combinePath(const QString &a, const QString &b)
{
return QDir::cleanPath(a + QDir::separator() + b);
}
} // namespace chatterino
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <QPointF>
#include <cmath>
namespace chatterino {
inline float distanceBetweenPoints(const QPointF &p1, const QPointF &p2)
{
QPointF tmp = p1 - p2;
float distance = 0.f;
distance += tmp.x() * tmp.x();
distance += tmp.y() * tmp.y();
return sqrt(distance);
}
} // namespace chatterino
+17
View File
@@ -0,0 +1,17 @@
#include "FunctionEventFilter.hpp"
namespace chatterino {
FunctionEventFilter::FunctionEventFilter(
QObject *parent, std::function<bool(QObject *, QEvent *)> function)
: QObject(parent)
, function_(std::move(function))
{
}
bool FunctionEventFilter::eventFilter(QObject *watched, QEvent *event)
{
return this->function_(watched, event);
}
} // namespace chatterino
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <QEvent>
#include <QObject>
#include <functional>
namespace chatterino {
class FunctionEventFilter : public QObject
{
Q_OBJECT
public:
FunctionEventFilter(QObject *parent,
std::function<bool(QObject *, QEvent *)> function);
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
private:
std::function<bool(QObject *, QEvent *)> function_;
};
} // namespace chatterino
+33
View File
@@ -0,0 +1,33 @@
#include "FuzzyConvert.hpp"
#include <QRegularExpression>
namespace chatterino {
int fuzzyToInt(const QString &str, int default_)
{
static auto intFinder = QRegularExpression("[0-9]+");
auto match = intFinder.match(str);
if (match.hasMatch())
{
return match.captured().toInt();
}
return default_;
}
float fuzzyToFloat(const QString &str, float default_)
{
static auto floatFinder = QRegularExpression("[0-9]+(\\.[0-9]+)?");
auto match = floatFinder.match(str);
if (match.hasMatch())
{
return match.captured().toFloat();
}
return default_;
}
} // namespace chatterino
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <QString>
namespace chatterino {
int fuzzyToInt(const QString &str, int default_);
float fuzzyToFloat(const QString &str, float default_);
} // namespace chatterino
+38
View File
@@ -0,0 +1,38 @@
#include "Helpers.hpp"
#include <QUuid>
namespace chatterino {
QString generateUuid()
{
auto uuid = QUuid::createUuid();
return uuid.toString();
}
QString formatRichLink(const QString &url, bool file)
{
return QString("<a href=\"") + (file ? "file:///" : "") + url + "\">" +
url + "</a>";
}
QString formatRichNamedLink(const QString &url, const QString &name, bool file)
{
return QString("<a href=\"") + (file ? "file:///" : "") + url + "\">" +
name + "</a>";
}
QString shortenString(const QString &str, unsigned maxWidth)
{
auto shortened = QString(str);
if (str.size() > int(maxWidth))
{
shortened.resize(int(maxWidth));
shortened += "...";
}
return shortened;
}
} // namespace chatterino
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <fmt/format.h>
#include <QString>
namespace chatterino {
template <typename... Args>
auto fS(Args &&... args)
{
return fmt::format(std::forward<Args>(args)...);
}
QString generateUuid();
QString formatRichLink(const QString &url, bool file = false);
QString formatRichNamedLink(const QString &url, const QString &name,
bool file = false);
QString shortenString(const QString &str, unsigned maxWidth = 50);
} // namespace chatterino
namespace fmt {
// format_arg for QString
inline void format_arg(BasicFormatter<char> &f, const char *&, const QString &v)
{
f.writer().write("{}", v.toStdString());
}
} // namespace fmt
+7 -9
View File
@@ -22,14 +22,12 @@ void initUpdateButton(Button &button,
dialog->buttonClicked.connect([&button](auto buttonType) {
switch (buttonType)
{
case UpdateDialog::Dismiss:
{
case UpdateDialog::Dismiss: {
button.hide();
}
break;
case UpdateDialog::Install:
{
Updates::getInstance().installUpdates();
case UpdateDialog::Install: {
Updates::instance().installUpdates();
}
break;
}
@@ -41,18 +39,18 @@ void initUpdateButton(Button &button,
// update image when state changes
auto updateChange = [&button](auto) {
button.setVisible(Updates::getInstance().shouldShowUpdateButton());
button.setVisible(Updates::instance().shouldShowUpdateButton());
auto imageUrl = Updates::getInstance().isError()
auto imageUrl = Updates::instance().isError()
? ":/buttons/updateError.png"
: ":/buttons/update.png";
button.setPixmap(QPixmap(imageUrl));
};
updateChange(Updates::getInstance().getStatus());
updateChange(Updates::instance().getStatus());
signalHolder.managedConnect(
Updates::getInstance().statusUpdated,
Updates::instance().statusUpdated,
[updateChange](auto status) { updateChange(status); });
}
+6 -12
View File
@@ -19,38 +19,32 @@ inline QString parseTagString(const QString &input)
switch (c.cell())
{
case 'n':
{
case 'n': {
output.replace(i, 2, '\n');
}
break;
case 'r':
{
case 'r': {
output.replace(i, 2, '\r');
}
break;
case 's':
{
case 's': {
output.replace(i, 2, ' ');
}
break;
case '\\':
{
case '\\': {
output.replace(i, 2, '\\');
}
break;
case ':':
{
case ':': {
output.replace(i, 2, ';');
}
break;
default:
{
default: {
output.remove(i, 1);
}
break;
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <QLayout>
#include <QWidget>
#include <boost/variant.hpp>
namespace chatterino {
using LayoutItem = boost::variant<QWidget *, QLayout *>;
template <typename T>
T *makeLayout(std::initializer_list<LayoutItem> items)
{
auto t = new T;
for (auto &item : items)
{
switch (item.which())
{
case 0:
t->addItem(new QWidgetItem(boost::get<QWidget *>(item)));
break;
case 1:
t->addItem(boost::get<QLayout *>(item));
break;
}
}
return t;
}
template <typename T, typename With>
T *makeWidget(With with)
{
auto t = new T;
with(t);
return t;
}
} // namespace chatterino
+59
View File
@@ -0,0 +1,59 @@
#pragma once
#include <QCoreApplication>
#include <QRunnable>
#include <QThreadPool>
#include <functional>
#define async_exec(a) \
QThreadPool::globalInstance()->start(new LambdaRunnable(a));
namespace chatterino {
class LambdaRunnable : public QRunnable
{
public:
LambdaRunnable(std::function<void()> action)
{
this->action_ = action;
}
void run()
{
this->action_();
}
private:
std::function<void()> action_;
};
// Taken from
// https://stackoverflow.com/questions/21646467/how-to-execute-a-functor-or-a-lambda-in-a-given-thread-in-qt-gcd-style
// Qt 5/4 - preferred, has least allocations
template <typename F>
static void postToThread(F &&fun, QObject *obj = qApp)
{
struct Event : public QEvent {
using Fun = typename std::decay<F>::type;
Fun fun;
Event(Fun &&fun)
: QEvent(QEvent::None)
, fun(std::move(fun))
{
}
Event(const Fun &fun)
: QEvent(QEvent::None)
, fun(fun)
{
}
~Event() override
{
fun();
}
};
QCoreApplication::postEvent(obj, new Event(std::forward<F>(fun)));
}
} // namespace chatterino
+4 -4
View File
@@ -61,10 +61,10 @@ private:
// new
if (other)
{
this->conn_ =
QObject::connect(other, &QObject::destroyed, qApp,
[this](QObject *) { this->set(nullptr); },
Qt::DirectConnection);
this->conn_ = QObject::connect(
other, &QObject::destroyed, qApp,
[this](QObject *) { this->set(nullptr); },
Qt::DirectConnection);
}
this->t_ = other;
+2
View File
@@ -5,6 +5,7 @@
namespace std {
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
template <>
struct hash<QString> {
std::size_t operator()(const QString &s) const
@@ -12,5 +13,6 @@ struct hash<QString> {
return qHash(s);
}
};
#endif
} // namespace std
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include <QString>
#include <pajlada/serialize.hpp>
namespace pajlada {
template <>
struct Serialize<QString> {
static rapidjson::Value get(const QString &value,
rapidjson::Document::AllocatorType &a)
{
return rapidjson::Value(value.toUtf8(), a);
}
};
template <>
struct Deserialize<QString> {
static QString get(const rapidjson::Value &value, bool *error = nullptr)
{
if (!value.IsString())
{
PAJLADA_REPORT_ERROR(error)
return QString{};
}
try
{
return QString::fromUtf8(value.GetString(),
value.GetStringLength());
}
catch (const std::exception &)
{
// int x = 5;
}
catch (...)
{
// int y = 5;
}
return QString{};
}
};
} // namespace pajlada
+174
View File
@@ -0,0 +1,174 @@
#pragma once
#include <QStringList>
namespace chatterino {
QStringList getValidLinks()
{
return {
R"(http://github.com/)",
R"(https://github.com/)",
R"(http://username@github.com/)",
R"(https://username@github.com/)",
R"(http://pajlada.github.io)",
R"(https://pajlada.github.io)",
R"(http://pajlada.github.io/)",
R"(https://pajlada.github.io/)",
R"(http://github.com/some/random/path)",
R"(https://github.com/some/random/path)",
R"(http://github.com?query=value)",
R"(https://github.com?query=value)",
R"(http://github.com?query=value&abc=123)",
R"(https://github.com?query=value&abc=123)",
R"(http://github.com/?query=value&abc=123&yhf=abc_def)",
R"(http://github.com/?query=value&abc=123&yhf)",
R"(http://github.com?query=value&abc=)",
R"(http://github.com?query=value&abc=)",
R"(http://github.com/#block)",
R"(https://github.com/#anchor)",
R"(http://github.com/path/?qs=true#block)",
R"(https://github.com/path/?qs=true#anchor)",
R"(github.com/)",
R"(username@github.com/)",
R"(pajlada.github.io)",
R"(pajlada.github.io/)",
R"(github.com/some/random/path)",
R"(github.com?query=value)",
R"(github.com?query=value&abc=123)",
R"(github.com/?query=value&abc=123&yhf=abc_def)",
R"(github.com/?query=value&abc=123&yhf)",
R"(github.com?query=value&abc=)",
R"(github.com?query=value&abc=)",
R"(github.com/#block)",
R"(github.com/path/?qs=true#block)",
R"(HTTP://GITHUB.COM/)",
R"(HTTPS://GITHUB.COM/)",
R"(HTTP://USERNAME@GITHUB.COM/)",
R"(HTTPS://USERNAME@GITHUB.COM/)",
R"(HTTP://PAJLADA.GITHUB.IO)",
R"(HTTPS://PAJLADA.GITHUB.IO)",
R"(HTTP://PAJLADA.GITHUB.IO/)",
R"(HTTPS://PAJLADA.GITHUB.IO/)",
R"(HTTP://GITHUB.COM/SOME/RANDOM/PATH)",
R"(HTTPS://GITHUB.COM/SOME/RANDOM/PATH)",
R"(HTTP://GITHUB.COM?QUERY=VALUE)",
R"(HTTPS://GITHUB.COM?QUERY=VALUE)",
R"(HTTP://GITHUB.COM?QUERY=VALUE&ABC=123)",
R"(HTTPS://GITHUB.COM?QUERY=VALUE&ABC=123)",
R"(HTTP://GITHUB.COM/?QUERY=VALUE&ABC=123&YHF=ABC_DEF)",
R"(HTTP://GITHUB.COM/?QUERY=VALUE&ABC=123&YHF)",
R"(HTTP://GITHUB.COM?QUERY=VALUE&ABC=)",
R"(HTTP://GITHUB.COM?QUERY=VALUE&ABC=)",
R"(HTTP://GITHUB.COM/#BLOCK)",
R"(HTTPS://GITHUB.COM/#ANCHOR)",
R"(HTTP://GITHUB.COM/PATH/?QS=TRUE#BLOCK)",
R"(HTTPS://GITHUB.COM/PATH/?QS=TRUE#ANCHOR)",
R"(GITHUB.COM/)",
R"(USERNAME@GITHUB.COM/)",
R"(PAJLADA.GITHUB.IO)",
R"(PAJLADA.GITHUB.IO/)",
R"(GITHUB.COM/SOME/RANDOM/PATH)",
R"(GITHUB.COM?QUERY=VALUE)",
R"(GITHUB.COM?QUERY=VALUE&ABC=123)",
R"(GITHUB.COM/?QUERY=VALUE&ABC=123&YHF=ABC_DEF)",
R"(GITHUB.COM/?QUERY=VALUE&ABC=123&YHF)",
R"(GITHUB.COM?QUERY=VALUE&ABC=)",
R"(GITHUB.COM?QUERY=VALUE&ABC=)",
R"(GITHUB.COM/#BLOCK)",
R"(GITHUB.COM/PATH/?QS=TRUE#BLOCK)",
R"(http://foo.com/blah_blah)",
R"(http://foo.com/blah_blah/)",
R"(http://foo.com/blah_blah_(wikipedia))",
R"(http://foo.com/blah_blah_(wikipedia)_(again))",
R"(http://www.example.com/wpstyle/?p=364)",
R"(https://www.example.com/foo/?bar=baz&inga=42&quux)",
R"(http://✪df.ws/123)",
R"(http://userid@example.com)",
R"(http://userid@example.com/)",
R"(http://userid@example.com:8080)",
R"(http://userid@example.com:8080/)",
R"(http://142.42.1.1/)",
R"(http://142.42.1.1:8080/)",
R"(http://➡.ws/䨹)",
R"(http://⌘.ws)",
R"(http://⌘.ws/)",
R"(http://foo.com/blah_(wikipedia)#cite-1)",
R"(http://foo.com/blah_(wikipedia)_blah#cite-1)",
R"(http://foo.com/unicode_(✪)_in_parens)",
R"(http://foo.com/(something)?after=parens)",
R"(http://☺.damowmow.com/)",
R"(http://code.google.com/events/#&product=browser)",
R"(http://j.mp)",
R"(ftp://foo.bar/baz)",
R"(http://foo.bar/?q=Test%20URL-encoded%20stuff)",
R"(http://مثال.إختبار)",
R"(http://例子.测试)",
R"(http://उदाहरण.परीक्षा)",
R"(http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com)",
R"(http://1337.net)",
R"(http://a.b-c.de)",
R"(http://223.255.255.254)",
};
}
QStringList getValidButIgnoredLinks()
{
return {
R"(http://username:password@github.com/)",
R"(https://username:password@github.com/)",
R"(http://userid:password@example.com)",
R"(http://userid:password@example.com/)",
R"(http://userid:password@example.com:8080)",
R"(http://userid:password@example.com:8080/)",
};
}
QStringList getInvalidLinks()
{
return {
R"(1.40)",
R"(test..)",
R"(test.)",
R"(http://)",
R"(http://.)",
R"(http://..)",
R"(http://../)",
R"(http://?)",
R"(http://??)",
R"(http://??/)",
R"(http://#)",
R"(http://##)",
R"(http://##/)",
R"(http://foo.bar?q=Spaces should be encoded)",
R"(//)",
R"(//a)",
R"(///a)",
R"(///)",
R"(http:///a)",
R"(foo.com)",
R"(rdar://1234)",
R"(h://test)",
R"(http:// shouldfail.com)",
R"(:// should fail)",
R"(http://foo.bar/foo(bar)baz quux)",
R"(ftps://foo.bar/)",
R"(http://-error-.invalid/)",
R"(http://a.b--c.de/)",
R"(http://-a.b.co)",
R"(http://a.b-.co)",
R"(http://0.0.0.0)",
R"(http://10.1.1.0)",
R"(http://10.1.1.255)",
R"(http://224.1.1.1)",
R"(http://1.1.1.1.1)",
R"(http://123.123.123)",
R"(http://3628126748)",
R"(http://.www.foo.bar/)",
R"(http://www.foo.bar./)",
R"(http://.www.foo.bar./)",
R"(http://10.1.1.1)",
};
}
} // namespace chatterino
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <QShortcut>
#include <QWidget>
namespace chatterino {
template <typename WidgetType, typename Func>
inline void createShortcut(WidgetType *w, const char *key, Func func)
{
auto s = new QShortcut(QKeySequence(key), w);
s->setContext(Qt::WidgetWithChildrenShortcut);
QObject::connect(s, &QShortcut::activated, w, func);
}
template <typename WidgetType, typename Func>
inline void createWindowShortcut(WidgetType *w, const char *key, Func func)
{
auto s = new QShortcut(QKeySequence(key), w);
s->setContext(Qt::WindowShortcut);
QObject::connect(s, &QShortcut::activated, w, func);
}
} // namespace chatterino
+1 -1
View File
@@ -199,7 +199,7 @@ void openStreamlinkForChannel(const QString &channel)
{
QString channelURL = "twitch.tv/" + channel;
QString preferredQuality = getSettings()->preferredQuality;
QString preferredQuality = getSettings()->preferredQuality.getValue();
preferredQuality = preferredQuality.toLower();
if (preferredQuality == "choose")
+86
View File
@@ -0,0 +1,86 @@
#include "WindowsHelper.hpp"
#include <QSettings>
#ifdef USEWINSDK
namespace chatterino {
typedef enum MONITOR_DPI_TYPE {
MDT_EFFECTIVE_DPI = 0,
MDT_ANGULAR_DPI = 1,
MDT_RAW_DPI = 2,
MDT_DEFAULT = MDT_EFFECTIVE_DPI
} MONITOR_DPI_TYPE;
typedef HRESULT(CALLBACK *GetDpiForMonitor_)(HMONITOR, MONITOR_DPI_TYPE, UINT *,
UINT *);
boost::optional<UINT> getWindowDpi(HWND hwnd)
{
static HINSTANCE shcore = LoadLibrary(L"Shcore.dll");
if (shcore != nullptr)
{
if (auto getDpiForMonitor =
GetDpiForMonitor_(GetProcAddress(shcore, "GetDpiForMonitor")))
{
HMONITOR monitor =
MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
UINT xScale, yScale;
getDpiForMonitor(monitor, MDT_DEFAULT, &xScale, &yScale);
return xScale;
}
}
return boost::none;
}
typedef HRESULT(CALLBACK *OleFlushClipboard_)();
void flushClipboard()
{
static HINSTANCE ole32 = LoadLibrary(L"Ole32.dll");
if (ole32 != nullptr)
{
if (auto oleFlushClipboard =
OleFlushClipboard_(GetProcAddress(ole32, "OleFlushClipboard")))
{
oleFlushClipboard();
}
}
}
constexpr const char *runKey =
"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
bool isRegisteredForStartup()
{
QSettings settings(runKey, QSettings::NativeFormat);
return !settings.value("Chatterino").toString().isEmpty();
}
void setRegisteredForStartup(bool isRegistered)
{
QSettings settings(runKey, QSettings::NativeFormat);
if (isRegistered)
{
auto exePath = QFileInfo(QCoreApplication::applicationFilePath())
.absoluteFilePath()
.replace('/', '\\');
settings.setValue("Chatterino", "\"" + exePath + "\" --autorun");
}
else
{
settings.remove("Chatterino");
}
}
} // namespace chatterino
#endif
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#ifdef USEWINSDK
# include <Windows.h>
# include <boost/optional.hpp>
namespace chatterino {
boost::optional<UINT> getWindowDpi(HWND hwnd);
void flushClipboard();
bool isRegisteredForStartup();
void setRegisteredForStartup(bool isRegistered);
} // namespace chatterino
#endif
+13 -6
View File
@@ -1,6 +1,7 @@
#include "AttachedWindow.hpp"
#include "Application.hpp"
#include "singletons/Settings.hpp"
#include "util/DebugCount.hpp"
#include "widgets/splits/Split.hpp"
@@ -180,13 +181,19 @@ void AttachedWindow::attachToHwnd(void *_attachedPtr)
QString qfilename =
QString::fromWCharArray(filename.get(), int(filenameLength));
if (!qfilename.endsWith("chrome.exe") &&
!qfilename.endsWith("firefox.exe"))
if (!getSettings()->attachExtensionToAnyProcess)
{
qDebug() << "NM Illegal caller" << qfilename;
this->timer_.stop();
this->deleteLater();
return;
// We don't attach to non-browser processes by default.
if (!qfilename.endsWith("chrome.exe") &&
!qfilename.endsWith("firefox.exe") &&
!qfilename.endsWith("vivaldi.exe") &&
!qfilename.endsWith("opera.exe"))
{
qDebug() << "NM Illegal caller" << qfilename;
this->timer_.stop();
this->deleteLater();
return;
}
}
this->validProcessName_ = true;
}

Some files were not shown because too many files have changed in this diff Show More