diff --git a/chatterino.pro b/chatterino.pro index 24231d39c..b08abaccc 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -95,7 +95,8 @@ SOURCES += \ src/widgets/basewidget.cpp \ src/widgets/resizingtextedit.cpp \ src/completionmanager.cpp \ - src/widgets/logindialog.cpp + src/widgets/logindialog.cpp \ + src/widgets/qualitypopup.cpp HEADERS += \ src/asyncexec.hpp \ @@ -155,7 +156,8 @@ HEADERS += \ src/widgets/accountpopup.hpp \ src/util/distancebetweenpoints.hpp \ src/widgets/basewidget.hpp \ - src/completionmanager.hpp + src/completionmanager.hpp \ + src/widgets/qualitypopup.h PRECOMPILED_HEADER = diff --git a/src/accountmanager.cpp b/src/accountmanager.cpp index 38d7e8917..5a70a5653 100644 --- a/src/accountmanager.cpp +++ b/src/accountmanager.cpp @@ -22,15 +22,6 @@ inline QString getEnvString(const char *target) AccountManager::AccountManager() : twitchAnonymousUser("justinfan64537", "", "") { - QString envUsername = getEnvString("CHATTERINO2_USERNAME"); - QString envOauthToken = getEnvString("CHATTERINO2_OAUTH"); - - if (!envUsername.isEmpty() && !envOauthToken.isEmpty()) { - this->addTwitchUser(twitch::TwitchUser(envUsername, envOauthToken, "")); - } - - pajlada::Settings::Setting::set( - "/accounts/current/roomID", "11148817", pajlada::Settings::SettingOption::DoNotWriteToJSON); } void AccountManager::load() @@ -58,6 +49,8 @@ void AccountManager::load() twitch::TwitchUser user(qS(username), qS(oauthToken), qS(clientID)); this->addTwitchUser(user); + + printf("Adding user %s(%s)\n", username.c_str(), userID.c_str()); } } @@ -74,9 +67,24 @@ twitch::TwitchUser &AccountManager::getTwitchUser() return this->getTwitchAnon(); } + std::string currentUser = pajlada::Settings::Setting::get("/accounts/current"); + + QString currentUsername = QString::fromStdString(currentUser); + + for (auto &user : this->twitchUsers) { + if (user.getUserName() == currentUsername) { + return user; + } + } + return this->twitchUsers.front(); } +void AccountManager::setCurrentTwitchUser(const QString &username) +{ + pajlada::Settings::Setting::set("/accounts/current", username.toStdString()); +} + std::vector AccountManager::getTwitchUsers() { std::lock_guard lock(this->twitchUsersMutex); diff --git a/src/accountmanager.hpp b/src/accountmanager.hpp index a6b89bd0e..5e2858e64 100644 --- a/src/accountmanager.hpp +++ b/src/accountmanager.hpp @@ -29,6 +29,8 @@ public: // Remove twitch user with the given username bool removeTwitchUser(const QString &userName); + void setCurrentTwitchUser(const QString &username); + // Add twitch user to the list of available twitch users void addTwitchUser(const twitch::TwitchUser &user); diff --git a/src/channel.cpp b/src/channel.cpp index 311ecd03c..af979c6a4 100644 --- a/src/channel.cpp +++ b/src/channel.cpp @@ -29,6 +29,7 @@ Channel::Channel(WindowManager &_windowManager, EmoteManager &_emoteManager, , _subLink("https://www.twitch.tv/" + name + "/subscribe?ref=in_chat_subscriber_link") , _channelLink("https://twitch.tv/" + name) , _popoutPlayerLink("https://player.twitch.tv/?channel=" + name) + , isLive(false) // , _loggingChannel(logging::get(_name)) { qDebug() << "Open channel:" << this->name; @@ -62,24 +63,10 @@ const QString &Channel::getPopoutPlayerLink() const return _popoutPlayerLink; } -bool Channel::getIsLive() const +void Channel::setRoomID(std::string id) { - return _isLive; -} - -int Channel::getStreamViewerCount() const -{ - return _streamViewerCount; -} - -const QString &Channel::getStreamStatus() const -{ - return _streamStatus; -} - -const QString &Channel::getStreamGame() const -{ - return _streamGame; + this->roomID = id; + this->roomIDchanged(); } messages::LimitedQueueSnapshot Channel::getMessageSnapshot() diff --git a/src/channel.hpp b/src/channel.hpp index ec3e88766..5d4c8cbce 100644 --- a/src/channel.hpp +++ b/src/channel.hpp @@ -39,10 +39,6 @@ public: const QString &getSubLink() const; const QString &getChannelLink() const; const QString &getPopoutPlayerLink() const; - bool getIsLive() const; - int getStreamViewerCount() const; - const QString &getStreamStatus() const; - const QString &getStreamGame() const; messages::LimitedQueueSnapshot getMessageSnapshot(); // methods @@ -53,6 +49,14 @@ public: std::string roomID; const QString name; + bool isLive; + QString streamViewerCount; + QString streamStatus; + QString streamGame; + QString streamUptime; + + void setRoomID(std::string id); + boost::signals2::signal roomIDchanged; private: // variables @@ -67,10 +71,6 @@ private: QString _channelLink; QString _popoutPlayerLink; - bool _isLive; - int _streamViewerCount; - QString _streamStatus; - QString _streamGame; // std::shared_ptr _loggingChannel; }; diff --git a/src/credentials.hpp b/src/credentials.hpp new file mode 100644 index 000000000..e66cd2d34 --- /dev/null +++ b/src/credentials.hpp @@ -0,0 +1,10 @@ +#pragma once + +namespace chatterino { + +inline QByteArray getDefaultClientID() +{ + return QByteArray("7ue61iz46fz11y3cugd0l3tawb4taal"); +} + +} // namespace chatterino diff --git a/src/ircmanager.cpp b/src/ircmanager.cpp index 8dc1701a8..ca2d1a972 100644 --- a/src/ircmanager.cpp +++ b/src/ircmanager.cpp @@ -25,16 +25,19 @@ using namespace chatterino::messages; namespace chatterino { -const QString IrcManager::defaultClientId("7ue61iz46fz11y3cugd0l3tawb4taal"); - IrcManager::IrcManager(ChannelManager &_channelManager, Resources &_resources, EmoteManager &_emoteManager, WindowManager &_windowManager) : channelManager(_channelManager) , resources(_resources) , emoteManager(_emoteManager) , windowManager(_windowManager) - , _account(AccountManager::getInstance().getTwitchUser()) + , _account(AccountManager::getInstance().getTwitchAnon()) + , currentUser("/accounts/current") { + this->currentUser.getValueChangedSignal().connect([](const auto &newUsername) { + // TODO: Implement + qDebug() << "Current user changed, fetch new credentials and reconnect"; + }); } const twitch::TwitchUser &IrcManager::getUser() const @@ -279,6 +282,9 @@ void IrcManager::handleRoomStateMessage(Communi::IrcMessage *message) if (iterator != tags.end()) { std::string roomID = iterator.value().toString().toStdString(); + auto channel = QString(message->toData()).split("#").at(1); + channelManager.getChannel(channel)->setRoomID(roomID); + this->resources.loadChannelData(roomID); } } diff --git a/src/ircmanager.hpp b/src/ircmanager.hpp index b658ae413..f24810cad 100644 --- a/src/ircmanager.hpp +++ b/src/ircmanager.hpp @@ -30,8 +30,6 @@ public: IrcManager(ChannelManager &_channelManager, Resources &_resources, EmoteManager &_emoteManager, WindowManager &_windowManager); - static const QString defaultClientId; - void connect(); void disconnect(); @@ -60,6 +58,8 @@ private: // variables twitch::TwitchUser _account; + pajlada::Settings::Setting currentUser; + std::shared_ptr writeConnection = nullptr; std::shared_ptr readConnection = nullptr; diff --git a/src/settingsmanager.cpp b/src/settingsmanager.cpp index b54a1114a..f3a42f194 100644 --- a/src/settingsmanager.cpp +++ b/src/settingsmanager.cpp @@ -14,8 +14,8 @@ SettingsManager::SettingsManager() , showTimestamps("/appearance/messages/showTimestamps", true) , showTimestampSeconds("/appearance/messages/showTimestampSeconds", true) , showBadges("/appearance/messages/showBadges", true) - , streamlinkPath("/behaviour/streamlinkPath", "") - , selectedUser(_settingsItems, "selectedUser", "") + , streamlinkPath("/behaviour/streamlink/path", "") + , preferredQuality("/behaviour/streamlink/quality", "Choose") , emoteScale(_settingsItems, "emoteScale", 1.0) , mouseScrollMultiplier(_settingsItems, "mouseScrollMultiplier", 1.0) , scaleEmotesByLineHeight(_settingsItems, "scaleEmotesByLineHeight", false) diff --git a/src/settingsmanager.hpp b/src/settingsmanager.hpp index b1613432f..66c3ff738 100644 --- a/src/settingsmanager.hpp +++ b/src/settingsmanager.hpp @@ -44,9 +44,9 @@ public: pajlada::Settings::Setting showBadges; pajlada::Settings::Setting streamlinkPath; + pajlada::Settings::Setting preferredQuality; // Settings - Setting selectedUser; Setting emoteScale; Setting mouseScrollMultiplier; Setting scaleEmotesByLineHeight; diff --git a/src/twitch/twitchmessagebuilder.cpp b/src/twitch/twitchmessagebuilder.cpp index 22b0a656d..acd9c479f 100644 --- a/src/twitch/twitchmessagebuilder.cpp +++ b/src/twitch/twitchmessagebuilder.cpp @@ -365,8 +365,11 @@ void TwitchMessageBuilder::parseHighlights() { static auto player = new QMediaPlayer; SettingsManager &settings = SettingsManager::getInstance(); + static pajlada::Settings::Setting currentUser("/accounts/currentUser"); - if (this->ircMessage->nick() == settings.selectedUser.get()) { + QString currentUsername = QString::fromStdString(currentUser.getValue()); + + if (this->ircMessage->nick() == currentUsername) { // Do nothing. Highlights cannot be triggered by yourself return; } @@ -393,9 +396,8 @@ void TwitchMessageBuilder::parseHighlights() // TODO: This vector should only be rebuilt upon highlights being changed std::vector activeHighlights; - if (settings.enableHighlightsSelf.get() && settings.selectedUser.get().size() > 0) { - activeHighlights.emplace_back(settings.selectedUser.get(), - settings.enableHighlightSound.get(), + if (settings.enableHighlightsSelf.get() && currentUsername.size() > 0) { + activeHighlights.emplace_back(currentUsername, settings.enableHighlightSound.get(), settings.enableHighlightTaskbar.get()); } const auto &highlightProperties = settings.highlightProperties.get(); diff --git a/src/util/urlfetch.hpp b/src/util/urlfetch.hpp index d68e5e1c9..e86ed4074 100644 --- a/src/util/urlfetch.hpp +++ b/src/util/urlfetch.hpp @@ -1,5 +1,7 @@ #pragma once +#include "credentials.hpp" + #include #include #include @@ -16,6 +18,67 @@ namespace chatterino { namespace util { +namespace twitch { + +static void get(QString url, std::function successCallback) +{ + auto manager = new QNetworkAccessManager(); + + QUrl requestUrl(url); + QNetworkRequest request(requestUrl); + + request.setRawHeader("Client-ID", getDefaultClientID()); + request.setRawHeader("Accept", "application/vnd.twitchtv.v5+json"); + + QNetworkReply *reply = manager->get(request); + + QObject::connect(reply, &QNetworkReply::finished, [=] { + if (reply->error() == QNetworkReply::NetworkError::NoError) { + QByteArray data = reply->readAll(); + QJsonDocument jsonDoc(QJsonDocument::fromJson(data)); + + if (!jsonDoc.isNull()) { + QJsonObject rootNode = jsonDoc.object(); + + successCallback(rootNode); + } + } + + reply->deleteLater(); + manager->deleteLater(); + }); +} + +static void getUserID(QString username, std::function successCallback) +{ + get("https://api.twitch.tv/kraken/users?login=" + username, [=](const QJsonObject &root) { + if (!root.value("users").isArray()) { + qDebug() << "API Error while getting user id, users is not an array"; + return; + } + + auto users = root.value("users").toArray(); + if (users.size() != 1) { + qDebug() << "API Error while getting user id, users array size is not 1"; + return; + } + if (!users[0].isObject()) { + qDebug() << "API Error while getting user id, first user is not an object"; + return; + } + auto firstUser = users[0].toObject(); + auto id = firstUser.value("_id"); + if (!id.isString()) { + qDebug() + << "API Error: while getting user id, first user object `_id` key is not a string"; + return; + } + successCallback(id.toString()); + }); +} + +} // namespace twitch + static void urlFetch(const QString &url, std::function successCallback, QNetworkAccessManager *manager = nullptr) { diff --git a/src/widgets/accountpopup.cpp b/src/widgets/accountpopup.cpp index e54b70840..a58a8a493 100644 --- a/src/widgets/accountpopup.cpp +++ b/src/widgets/accountpopup.cpp @@ -1,5 +1,6 @@ #include "widgets/accountpopup.hpp" #include "channel.hpp" +#include "credentials.hpp" #include "ui_accountpopupform.h" #include @@ -42,7 +43,7 @@ void AccountPopupWidget::getUserId() QNetworkRequest req(nameUrl); req.setRawHeader(QByteArray("Accept"), QByteArray("application/vnd.twitchtv.v5+json")); - req.setRawHeader(QByteArray("Client-ID"), QByteArray("7ue61iz46fz11y3cugd0l3tawb4taal")); + req.setRawHeader(QByteArray("Client-ID"), getDefaultClientID()); static auto manager = new QNetworkAccessManager(); auto *reply = manager->get(req); @@ -66,7 +67,7 @@ void AccountPopupWidget::getUserData() QNetworkRequest req(idUrl); req.setRawHeader(QByteArray("Accept"), QByteArray("application/vnd.twitchtv.v5+json")); - req.setRawHeader(QByteArray("Client-ID"), QByteArray("7ue61iz46fz11y3cugd0l3tawb4taal")); + req.setRawHeader(QByteArray("Client-ID"), getDefaultClientID()); static auto manager = new QNetworkAccessManager(); auto *reply = manager->get(req); diff --git a/src/widgets/chatwidget.cpp b/src/widgets/chatwidget.cpp index e0d8ba143..bc5867c2b 100644 --- a/src/widgets/chatwidget.cpp +++ b/src/widgets/chatwidget.cpp @@ -3,14 +3,18 @@ #include "colorscheme.hpp" #include "notebookpage.hpp" #include "settingsmanager.hpp" +#include "util/urlfetch.hpp" #include "widgets/textinputdialog.hpp" +#include "widgets/qualitypopup.h" #include #include #include +#include #include #include #include +#include #include #include #include @@ -96,6 +100,9 @@ std::shared_ptr &ChatWidget::getChannelRef() void ChatWidget::setChannel(std::shared_ptr _newChannel) { this->channel = _newChannel; + this->channel->roomIDchanged.connect([this](){ + this->header.checkLive(); + }); // on new message this->messageAppendedConnection = @@ -252,6 +259,9 @@ void ChatWidget::doCloseSplit() { NotebookPage *page = static_cast(this->parentWidget()); page->removeFromLayout(this); + QTimer* timer = this->header.findChild(); + timer->stop(); + timer->deleteLater(); } void ChatWidget::doChangeChannel() @@ -292,18 +302,146 @@ void ChatWidget::doOpenPopupPlayer() void ChatWidget::doOpenStreamlink() { SettingsManager &settings = SettingsManager::getInstance(); + QString preferredQuality = QString::fromStdString(settings.preferredQuality.getValue()).toLower(); + // TODO(Confuseh): Default streamlink paths QString path = QString::fromStdString(settings.streamlinkPath.getValue()); + QString channel = QString::fromStdString(this->channelName.getValue()); QFileInfo fileinfo = QFileInfo(path); - // TODO(Confuseh): Add default checks for streamlink/livestreamer - // TODO(Confuseh): Add quality switcher if (fileinfo.exists() && fileinfo.isExecutable()) { - // works on leenux, idk whether it would work on whindows or mehOS - QProcess::startDetached( - path, QStringList({"twitch.tv/" + QString::fromStdString(this->channelName.getValue()), - "best"})); + if (preferredQuality != "choose") { + QStringList args = {"twitch.tv/" + channel}; + QString quality = ""; + QString exclude = ""; + if (preferredQuality == "high") { + exclude = ">720p30"; + quality = "high,best"; + } else if (preferredQuality == "medium") { + exclude = ">540p30"; + quality = "medium,best"; + } else if (preferredQuality == "low") { + exclude = ">360p30"; + quality = "low,best"; + } else if (preferredQuality == "audio only") { + quality = "audio,audio_only"; + } else { + quality = "best"; + } + if (quality != "") + args << quality; + if (exclude != "") + args << "--stream-sorting-excludes" << exclude; + QProcess::startDetached(path, args); + } else { + QProcess *p = new QProcess(); + // my god that signal though + QObject::connect(p, static_cast(&QProcess::finished), this, + [path, channel, p](int exitCode) { + if (exitCode > 0) { + return; + } + QString lastLine = QString(p->readAllStandardOutput()); + lastLine = lastLine.trimmed().split('\n').last(); + if (lastLine.startsWith("Available streams: ")) { + QStringList options; + QStringList split = lastLine.right(lastLine.length() - 19).split(", "); + + for (int i = split.length() - 1; i >= 0; i--) { + QString option = split.at(i); + if (option.endsWith(" (worst)")) { + options << option.left(option.length() - 8); + } else if (option.endsWith(" (best)")) { + options << option.left(option.length() - 7); + } else { + options << option; + } + } + + QualityPopup::showDialog(channel, path, options); + } + }); + p->start(path, {"twitch.tv/" + channel}); + } } } +void ChatWidget::doOpenViewerList() +{ + auto viewerDock = new QDockWidget("Viewer List",this); + viewerDock->setAllowedAreas(Qt::LeftDockWidgetArea); + viewerDock->setFeatures(QDockWidget::DockWidgetVerticalTitleBar | + QDockWidget::DockWidgetClosable | + QDockWidget::DockWidgetFloatable); + viewerDock->setMaximumHeight(this->height()); + viewerDock->resize(0.5*this->width(),this->height()); + + auto multiWidget = new QWidget(viewerDock); + auto dockVbox = new QVBoxLayout(viewerDock); + auto searchBar = new QLineEdit(viewerDock); + + auto chattersList = new QListWidget(); + auto resultList = new QListWidget(); + + static QStringList labels = {"Moderators", "Staff", "Admins", "Global Moderators", "Viewers"}; + static QStringList jsonLabels = {"moderators", "staff", "admins", "global_mods", "viewers"}; + QList labelList; + for(auto &x : labels) + { + auto label = new QListWidgetItem(x); + label->setBackgroundColor(this->colorScheme.ChatHeaderBackground); + labelList.append(label); + } + auto loadingLabel = new QLabel("Loading..."); + + util::twitch::get("https://tmi.twitch.tv/group/user/" + channel->name + "/chatters",[=](QJsonObject obj){ + QJsonObject chattersObj = obj.value("chatters").toObject(); + + loadingLabel->hide(); + for(int i = 0; i < jsonLabels.size(); i++) + { + chattersList->addItem(labelList.at(i)); + foreach (const QJsonValue & v, chattersObj.value(jsonLabels.at(i)).toArray()) + chattersList->addItem(v.toString()); + } + }); + + searchBar->setPlaceholderText("Search User..."); + QObject::connect(searchBar,&QLineEdit::textEdited,this,[=](){ + auto query = searchBar->text(); + if(!query.isEmpty()) + { + auto results = chattersList->findItems(query,Qt::MatchStartsWith); + chattersList->hide(); + resultList->clear(); + for (auto & item : results) + { + if(!labels.contains(item->text())) + resultList->addItem(item->text()); + } + resultList->show(); + } + else + { + resultList->hide(); + chattersList->show(); + } + }); + + QObject::connect(viewerDock,&QDockWidget::topLevelChanged,this,[=](){ + viewerDock->setMinimumWidth(300); + }); + + dockVbox->addWidget(searchBar); + dockVbox->addWidget(loadingLabel); + dockVbox->addWidget(chattersList); + dockVbox->addWidget(resultList); + resultList->hide(); + + multiWidget->setStyleSheet(this->colorScheme.InputStyleSheet); + multiWidget->setLayout(dockVbox); + viewerDock->setWidget(multiWidget); + viewerDock->show(); +} + void ChatWidget::doCopy() { QApplication::clipboard()->setText(this->view.getSelectedText()); diff --git a/src/widgets/chatwidget.hpp b/src/widgets/chatwidget.hpp index ff11de1bf..56a4cf20e 100644 --- a/src/widgets/chatwidget.hpp +++ b/src/widgets/chatwidget.hpp @@ -118,6 +118,9 @@ public slots: // Copy text from chat void doCopy(); + + // Open viewer list of the channel + void doOpenViewerList(); }; } // namespace widgets diff --git a/src/widgets/chatwidgetheader.cpp b/src/widgets/chatwidgetheader.cpp index 97964892d..6eb99b048 100644 --- a/src/widgets/chatwidgetheader.cpp +++ b/src/widgets/chatwidgetheader.cpp @@ -2,6 +2,7 @@ #include "colorscheme.hpp" #include "widgets/chatwidget.hpp" #include "widgets/notebookpage.hpp" +#include "util/urlfetch.hpp" #include #include @@ -44,6 +45,7 @@ ChatWidgetHeader::ChatWidgetHeader(ChatWidget *_chatWidget) QKeySequence(tr("Ctrl+W"))); this->leftMenu.addAction("Move split", this, SLOT(menuMoveSplit())); this->leftMenu.addAction("Popup", this->chatWidget, &ChatWidget::doPopup); + this->leftMenu.addAction("Open viewer list", this->chatWidget, &ChatWidget::doOpenViewerList); this->leftMenu.addSeparator(); this->leftMenu.addAction("Change channel", this->chatWidget, &ChatWidget::doChangeChannel, QKeySequence(tr("Ctrl+R"))); @@ -67,16 +69,36 @@ ChatWidgetHeader::ChatWidgetHeader(ChatWidget *_chatWidget) this->rightLabel.setMinimumWidth(this->height()); this->rightLabel.getLabel().setTextFormat(Qt::RichText); this->rightLabel.getLabel().setText("ayy"); + + QTimer *timer = new QTimer(this); + connect(timer, &QTimer::timeout, this, checkLive); + timer->start(60000); } void ChatWidgetHeader::updateChannelText() { const std::string channelName = this->chatWidget->channelName; - if (channelName.empty()) { this->channelNameLabel.setText(""); } else { - this->channelNameLabel.setText(QString::fromStdString(channelName)); + if(this->chatWidget->getChannelRef()->isLive) + { + auto channel = this->chatWidget->getChannelRef(); + this->channelNameLabel.setText(QString::fromStdString(channelName) + " (live)"); + this->setToolTip("" \ + "

" + \ + channel->streamStatus + "

" + \ + channel->streamGame + "
" \ + "Live for " + channel->streamUptime + \ + " with " + channel->streamViewerCount + " viewers" \ + "

" + ); + } + else + { + this->channelNameLabel.setText(QString::fromStdString(channelName)); + this->setToolTip(""); + } } } @@ -173,5 +195,30 @@ void ChatWidgetHeader::menuShowChangelog() { } +void ChatWidgetHeader::checkLive() +{ + auto channel = this->chatWidget->getChannelRef(); + auto id = QString::fromStdString(channel->roomID); + util::twitch::get("https://api.twitch.tv/kraken/streams/" + id,[=](QJsonObject obj){ + if(obj.value("stream").isNull()) + { + channel->isLive = false; + this->updateChannelText(); + } + else + { + channel->isLive = true; + auto stream = obj.value("stream").toObject(); + channel->streamViewerCount = QString::number(stream.value("viewers").toDouble()); + channel->streamGame = stream.value("game").toString(); + channel->streamStatus = stream.value("channel").toObject().value("status").toString(); + QDateTime since = QDateTime::fromString(stream.value("created_at").toString(),Qt::ISODate); + auto diff = since.secsTo(QDateTime::currentDateTime()); + channel->streamUptime = QString::number(diff/3600) + "h " + QString::number(diff % 3600 / 60) + "m"; + this->updateChannelText(); + } + }); +} + } // namespace widgets } // namespace chatterino diff --git a/src/widgets/chatwidgetheader.hpp b/src/widgets/chatwidgetheader.hpp index 8ce3ca740..fe437862e 100644 --- a/src/widgets/chatwidgetheader.hpp +++ b/src/widgets/chatwidgetheader.hpp @@ -27,9 +27,9 @@ class ChatWidgetHeader : public BaseWidget public: explicit ChatWidgetHeader(ChatWidget *_chatWidget); - // Update channel text from chat widget void updateChannelText(); + void checkLive(); protected: virtual void paintEvent(QPaintEvent *) override; @@ -66,6 +66,7 @@ public slots: void menuReloadChannelEmotes(); void menuManualReconnect(); void menuShowChangelog(); + }; } // namespace widgets diff --git a/src/widgets/chatwidgetview.cpp b/src/widgets/chatwidgetview.cpp index b0b2c210f..2286dbb3a 100644 --- a/src/widgets/chatwidgetview.cpp +++ b/src/widgets/chatwidgetview.cpp @@ -356,10 +356,15 @@ void ChatWidgetView::updateMessageBuffer(messages::MessageRef *messageRef, QPixm QPainter painter(buffer); // draw background + // if (this->selectionMin.messageIndex <= messageIndex && + // this->selectionMax.messageIndex >= messageIndex) { + // painter.fillRect(buffer->rect(), QColor(24, 55, 25)); + //} else { painter.fillRect(buffer->rect(), (messageRef->getMessage()->getCanHighlightTab()) ? this->colorScheme.ChatBackgroundHighlighted : this->colorScheme.ChatBackground); + //} // draw selection if (!selection.isEmpty()) { diff --git a/src/widgets/logindialog.cpp b/src/widgets/logindialog.cpp index ebc63b356..5d8df8b75 100644 --- a/src/widgets/logindialog.cpp +++ b/src/widgets/logindialog.cpp @@ -1,4 +1,5 @@ #include "widgets/logindialog.hpp" +#include "util/urlfetch.hpp" #include #include @@ -9,9 +10,9 @@ namespace chatterino { namespace widgets { -LoginWidget::LoginWidget() +BasicLoginWidget::BasicLoginWidget() { - this->setLayout(&this->ui.mainLayout); + this->setLayout(&this->ui.layout); this->ui.loginButton.setText("Log in (Opens in browser)"); this->ui.pasteCodeButton.setText("Paste code"); @@ -19,22 +20,11 @@ LoginWidget::LoginWidget() this->ui.horizontalLayout.addWidget(&this->ui.loginButton); this->ui.horizontalLayout.addWidget(&this->ui.pasteCodeButton); - this->ui.verticalLayout.addLayout(&this->ui.horizontalLayout); - - this->ui.mainLayout.addLayout(&this->ui.verticalLayout); - - this->ui.buttonBox.setStandardButtons(QDialogButtonBox::Close); - - this->ui.mainLayout.addWidget(&this->ui.buttonBox); - - connect(&this->ui.buttonBox, &QDialogButtonBox::rejected, [this]() { - this->close(); // - }); + this->ui.layout.addLayout(&this->ui.horizontalLayout); connect(&this->ui.loginButton, &QPushButton::clicked, []() { printf("open login in browser\n"); QDesktopServices::openUrl(QUrl("https://pajlada.se/chatterino/#chatterino")); - }); connect(&this->ui.pasteCodeButton, &QPushButton::clicked, []() { @@ -81,5 +71,108 @@ LoginWidget::LoginWidget() }); } +AdvancedLoginWidget::AdvancedLoginWidget() +{ + this->setLayout(&this->ui.layout); + + this->ui.instructionsLabel.setText("1. Fill in your username\n2. Fill in your user ID or press " + "the 'Get user ID from username' button\n3. Fill in your " + "Client ID\n4. Fill in your OAuth Token\n5. Press Add User"); + this->ui.instructionsLabel.setWordWrap(true); + + this->ui.layout.addWidget(&this->ui.instructionsLabel); + this->ui.layout.addLayout(&this->ui.formLayout); + this->ui.layout.addLayout(&this->ui.buttonUpperRow.layout); + this->ui.layout.addLayout(&this->ui.buttonLowerRow.layout); + + this->refreshButtons(); + + /// Form + this->ui.formLayout.addRow("Username", &this->ui.usernameInput); + this->ui.formLayout.addRow("User ID", &this->ui.userIDInput); + this->ui.formLayout.addRow("Client ID", &this->ui.clientIDInput); + this->ui.formLayout.addRow("Oauth token", &this->ui.oauthTokenInput); + + this->ui.oauthTokenInput.setEchoMode(QLineEdit::Password); + + connect(&this->ui.userIDInput, &QLineEdit::textChanged, [=]() { this->refreshButtons(); }); + connect(&this->ui.usernameInput, &QLineEdit::textChanged, [=]() { this->refreshButtons(); }); + connect(&this->ui.clientIDInput, &QLineEdit::textChanged, [=]() { this->refreshButtons(); }); + connect(&this->ui.oauthTokenInput, &QLineEdit::textChanged, [=]() { this->refreshButtons(); }); + + /// Upper button row + + this->ui.buttonUpperRow.addUserButton.setText("Add user"); + this->ui.buttonUpperRow.clearFieldsButton.setText("Clear fields"); + + this->ui.buttonUpperRow.layout.addWidget(&this->ui.buttonUpperRow.addUserButton); + this->ui.buttonUpperRow.layout.addWidget(&this->ui.buttonUpperRow.clearFieldsButton); + + connect(&this->ui.buttonUpperRow.clearFieldsButton, &QPushButton::clicked, [=]() { + this->ui.userIDInput.clear(); + this->ui.usernameInput.clear(); + this->ui.clientIDInput.clear(); + this->ui.oauthTokenInput.clear(); + }); + + connect(&this->ui.buttonUpperRow.addUserButton, &QPushButton::clicked, [=]() { + std::string userID = this->ui.userIDInput.text().toStdString(); + std::string username = this->ui.usernameInput.text().toStdString(); + std::string clientID = this->ui.clientIDInput.text().toStdString(); + std::string oauthToken = this->ui.oauthTokenInput.text().toStdString(); + + qDebug() << "Success! mr"; + pajlada::Settings::Setting::set("/accounts/uid" + userID + "/username", + username); + pajlada::Settings::Setting::set("/accounts/uid" + userID + "/userID", userID); + pajlada::Settings::Setting::set("/accounts/uid" + userID + "/clientID", + clientID); + pajlada::Settings::Setting::set("/accounts/uid" + userID + "/oauthToken", + oauthToken); + }); + + /// Lower button row + this->ui.buttonLowerRow.fillInUserIDButton.setText("Get user ID from username"); + + this->ui.buttonLowerRow.layout.addWidget(&this->ui.buttonLowerRow.fillInUserIDButton); + + connect(&this->ui.buttonLowerRow.fillInUserIDButton, &QPushButton::clicked, [=]() { + util::twitch::getUserID(this->ui.usernameInput.text(), [=](const QString &userID) { + this->ui.userIDInput.setText(userID); // + }); + }); +} + +void AdvancedLoginWidget::refreshButtons() +{ + this->ui.buttonLowerRow.fillInUserIDButton.setEnabled(!this->ui.usernameInput.text().isEmpty()); + + if (this->ui.userIDInput.text().isEmpty() || this->ui.usernameInput.text().isEmpty() || + this->ui.clientIDInput.text().isEmpty() || this->ui.oauthTokenInput.text().isEmpty()) { + this->ui.buttonUpperRow.addUserButton.setEnabled(false); + } else { + this->ui.buttonUpperRow.addUserButton.setEnabled(true); + } +} + +LoginWidget::LoginWidget() +{ + this->setLayout(&this->ui.mainLayout); + + this->ui.mainLayout.addWidget(&this->ui.tabWidget); + + this->ui.tabWidget.addTab(&this->ui.basic, "Basic"); + + this->ui.tabWidget.addTab(&this->ui.advanced, "Advanced"); + + this->ui.buttonBox.setStandardButtons(QDialogButtonBox::Close); + + connect(&this->ui.buttonBox, &QDialogButtonBox::rejected, [this]() { + this->close(); // + }); + + this->ui.mainLayout.addWidget(&this->ui.buttonBox); +} + } // namespace widgets } // namespace chatterino diff --git a/src/widgets/logindialog.hpp b/src/widgets/logindialog.hpp index 0f864bd18..b6a17e627 100644 --- a/src/widgets/logindialog.hpp +++ b/src/widgets/logindialog.hpp @@ -8,14 +8,65 @@ #include #include #include +#include #include #include +#include +#include #include +#include #include namespace chatterino { namespace widgets { +class BasicLoginWidget : public QWidget +{ +public: + BasicLoginWidget(); + + struct { + QVBoxLayout layout; + QHBoxLayout horizontalLayout; + QPushButton loginButton; + QPushButton pasteCodeButton; + } ui; +}; + +class AdvancedLoginWidget : public QWidget +{ +public: + AdvancedLoginWidget(); + + void refreshButtons(); + + struct { + QVBoxLayout layout; + + QLabel instructionsLabel; + + QFormLayout formLayout; + + QLineEdit userIDInput; + QLineEdit usernameInput; + QLineEdit clientIDInput; + QLineEdit oauthTokenInput; + + struct { + QHBoxLayout layout; + + QPushButton addUserButton; + QPushButton clearFieldsButton; + } buttonUpperRow; + + struct { + QHBoxLayout layout; + + QPushButton fillInUserIDButton; + } buttonLowerRow; + } ui; +}; + class LoginWidget : public QDialog { public: @@ -25,11 +76,13 @@ private: struct { QVBoxLayout mainLayout; - QVBoxLayout verticalLayout; - QHBoxLayout horizontalLayout; - QPushButton loginButton; - QPushButton pasteCodeButton; + QTabWidget tabWidget; + QDialogButtonBox buttonBox; + + BasicLoginWidget basic; + + AdvancedLoginWidget advanced; } ui; }; diff --git a/src/widgets/qualitypopup.cpp b/src/widgets/qualitypopup.cpp new file mode 100644 index 000000000..e844b16c2 --- /dev/null +++ b/src/widgets/qualitypopup.cpp @@ -0,0 +1,53 @@ +#include "qualitypopup.h" + +#include + +namespace chatterino { +namespace widgets { + +QualityPopup::QualityPopup(const QString &channel, const QString &path, QStringList options) + : channel(channel) + , path(path) +{ + this->ui.okButton.setText("OK"); + this->ui.cancelButton.setText("Cancel"); + + QObject::connect(&this->ui.okButton, &QPushButton::clicked, this, &QualityPopup::okButtonClicked); + QObject::connect(&this->ui.cancelButton, &QPushButton::clicked, this, + &QualityPopup::cancelButtonClicked); + + this->ui.buttonBox.addButton(&this->ui.okButton, QDialogButtonBox::ButtonRole::AcceptRole); + this->ui.buttonBox.addButton(&this->ui.cancelButton, QDialogButtonBox::ButtonRole::RejectRole); + + for (int i = 0; i < options.length(); ++i) { + this->ui.selector.addItem(options.at(i)); + } + + this->ui.vbox.addWidget(&this->ui.selector); + this->ui.vbox.addWidget(&this->ui.buttonBox); + + this->setLayout(&this->ui.vbox); +} + +void QualityPopup::showDialog(const QString &channel, const QString &path, QStringList options) { + static QualityPopup *instance = new QualityPopup(channel, path, options); + + instance->show(); + instance->activateWindow(); + instance->raise(); + instance->setFocus(); +} + +void QualityPopup::okButtonClicked() { + QProcess::startDetached(this->path, + {"twitch.tv/" + this->channel, this->ui.selector.currentText()}); + this->close(); +} + +void QualityPopup::cancelButtonClicked() +{ + this->close(); +} + +} // namespace widgets +} // namespace chatterino diff --git a/src/widgets/qualitypopup.h b/src/widgets/qualitypopup.h new file mode 100644 index 000000000..28c444d88 --- /dev/null +++ b/src/widgets/qualitypopup.h @@ -0,0 +1,39 @@ +#ifndef QUALITYPOPUP_H +#define QUALITYPOPUP_H + +#include +#include +#include +#include +#include +#include + +namespace chatterino { + +namespace widgets { + +class QualityPopup : public QWidget +{ +public: + QualityPopup(const QString &channel, const QString &path, QStringList options); + static void showDialog(const QString &channel, const QString &path, QStringList options); +private: + struct { + QVBoxLayout vbox; + QComboBox selector; + QDialogButtonBox buttonBox; + QPushButton okButton; + QPushButton cancelButton; + } ui; + + QString channel; + QString path; + + void okButtonClicked(); + void cancelButtonClicked(); +}; + +} // namespace widgets +} // namespace chatterino + +#endif // QUALITYPOPUP_H diff --git a/src/widgets/scrollbar.cpp b/src/widgets/scrollbar.cpp index 7463d1ba5..e5fdf6ec7 100644 --- a/src/widgets/scrollbar.cpp +++ b/src/widgets/scrollbar.cpp @@ -82,7 +82,7 @@ void ScrollBar::scrollToBottom() bool ScrollBar::isAtBottom() const { - return ((this->getMaximum() - this->getLargeChange()) - this->getCurrentValue()) <= 0.00001; + return ((this->getMaximum() - this->getLargeChange()) - this->getCurrentValue()) <= 1; } void ScrollBar::setMaximum(qreal value) diff --git a/src/widgets/settingsdialog.cpp b/src/widgets/settingsdialog.cpp index df20d408a..d5060290d 100644 --- a/src/widgets/settingsdialog.cpp +++ b/src/widgets/settingsdialog.cpp @@ -108,21 +108,22 @@ void SettingsDialog::addTabs() listWidget->addItem(user.getUserName()); } - if (listWidget->count()) { - int itemIndex = 0; - for (; itemIndex < listWidget->count(); ++itemIndex) { - if (listWidget->item(itemIndex)->text().compare(settings.selectedUser.get(), - Qt::CaseInsensitive)) { - ++itemIndex; + if (listWidget->count() > 0) { + const auto ¤tUser = AccountManager::getInstance().getTwitchUser(); + QString currentUsername = currentUser.getUserName(); + for (int i = 0; i < listWidget->count(); ++i) { + QString itemText = listWidget->item(i)->text(); + if (itemText.compare(currentUsername, Qt::CaseInsensitive) == 0) { + listWidget->setCurrentRow(i); break; } } - listWidget->setCurrentRow(itemIndex); } QObject::connect(listWidget, &QListWidget::clicked, this, [&, listWidget] { if (!listWidget->selectedItems().isEmpty()) { - settings.selectedUser.set(listWidget->currentItem()->text()); + AccountManager::getInstance().setCurrentTwitchUser( + listWidget->currentItem()->text()); } }); @@ -312,7 +313,14 @@ void SettingsDialog::addTabs() auto scroll = new QSlider(Qt::Horizontal); form->addRow("Mouse scroll speed:", scroll); - form->addRow("Streamlink Path", createLineEdit(settings.streamlinkPath)); + form->addRow("Streamlink path:", createLineEdit(settings.streamlinkPath)); + form->addRow(this->createCombobox( + "Preferred quality:", settings.preferredQuality, + {"Choose", "Source", "High", "Medium", "Low", "Audio only"}, + [](const QString &newValue, pajlada::Settings::Setting &setting) { + setting = newValue.toStdString(); + }) + ); // v->addWidget(scroll); // v->addStretch(1); @@ -585,6 +593,27 @@ QHBoxLayout *SettingsDialog::createCombobox( return box; } +QHBoxLayout *SettingsDialog::createCombobox( + const QString &title, pajlada::Settings::Setting &setting, QStringList items, + std::function &)> cb) +{ + auto box = new QHBoxLayout(); + auto label = new QLabel(title); + auto widget = new QComboBox(); + widget->addItems(items); + widget->setCurrentText(QString::fromStdString(setting.getValue())); + + QObject::connect(widget, &QComboBox::currentTextChanged, this, + [&setting, cb](const QString &newValue) { + cb(newValue, setting); // + }); + + box->addWidget(label); + box->addWidget(widget); + + return box; +} + QLineEdit *SettingsDialog::createLineEdit(pajlada::Settings::Setting &setting) { auto widget = new QLineEdit(QString::fromStdString(setting.getValue())); diff --git a/src/widgets/settingsdialog.hpp b/src/widgets/settingsdialog.hpp index 3d292f310..a3ee48dce 100644 --- a/src/widgets/settingsdialog.hpp +++ b/src/widgets/settingsdialog.hpp @@ -60,6 +60,9 @@ private: QHBoxLayout *createCombobox(const QString &title, pajlada::Settings::Setting &setting, QStringList items, std::function &)> cb); + QHBoxLayout *createCombobox(const QString &title, pajlada::Settings::Setting &setting, + QStringList items, + std::function &)> cb); QLineEdit *createLineEdit(pajlada::Settings::Setting &setting); void okButtonClicked();