Merge branch 'master' into git_is_pepega
This commit is contained in:
+3
-3
@@ -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
@@ -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
@@ -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{};
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -50,4 +50,4 @@ Resources2::Resources2()
|
||||
this->twitch.vip = QPixmap(":/twitch/vip.png");
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <QPixmap>
|
||||
|
||||
#include "common/Singleton.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
@@ -64,4 +65,4 @@ public:
|
||||
} twitch;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -14,7 +14,8 @@ public:
|
||||
QString getCaptured() const;
|
||||
|
||||
private:
|
||||
QRegularExpressionMatch match_;
|
||||
bool hasMatch_{false};
|
||||
QString match_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -27,7 +27,7 @@ Modes::Modes()
|
||||
}
|
||||
}
|
||||
|
||||
const Modes &Modes::getInstance()
|
||||
const Modes &Modes::instance()
|
||||
{
|
||||
static Modes instance;
|
||||
return instance;
|
||||
|
||||
@@ -7,7 +7,7 @@ class Modes
|
||||
public:
|
||||
Modes();
|
||||
|
||||
static const Modes &getInstance();
|
||||
static const Modes &instance();
|
||||
|
||||
bool isNightly{};
|
||||
bool isPortable{};
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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_;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()) /
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -36,7 +36,7 @@ class Irc
|
||||
public:
|
||||
Irc();
|
||||
|
||||
static Irc &getInstance();
|
||||
static Irc &instance();
|
||||
|
||||
static inline void *const noEraseCredentialCaller =
|
||||
reinterpret_cast<void *>(1);
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -18,6 +18,7 @@ using EmotePtr = std::shared_ptr<const Emote>;
|
||||
struct CheerEmote {
|
||||
QColor color;
|
||||
int minBits;
|
||||
QRegularExpression regex;
|
||||
|
||||
EmotePtr animatedEmote;
|
||||
EmotePtr staticEmote;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -39,7 +39,7 @@ public:
|
||||
private:
|
||||
void initAppFilePathHash();
|
||||
void initCheckPortable();
|
||||
void initAppDataDirectory();
|
||||
void initRootDirectory();
|
||||
void initSubDirectories();
|
||||
|
||||
boost::optional<bool> portable_;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -134,8 +134,7 @@ public:
|
||||
}
|
||||
QDesktopServices::openUrl(QUrl(link));
|
||||
break;
|
||||
case ToastReaction::OpenInStreamlink:
|
||||
{
|
||||
case ToastReaction::OpenInStreamlink: {
|
||||
openStreamlinkForChannel(channelName_);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace chatterino {
|
||||
class TooltipPreviewImage
|
||||
{
|
||||
public:
|
||||
static TooltipPreviewImage &getInstance();
|
||||
static TooltipPreviewImage &instance();
|
||||
void setImage(ImagePtr image);
|
||||
|
||||
TooltipPreviewImage(const TooltipPreviewImage &) = delete;
|
||||
|
||||
@@ -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 ¤t)
|
||||
{
|
||||
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)
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user