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/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/ircmanager.cpp b/src/ircmanager.cpp index 76240b4e7..ca2d1a972 100644 --- a/src/ircmanager.cpp +++ b/src/ircmanager.cpp @@ -282,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/settingsmanager.cpp b/src/settingsmanager.cpp index 11aaf71a6..f3a42f194 100644 --- a/src/settingsmanager.cpp +++ b/src/settingsmanager.cpp @@ -14,7 +14,8 @@ SettingsManager::SettingsManager() , showTimestamps("/appearance/messages/showTimestamps", true) , showTimestampSeconds("/appearance/messages/showTimestampSeconds", true) , showBadges("/appearance/messages/showBadges", true) - , streamlinkPath("/behaviour/streamlinkPath", "") + , 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 f11596d35..66c3ff738 100644 --- a/src/settingsmanager.hpp +++ b/src/settingsmanager.hpp @@ -44,6 +44,7 @@ public: pajlada::Settings::Setting showBadges; pajlada::Settings::Setting streamlinkPath; + pajlada::Settings::Setting preferredQuality; // Settings Setting emoteScale; diff --git a/src/widgets/chatwidget.cpp b/src/widgets/chatwidget.cpp index 469e48ca2..e7c0481ce 100644 --- a/src/widgets/chatwidget.cpp +++ b/src/widgets/chatwidget.cpp @@ -3,12 +3,16 @@ #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 @@ -91,6 +95,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 = @@ -244,6 +251,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() @@ -284,17 +294,145 @@ 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(); +} + } // namespace widgets } // namespace chatterino diff --git a/src/widgets/chatwidget.hpp b/src/widgets/chatwidget.hpp index e7112294d..5defc2d6e 100644 --- a/src/widgets/chatwidget.hpp +++ b/src/widgets/chatwidget.hpp @@ -115,6 +115,9 @@ public slots: // Open twitch channel stream through streamlink void doOpenStreamlink(); + + // 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/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/settingsdialog.cpp b/src/widgets/settingsdialog.cpp index e9dae1c27..d5060290d 100644 --- a/src/widgets/settingsdialog.cpp +++ b/src/widgets/settingsdialog.cpp @@ -313,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); @@ -586,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();