feat: show live indicator in usercard (#6383)

This commit is contained in:
nerix
2025-08-17 12:05:52 +02:00
committed by GitHub
parent 0f11a9050a
commit bc85df04b1
11 changed files with 259 additions and 40 deletions
+1
View File
@@ -24,6 +24,7 @@
- Minor: Add a setting under Moderation -> Logs to customize the timestamp used for chat logs. (#6338)
- Minor: Add a setting under Moderation -> Logs to use server timestamp from the message instead of the local clock time for logging. (#6346)
- Minor: Add feature to search for only zero-width emotes when prepended by `:~`. (#6362)
- Minor: Usercards now show a live indicator if the user is currently streaming. (#6383)
- Bugfix: Commands are no longer tab-completable in the middle of messages. (#6273)
- Bugfix: Automatic streamer mode detection now works from Flatpak. (#6250)
- Bugfix: Don't create native messaging manifest file if browser directory doesn't exist. (#6116)
+4
View File
@@ -714,12 +714,16 @@ set(SOURCE_FILES
widgets/helper/RegExpItemDelegate.hpp
widgets/helper/ResizingTextEdit.cpp
widgets/helper/ResizingTextEdit.hpp
widgets/helper/ScalingSpacerItem.cpp
widgets/helper/ScalingSpacerItem.hpp
widgets/helper/ScrollbarHighlight.cpp
widgets/helper/ScrollbarHighlight.hpp
widgets/helper/SearchPopup.cpp
widgets/helper/SearchPopup.hpp
widgets/helper/SettingsDialogTab.cpp
widgets/helper/SettingsDialogTab.hpp
widgets/helper/LiveIndicator.cpp
widgets/helper/LiveIndicator.hpp
widgets/helper/TableStyles.cpp
widgets/helper/TableStyles.hpp
widgets/helper/TrimRegExpValidator.cpp
+21 -29
View File
@@ -15,6 +15,7 @@ Label::Label(BaseWidget *parent, QString text, FontStyle style)
: BaseWidget(parent)
, text_(std::move(text))
, fontStyle_(style)
, basePadding_(8, 0, 8, 0)
{
this->connections_.managedConnect(getApp()->getFonts()->fontChanged,
[this] {
@@ -36,7 +37,7 @@ void Label::setText(const QString &text)
if (this->shouldElide_)
{
this->updateElidedText(this->getFontMetrics(),
this->getInnerWidth());
this->textRect().width());
}
this->updateSize();
this->update();
@@ -59,9 +60,9 @@ void Label::setCentered(bool centered)
this->updateSize();
}
void Label::setHasPadding(bool hasPadding)
void Label::setPadding(QMargins padding)
{
this->hasPadding_ = hasPadding;
this->basePadding_ = padding;
this->updateSize();
}
@@ -113,11 +114,8 @@ void Label::paintEvent(QPaintEvent * /*event*/)
painter.setFont(
getApp()->getFonts()->getFont(this->getFontStyle(), this->scale()));
int padding = this->getPadding();
// draw text
QRect textRect(this->getPadding(), 0,
static_cast<int>(this->getInnerWidth()), this->height());
QRectF textRect = this->textRect();
auto text = [this] {
if (this->shouldElide_)
@@ -128,7 +126,7 @@ void Label::paintEvent(QPaintEvent * /*event*/)
return this->text_;
}();
int width = static_cast<int>(metrics.horizontalAdvance(text));
qreal width = metrics.horizontalAdvance(text);
Qt::Alignment alignment = !this->centered_ || width > textRect.width()
? Qt::AlignLeft | Qt::AlignVCenter
: Qt::AlignCenter;
@@ -157,7 +155,7 @@ void Label::resizeEvent(QResizeEvent *event)
if (this->shouldElide_)
{
auto metrics = this->getFontMetrics();
if (this->updateElidedText(metrics, this->getInnerWidth()))
if (this->updateElidedText(metrics, this->textRect().width()))
{
this->update();
}
@@ -172,27 +170,26 @@ QFontMetricsF Label::getFontMetrics() const
this->scale());
}
qreal Label::getInnerWidth() const
{
return this->width() - (2 * this->getPadding());
}
void Label::updateSize()
{
this->currentPadding_ = this->basePadding_.toMarginsF() * this->scale();
auto metrics = this->getFontMetrics();
auto yPadding =
this->currentPadding_.top() + this->currentPadding_.bottom();
auto height = metrics.height() + yPadding;
if (this->shouldElide_)
{
auto height = metrics.height();
this->updateElidedText(metrics, this->getInnerWidth());
this->updateElidedText(metrics, this->textRect().width());
this->sizeHint_ = QSizeF(-1, height).toSize();
this->minimumSizeHint_ = this->sizeHint_;
}
else
{
auto width =
metrics.horizontalAdvance(this->text_) + (2 * this->getPadding());
auto height = metrics.height();
auto width = metrics.horizontalAdvance(this->text_) +
this->currentPadding_.left() +
this->currentPadding_.right();
this->sizeHint_ = QSizeF(width, height).toSize();
this->minimumSizeHint_ = this->sizeHint_;
}
@@ -200,16 +197,6 @@ void Label::updateSize()
this->updateGeometry();
}
int Label::getPadding() const
{
if (this->hasPadding_)
{
return static_cast<int>(8 * this->scale());
}
return 0;
}
bool Label::updateElidedText(const QFontMetricsF &fontMetrics, qreal width)
{
assert(this->shouldElide_ == true);
@@ -225,4 +212,9 @@ bool Label::updateElidedText(const QFontMetricsF &fontMetrics, qreal width)
return false;
}
QRectF Label::textRect() const
{
return this->rect().toRectF().marginsRemoved(this->currentPadding_);
}
} // namespace chatterino
+8 -9
View File
@@ -26,8 +26,7 @@ public:
bool getCentered() const;
void setCentered(bool centered);
/// Enable or disable horizontal padding
void setHasPadding(bool hasPadding);
void setPadding(QMargins padding);
bool getWordWrap() const;
void setWordWrap(bool wrap);
@@ -46,16 +45,11 @@ protected:
private:
void updateSize();
/// Returns the horizontal padding, or 0 if hasPadding_ is false
int getPadding() const;
QRectF textRect() const;
/// Returns the current font style's font metric based on the current scale.
QFontMetricsF getFontMetrics() const;
/// Returns the width of this content without padding
qreal getInnerWidth() const;
/// Calculate the new elided text based on text_
///
/// Return true if the elided text changed
@@ -65,8 +59,13 @@ private:
FontStyle fontStyle_;
QSize sizeHint_;
QSize minimumSizeHint_;
/// The user specified padding (scale agnostic)
QMargins basePadding_;
/// The actual scaled padding
QMarginsF currentPadding_;
bool centered_ = false;
bool hasPadding_ = true;
bool wordWrap_ = false;
bool shouldElide_ = false;
/// The text, but elided. Only set if shouldElide_ is true
+33 -1
View File
@@ -32,6 +32,8 @@
#include "widgets/helper/ChannelView.hpp"
#include "widgets/helper/InvisibleSizeGrip.hpp"
#include "widgets/helper/Line.hpp"
#include "widgets/helper/LiveIndicator.hpp"
#include "widgets/helper/ScalingSpacerItem.hpp"
#include "widgets/Label.hpp"
#include "widgets/Notebook.hpp"
#include "widgets/Scrollbar.hpp"
@@ -355,6 +357,13 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, Split *split)
this->ui_.nameLabel = addCopyableLabel(box, "Copy name");
this->ui_.nameLabel->setFontStyle(FontStyle::UiMediumBold);
this->ui_.nameLabel->setPadding(QMargins(8, 0, 1, 0));
this->ui_.liveIndicator = new LiveIndicator;
this->ui_.liveIndicator->hide();
// addCopyableLabel adds the copy button last -> add the indicator before that
box->insertWidget(box->count() - 1, this->ui_.liveIndicator);
box->insertItem(box->count() - 1,
ScalingSpacerItem::horizontal(7));
box->addSpacing(5);
box->addStretch(1);
@@ -973,6 +982,29 @@ void UserInfoPopup::updateUserData()
qCWarning(chatterinoTwitch)
<< "Error getting followers:" << errorMessage;
});
getHelix()->getStreamById(
user.id,
[this, hack](bool isLive, const auto &stream) {
if (!hack.lock())
{
return;
}
if (isLive)
{
this->ui_.liveIndicator->setViewers(stream.viewerCount);
this->ui_.liveIndicator->show();
}
else
{
this->ui_.liveIndicator->hide();
}
},
[id{user.id}]() {
qCWarning(chatterinoWidget)
<< "Failed to get stream for user ID" << id;
},
[]() {});
// get ignore state
bool isIgnoring = currentUser->blockedUserIds().contains(user.id);
@@ -1169,7 +1201,7 @@ UserInfoPopup::TimeoutWidget::TimeoutWidget()
title->addStretch(1);
auto label = title.emplace<Label>(text);
label->setStyleSheet("color: #BBB");
label->setHasPadding(false);
label->setPadding(QMargins{});
title->addStretch(1);
auto hbox = vbox.emplace<QHBoxLayout>().withoutMargin();
+3
View File
@@ -18,6 +18,7 @@ class ChannelView;
class Split;
class LabelButton;
class PixmapButton;
class LiveIndicator;
class UserInfoPopup final : public DraggablePopup
{
@@ -85,6 +86,8 @@ private:
Label *followageLabel = nullptr;
Label *subageLabel = nullptr;
LiveIndicator *liveIndicator = nullptr;
QCheckBox *block = nullptr;
QCheckBox *ignoreHighlights = nullptr;
Label *notesPreview = nullptr;
+76
View File
@@ -0,0 +1,76 @@
#include "widgets/helper/LiveIndicator.hpp"
#include "singletons/Theme.hpp"
#include "util/Helpers.hpp"
#include <QPainter>
#include <QString>
namespace chatterino {
using namespace Qt::Literals;
LiveIndicator::LiveIndicator(QWidget *parent)
: BaseWidget(parent)
{
this->setMinimumHeight(5); // fixed min height for the circle to fit
this->setMouseTracking(true); // for hover and tooltip
this->updateScale();
}
void LiveIndicator::setViewers(int viewers)
{
this->setToolTip(u"Live with %1 viewers"_s.arg(localizeNumbers(viewers)));
this->updateScale();
}
void LiveIndicator::scaleChangedEvent(float /*newScale*/)
{
this->updateScale();
}
void LiveIndicator::paintEvent(QPaintEvent * /*event*/)
{
QPainter painter(this);
QColor color = getTheme()->tabs.liveIndicator;
// Indicate that there's a tooltip here
if (this->hovered)
{
if (getTheme()->isLightTheme())
{
color = color.darker(150);
}
else
{
color = color.lighter(150);
}
}
painter.setBrush(color);
painter.setPen(Qt::NoPen);
painter.setRenderHint(QPainter::Antialiasing);
painter.drawEllipse(QRect{
QPoint{0, 0},
QSize{5, 5} * this->scale(),
});
}
void LiveIndicator::enterEvent(QEnterEvent * /*event*/)
{
this->hovered = true;
this->update();
}
void LiveIndicator::leaveEvent(QEvent * /*event*/)
{
this->hovered = false;
this->update();
}
void LiveIndicator::updateScale()
{
this->setFixedWidth(qRound(6 * this->scale()));
this->update();
}
} // namespace chatterino
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include "widgets/BaseWidget.hpp"
namespace chatterino {
class LiveIndicator : public BaseWidget
{
public:
LiveIndicator(QWidget *parent = nullptr);
void setViewers(int viewers);
protected:
void scaleChangedEvent(float newScale) override;
void paintEvent(QPaintEvent *event) override;
void enterEvent(QEnterEvent *event) override;
void leaveEvent(QEvent *event) override;
private:
void updateScale();
bool hovered = false;
};
} // namespace chatterino
+52
View File
@@ -0,0 +1,52 @@
#include "widgets/helper/ScalingSpacerItem.hpp"
#include "singletons/Settings.hpp"
namespace chatterino {
ScalingSpacerItem *ScalingSpacerItem::horizontal(int baseWidth)
{
return new ScalingSpacerItem({baseWidth, 0}, QSizePolicy::Fixed,
QSizePolicy::Minimum);
}
ScalingSpacerItem *ScalingSpacerItem::vertical(int baseHeight)
{
return new ScalingSpacerItem({0, baseHeight}, QSizePolicy::Minimum,
QSizePolicy::Fixed);
}
ScalingSpacerItem::ScalingSpacerItem(QSize baseSize, QSizePolicy::Policy horiz,
QSizePolicy::Policy vert)
: QSpacerItem(
qRound(static_cast<float>(baseSize.width()) * getSettings()->uiScale),
qRound(static_cast<float>(baseSize.height()) *
getSettings()->uiScale),
horiz, vert)
, baseSize(baseSize)
, scaledSize(baseSize * getSettings()->uiScale)
, horizontalPolicy(horiz)
, verticalPolicy(vert)
{
getSettings()->uiScale.connect(
[this] {
this->refresh();
},
this->connections, false);
}
void ScalingSpacerItem::refresh()
{
auto nextSize = this->baseSize * getSettings()->uiScale;
if (nextSize == this->scaledSize)
{
return;
}
this->scaledSize = nextSize;
this->changeSize(this->scaledSize.width(), this->scaledSize.height(),
this->horizontalPolicy, this->verticalPolicy);
this->invalidate();
}
} // namespace chatterino
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include <pajlada/signals/scoped-connection.hpp>
#include <QSpacerItem>
#include <memory>
#include <vector>
namespace chatterino {
/// A QSpacerItem that scales with Chatterino's scale
class ScalingSpacerItem : public QSpacerItem
{
public:
static ScalingSpacerItem *horizontal(int baseWidth);
static ScalingSpacerItem *vertical(int baseHeight);
private:
ScalingSpacerItem(QSize baseSize, QSizePolicy::Policy horiz,
QSizePolicy::Policy vert);
void refresh();
QSize baseSize;
QSize scaledSize;
QSizePolicy::Policy horizontalPolicy;
QSizePolicy::Policy verticalPolicy;
std::vector<std::unique_ptr<pajlada::Signals::ScopedConnection>>
connections;
};
} // namespace chatterino
+1 -1
View File
@@ -315,7 +315,7 @@ void SplitHeader::initializeLayout()
w->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::Preferred);
w->setCentered(true);
w->setHasPadding(false);
w->setPadding(QMargins{});
}),
// space
makeWidget<BaseWidget>([](auto w) {