mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Merge pull request #678 from apa420/apa-notification-on-live
Notification when a stream goes live
This commit is contained in:
commit
45988fc510
4
.gitmodules
vendored
4
.gitmodules
vendored
|
@ -20,3 +20,7 @@
|
||||||
[submodule "lib/qBreakpad"]
|
[submodule "lib/qBreakpad"]
|
||||||
path = lib/qBreakpad
|
path = lib/qBreakpad
|
||||||
url = https://github.com/jiakuan/qBreakpad.git
|
url = https://github.com/jiakuan/qBreakpad.git
|
||||||
|
[submodule "lib/WinToast"]
|
||||||
|
path = lib/WinToast
|
||||||
|
url = https://github.com/mohabouje/WinToast
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,7 @@ include(dependencies/libcommuni.pri)
|
||||||
include(dependencies/websocketpp.pri)
|
include(dependencies/websocketpp.pri)
|
||||||
include(dependencies/openssl.pri)
|
include(dependencies/openssl.pri)
|
||||||
include(dependencies/boost.pri)
|
include(dependencies/boost.pri)
|
||||||
|
include(dependencies/wintoast.pri)
|
||||||
|
|
||||||
# Optional feature: QtWebEngine
|
# Optional feature: QtWebEngine
|
||||||
#exists ($(QTDIR)/include/QtWebEngine/QtWebEngine) {
|
#exists ($(QTDIR)/include/QtWebEngine/QtWebEngine) {
|
||||||
|
@ -124,6 +125,7 @@ SOURCES += \
|
||||||
src/controllers/highlights/UserHighlightModel.cpp \
|
src/controllers/highlights/UserHighlightModel.cpp \
|
||||||
src/controllers/ignores/IgnoreController.cpp \
|
src/controllers/ignores/IgnoreController.cpp \
|
||||||
src/controllers/ignores/IgnoreModel.cpp \
|
src/controllers/ignores/IgnoreModel.cpp \
|
||||||
|
src/controllers/notifications/NotificationController.cpp \
|
||||||
src/controllers/taggedusers/TaggedUser.cpp \
|
src/controllers/taggedusers/TaggedUser.cpp \
|
||||||
src/controllers/taggedusers/TaggedUsersController.cpp \
|
src/controllers/taggedusers/TaggedUsersController.cpp \
|
||||||
src/controllers/taggedusers/TaggedUsersModel.cpp \
|
src/controllers/taggedusers/TaggedUsersModel.cpp \
|
||||||
|
@ -205,6 +207,7 @@ SOURCES += \
|
||||||
src/widgets/settingspages/KeyboardSettingsPage.cpp \
|
src/widgets/settingspages/KeyboardSettingsPage.cpp \
|
||||||
src/widgets/settingspages/LogsPage.cpp \
|
src/widgets/settingspages/LogsPage.cpp \
|
||||||
src/widgets/settingspages/ModerationPage.cpp \
|
src/widgets/settingspages/ModerationPage.cpp \
|
||||||
|
src/widgets/settingspages/NotificationPage.cpp \
|
||||||
src/widgets/settingspages/SettingsPage.cpp \
|
src/widgets/settingspages/SettingsPage.cpp \
|
||||||
src/widgets/settingspages/SpecialChannelsPage.cpp \
|
src/widgets/settingspages/SpecialChannelsPage.cpp \
|
||||||
src/widgets/splits/Split.cpp \
|
src/widgets/splits/Split.cpp \
|
||||||
|
@ -250,6 +253,9 @@ SOURCES += \
|
||||||
src/BrowserExtension.cpp \
|
src/BrowserExtension.cpp \
|
||||||
src/util/FormatTime.cpp \
|
src/util/FormatTime.cpp \
|
||||||
src/util/FunctionEventFilter.cpp \
|
src/util/FunctionEventFilter.cpp \
|
||||||
|
src/controllers/notifications/NotificationModel.cpp \
|
||||||
|
src/singletons/Toasts.cpp \
|
||||||
|
src/common/DownloadManager.cpp \
|
||||||
src/widgets/helper/EffectLabel.cpp \
|
src/widgets/helper/EffectLabel.cpp \
|
||||||
src/widgets/helper/Button.cpp \
|
src/widgets/helper/Button.cpp \
|
||||||
src/messages/MessageContainer.cpp \
|
src/messages/MessageContainer.cpp \
|
||||||
|
@ -292,6 +298,7 @@ HEADERS += \
|
||||||
src/controllers/ignores/IgnoreController.hpp \
|
src/controllers/ignores/IgnoreController.hpp \
|
||||||
src/controllers/ignores/IgnoreModel.hpp \
|
src/controllers/ignores/IgnoreModel.hpp \
|
||||||
src/controllers/ignores/IgnorePhrase.hpp \
|
src/controllers/ignores/IgnorePhrase.hpp \
|
||||||
|
src/controllers/notifications/NotificationController.hpp \
|
||||||
src/controllers/taggedusers/TaggedUser.hpp \
|
src/controllers/taggedusers/TaggedUser.hpp \
|
||||||
src/controllers/taggedusers/TaggedUsersController.hpp \
|
src/controllers/taggedusers/TaggedUsersController.hpp \
|
||||||
src/controllers/taggedusers/TaggedUsersModel.hpp \
|
src/controllers/taggedusers/TaggedUsersModel.hpp \
|
||||||
|
@ -394,6 +401,7 @@ HEADERS += \
|
||||||
src/widgets/settingspages/KeyboardSettingsPage.hpp \
|
src/widgets/settingspages/KeyboardSettingsPage.hpp \
|
||||||
src/widgets/settingspages/LogsPage.hpp \
|
src/widgets/settingspages/LogsPage.hpp \
|
||||||
src/widgets/settingspages/ModerationPage.hpp \
|
src/widgets/settingspages/ModerationPage.hpp \
|
||||||
|
src/widgets/settingspages/NotificationPage.hpp \
|
||||||
src/widgets/settingspages/SettingsPage.hpp \
|
src/widgets/settingspages/SettingsPage.hpp \
|
||||||
src/widgets/settingspages/SpecialChannelsPage.hpp \
|
src/widgets/settingspages/SpecialChannelsPage.hpp \
|
||||||
src/widgets/splits/Split.hpp \
|
src/widgets/splits/Split.hpp \
|
||||||
|
@ -446,6 +454,9 @@ HEADERS += \
|
||||||
src/BrowserExtension.hpp \
|
src/BrowserExtension.hpp \
|
||||||
src/util/FormatTime.hpp \
|
src/util/FormatTime.hpp \
|
||||||
src/util/FunctionEventFilter.hpp \
|
src/util/FunctionEventFilter.hpp \
|
||||||
|
src/controllers/notifications/NotificationModel.hpp \
|
||||||
|
src/singletons/Toasts.hpp \
|
||||||
|
src/common/DownloadManager.hpp \
|
||||||
src/widgets/helper/EffectLabel.hpp \
|
src/widgets/helper/EffectLabel.hpp \
|
||||||
src/util/LayoutHelper.hpp \
|
src/util/LayoutHelper.hpp \
|
||||||
src/widgets/helper/Button.hpp \
|
src/widgets/helper/Button.hpp \
|
||||||
|
|
5
dependencies/wintoast.pri
vendored
Normal file
5
dependencies/wintoast.pri
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
win32 {
|
||||||
|
INCLUDEPATH += $$PWD/../lib/wintoast/src/
|
||||||
|
SOURCES += \
|
||||||
|
$$PWD/../lib/WinToast/src/wintoastlib.cpp
|
||||||
|
}
|
|
@ -5,6 +5,7 @@
|
||||||
#include "controllers/highlights/HighlightController.hpp"
|
#include "controllers/highlights/HighlightController.hpp"
|
||||||
#include "controllers/ignores/IgnoreController.hpp"
|
#include "controllers/ignores/IgnoreController.hpp"
|
||||||
#include "controllers/moderationactions/ModerationActions.hpp"
|
#include "controllers/moderationactions/ModerationActions.hpp"
|
||||||
|
#include "controllers/notifications/NotificationController.hpp"
|
||||||
#include "controllers/taggedusers/TaggedUsersController.hpp"
|
#include "controllers/taggedusers/TaggedUsersController.hpp"
|
||||||
#include "debug/Log.hpp"
|
#include "debug/Log.hpp"
|
||||||
#include "messages/MessageBuilder.hpp"
|
#include "messages/MessageBuilder.hpp"
|
||||||
|
@ -21,6 +22,7 @@
|
||||||
#include "singletons/Resources.hpp"
|
#include "singletons/Resources.hpp"
|
||||||
#include "singletons/Settings.hpp"
|
#include "singletons/Settings.hpp"
|
||||||
#include "singletons/Theme.hpp"
|
#include "singletons/Theme.hpp"
|
||||||
|
#include "singletons/Toasts.hpp"
|
||||||
#include "singletons/WindowManager.hpp"
|
#include "singletons/WindowManager.hpp"
|
||||||
#include "util/IsBigEndian.hpp"
|
#include "util/IsBigEndian.hpp"
|
||||||
#include "util/PostToThread.hpp"
|
#include "util/PostToThread.hpp"
|
||||||
|
@ -45,16 +47,19 @@ Application::Application(Settings &_settings, Paths &_paths)
|
||||||
, fonts(&this->emplace<Fonts>())
|
, fonts(&this->emplace<Fonts>())
|
||||||
, emotes(&this->emplace<Emotes>())
|
, emotes(&this->emplace<Emotes>())
|
||||||
, windows(&this->emplace<WindowManager>())
|
, windows(&this->emplace<WindowManager>())
|
||||||
|
, toasts(&this->emplace<Toasts>())
|
||||||
|
|
||||||
, accounts(&this->emplace<AccountController>())
|
, accounts(&this->emplace<AccountController>())
|
||||||
, commands(&this->emplace<CommandController>())
|
, commands(&this->emplace<CommandController>())
|
||||||
, highlights(&this->emplace<HighlightController>())
|
, highlights(&this->emplace<HighlightController>())
|
||||||
|
, notifications(&this->emplace<NotificationController>())
|
||||||
, ignores(&this->emplace<IgnoreController>())
|
, ignores(&this->emplace<IgnoreController>())
|
||||||
, taggedUsers(&this->emplace<TaggedUsersController>())
|
, taggedUsers(&this->emplace<TaggedUsersController>())
|
||||||
, moderationActions(&this->emplace<ModerationActions>())
|
, moderationActions(&this->emplace<ModerationActions>())
|
||||||
, twitch2(&this->emplace<TwitchServer>())
|
, twitch2(&this->emplace<TwitchServer>())
|
||||||
, chatterinoBadges(&this->emplace<ChatterinoBadges>())
|
, chatterinoBadges(&this->emplace<ChatterinoBadges>())
|
||||||
, logging(&this->emplace<Logging>())
|
, logging(&this->emplace<Logging>())
|
||||||
|
|
||||||
{
|
{
|
||||||
this->instance = this;
|
this->instance = this;
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ class IgnoreController;
|
||||||
class TaggedUsersController;
|
class TaggedUsersController;
|
||||||
class AccountController;
|
class AccountController;
|
||||||
class ModerationActions;
|
class ModerationActions;
|
||||||
|
class NotificationController;
|
||||||
|
|
||||||
class Theme;
|
class Theme;
|
||||||
class WindowManager;
|
class WindowManager;
|
||||||
|
@ -26,6 +27,7 @@ class Emotes;
|
||||||
class Settings;
|
class Settings;
|
||||||
class Fonts;
|
class Fonts;
|
||||||
class Resources2;
|
class Resources2;
|
||||||
|
class Toasts;
|
||||||
class ChatterinoBadges;
|
class ChatterinoBadges;
|
||||||
|
|
||||||
class Application
|
class Application
|
||||||
|
@ -53,10 +55,12 @@ public:
|
||||||
Fonts *const fonts{};
|
Fonts *const fonts{};
|
||||||
Emotes *const emotes{};
|
Emotes *const emotes{};
|
||||||
WindowManager *const windows{};
|
WindowManager *const windows{};
|
||||||
|
Toasts *const toasts{};
|
||||||
|
|
||||||
AccountController *const accounts{};
|
AccountController *const accounts{};
|
||||||
CommandController *const commands{};
|
CommandController *const commands{};
|
||||||
HighlightController *const highlights{};
|
HighlightController *const highlights{};
|
||||||
|
NotificationController *const notifications{};
|
||||||
IgnoreController *const ignores{};
|
IgnoreController *const ignores{};
|
||||||
TaggedUsersController *const taggedUsers{};
|
TaggedUsersController *const taggedUsers{};
|
||||||
ModerationActions *const moderationActions{};
|
ModerationActions *const moderationActions{};
|
||||||
|
|
|
@ -17,6 +17,7 @@ enum class HighlightState {
|
||||||
None,
|
None,
|
||||||
Highlighted,
|
Highlighted,
|
||||||
NewMessage,
|
NewMessage,
|
||||||
|
Notification,
|
||||||
};
|
};
|
||||||
|
|
||||||
inline QString qS(const std::string &string)
|
inline QString qS(const std::string &string)
|
||||||
|
|
79
src/common/DownloadManager.cpp
Normal file
79
src/common/DownloadManager.cpp
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
#include "DownloadManager.hpp"
|
||||||
|
|
||||||
|
#include "singletons/Paths.hpp"
|
||||||
|
|
||||||
|
#include <QDesktopServices>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
DownloadManager::DownloadManager(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
manager = new QNetworkAccessManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadManager::~DownloadManager()
|
||||||
|
{
|
||||||
|
manager->deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DownloadManager::setFile(QString fileURL, const QString &channelName)
|
||||||
|
{
|
||||||
|
QString filePath = fileURL;
|
||||||
|
QString saveFilePath;
|
||||||
|
QStringList filePathList = filePath.split('/');
|
||||||
|
saveFilePath =
|
||||||
|
getPaths()->twitchProfileAvatars + "/twitch/" + channelName + ".png";
|
||||||
|
QNetworkRequest request;
|
||||||
|
request.setUrl(QUrl(fileURL));
|
||||||
|
reply = manager->get(request);
|
||||||
|
|
||||||
|
file = new QFile;
|
||||||
|
file->setFileName(saveFilePath);
|
||||||
|
file->open(QIODevice::WriteOnly);
|
||||||
|
|
||||||
|
connect(reply, SIGNAL(downloadProgress(qint64, qint64)), this,
|
||||||
|
SLOT(onDownloadProgress(qint64, qint64)));
|
||||||
|
connect(manager, SIGNAL(finished(QNetworkReply *)), this,
|
||||||
|
SLOT(onFinished(QNetworkReply *)));
|
||||||
|
connect(reply, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
|
||||||
|
connect(reply, SIGNAL(finished()), this, SLOT(onReplyFinished()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DownloadManager::onDownloadProgress(qint64 bytesRead, qint64 bytesTotal)
|
||||||
|
{
|
||||||
|
qDebug(QString::number(bytesRead).toLatin1() + " - " +
|
||||||
|
QString::number(bytesTotal).toLatin1());
|
||||||
|
}
|
||||||
|
|
||||||
|
void DownloadManager::onFinished(QNetworkReply *reply)
|
||||||
|
{
|
||||||
|
switch (reply->error()) {
|
||||||
|
case QNetworkReply::NoError: {
|
||||||
|
qDebug("file is downloaded successfully.");
|
||||||
|
} break;
|
||||||
|
default: {
|
||||||
|
qDebug(reply->errorString().toLatin1());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file->isOpen()) {
|
||||||
|
file->close();
|
||||||
|
file->deleteLater();
|
||||||
|
}
|
||||||
|
emit downloadComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DownloadManager::onReadyRead()
|
||||||
|
{
|
||||||
|
file->write(reply->readAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
void DownloadManager::onReplyFinished()
|
||||||
|
{
|
||||||
|
if (file->isOpen()) {
|
||||||
|
file->close();
|
||||||
|
file->deleteLater();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace chatterino
|
36
src/common/DownloadManager.hpp
Normal file
36
src/common/DownloadManager.hpp
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Application.hpp"
|
||||||
|
|
||||||
|
#include <QFile>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QNetworkRequest>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class DownloadManager : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit DownloadManager(QObject *parent = nullptr);
|
||||||
|
virtual ~DownloadManager();
|
||||||
|
void setFile(QString fileURL, const QString &channelName);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QNetworkAccessManager *manager;
|
||||||
|
QNetworkReply *reply;
|
||||||
|
QFile *file;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onDownloadProgress(qint64, qint64);
|
||||||
|
void onFinished(QNetworkReply *);
|
||||||
|
void onReadyRead();
|
||||||
|
void onReplyFinished();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void downloadComplete();
|
||||||
|
};
|
||||||
|
} // namespace chatterino
|
203
src/controllers/notifications/NotificationController.cpp
Normal file
203
src/controllers/notifications/NotificationController.cpp
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
#include "controllers/notifications/NotificationController.hpp"
|
||||||
|
|
||||||
|
#include "Application.hpp"
|
||||||
|
#include "common/NetworkRequest.hpp"
|
||||||
|
#include "common/Outcome.hpp"
|
||||||
|
#include "controllers/notifications/NotificationModel.hpp"
|
||||||
|
#include "debug/Log.hpp"
|
||||||
|
#include "providers/twitch/TwitchApi.hpp"
|
||||||
|
#include "providers/twitch/TwitchServer.hpp"
|
||||||
|
#include "singletons/Toasts.hpp"
|
||||||
|
#include "singletons/WindowManager.hpp"
|
||||||
|
#include "widgets/Window.hpp"
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
# include <wintoastlib.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <QDesktopServices>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QMediaPlayer>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
void NotificationController::initialize(Settings &settings, Paths &paths)
|
||||||
|
{
|
||||||
|
this->initialized_ = true;
|
||||||
|
for (const QString &channelName : this->twitchSetting_.getValue()) {
|
||||||
|
this->channelMap[Platform::Twitch].appendItem(channelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->channelMap[Platform::Twitch].delayedItemsChanged.connect([this] { //
|
||||||
|
this->twitchSetting_.setValue(
|
||||||
|
this->channelMap[Platform::Twitch].getVector());
|
||||||
|
});
|
||||||
|
/*
|
||||||
|
for (const QString &channelName : this->mixerSetting_.getValue()) {
|
||||||
|
this->channelMap[Platform::Mixer].appendItem(channelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->channelMap[Platform::Mixer].delayedItemsChanged.connect([this] { //
|
||||||
|
this->mixerSetting_.setValue(
|
||||||
|
this->channelMap[Platform::Mixer].getVector());
|
||||||
|
});*/
|
||||||
|
|
||||||
|
liveStatusTimer_ = new QTimer();
|
||||||
|
|
||||||
|
this->fetchFakeChannels();
|
||||||
|
|
||||||
|
QObject::connect(this->liveStatusTimer_, &QTimer::timeout,
|
||||||
|
[=] { this->fetchFakeChannels(); });
|
||||||
|
this->liveStatusTimer_->start(60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotificationController::updateChannelNotification(
|
||||||
|
const QString &channelName, Platform p)
|
||||||
|
{
|
||||||
|
if (isChannelNotified(channelName, p)) {
|
||||||
|
removeChannelNotification(channelName, p);
|
||||||
|
} else {
|
||||||
|
addChannelNotification(channelName, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NotificationController::isChannelNotified(const QString &channelName,
|
||||||
|
Platform p)
|
||||||
|
{
|
||||||
|
for (const auto &channel : this->channelMap[p].getVector()) {
|
||||||
|
if (channelName.toLower() == channel.toLower()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotificationController::addChannelNotification(const QString &channelName,
|
||||||
|
Platform p)
|
||||||
|
{
|
||||||
|
channelMap[p].appendItem(channelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotificationController::removeChannelNotification(
|
||||||
|
const QString &channelName, Platform p)
|
||||||
|
{
|
||||||
|
for (std::vector<int>::size_type i = 0;
|
||||||
|
i != channelMap[p].getVector().size(); i++) {
|
||||||
|
if (channelMap[p].getVector()[i].toLower() == channelName.toLower()) {
|
||||||
|
channelMap[p].removeItem(i);
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void NotificationController::playSound()
|
||||||
|
{
|
||||||
|
static auto player = new QMediaPlayer;
|
||||||
|
static QUrl currentPlayerUrl;
|
||||||
|
|
||||||
|
QUrl highlightSoundUrl;
|
||||||
|
if (getSettings()->notificationCustomSound) {
|
||||||
|
highlightSoundUrl = QUrl::fromLocalFile(
|
||||||
|
getSettings()->notificationPathSound.getValue());
|
||||||
|
} else {
|
||||||
|
highlightSoundUrl = QUrl("qrc:/sounds/ping2.wav");
|
||||||
|
}
|
||||||
|
if (currentPlayerUrl != highlightSoundUrl) {
|
||||||
|
player->setMedia(highlightSoundUrl);
|
||||||
|
|
||||||
|
currentPlayerUrl = highlightSoundUrl;
|
||||||
|
}
|
||||||
|
player->play();
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationModel *NotificationController::createModel(QObject *parent,
|
||||||
|
Platform p)
|
||||||
|
{
|
||||||
|
NotificationModel *model = new NotificationModel(parent);
|
||||||
|
model->init(&this->channelMap[p]);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotificationController::fetchFakeChannels()
|
||||||
|
{
|
||||||
|
for (std::vector<int>::size_type i = 0;
|
||||||
|
i != channelMap[Platform::Twitch].getVector().size(); i++) {
|
||||||
|
auto chan = getApp()->twitch.server->getChannelOrEmpty(
|
||||||
|
channelMap[Platform::Twitch].getVector()[i]);
|
||||||
|
if (chan->isEmpty()) {
|
||||||
|
getFakeTwitchChannelLiveStatus(
|
||||||
|
channelMap[Platform::Twitch].getVector()[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotificationController::getFakeTwitchChannelLiveStatus(
|
||||||
|
const QString &channelName)
|
||||||
|
{
|
||||||
|
TwitchApi::findUserId(channelName, [channelName, this](QString roomID) {
|
||||||
|
if (roomID.isEmpty()) {
|
||||||
|
log("[TwitchChannel:{}] Refreshing live status (Missing ID)",
|
||||||
|
channelName);
|
||||||
|
removeFakeChannel(channelName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log("[TwitchChannel:{}] Refreshing live status", channelName);
|
||||||
|
|
||||||
|
QString url("https://api.twitch.tv/kraken/streams/" + roomID);
|
||||||
|
auto request = NetworkRequest::twitchRequest(url);
|
||||||
|
request.setCaller(QThread::currentThread());
|
||||||
|
|
||||||
|
request.onSuccess([this, channelName](auto result) -> Outcome {
|
||||||
|
rapidjson::Document document = result.parseRapidJson();
|
||||||
|
if (!document.IsObject()) {
|
||||||
|
log("[TwitchChannel:refreshLiveStatus]root is not an object");
|
||||||
|
return Failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!document.HasMember("stream")) {
|
||||||
|
log("[TwitchChannel:refreshLiveStatus] Missing stream in root");
|
||||||
|
return Failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &stream = document["stream"];
|
||||||
|
|
||||||
|
if (!stream.IsObject()) {
|
||||||
|
// Stream is offline (stream is most likely null)
|
||||||
|
// removeFakeChannel(channelName);
|
||||||
|
return Failure;
|
||||||
|
}
|
||||||
|
// Stream is live
|
||||||
|
auto i = std::find(fakeTwitchChannels.begin(),
|
||||||
|
fakeTwitchChannels.end(), channelName);
|
||||||
|
|
||||||
|
if (!(i != fakeTwitchChannels.end())) {
|
||||||
|
fakeTwitchChannels.push_back(channelName);
|
||||||
|
if (Toasts::isEnabled()) {
|
||||||
|
getApp()->toasts->sendChannelNotification(channelName,
|
||||||
|
Platform::Twitch);
|
||||||
|
}
|
||||||
|
if (getSettings()->notificationPlaySound) {
|
||||||
|
getApp()->notifications->playSound();
|
||||||
|
}
|
||||||
|
if (getSettings()->notificationFlashTaskbar) {
|
||||||
|
QApplication::alert(
|
||||||
|
getApp()->windows->getMainWindow().window(), 2500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Success;
|
||||||
|
});
|
||||||
|
|
||||||
|
request.execute();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotificationController::removeFakeChannel(const QString channelName)
|
||||||
|
{
|
||||||
|
auto i = std::find(fakeTwitchChannels.begin(), fakeTwitchChannels.end(),
|
||||||
|
channelName);
|
||||||
|
if (i != fakeTwitchChannels.end()) {
|
||||||
|
fakeTwitchChannels.erase(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
57
src/controllers/notifications/NotificationController.hpp
Normal file
57
src/controllers/notifications/NotificationController.hpp
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/SignalVector.hpp"
|
||||||
|
#include "common/Singleton.hpp"
|
||||||
|
#include "singletons/Settings.hpp"
|
||||||
|
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class Settings;
|
||||||
|
class Paths;
|
||||||
|
|
||||||
|
class NotificationModel;
|
||||||
|
|
||||||
|
enum class Platform : uint8_t {
|
||||||
|
Twitch, // 0
|
||||||
|
// Mixer, // 1
|
||||||
|
};
|
||||||
|
|
||||||
|
class NotificationController final : public Singleton, private QObject
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual void initialize(Settings &settings, Paths &paths) override;
|
||||||
|
|
||||||
|
bool isChannelNotified(const QString &channelName, Platform p);
|
||||||
|
void updateChannelNotification(const QString &channelName, Platform p);
|
||||||
|
void addChannelNotification(const QString &channelName, Platform p);
|
||||||
|
void removeChannelNotification(const QString &channelName, Platform p);
|
||||||
|
|
||||||
|
void playSound();
|
||||||
|
|
||||||
|
UnsortedSignalVector<QString> getVector(Platform p);
|
||||||
|
|
||||||
|
std::map<Platform, UnsortedSignalVector<QString>> channelMap;
|
||||||
|
|
||||||
|
NotificationModel *createModel(QObject *parent, Platform p);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool initialized_ = false;
|
||||||
|
|
||||||
|
void fetchFakeChannels();
|
||||||
|
void removeFakeChannel(const QString channelName);
|
||||||
|
void getFakeTwitchChannelLiveStatus(const QString &channelName);
|
||||||
|
|
||||||
|
std::vector<QString> fakeTwitchChannels;
|
||||||
|
QTimer *liveStatusTimer_;
|
||||||
|
|
||||||
|
ChatterinoSetting<std::vector<QString>> twitchSetting_ = {
|
||||||
|
"/notifications/twitch"};
|
||||||
|
/*
|
||||||
|
ChatterinoSetting<std::vector<QString>> mixerSetting_ = {
|
||||||
|
"/notifications/mixer"};
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
28
src/controllers/notifications/NotificationModel.cpp
Normal file
28
src/controllers/notifications/NotificationModel.cpp
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
#include "NotificationModel.hpp"
|
||||||
|
|
||||||
|
#include "Application.hpp"
|
||||||
|
#include "singletons/Settings.hpp"
|
||||||
|
#include "util/StandardItemHelper.hpp"
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
NotificationModel::NotificationModel(QObject *parent)
|
||||||
|
: SignalVectorModel<QString>(1, parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// turn a vector item into a model row
|
||||||
|
QString NotificationModel::getItemFromRow(std::vector<QStandardItem *> &row,
|
||||||
|
const QString &original)
|
||||||
|
{
|
||||||
|
return QString(row[0]->data(Qt::DisplayRole).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// turn a model
|
||||||
|
void NotificationModel::getRowFromItem(const QString &item,
|
||||||
|
std::vector<QStandardItem *> &row)
|
||||||
|
{
|
||||||
|
setStringItem(row[0], item);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
28
src/controllers/notifications/NotificationModel.hpp
Normal file
28
src/controllers/notifications/NotificationModel.hpp
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include "common/SignalVectorModel.hpp"
|
||||||
|
#include "controllers/notifications/NotificationController.hpp"
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class NotificationController;
|
||||||
|
|
||||||
|
class NotificationModel : public SignalVectorModel<QString>
|
||||||
|
{
|
||||||
|
explicit NotificationModel(QObject *parent);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// turn a vector item into a model row
|
||||||
|
virtual QString getItemFromRow(std::vector<QStandardItem *> &row,
|
||||||
|
const QString &original) override;
|
||||||
|
|
||||||
|
// turns a row in the model into a vector item
|
||||||
|
virtual void getRowFromItem(const QString &item,
|
||||||
|
std::vector<QStandardItem *> &row) override;
|
||||||
|
|
||||||
|
friend class NotificationController;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
|
@ -26,6 +26,7 @@ enum class MessageFlag : uint16_t {
|
||||||
Untimeout = (1 << 9),
|
Untimeout = (1 << 9),
|
||||||
PubSub = (1 << 10),
|
PubSub = (1 << 10),
|
||||||
Subscription = (1 << 11),
|
Subscription = (1 << 11),
|
||||||
|
Notification = (1 << 12),
|
||||||
};
|
};
|
||||||
using MessageFlags = FlagsEnum<MessageFlag>;
|
using MessageFlags = FlagsEnum<MessageFlag>;
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
#include "providers/twitch/TwitchCommon.hpp"
|
#include "providers/twitch/TwitchCommon.hpp"
|
||||||
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QThread>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
#include "common/Common.hpp"
|
#include "common/Common.hpp"
|
||||||
#include "common/NetworkRequest.hpp"
|
#include "common/NetworkRequest.hpp"
|
||||||
#include "controllers/accounts/AccountController.hpp"
|
#include "controllers/accounts/AccountController.hpp"
|
||||||
|
#include "controllers/notifications/NotificationController.hpp"
|
||||||
#include "debug/Log.hpp"
|
#include "debug/Log.hpp"
|
||||||
#include "messages/Message.hpp"
|
#include "messages/Message.hpp"
|
||||||
#include "providers/bttv/BttvEmotes.hpp"
|
#include "providers/bttv/BttvEmotes.hpp"
|
||||||
|
@ -14,7 +15,10 @@
|
||||||
#include "providers/twitch/TwitchParseCheerEmotes.hpp"
|
#include "providers/twitch/TwitchParseCheerEmotes.hpp"
|
||||||
#include "singletons/Emotes.hpp"
|
#include "singletons/Emotes.hpp"
|
||||||
#include "singletons/Settings.hpp"
|
#include "singletons/Settings.hpp"
|
||||||
|
#include "singletons/Toasts.hpp"
|
||||||
|
#include "singletons/WindowManager.hpp"
|
||||||
#include "util/PostToThread.hpp"
|
#include "util/PostToThread.hpp"
|
||||||
|
#include "widgets/Window.hpp"
|
||||||
|
|
||||||
#include <IrcConnection>
|
#include <IrcConnection>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
|
@ -86,6 +90,12 @@ TwitchChannel::TwitchChannel(const QString &name,
|
||||||
{
|
{
|
||||||
log("[TwitchChannel:{}] Opened", name);
|
log("[TwitchChannel:{}] Opened", name);
|
||||||
|
|
||||||
|
this->tabHighlightRequested.connect([](HighlightState state) {});
|
||||||
|
this->liveStatusChanged.connect([this]() {
|
||||||
|
if (this->isLive() == 1) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this->managedConnect(getApp()->accounts->twitch.currentUserChanged,
|
this->managedConnect(getApp()->accounts->twitch.currentUserChanged,
|
||||||
[=] { this->setMod(false); });
|
[=] { this->setMod(false); });
|
||||||
|
|
||||||
|
@ -385,6 +395,30 @@ void TwitchChannel::setLive(bool newLiveStatus)
|
||||||
auto guard = this->streamStatus_.access();
|
auto guard = this->streamStatus_.access();
|
||||||
if (guard->live != newLiveStatus) {
|
if (guard->live != newLiveStatus) {
|
||||||
gotNewLiveStatus = true;
|
gotNewLiveStatus = true;
|
||||||
|
if (newLiveStatus) {
|
||||||
|
if (getApp()->notifications->isChannelNotified(
|
||||||
|
this->getName(), Platform::Twitch)) {
|
||||||
|
if (Toasts::isEnabled()) {
|
||||||
|
getApp()->toasts->sendChannelNotification(
|
||||||
|
this->getName(), Platform::Twitch);
|
||||||
|
}
|
||||||
|
if (getSettings()->notificationPlaySound) {
|
||||||
|
getApp()->notifications->playSound();
|
||||||
|
}
|
||||||
|
if (getSettings()->notificationFlashTaskbar) {
|
||||||
|
QApplication::alert(
|
||||||
|
getApp()->windows->getMainWindow().window(), 2500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto live = makeSystemMessage(this->getName() + " is live");
|
||||||
|
this->addMessage(live);
|
||||||
|
this->tabHighlightRequested.invoke(
|
||||||
|
HighlightState::Notification);
|
||||||
|
} else {
|
||||||
|
auto offline =
|
||||||
|
makeSystemMessage(this->getName() + " is offline");
|
||||||
|
this->addMessage(offline);
|
||||||
|
}
|
||||||
guard->live = newLiveStatus;
|
guard->live = newLiveStatus;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -466,7 +500,6 @@ Outcome TwitchChannel::parseLiveStatus(const rapidjson::Document &document)
|
||||||
|
|
||||||
{
|
{
|
||||||
auto status = this->streamStatus_.access();
|
auto status = this->streamStatus_.access();
|
||||||
status->live = true;
|
|
||||||
status->viewerCount = stream["viewers"].GetUint();
|
status->viewerCount = stream["viewers"].GetUint();
|
||||||
status->game = stream["game"].GetString();
|
status->game = stream["game"].GetString();
|
||||||
status->title = streamChannel["status"].GetString();
|
status->title = streamChannel["status"].GetString();
|
||||||
|
@ -495,7 +528,7 @@ Outcome TwitchChannel::parseLiveStatus(const rapidjson::Document &document)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setLive(true);
|
||||||
// Signal all listeners that the stream status has been updated
|
// Signal all listeners that the stream status has been updated
|
||||||
this->liveStatusChanged.invoke();
|
this->liveStatusChanged.invoke();
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,8 @@
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
|
enum class HighlightState;
|
||||||
|
|
||||||
struct Emote;
|
struct Emote;
|
||||||
using EmotePtr = std::shared_ptr<const Emote>;
|
using EmotePtr = std::shared_ptr<const Emote>;
|
||||||
class EmoteMap;
|
class EmoteMap;
|
||||||
|
@ -90,6 +92,7 @@ public:
|
||||||
pajlada::Signals::NoArgSignal userStateChanged;
|
pajlada::Signals::NoArgSignal userStateChanged;
|
||||||
pajlada::Signals::NoArgSignal liveStatusChanged;
|
pajlada::Signals::NoArgSignal liveStatusChanged;
|
||||||
pajlada::Signals::NoArgSignal roomModesChanged;
|
pajlada::Signals::NoArgSignal roomModesChanged;
|
||||||
|
pajlada::Signals::Signal<HighlightState> tabHighlightRequested;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void addRecentChatter(const MessagePtr &message) override;
|
void addRecentChatter(const MessagePtr &message) override;
|
||||||
|
|
|
@ -130,6 +130,8 @@ void Paths::initSubDirectories()
|
||||||
this->cacheDirectory_ = makePath("Cache");
|
this->cacheDirectory_ = makePath("Cache");
|
||||||
this->messageLogDirectory = makePath("Logs");
|
this->messageLogDirectory = makePath("Logs");
|
||||||
this->miscDirectory = makePath("Misc");
|
this->miscDirectory = makePath("Misc");
|
||||||
|
this->twitchProfileAvatars = makePath("ProfileAvatars");
|
||||||
|
QDir().mkdir(this->twitchProfileAvatars + "/twitch");
|
||||||
}
|
}
|
||||||
|
|
||||||
Paths *getPaths()
|
Paths *getPaths()
|
||||||
|
|
|
@ -28,6 +28,9 @@ public:
|
||||||
// Hash of QCoreApplication::applicationFilePath()
|
// Hash of QCoreApplication::applicationFilePath()
|
||||||
QString applicationFilePathHash;
|
QString applicationFilePathHash;
|
||||||
|
|
||||||
|
// Profile avatars for twitch <appDataDirectory>/cache/twitch
|
||||||
|
QString twitchProfileAvatars;
|
||||||
|
|
||||||
bool createFolder(const QString &folderPath);
|
bool createFolder(const QString &folderPath);
|
||||||
bool isPortable();
|
bool isPortable();
|
||||||
|
|
||||||
|
|
|
@ -152,6 +152,18 @@ public:
|
||||||
|
|
||||||
BoolSetting inlineWhispers = {"/whispers/enableInlineWhispers", true};
|
BoolSetting inlineWhispers = {"/whispers/enableInlineWhispers", true};
|
||||||
|
|
||||||
|
/// Notifications
|
||||||
|
BoolSetting notificationFlashTaskbar = {"/notifications/enableFlashTaskbar",
|
||||||
|
false};
|
||||||
|
BoolSetting notificationPlaySound = {"/notifications/enablePlaySound",
|
||||||
|
false};
|
||||||
|
BoolSetting notificationCustomSound = {"/notifications/customPlaySound",
|
||||||
|
false};
|
||||||
|
QStringSetting notificationPathSound = {"/notifications/highlightSoundPath",
|
||||||
|
"qrc:/sounds/ping3.wav"};
|
||||||
|
|
||||||
|
BoolSetting notificationToast = {"/notifications/enableToast", false};
|
||||||
|
|
||||||
/// External tools
|
/// External tools
|
||||||
// Streamlink
|
// Streamlink
|
||||||
BoolSetting streamlinkUseCustomPath = {"/external/streamlink/useCustomPath",
|
BoolSetting streamlinkUseCustomPath = {"/external/streamlink/useCustomPath",
|
||||||
|
|
|
@ -105,6 +105,10 @@ void Theme::actuallyUpdate(double hue, double multiplier)
|
||||||
QColor("#000"),
|
QColor("#000"),
|
||||||
{QColor("#b4d7ff"), QColor("#b4d7ff"), QColor("#b4d7ff")},
|
{QColor("#b4d7ff"), QColor("#b4d7ff"), QColor("#b4d7ff")},
|
||||||
{QColor("#00aeef"), QColor("#00aeef"), QColor("#00aeef")}};
|
{QColor("#00aeef"), QColor("#00aeef"), QColor("#00aeef")}};
|
||||||
|
this->tabs.notified = {
|
||||||
|
fg,
|
||||||
|
{QColor("#252525"), QColor("#252525"), QColor("#252525")},
|
||||||
|
{QColor("#F824A8"), QColor("#F824A8"), QColor("#F824A8")}};
|
||||||
} else {
|
} else {
|
||||||
this->tabs.regular = {
|
this->tabs.regular = {
|
||||||
QColor("#aaa"),
|
QColor("#aaa"),
|
||||||
|
@ -123,6 +127,10 @@ void Theme::actuallyUpdate(double hue, double multiplier)
|
||||||
QColor("#fff"),
|
QColor("#fff"),
|
||||||
{QColor("#555555"), QColor("#555555"), QColor("#555555")},
|
{QColor("#555555"), QColor("#555555"), QColor("#555555")},
|
||||||
{QColor("#00aeef"), QColor("#00aeef"), QColor("#00aeef")}};
|
{QColor("#00aeef"), QColor("#00aeef"), QColor("#00aeef")}};
|
||||||
|
this->tabs.notified = {
|
||||||
|
fg,
|
||||||
|
{QColor("#252525"), QColor("#252525"), QColor("#252525")},
|
||||||
|
{QColor("#F824A8"), QColor("#F824A8"), QColor("#F824A8")}};
|
||||||
}
|
}
|
||||||
|
|
||||||
this->splits.input.focusedLine = highlighted;
|
this->splits.input.focusedLine = highlighted;
|
||||||
|
@ -150,7 +158,7 @@ void Theme::actuallyUpdate(double hue, double multiplier)
|
||||||
// QColor("#777"), QColor("#666")}};
|
// QColor("#777"), QColor("#666")}};
|
||||||
|
|
||||||
this->tabs.bottomLine = this->tabs.selected.backgrounds.regular.color();
|
this->tabs.bottomLine = this->tabs.selected.backgrounds.regular.color();
|
||||||
}
|
} // namespace chatterino
|
||||||
|
|
||||||
// Split
|
// Split
|
||||||
bool flat = isLight_;
|
bool flat = isLight_;
|
||||||
|
@ -232,7 +240,7 @@ void Theme::actuallyUpdate(double hue, double multiplier)
|
||||||
isLightTheme() ? QColor(0, 0, 0, 64) : QColor(255, 255, 255, 64);
|
isLightTheme() ? QColor(0, 0, 0, 64) : QColor(255, 255, 255, 64);
|
||||||
|
|
||||||
this->updated.invoke();
|
this->updated.invoke();
|
||||||
}
|
} // namespace chatterino
|
||||||
|
|
||||||
QColor Theme::blendColors(const QColor &color1, const QColor &color2,
|
QColor Theme::blendColors(const QColor &color1, const QColor &color2,
|
||||||
qreal ratio)
|
qreal ratio)
|
||||||
|
|
|
@ -49,6 +49,7 @@ public:
|
||||||
TabColors newMessage;
|
TabColors newMessage;
|
||||||
TabColors highlighted;
|
TabColors highlighted;
|
||||||
TabColors selected;
|
TabColors selected;
|
||||||
|
TabColors notified;
|
||||||
QColor border;
|
QColor border;
|
||||||
QColor bottomLine;
|
QColor bottomLine;
|
||||||
} tabs;
|
} tabs;
|
||||||
|
|
192
src/singletons/Toasts.cpp
Normal file
192
src/singletons/Toasts.cpp
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
#include "Toasts.hpp"
|
||||||
|
|
||||||
|
#include "Application.hpp"
|
||||||
|
#include "common/DownloadManager.hpp"
|
||||||
|
#include "common/NetworkRequest.hpp"
|
||||||
|
#include "controllers/notifications/NotificationController.hpp"
|
||||||
|
#include "providers/twitch/TwitchChannel.hpp"
|
||||||
|
#include "providers/twitch/TwitchCommon.hpp"
|
||||||
|
#include "providers/twitch/TwitchServer.hpp"
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
|
||||||
|
# include <wintoastlib.h>
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <QDesktopServices>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
bool Toasts::isEnabled()
|
||||||
|
{
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
return WinToastLib::WinToast::isCompatible() &&
|
||||||
|
getSettings()->notificationToast;
|
||||||
|
#endif
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Toasts::sendChannelNotification(const QString &channelName, Platform p)
|
||||||
|
{
|
||||||
|
// Fetch user profile avatar
|
||||||
|
if (p == Platform::Twitch) {
|
||||||
|
QFileInfo check_file(getPaths()->twitchProfileAvatars + "/twitch/" +
|
||||||
|
channelName + ".png");
|
||||||
|
if (check_file.exists() && check_file.isFile()) {
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
this->sendWindowsNotification(channelName, p);
|
||||||
|
#endif
|
||||||
|
// OSX
|
||||||
|
|
||||||
|
// LINUX
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this->fetchChannelAvatar(
|
||||||
|
channelName, [this, channelName, p](QString avatarLink) {
|
||||||
|
DownloadManager *manager = new DownloadManager();
|
||||||
|
manager->setFile(avatarLink, channelName);
|
||||||
|
manager->connect(
|
||||||
|
manager, &DownloadManager::downloadComplete,
|
||||||
|
[this, channelName, p]() {
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
this->sendWindowsNotification(channelName, p);
|
||||||
|
#endif
|
||||||
|
// OSX
|
||||||
|
|
||||||
|
// LINUX
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
|
||||||
|
class CustomHandler : public WinToastLib::IWinToastHandler
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
QString channelName_;
|
||||||
|
Platform platform_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
CustomHandler(QString channelName, Platform p)
|
||||||
|
: channelName_(channelName)
|
||||||
|
, platform_(p)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
void toastActivated() const
|
||||||
|
{
|
||||||
|
QString link;
|
||||||
|
if (platform_ == Platform::Twitch) {
|
||||||
|
link = "http://www.twitch.tv/" + channelName_;
|
||||||
|
}
|
||||||
|
QDesktopServices::openUrl(QUrl(link));
|
||||||
|
}
|
||||||
|
|
||||||
|
void toastActivated(int actionIndex) const
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void toastFailed() const
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void toastDismissed(WinToastDismissalReason state) const
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void Toasts::sendWindowsNotification(const QString &channelName, Platform p)
|
||||||
|
{
|
||||||
|
WinToastLib::WinToastTemplate templ = WinToastLib::WinToastTemplate(
|
||||||
|
WinToastLib::WinToastTemplate::ImageAndText03);
|
||||||
|
QString str = channelName + " is live!";
|
||||||
|
std::string utf8_text = str.toUtf8().constData();
|
||||||
|
std::wstring widestr = std::wstring(utf8_text.begin(), utf8_text.end());
|
||||||
|
|
||||||
|
templ.setTextField(widestr, WinToastLib::WinToastTemplate::FirstLine);
|
||||||
|
templ.setTextField(L"Click here to open in browser",
|
||||||
|
WinToastLib::WinToastTemplate::SecondLine);
|
||||||
|
QString Path;
|
||||||
|
if (p == Platform::Twitch) {
|
||||||
|
Path = getPaths()->twitchProfileAvatars + "/twitch/" + channelName +
|
||||||
|
".png";
|
||||||
|
}
|
||||||
|
std::string temp_Utf8 = Path.toUtf8().constData();
|
||||||
|
std::wstring imagePath = std::wstring(temp_Utf8.begin(), temp_Utf8.end());
|
||||||
|
templ.setImagePath(imagePath);
|
||||||
|
if (getSettings()->notificationPlaySound) {
|
||||||
|
templ.setAudioOption(
|
||||||
|
WinToastLib::WinToastTemplate::AudioOption::Silent);
|
||||||
|
}
|
||||||
|
WinToastLib::WinToast::instance()->setAppName(L"Chatterino2");
|
||||||
|
int mbstowcs(wchar_t * aumi_version, const char *CHATTERINO_VERSION,
|
||||||
|
size_t size);
|
||||||
|
std::string(CHATTERINO_VERSION);
|
||||||
|
std::wstring aumi_version =
|
||||||
|
std::wstring(CHATTERINO_VERSION.begin(), CHATTERINO_VERSION.end());
|
||||||
|
WinToastLib::WinToast::instance()->setAppUserModelId(
|
||||||
|
WinToastLib::WinToast::configureAUMI(L"", L"Chatterino 2", L"",
|
||||||
|
aumi_version));
|
||||||
|
WinToastLib::WinToast::instance()->initialize();
|
||||||
|
WinToastLib::WinToast::instance()->showToast(
|
||||||
|
templ, new CustomHandler(channelName, p));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void Toasts::fetchChannelAvatar(const QString channelName,
|
||||||
|
std::function<void(QString)> successCallback)
|
||||||
|
{
|
||||||
|
QString requestUrl("https://api.twitch.tv/kraken/users?login=" +
|
||||||
|
channelName);
|
||||||
|
|
||||||
|
NetworkRequest request(requestUrl);
|
||||||
|
request.setCaller(QThread::currentThread());
|
||||||
|
request.makeAuthorizedV5(getDefaultClientID());
|
||||||
|
request.setTimeout(30000);
|
||||||
|
request.onSuccess([successCallback](auto result) mutable -> Outcome {
|
||||||
|
auto root = result.parseJson();
|
||||||
|
if (!root.value("users").isArray()) {
|
||||||
|
// log("API Error while getting user id, users is not an array");
|
||||||
|
successCallback("");
|
||||||
|
return Failure;
|
||||||
|
}
|
||||||
|
auto users = root.value("users").toArray();
|
||||||
|
if (users.size() != 1) {
|
||||||
|
// log("API Error while getting user id, users array size is not
|
||||||
|
// 1");
|
||||||
|
successCallback("");
|
||||||
|
return Failure;
|
||||||
|
}
|
||||||
|
if (!users[0].isObject()) {
|
||||||
|
// log("API Error while getting user id, first user is not an
|
||||||
|
// object");
|
||||||
|
successCallback("");
|
||||||
|
return Failure;
|
||||||
|
}
|
||||||
|
auto firstUser = users[0].toObject();
|
||||||
|
auto avatar = firstUser.value("logo");
|
||||||
|
if (!avatar.isString()) {
|
||||||
|
// log("API Error: while getting user avatar, first user object "
|
||||||
|
// "`avatar` key "
|
||||||
|
// "is not a "
|
||||||
|
// "string");
|
||||||
|
successCallback("");
|
||||||
|
return Failure;
|
||||||
|
}
|
||||||
|
successCallback(avatar.toString());
|
||||||
|
return Success;
|
||||||
|
});
|
||||||
|
|
||||||
|
request.execute();
|
||||||
|
}
|
||||||
|
} // namespace chatterino
|
25
src/singletons/Toasts.hpp
Normal file
25
src/singletons/Toasts.hpp
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Application.hpp"
|
||||||
|
#include "common/Singleton.hpp"
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
enum class Platform : uint8_t;
|
||||||
|
|
||||||
|
class Toasts final : public Singleton
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void sendChannelNotification(const QString &channelName, Platform p);
|
||||||
|
|
||||||
|
static bool isEnabled();
|
||||||
|
|
||||||
|
private:
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
void sendWindowsNotification(const QString &channelName, Platform p);
|
||||||
|
#endif
|
||||||
|
static void fetchChannelAvatar(
|
||||||
|
const QString channelName,
|
||||||
|
std::function<void(QString)> successCallback);
|
||||||
|
};
|
||||||
|
} // namespace chatterino
|
|
@ -17,6 +17,7 @@
|
||||||
#include "widgets/settingspages/LogsPage.hpp"
|
#include "widgets/settingspages/LogsPage.hpp"
|
||||||
#include "widgets/settingspages/LookPage.hpp"
|
#include "widgets/settingspages/LookPage.hpp"
|
||||||
#include "widgets/settingspages/ModerationPage.hpp"
|
#include "widgets/settingspages/ModerationPage.hpp"
|
||||||
|
#include "widgets/settingspages/NotificationPage.hpp"
|
||||||
#include "widgets/settingspages/SpecialChannelsPage.hpp"
|
#include "widgets/settingspages/SpecialChannelsPage.hpp"
|
||||||
|
|
||||||
#include <QDialogButtonBox>
|
#include <QDialogButtonBox>
|
||||||
|
@ -102,6 +103,7 @@ void SettingsDialog::addTabs()
|
||||||
this->addTab(new KeyboardSettingsPage);
|
this->addTab(new KeyboardSettingsPage);
|
||||||
// this->addTab(new LogsPage);
|
// this->addTab(new LogsPage);
|
||||||
this->addTab(new ModerationPage);
|
this->addTab(new ModerationPage);
|
||||||
|
this->addTab(new NotificationPage);
|
||||||
// this->addTab(new SpecialChannelsPage);
|
// this->addTab(new SpecialChannelsPage);
|
||||||
this->addTab(new BrowserExtensionPage);
|
this->addTab(new BrowserExtensionPage);
|
||||||
this->addTab(new ExternalToolsPage);
|
this->addTab(new ExternalToolsPage);
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
#include "messages/Message.hpp"
|
#include "messages/Message.hpp"
|
||||||
#include "messages/MessageElement.hpp"
|
#include "messages/MessageElement.hpp"
|
||||||
#include "messages/layouts/MessageLayout.hpp"
|
#include "messages/layouts/MessageLayout.hpp"
|
||||||
|
#include "providers/twitch/TwitchChannel.hpp"
|
||||||
#include "messages/layouts/MessageLayoutElement.hpp"
|
#include "messages/layouts/MessageLayoutElement.hpp"
|
||||||
#include "providers/twitch/TwitchServer.hpp"
|
#include "providers/twitch/TwitchServer.hpp"
|
||||||
#include "singletons/Settings.hpp"
|
#include "singletons/Settings.hpp"
|
||||||
|
@ -535,6 +536,14 @@ void ChannelView::setChannel(ChannelPtr newChannel)
|
||||||
|
|
||||||
this->layoutMessages();
|
this->layoutMessages();
|
||||||
this->queueUpdate();
|
this->queueUpdate();
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
TwitchChannel *tc = dynamic_cast<TwitchChannel *>(newChannel.get());
|
||||||
|
if (tc != nullptr) {
|
||||||
|
tc->tabHighlightRequested.connect([this](HighlightState state) {
|
||||||
|
this->tabHighlightRequested.invoke(HighlightState::Notification);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChannelView::detachChannel()
|
void ChannelView::detachChannel()
|
||||||
|
|
|
@ -171,8 +171,8 @@ void NotebookTab::setHighlightState(HighlightState newHighlightStyle)
|
||||||
if (this->isSelected()) {
|
if (this->isSelected()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this->highlightState_ != HighlightState::Highlighted &&
|
||||||
if (this->highlightState_ != HighlightState::Highlighted) {
|
this->highlightState_ != HighlightState::Notification) {
|
||||||
this->highlightState_ = newHighlightStyle;
|
this->highlightState_ = newHighlightStyle;
|
||||||
|
|
||||||
this->update();
|
this->update();
|
||||||
|
@ -239,6 +239,8 @@ void NotebookTab::paintEvent(QPaintEvent *)
|
||||||
colors = this->theme->tabs.selected;
|
colors = this->theme->tabs.selected;
|
||||||
} else if (this->highlightState_ == HighlightState::Highlighted) {
|
} else if (this->highlightState_ == HighlightState::Highlighted) {
|
||||||
colors = this->theme->tabs.highlighted;
|
colors = this->theme->tabs.highlighted;
|
||||||
|
} else if (this->highlightState_ == HighlightState::Notification) {
|
||||||
|
colors = this->theme->tabs.notified;
|
||||||
} else if (this->highlightState_ == HighlightState::NewMessage) {
|
} else if (this->highlightState_ == HighlightState::NewMessage) {
|
||||||
colors = this->theme->tabs.newMessage;
|
colors = this->theme->tabs.newMessage;
|
||||||
} else {
|
} else {
|
||||||
|
|
123
src/widgets/settingspages/NotificationPage.cpp
Normal file
123
src/widgets/settingspages/NotificationPage.cpp
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
#include "NotificationPage.hpp"
|
||||||
|
|
||||||
|
#include "Application.hpp"
|
||||||
|
#include "controllers/notifications/NotificationController.hpp"
|
||||||
|
#include "controllers/notifications/NotificationModel.hpp"
|
||||||
|
#include "singletons/Settings.hpp"
|
||||||
|
#include "util/LayoutCreator.hpp"
|
||||||
|
#include "widgets/helper/EditableModelView.hpp"
|
||||||
|
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QGroupBox>
|
||||||
|
#include <QHeaderView>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QListView>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QTableView>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
NotificationPage::NotificationPage()
|
||||||
|
: SettingsPage("Notifications", "")
|
||||||
|
{
|
||||||
|
LayoutCreator<NotificationPage> layoutCreator(this);
|
||||||
|
|
||||||
|
auto layout = layoutCreator.emplace<QVBoxLayout>().withoutMargin();
|
||||||
|
{
|
||||||
|
auto tabs = layout.emplace<QTabWidget>();
|
||||||
|
{
|
||||||
|
auto settings = tabs.appendTab(new QVBoxLayout, "Options");
|
||||||
|
{
|
||||||
|
settings.emplace<QLabel>("Enable for selected channels");
|
||||||
|
settings.append(this->createCheckBox(
|
||||||
|
"Flash taskbar",
|
||||||
|
getSettings()->notificationFlashTaskbar));
|
||||||
|
settings.append(this->createCheckBox(
|
||||||
|
"Playsound (doesn't mute the Windows 8.x sound of toasts)",
|
||||||
|
getSettings()->notificationPlaySound));
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
settings.append(this->createCheckBox(
|
||||||
|
"Enable toasts (currently only for windows 8.x or 10)",
|
||||||
|
getSettings()->notificationToast));
|
||||||
|
#endif
|
||||||
|
auto customSound =
|
||||||
|
layout.emplace<QHBoxLayout>().withoutMargin();
|
||||||
|
{
|
||||||
|
customSound.append(this->createCheckBox(
|
||||||
|
"Custom sound",
|
||||||
|
getSettings()->notificationCustomSound));
|
||||||
|
auto selectFile = customSound.emplace<QPushButton>(
|
||||||
|
"Select custom sound file");
|
||||||
|
QObject::connect(
|
||||||
|
selectFile.getElement(), &QPushButton::clicked, this,
|
||||||
|
[this] {
|
||||||
|
auto fileName = QFileDialog::getOpenFileName(
|
||||||
|
this, tr("Open Sound"), "",
|
||||||
|
tr("Audio Files (*.mp3 *.wav)"));
|
||||||
|
getSettings()->notificationPathSound =
|
||||||
|
fileName;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
settings->addStretch(1);
|
||||||
|
}
|
||||||
|
auto twitchChannels = tabs.appendTab(new QVBoxLayout, "Twitch");
|
||||||
|
{
|
||||||
|
EditableModelView *view =
|
||||||
|
twitchChannels
|
||||||
|
.emplace<EditableModelView>(
|
||||||
|
getApp()->notifications->createModel(
|
||||||
|
nullptr, Platform::Twitch))
|
||||||
|
.getElement();
|
||||||
|
view->setTitles({"Twitch channels"});
|
||||||
|
|
||||||
|
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
||||||
|
QHeaderView::Fixed);
|
||||||
|
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
||||||
|
0, QHeaderView::Stretch);
|
||||||
|
|
||||||
|
QTimer::singleShot(1, [view] {
|
||||||
|
view->getTableView()->resizeColumnsToContents();
|
||||||
|
view->getTableView()->setColumnWidth(0, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
view->addButtonPressed.connect([] {
|
||||||
|
getApp()
|
||||||
|
->notifications->channelMap[Platform::Twitch]
|
||||||
|
.appendItem("channel");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
auto mixerChannels = tabs.appendTab(new QVBoxLayout, "Mixer");
|
||||||
|
{
|
||||||
|
EditableModelView *view =
|
||||||
|
mixerChannels
|
||||||
|
.emplace<EditableModelView>(
|
||||||
|
getApp()->notifications->createModel(
|
||||||
|
nullptr, Platform::Mixer))
|
||||||
|
.getElement();
|
||||||
|
view->setTitles({"Mixer channels"});
|
||||||
|
|
||||||
|
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
||||||
|
QHeaderView::Fixed);
|
||||||
|
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
||||||
|
0, QHeaderView::Stretch);
|
||||||
|
|
||||||
|
QTimer::singleShot(1, [view] {
|
||||||
|
view->getTableView()->resizeColumnsToContents();
|
||||||
|
view->getTableView()->setColumnWidth(0, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
view->addButtonPressed.connect([] {
|
||||||
|
getApp()
|
||||||
|
->notifications->channelMap[Platform::Mixer]
|
||||||
|
.appendItem("channel");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace chatterino
|
20
src/widgets/settingspages/NotificationPage.hpp
Normal file
20
src/widgets/settingspages/NotificationPage.hpp
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "widgets/settingspages/SettingsPage.hpp"
|
||||||
|
|
||||||
|
class QPushButton;
|
||||||
|
class QListWidget;
|
||||||
|
|
||||||
|
class QVBoxLayout;
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class NotificationPage : public SettingsPage
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
NotificationPage();
|
||||||
|
|
||||||
|
private:
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include "Application.hpp"
|
#include "Application.hpp"
|
||||||
#include "controllers/accounts/AccountController.hpp"
|
#include "controllers/accounts/AccountController.hpp"
|
||||||
|
#include "controllers/notifications/NotificationController.hpp"
|
||||||
#include "providers/twitch/TwitchChannel.hpp"
|
#include "providers/twitch/TwitchChannel.hpp"
|
||||||
#include "providers/twitch/TwitchServer.hpp"
|
#include "providers/twitch/TwitchServer.hpp"
|
||||||
#include "singletons/Resources.hpp"
|
#include "singletons/Resources.hpp"
|
||||||
|
@ -211,6 +212,22 @@ std::unique_ptr<QMenu> SplitHeader::createMainMenu()
|
||||||
&Split::openBrowserPlayer);
|
&Split::openBrowserPlayer);
|
||||||
#endif
|
#endif
|
||||||
menu->addAction("Open streamlink", this->split_, &Split::openInStreamlink);
|
menu->addAction("Open streamlink", this->split_, &Split::openInStreamlink);
|
||||||
|
|
||||||
|
auto action = new QAction(this);
|
||||||
|
action->setText("Notify when live");
|
||||||
|
action->setCheckable(true);
|
||||||
|
|
||||||
|
QObject::connect(menu.get(), &QMenu::aboutToShow, this, [action, this]() {
|
||||||
|
action->setChecked(getApp()->notifications->isChannelNotified(
|
||||||
|
this->split_->getChannel()->getName(), Platform::Twitch));
|
||||||
|
});
|
||||||
|
action->connect(action, &QAction::triggered, this, [this]() {
|
||||||
|
getApp()->notifications->updateChannelNotification(
|
||||||
|
this->split_->getChannel()->getName(), Platform::Twitch);
|
||||||
|
});
|
||||||
|
|
||||||
|
menu->addAction(action);
|
||||||
|
|
||||||
menu->addSeparator();
|
menu->addSeparator();
|
||||||
menu->addAction("Reload channel emotes", this, SLOT(reloadChannelEmotes()));
|
menu->addAction("Reload channel emotes", this, SLOT(reloadChannelEmotes()));
|
||||||
menu->addAction("Reconnect", this, SLOT(reconnect()));
|
menu->addAction("Reconnect", this, SLOT(reconnect()));
|
||||||
|
|
Loading…
Reference in a new issue