perf: smooth UI animations and reduce repaint overhead
Batch of performance and animation work: shared FadeHelper/MotionPrefs primitives, hover animators, shadow pixmap caching, smooth scrolling, seek time bubble, and widespread component repaint reductions.
This commit is contained in:
@@ -2,11 +2,15 @@
|
||||
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QPushButton>
|
||||
#include <QStyle>
|
||||
#include <QStyleOptionButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
#include "../utils/motionprefs.h"
|
||||
|
||||
namespace {
|
||||
class SectionToggleButton : public QPushButton {
|
||||
public:
|
||||
@@ -94,6 +98,16 @@ CollapsibleSection::CollapsibleSection(const QString &title, QWidget *parent)
|
||||
m_contentLayout->setSpacing(8);
|
||||
mainLayout->addWidget(m_content);
|
||||
|
||||
m_heightAnim = new QPropertyAnimation(m_content, "maximumHeight", this);
|
||||
m_heightAnim->setDuration(AnimationTokens::DurationMs::kNormal);
|
||||
connect(m_heightAnim, &QPropertyAnimation::finished, this, [this]() {
|
||||
if (m_expanded) {
|
||||
m_content->setMaximumHeight(QWIDGETSIZE_MAX);
|
||||
} else {
|
||||
m_content->setVisible(false);
|
||||
}
|
||||
});
|
||||
|
||||
updateToggleText();
|
||||
|
||||
connect(m_toggle, &QPushButton::clicked, this,
|
||||
@@ -101,9 +115,33 @@ CollapsibleSection::CollapsibleSection(const QString &title, QWidget *parent)
|
||||
}
|
||||
|
||||
void CollapsibleSection::setExpanded(bool expanded) {
|
||||
if (m_expanded == expanded) {
|
||||
updateToggleText();
|
||||
return;
|
||||
}
|
||||
m_expanded = expanded;
|
||||
|
||||
m_heightAnim->stop();
|
||||
if (MotionPrefs::reduceMotion()) {
|
||||
m_content->setMaximumHeight(QWIDGETSIZE_MAX);
|
||||
m_content->setVisible(expanded);
|
||||
updateToggleText();
|
||||
return;
|
||||
}
|
||||
|
||||
if (expanded) {
|
||||
m_content->setMaximumHeight(0);
|
||||
m_content->setVisible(true);
|
||||
m_heightAnim->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
m_heightAnim->setStartValue(0);
|
||||
m_heightAnim->setEndValue(m_content->sizeHint().height());
|
||||
} else {
|
||||
m_heightAnim->setEasingCurve(AnimationTokens::Curve::exit());
|
||||
m_heightAnim->setStartValue(m_content->height());
|
||||
m_heightAnim->setEndValue(0);
|
||||
}
|
||||
m_heightAnim->start();
|
||||
updateToggleText();
|
||||
}
|
||||
|
||||
void CollapsibleSection::updateToggleText() {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
class QPropertyAnimation;
|
||||
class QPushButton;
|
||||
class QVBoxLayout;
|
||||
|
||||
@@ -22,6 +23,7 @@ private:
|
||||
QPushButton* m_toggle;
|
||||
QWidget* m_content;
|
||||
QVBoxLayout* m_contentLayout;
|
||||
QPropertyAnimation* m_heightAnim = nullptr;
|
||||
bool m_expanded = false;
|
||||
QString m_title;
|
||||
};
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
#include "collectioncard.h"
|
||||
#include "collectiongrid.h"
|
||||
#include "../managers/thememanager.h"
|
||||
#include "../utils/hoveranimator.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
|
||||
namespace {
|
||||
QColor lerpColor(const QColor& from, const QColor& to, qreal t)
|
||||
{
|
||||
return QColor(qRound(from.red() + (to.red() - from.red()) * t),
|
||||
qRound(from.green() + (to.green() - from.green()) * t),
|
||||
qRound(from.blue() + (to.blue() - from.blue()) * t),
|
||||
qRound(from.alpha() + (to.alpha() - from.alpha()) * t));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -40,6 +51,9 @@ CollectionCard::CollectionCard(const QString& itemId,
|
||||
m_btnRename->setToolTip(tr("Rename"));
|
||||
m_btnDelete->setToolTip(tr("Delete"));
|
||||
|
||||
m_hover = new HoverAnimator(this);
|
||||
m_hover->setCallback([this](qreal) { update(); });
|
||||
|
||||
updateButtonLayout();
|
||||
|
||||
connect(m_btnRename, &QPushButton::clicked, this,
|
||||
@@ -115,6 +129,14 @@ void CollectionCard::paintEvent(QPaintEvent* event)
|
||||
QColor typeFg = isDark ? QColor("#94A3B8") : QColor("#64748B");
|
||||
const QColor accent("#3B82F6");
|
||||
|
||||
const qreal hp = m_hover->progress();
|
||||
if (hp > 0.0 && !m_selected && !m_dragging) {
|
||||
const QColor hoverBorder = isDark ? QColor(96, 165, 250, 190) : QColor(59, 130, 246, 190);
|
||||
const QColor hoverBg = isDark ? QColor(51, 65, 85, 190) : QColor(255, 255, 255, 255);
|
||||
border = lerpColor(border, hoverBorder, hp);
|
||||
cardBg = lerpColor(cardBg, hoverBg, hp);
|
||||
}
|
||||
|
||||
if (m_selected) {
|
||||
cardBg = isDark ? QColor(30, 41, 59, 220) : QColor(239, 246, 255, 245);
|
||||
border = isDark ? QColor(96, 165, 250, 220) : QColor(59, 130, 246, 220);
|
||||
@@ -298,6 +320,18 @@ void CollectionCard::resizeEvent(QResizeEvent* event)
|
||||
updateButtonLayout();
|
||||
}
|
||||
|
||||
void CollectionCard::enterEvent(QEnterEvent* event)
|
||||
{
|
||||
QFrame::enterEvent(event);
|
||||
m_hover->setHovered(true);
|
||||
}
|
||||
|
||||
void CollectionCard::leaveEvent(QEvent* event)
|
||||
{
|
||||
QFrame::leaveEvent(event);
|
||||
m_hover->setHovered(false);
|
||||
}
|
||||
|
||||
void CollectionCard::toggleSelected()
|
||||
{
|
||||
m_selected = !m_selected;
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include <QPushButton>
|
||||
|
||||
class CollectionGrid;
|
||||
class HoverAnimator;
|
||||
class QEnterEvent;
|
||||
class QMouseEvent;
|
||||
class QResizeEvent;
|
||||
|
||||
@@ -44,6 +46,8 @@ protected:
|
||||
void mouseMoveEvent(QMouseEvent* event) override;
|
||||
void mouseReleaseEvent(QMouseEvent* event) override;
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
void enterEvent(QEnterEvent* event) override;
|
||||
void leaveEvent(QEvent* event) override;
|
||||
|
||||
private:
|
||||
void updateButtonLayout();
|
||||
@@ -61,6 +65,7 @@ private:
|
||||
|
||||
QPushButton* m_btnRename;
|
||||
QPushButton* m_btnDelete;
|
||||
HoverAnimator* m_hover = nullptr;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include <QScrollBar>
|
||||
#include <QSet>
|
||||
#include <QTimer>
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
#include "../utils/motionprefs.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -249,12 +251,12 @@ void CollectionGrid::relayout(bool animate)
|
||||
}
|
||||
|
||||
QPoint target = posForIndex(i, m_columns);
|
||||
if (animate && card->pos() != target) {
|
||||
if (animate && !MotionPrefs::reduceMotion() && card->pos() != target) {
|
||||
stopPosAnimations(card);
|
||||
|
||||
auto* anim = new QPropertyAnimation(card, "pos", card);
|
||||
anim->setDuration(220);
|
||||
anim->setEasingCurve(QEasingCurve::OutCubic);
|
||||
anim->setDuration(AnimationTokens::DurationMs::kNormal);
|
||||
anim->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
anim->setEndValue(target);
|
||||
anim->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
} else {
|
||||
@@ -319,11 +321,15 @@ void CollectionGrid::onCardDragEnd(CollectionCard* card)
|
||||
|
||||
stopPosAnimations(card);
|
||||
const QPoint target = posForIndex(finalIndex, m_columns);
|
||||
if (MotionPrefs::reduceMotion()) {
|
||||
card->move(target);
|
||||
} else {
|
||||
auto* anim = new QPropertyAnimation(card, "pos", card);
|
||||
anim->setDuration(180);
|
||||
anim->setEasingCurve(QEasingCurve::OutCubic);
|
||||
anim->setDuration(AnimationTokens::DurationMs::kFast);
|
||||
anim->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
anim->setEndValue(target);
|
||||
anim->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
}
|
||||
|
||||
if (changed && dragCard) {
|
||||
QStringList newOrderIds;
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include <QPropertyAnimation>
|
||||
#include <QResizeEvent>
|
||||
#include <QStyle>
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
#include "../utils/motionprefs.h"
|
||||
|
||||
namespace {
|
||||
constexpr int StripHeight = 36;
|
||||
@@ -16,7 +18,7 @@ constexpr int ChipHeight = 28;
|
||||
constexpr int ChipSpacing = 8;
|
||||
constexpr int ChipHorizontalPadding = 12;
|
||||
constexpr int DragPreviewYOffset = 5;
|
||||
constexpr int ChipAnimationDuration = 220;
|
||||
constexpr int ChipAnimationDuration = AnimationTokens::DurationMs::kNormal;
|
||||
constexpr qreal DragPreviewOpacity = 0.82;
|
||||
}
|
||||
|
||||
@@ -485,14 +487,14 @@ void DashboardSectionOrderStrip::moveWidgetTo(QWidget* widget,
|
||||
}
|
||||
|
||||
stopPosAnimations(widget);
|
||||
if (!animate || widget->pos() == targetPos) {
|
||||
if (!animate || MotionPrefs::reduceMotion() || widget->pos() == targetPos) {
|
||||
widget->move(targetPos);
|
||||
return;
|
||||
}
|
||||
|
||||
auto* animation = new QPropertyAnimation(widget, "pos", widget);
|
||||
animation->setDuration(duration);
|
||||
animation->setEasingCurve(QEasingCurve::OutCubic);
|
||||
animation->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
animation->setEndValue(targetPos);
|
||||
animation->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "../managers/thememanager.h"
|
||||
#include "../utils/mediasourcepreferenceutils.h"
|
||||
#include "../utils/playerpreferenceutils.h"
|
||||
#include "../utils/popfeedback.h"
|
||||
#include "modernmenubutton.h"
|
||||
#include "splitplayerbutton.h"
|
||||
#include <QFileInfo>
|
||||
@@ -105,10 +106,18 @@ DetailActionWidget::DetailActionWidget(QWidget *parent) : QWidget(parent) {
|
||||
&DetailActionWidget::playRequested);
|
||||
connect(m_resumeBtn, &QPushButton::clicked, this,
|
||||
&DetailActionWidget::resumeRequested);
|
||||
connect(m_favBtn, &QPushButton::clicked, this,
|
||||
&DetailActionWidget::favoriteRequested);
|
||||
connect(m_playedBtn, &QPushButton::clicked, this,
|
||||
&DetailActionWidget::playedToggleRequested);
|
||||
connect(m_favBtn, &QPushButton::clicked, this, [this]() {
|
||||
PopFeedback::play(m_favBtn, [btn = m_favBtn](qreal scale) {
|
||||
btn->setIconSize(QSize(qRound(20 * scale), qRound(20 * scale)));
|
||||
});
|
||||
emit favoriteRequested();
|
||||
});
|
||||
connect(m_playedBtn, &QPushButton::clicked, this, [this]() {
|
||||
PopFeedback::play(m_playedBtn, [btn = m_playedBtn](qreal scale) {
|
||||
btn->setIconSize(QSize(qRound(20 * scale), qRound(20 * scale)));
|
||||
});
|
||||
emit playedToggleRequested();
|
||||
});
|
||||
connect(m_versionComboBox, &ModernMenuButton::currentIndexChanged, this,
|
||||
&DetailActionWidget::sourceVersionChanged);
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
#include <QPainter>
|
||||
#include <QStyleOption>
|
||||
#include <QLinearGradient>
|
||||
#include <QPropertyAnimation>
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
#include "../utils/motionprefs.h"
|
||||
|
||||
DetailContentWidget::DetailContentWidget(QWidget* parent)
|
||||
: QWidget(parent),
|
||||
@@ -14,8 +17,37 @@ DetailContentWidget::DetailContentWidget(QWidget* parent)
|
||||
}
|
||||
|
||||
void DetailContentWidget::setBackdrop(const QPixmap& pix) {
|
||||
const bool hadBackdrop = !m_backdrop.isNull();
|
||||
m_backdrop = pix;
|
||||
m_cachedWidth = -1;
|
||||
|
||||
// Fade the hero backdrop in only on first reveal (null -> image), not when a
|
||||
// network refresh replaces an already-shown backdrop (that would flicker).
|
||||
if (!pix.isNull() && !hadBackdrop) {
|
||||
if (MotionPrefs::reduceMotion()) {
|
||||
m_backdropOpacity = 1.0;
|
||||
} else {
|
||||
if (!m_backdropFade) {
|
||||
m_backdropFade = new QPropertyAnimation(this, "backdropOpacity", this);
|
||||
m_backdropFade->setDuration(AnimationTokens::DurationMs::kNormal);
|
||||
m_backdropFade->setEasingCurve(AnimationTokens::Curve::fade());
|
||||
}
|
||||
m_backdropFade->stop();
|
||||
m_backdropFade->setStartValue(0.0);
|
||||
m_backdropFade->setEndValue(1.0);
|
||||
m_backdropFade->start();
|
||||
}
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
qreal DetailContentWidget::backdropOpacity() const {
|
||||
return m_backdropOpacity;
|
||||
}
|
||||
|
||||
void DetailContentWidget::setBackdropOpacity(qreal opacity) {
|
||||
m_backdropOpacity = opacity;
|
||||
update();
|
||||
}
|
||||
|
||||
@@ -73,7 +105,9 @@ void DetailContentWidget::paintEvent(QPaintEvent* event) {
|
||||
}
|
||||
|
||||
|
||||
painter.setOpacity(m_backdropOpacity);
|
||||
painter.drawPixmap(0, 0, m_scaledBackdrop);
|
||||
painter.setOpacity(1.0);
|
||||
|
||||
|
||||
QLinearGradient gradient(0, 0, 0, targetHeight);
|
||||
|
||||
@@ -5,10 +5,13 @@
|
||||
#include <QPixmap>
|
||||
#include <QColor>
|
||||
|
||||
class QPropertyAnimation;
|
||||
|
||||
class DetailContentWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QColor baseColor READ baseColor WRITE setBaseColor)
|
||||
Q_PROPERTY(QColor gradientColor READ gradientColor WRITE setGradientColor)
|
||||
Q_PROPERTY(qreal backdropOpacity READ backdropOpacity WRITE setBackdropOpacity)
|
||||
|
||||
public:
|
||||
explicit DetailContentWidget(QWidget* parent = nullptr);
|
||||
@@ -23,6 +26,9 @@ public:
|
||||
QColor gradientColor() const;
|
||||
void setGradientColor(const QColor& color);
|
||||
|
||||
qreal backdropOpacity() const;
|
||||
void setBackdropOpacity(qreal opacity);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
|
||||
@@ -34,6 +40,9 @@ private:
|
||||
|
||||
QColor m_baseColor;
|
||||
QColor m_gradientColor;
|
||||
|
||||
qreal m_backdropOpacity = 1.0;
|
||||
QPropertyAnimation* m_backdropFade = nullptr;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -8,6 +8,28 @@
|
||||
#include <QScrollBar>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QEvent>
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
#include "../utils/fadehelper.h"
|
||||
#include "../utils/motionprefs.h"
|
||||
|
||||
namespace {
|
||||
// Fade the nav arrows instead of popping; only reacts to desired-visibility
|
||||
// changes (this runs on every mouse move).
|
||||
void setNavButtonVisible(QPushButton* btn, bool visible)
|
||||
{
|
||||
const QVariant last = btn->property("navVisible");
|
||||
if (!last.isNull() && last.toBool() == visible) {
|
||||
return;
|
||||
}
|
||||
btn->setProperty("navVisible", visible);
|
||||
if (visible) {
|
||||
FadeHelper::fadeIn(btn, AnimationTokens::DurationMs::kQuick);
|
||||
} else {
|
||||
FadeHelper::fadeOut(btn, AnimationTokens::DurationMs::kMicro,
|
||||
AnimationTokens::Curve::exit(), [btn]() { btn->hide(); });
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
#include <QWheelEvent>
|
||||
#include <QCursor>
|
||||
#include <QScroller>
|
||||
@@ -65,8 +87,8 @@ HorizontalListViewGallery::HorizontalListViewGallery(QEmbyCore* core, QWidget* p
|
||||
|
||||
|
||||
m_hScrollAnim = new QPropertyAnimation(m_listView->horizontalScrollBar(), "value", this);
|
||||
m_hScrollAnim->setEasingCurve(QEasingCurve::OutCubic);
|
||||
m_hScrollAnim->setDuration(450);
|
||||
m_hScrollAnim->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
m_hScrollAnim->setDuration(AnimationTokens::DurationMs::kSlow);
|
||||
|
||||
mainLayout->addWidget(m_listView);
|
||||
|
||||
@@ -122,9 +144,13 @@ HorizontalListViewGallery::HorizontalListViewGallery(QEmbyCore* core, QWidget* p
|
||||
int targetValue = bar->value() + directionMultiplier * step;
|
||||
targetValue = qBound(0, targetValue, bar->maximum());
|
||||
|
||||
if (MotionPrefs::reduceMotion()) {
|
||||
bar->setValue(targetValue);
|
||||
return;
|
||||
}
|
||||
auto* anim = new QPropertyAnimation(bar, "value", this);
|
||||
anim->setDuration(400);
|
||||
anim->setEasingCurve(QEasingCurve::OutCubic);
|
||||
anim->setDuration(AnimationTokens::DurationMs::kSlow);
|
||||
anim->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
anim->setStartValue(bar->value());
|
||||
anim->setEndValue(targetValue);
|
||||
anim->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
@@ -166,9 +192,15 @@ void HorizontalListViewGallery::setItems(const QList<MediaItem>& items)
|
||||
}
|
||||
|
||||
if (m_shimmer && m_shimmer->isVisible() && !items.isEmpty()) {
|
||||
// Cross-fade the skeleton out to reveal the now-populated list underneath.
|
||||
FadeHelper::fadeOut(m_shimmer, AnimationTokens::DurationMs::kNormal,
|
||||
AnimationTokens::Curve::fade(), [this]() {
|
||||
if (m_shimmer) {
|
||||
m_shimmer->stopAnimation();
|
||||
m_shimmer->hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -360,8 +392,17 @@ void HorizontalListViewGallery::setLoading(bool loading)
|
||||
m_cardStyle == MediaCardDelegate::Cast);
|
||||
m_shimmer->setGeometry(m_listView->geometry());
|
||||
m_shimmer->raise();
|
||||
m_shimmer->show();
|
||||
// Show + snap opacity back to 1.0 (a prior reveal fade left the effect at 0).
|
||||
FadeHelper::fadeIn(m_shimmer, 0);
|
||||
m_shimmer->startAnimation();
|
||||
} else if (m_shimmer->isVisible()) {
|
||||
FadeHelper::fadeOut(m_shimmer, AnimationTokens::DurationMs::kNormal,
|
||||
AnimationTokens::Curve::fade(), [this]() {
|
||||
if (m_shimmer) {
|
||||
m_shimmer->stopAnimation();
|
||||
m_shimmer->hide();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
m_shimmer->stopAnimation();
|
||||
m_shimmer->hide();
|
||||
@@ -420,8 +461,8 @@ bool HorizontalListViewGallery::eventFilter(QObject* obj, QEvent* event)
|
||||
} else if (event->type() == QEvent::Leave) {
|
||||
QPoint globalPos = QCursor::pos();
|
||||
if (!this->rect().contains(this->mapFromGlobal(globalPos))) {
|
||||
m_btnLeft->hide();
|
||||
m_btnRight->hide();
|
||||
setNavButtonVisible(m_btnLeft, false);
|
||||
setNavButtonVisible(m_btnRight, false);
|
||||
}
|
||||
} else if (obj == m_listView->viewport() && event->type() == QEvent::ToolTip) {
|
||||
return TextWrapUtils::showWrappedMediaItemToolTip(m_listView, event);
|
||||
@@ -447,11 +488,15 @@ bool HorizontalListViewGallery::eventFilter(QObject* obj, QEvent* event)
|
||||
if (newTarget != hBar->value()) {
|
||||
m_hScrollTarget = newTarget;
|
||||
m_hScrollAnim->stop();
|
||||
if (MotionPrefs::reduceMotion()) {
|
||||
hBar->setValue(m_hScrollTarget);
|
||||
} else {
|
||||
m_hScrollAnim->setStartValue(hBar->value());
|
||||
m_hScrollAnim->setEndValue(m_hScrollTarget);
|
||||
m_hScrollAnim->start();
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
|
||||
@@ -468,8 +513,8 @@ void HorizontalListViewGallery::updateButtonsVisibility()
|
||||
QPoint localPos = this->mapFromGlobal(globalPos);
|
||||
|
||||
if (!this->rect().contains(localPos)) {
|
||||
m_btnLeft->hide();
|
||||
m_btnRight->hide();
|
||||
setNavButtonVisible(m_btnLeft, false);
|
||||
setNavButtonVisible(m_btnRight, false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -478,8 +523,8 @@ void HorizontalListViewGallery::updateButtonsVisibility()
|
||||
|
||||
bool isLeftHalf = localPos.x() < (currentWidth / 2);
|
||||
|
||||
m_btnLeft->setVisible(isLeftHalf && bar->value() > 0);
|
||||
m_btnRight->setVisible(!isLeftHalf && bar->value() < bar->maximum());
|
||||
setNavButtonVisible(m_btnLeft, isLeftHalf && bar->value() > 0);
|
||||
setNavButtonVisible(m_btnRight, !isLeftHalf && bar->value() < bar->maximum());
|
||||
}
|
||||
|
||||
void HorizontalListViewGallery::updateVisibleImagePriority()
|
||||
|
||||
@@ -6,6 +6,28 @@
|
||||
#include <QPropertyAnimation>
|
||||
#include <QEvent>
|
||||
#include <QWheelEvent>
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
#include "../utils/fadehelper.h"
|
||||
#include "../utils/motionprefs.h"
|
||||
|
||||
namespace {
|
||||
// Fade the nav arrows instead of popping; only reacts to desired-visibility
|
||||
// changes (this runs on every mouse move).
|
||||
void setNavButtonVisible(QPushButton* btn, bool visible)
|
||||
{
|
||||
const QVariant last = btn->property("navVisible");
|
||||
if (!last.isNull() && last.toBool() == visible) {
|
||||
return;
|
||||
}
|
||||
btn->setProperty("navVisible", visible);
|
||||
if (visible) {
|
||||
FadeHelper::fadeIn(btn, AnimationTokens::DurationMs::kQuick);
|
||||
} else {
|
||||
FadeHelper::fadeOut(btn, AnimationTokens::DurationMs::kMicro,
|
||||
AnimationTokens::Curve::exit(), [btn]() { btn->hide(); });
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
#include <QTimer>
|
||||
#include <QCursor>
|
||||
#include <QScroller>
|
||||
@@ -52,8 +74,8 @@ HorizontalWidgetGallery::HorizontalWidgetGallery(QWidget* parent)
|
||||
|
||||
|
||||
m_hScrollAnim = new QPropertyAnimation(m_scrollArea->horizontalScrollBar(), "value", this);
|
||||
m_hScrollAnim->setEasingCurve(QEasingCurve::OutCubic);
|
||||
m_hScrollAnim->setDuration(450);
|
||||
m_hScrollAnim->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
m_hScrollAnim->setDuration(AnimationTokens::DurationMs::kSlow);
|
||||
|
||||
m_contentWidget = new QWidget(m_scrollArea);
|
||||
m_contentWidget->setObjectName("gallery-content-widget");
|
||||
@@ -96,9 +118,13 @@ HorizontalWidgetGallery::HorizontalWidgetGallery(QWidget* parent)
|
||||
int targetValue = bar->value() + directionMultiplier * step;
|
||||
targetValue = qBound(0, targetValue, bar->maximum());
|
||||
|
||||
if (MotionPrefs::reduceMotion()) {
|
||||
bar->setValue(targetValue);
|
||||
return;
|
||||
}
|
||||
auto* anim = new QPropertyAnimation(bar, "value", this);
|
||||
anim->setDuration(400);
|
||||
anim->setEasingCurve(QEasingCurve::OutCubic);
|
||||
anim->setDuration(AnimationTokens::DurationMs::kSlow);
|
||||
anim->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
anim->setStartValue(bar->value());
|
||||
anim->setEndValue(targetValue);
|
||||
anim->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
@@ -192,8 +218,8 @@ bool HorizontalWidgetGallery::eventFilter(QObject* obj, QEvent* event)
|
||||
} else if (event->type() == QEvent::Leave) {
|
||||
QPoint globalPos = QCursor::pos();
|
||||
if (!this->rect().contains(this->mapFromGlobal(globalPos))) {
|
||||
m_btnLeft->hide();
|
||||
m_btnRight->hide();
|
||||
setNavButtonVisible(m_btnLeft, false);
|
||||
setNavButtonVisible(m_btnRight, false);
|
||||
}
|
||||
} else if (event->type() == QEvent::Wheel) {
|
||||
|
||||
@@ -213,11 +239,15 @@ bool HorizontalWidgetGallery::eventFilter(QObject* obj, QEvent* event)
|
||||
if (newTarget != hBar->value()) {
|
||||
m_hScrollTarget = newTarget;
|
||||
m_hScrollAnim->stop();
|
||||
if (MotionPrefs::reduceMotion()) {
|
||||
hBar->setValue(m_hScrollTarget);
|
||||
} else {
|
||||
m_hScrollAnim->setStartValue(hBar->value());
|
||||
m_hScrollAnim->setEndValue(m_hScrollTarget);
|
||||
m_hScrollAnim->start();
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
|
||||
@@ -240,8 +270,8 @@ void HorizontalWidgetGallery::updateButtonsVisibility()
|
||||
QPoint localPos = this->mapFromGlobal(globalPos);
|
||||
|
||||
if (!this->rect().contains(localPos)) {
|
||||
m_btnLeft->hide();
|
||||
m_btnRight->hide();
|
||||
setNavButtonVisible(m_btnLeft, false);
|
||||
setNavButtonVisible(m_btnRight, false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -250,6 +280,6 @@ void HorizontalWidgetGallery::updateButtonsVisibility()
|
||||
|
||||
bool isLeftHalf = localPos.x() < (currentWidth / 2);
|
||||
|
||||
m_btnLeft->setVisible(isLeftHalf && bar->value() > 0);
|
||||
m_btnRight->setVisible(!isLeftHalf && bar->value() < bar->maximum());
|
||||
setNavButtonVisible(m_btnLeft, isLeftHalf && bar->value() > 0);
|
||||
setNavButtonVisible(m_btnRight, !isLeftHalf && bar->value() < bar->maximum());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "libraryactionmenu.h"
|
||||
|
||||
#include "../managers/thememanager.h"
|
||||
#include "../utils/popupfade.h"
|
||||
#include <QAction>
|
||||
#include <QFile>
|
||||
#include <QIcon>
|
||||
@@ -16,6 +17,8 @@ LibraryActionMenu::LibraryActionMenu(const VirtualFolder& folder,
|
||||
Qt::NoDropShadowWindowHint);
|
||||
setAttribute(Qt::WA_TranslucentBackground);
|
||||
|
||||
PopupFade::attach(this);
|
||||
|
||||
setupMenu();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#include "loadingoverlay.h"
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
#include "../utils/motionprefs.h"
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QEasingCurve>
|
||||
#include <QConicalGradient>
|
||||
#include <QLinearGradient>
|
||||
#include <QMouseEvent>
|
||||
@@ -14,17 +15,26 @@ LoadingOverlay::LoadingOverlay(QWidget* parent) : QWidget(parent) {
|
||||
m_rotateAnim = new QPropertyAnimation(this, "angle", this);
|
||||
m_rotateAnim->setStartValue(0.0);
|
||||
m_rotateAnim->setEndValue(360.0);
|
||||
m_rotateAnim->setDuration(1000);
|
||||
m_rotateAnim->setDuration(AnimationTokens::DurationMs::kGentle);
|
||||
m_rotateAnim->setLoopCount(-1);
|
||||
|
||||
m_fadeAnim = new QPropertyAnimation(this, "opacity", this);
|
||||
m_fadeAnim->setDuration(300);
|
||||
m_fadeAnim->setEasingCurve(QEasingCurve::InOutQuad);
|
||||
m_fadeAnim->setDuration(AnimationTokens::DurationMs::kNormal);
|
||||
m_fadeAnim->setEasingCurve(AnimationTokens::Curve::fade());
|
||||
}
|
||||
|
||||
void LoadingOverlay::start() {
|
||||
show();
|
||||
raise();
|
||||
|
||||
if (MotionPrefs::reduceMotion()) {
|
||||
// Reduce-motion: static indicator, no spin, no fade.
|
||||
m_fadeAnim->stop();
|
||||
m_opacity = 1.0;
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
m_rotateAnim->start();
|
||||
|
||||
m_fadeAnim->disconnect();
|
||||
@@ -34,6 +44,11 @@ void LoadingOverlay::start() {
|
||||
}
|
||||
|
||||
void LoadingOverlay::stop() {
|
||||
if (MotionPrefs::reduceMotion()) {
|
||||
forceStop();
|
||||
return;
|
||||
}
|
||||
|
||||
m_fadeAnim->disconnect();
|
||||
connect(m_fadeAnim, &QAbstractAnimation::finished, this, [this]() {
|
||||
hide();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "../utils/mediaidentifyutils.h"
|
||||
#include "../utils/mediaitemutils.h"
|
||||
#include "../utils/playlistutils.h"
|
||||
#include "../utils/popupfade.h"
|
||||
#include <qembycore.h>
|
||||
#include <services/manager/servermanager.h>
|
||||
#include <models/profile/serverprofile.h>
|
||||
@@ -33,6 +34,8 @@ MediaActionMenu::MediaActionMenu(const MediaItem& item, QEmbyCore* core,
|
||||
|
||||
setAttribute(Qt::WA_TranslucentBackground);
|
||||
|
||||
PopupFade::attach(this);
|
||||
|
||||
setupMenu();
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
#include <QSet>
|
||||
#include <QStyleOptionViewItem>
|
||||
#include <algorithm>
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
#include "../utils/fadehelper.h"
|
||||
|
||||
MediaGridWidget::MediaGridWidget(QEmbyCore* core, QWidget* parent)
|
||||
: QWidget(parent), m_core(core), m_basePadding(20), m_currentStyle(MediaCardDelegate::Poster),
|
||||
@@ -59,7 +61,7 @@ MediaGridWidget::MediaGridWidget(QEmbyCore* core, QWidget* parent)
|
||||
|
||||
m_vScrollController =
|
||||
new SmoothScrollController(m_listView->verticalScrollBar(), this);
|
||||
m_vScrollController->setDuration(160);
|
||||
m_vScrollController->setDuration(AnimationTokens::DurationMs::kFast);
|
||||
|
||||
|
||||
m_listView->viewport()->installEventFilter(this);
|
||||
@@ -141,8 +143,17 @@ void MediaGridWidget::setLoading(bool loading)
|
||||
m_shimmer->setShowSubtitle(false);
|
||||
m_shimmer->setGeometry(m_listView->geometry());
|
||||
m_shimmer->raise();
|
||||
m_shimmer->show();
|
||||
// Show + snap opacity back to 1.0 (a prior reveal fade left the effect at 0).
|
||||
FadeHelper::fadeIn(m_shimmer, 0);
|
||||
m_shimmer->startAnimation();
|
||||
} else if (m_shimmer->isVisible()) {
|
||||
FadeHelper::fadeOut(m_shimmer, AnimationTokens::DurationMs::kNormal,
|
||||
AnimationTokens::Curve::fade(), [this]() {
|
||||
if (m_shimmer) {
|
||||
m_shimmer->stopAnimation();
|
||||
m_shimmer->hide();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
m_shimmer->stopAnimation();
|
||||
m_shimmer->hide();
|
||||
@@ -155,9 +166,15 @@ void MediaGridWidget::setItems(const QList<MediaItem>& items) {
|
||||
|
||||
|
||||
if (m_shimmer && m_shimmer->isVisible() && !items.isEmpty()) {
|
||||
// Cross-fade the skeleton out to reveal the now-populated grid underneath.
|
||||
FadeHelper::fadeOut(m_shimmer, AnimationTokens::DurationMs::kNormal,
|
||||
AnimationTokens::Curve::fade(), [this]() {
|
||||
if (m_shimmer) {
|
||||
m_shimmer->stopAnimation();
|
||||
m_shimmer->hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!items.isEmpty()) {
|
||||
QMetaObject::invokeMethod(
|
||||
this,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "moderncombobox.h"
|
||||
#include "../utils/fadehelper.h"
|
||||
#include "../utils/popupfade.h"
|
||||
#include <QAbstractItemView>
|
||||
#include <QApplication>
|
||||
#include <QEvent>
|
||||
@@ -132,6 +134,8 @@ void ModernComboBox::showPopup() {
|
||||
return;
|
||||
}
|
||||
|
||||
PopupFade::run(popupContainer);
|
||||
|
||||
|
||||
|
||||
auto *vScrollBar = popupView->verticalScrollBar();
|
||||
@@ -267,6 +271,8 @@ void ModernComboBox::showEmbeddedPopup() {
|
||||
overlay->show();
|
||||
overlay->raise();
|
||||
listFrame->show();
|
||||
// Child overlay (not a top-level window) — FadeHelper, not PopupFade.
|
||||
FadeHelper::fadeIn(listFrame, AnimationTokens::DurationMs::kQuick);
|
||||
listWidget->setFocus();
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "modernmenubutton.h"
|
||||
#include "../utils/popupfade.h"
|
||||
#include <QAction>
|
||||
#include <QLabel>
|
||||
#include <QStyle>
|
||||
@@ -91,6 +92,7 @@ ModernMenuButton::ModernMenuButton(QWidget *parent)
|
||||
m_menu->setWindowFlags(Qt::Popup | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint);
|
||||
m_menu->setAttribute(Qt::WA_TranslucentBackground, true);
|
||||
m_menu->setAutoFillBackground(false);
|
||||
PopupFade::attach(m_menu);
|
||||
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "modernslider.h"
|
||||
#include "../utils/hoveranimator.h"
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
|
||||
@@ -7,6 +8,9 @@ ModernSlider::ModernSlider(Qt::Orientation orientation, QWidget *parent)
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
setMouseTracking(true);
|
||||
setAttribute(Qt::WA_Hover, true);
|
||||
|
||||
m_hover = new HoverAnimator(this);
|
||||
m_hover->setCallback([this](qreal) { update(); });
|
||||
}
|
||||
|
||||
void ModernSlider::setBufferValue(int value) {
|
||||
@@ -56,17 +60,19 @@ void ModernSlider::mouseReleaseEvent(QMouseEvent *ev) {
|
||||
}
|
||||
|
||||
void ModernSlider::mouseMoveEvent(QMouseEvent *ev) {
|
||||
if (!m_isPressed) {
|
||||
emit hoverMoved(ev->pos());
|
||||
}
|
||||
QSlider::mouseMoveEvent(ev);
|
||||
}
|
||||
|
||||
|
||||
bool ModernSlider::event(QEvent *ev) {
|
||||
if (ev->type() == QEvent::Enter) {
|
||||
m_isHovered = true;
|
||||
update();
|
||||
m_hover->setHovered(true);
|
||||
} else if (ev->type() == QEvent::Leave) {
|
||||
m_isHovered = false;
|
||||
update();
|
||||
m_hover->setHovered(false);
|
||||
emit hoverLeft();
|
||||
}
|
||||
return QSlider::event(ev);
|
||||
}
|
||||
@@ -77,10 +83,12 @@ void ModernSlider::paintEvent(QPaintEvent *ev) {
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
const int trackHeight =
|
||||
(m_isHovered || m_isPressed) ? m_hoverTrackHeight : m_normalTrackHeight;
|
||||
const int handleRadius =
|
||||
(m_isHovered || m_isPressed) ? m_activeHandleRadius : m_normalHandleRadius;
|
||||
// Pressed shows the active size instantly; hover grows/shrinks smoothly.
|
||||
const qreal hp = m_isPressed ? 1.0 : m_hover->progress();
|
||||
const qreal trackHeight =
|
||||
m_normalTrackHeight + hp * (m_hoverTrackHeight - m_normalTrackHeight);
|
||||
const qreal handleRadius =
|
||||
m_normalHandleRadius + hp * (m_activeHandleRadius - m_normalHandleRadius);
|
||||
const int edgePadding = qMax(m_normalHandleRadius, m_activeHandleRadius);
|
||||
|
||||
QRectF trackRect;
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <QColor>
|
||||
#include <QtGlobal>
|
||||
|
||||
class HoverAnimator;
|
||||
|
||||
class ModernSlider : public QSlider {
|
||||
Q_OBJECT
|
||||
|
||||
@@ -62,6 +64,12 @@ public:
|
||||
update();
|
||||
}
|
||||
|
||||
Q_SIGNALS:
|
||||
// Cursor position while hovering the (un-pressed) slider — drives hover
|
||||
// affordances like the player's seek time bubble.
|
||||
void hoverMoved(const QPoint &pos);
|
||||
void hoverLeft();
|
||||
|
||||
protected:
|
||||
void mousePressEvent(QMouseEvent *ev) override;
|
||||
void mouseReleaseEvent(QMouseEvent *ev) override;
|
||||
@@ -71,8 +79,8 @@ protected:
|
||||
|
||||
private:
|
||||
int m_bufferValue = 0;
|
||||
bool m_isHovered = false;
|
||||
bool m_isPressed = false;
|
||||
HoverAnimator *m_hover = nullptr;
|
||||
|
||||
|
||||
QColor m_themeColor = QColor("#3B82F6");
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#include "modernsortbutton.h"
|
||||
#include "../utils/popupfade.h"
|
||||
#include <QAction>
|
||||
|
||||
ModernSortButton::ModernSortButton(QWidget *parent)
|
||||
: QPushButton(parent), m_currentIndex(-1), m_descending(true)
|
||||
{
|
||||
m_menu = new QMenu(this);
|
||||
PopupFade::attach(m_menu);
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "modernswitch.h"
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
#include "../utils/motionprefs.h"
|
||||
#include <QPainterPath>
|
||||
|
||||
ModernSwitch::ModernSwitch(QWidget* parent)
|
||||
@@ -14,8 +16,10 @@ ModernSwitch::ModernSwitch(QWidget* parent)
|
||||
|
||||
|
||||
m_animation = new QPropertyAnimation(this, "offset", this);
|
||||
m_animation->setDuration(150);
|
||||
m_animation->setEasingCurve(QEasingCurve::InOutSine);
|
||||
m_animation->setDuration(AnimationTokens::DurationMs::kMicro);
|
||||
// A binary toggle wants a decisive settle, not the flat slow-in/slow-out of
|
||||
// InOutSine; OutCubic reads as a proper switch.
|
||||
m_animation->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
|
||||
connect(this, &ModernSwitch::toggled, this, &ModernSwitch::updateAnimation);
|
||||
}
|
||||
@@ -110,6 +114,10 @@ void ModernSwitch::leaveEvent(QEvent* event) {
|
||||
|
||||
void ModernSwitch::updateAnimation(bool checked) {
|
||||
m_animation->stop();
|
||||
if (MotionPrefs::reduceMotion()) {
|
||||
setOffset(checked ? 1.0 : 0.0);
|
||||
return;
|
||||
}
|
||||
m_animation->setStartValue(m_offset);
|
||||
m_animation->setEndValue(checked ? 1.0 : 0.0);
|
||||
m_animation->start();
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include <QPainter>
|
||||
#include <QStyleOption>
|
||||
#include <QEasingCurve>
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
#include "../utils/motionprefs.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -66,12 +68,12 @@ ModernToast::ModernToast(QWidget *parent)
|
||||
|
||||
|
||||
m_showOpacity = new QPropertyAnimation(this, "windowOpacity", this);
|
||||
m_showOpacity->setDuration(200);
|
||||
m_showOpacity->setEasingCurve(QEasingCurve::OutQuad);
|
||||
m_showOpacity->setDuration(AnimationTokens::DurationMs::kFast);
|
||||
m_showOpacity->setEasingCurve(AnimationTokens::Curve::fade());
|
||||
|
||||
m_showSlide = new QPropertyAnimation(this, "geometry", this);
|
||||
m_showSlide->setDuration(350);
|
||||
QEasingCurve slideCurve(QEasingCurve::OutBack);
|
||||
m_showSlide->setDuration(AnimationTokens::DurationMs::kSlow);
|
||||
QEasingCurve slideCurve(AnimationTokens::Curve::bounce());
|
||||
slideCurve.setOvershoot(1.2);
|
||||
m_showSlide->setEasingCurve(slideCurve);
|
||||
|
||||
@@ -83,16 +85,16 @@ ModernToast::ModernToast(QWidget *parent)
|
||||
|
||||
|
||||
m_crtSquashY = new QPropertyAnimation(this, "geometry", this);
|
||||
m_crtSquashY->setDuration(160);
|
||||
m_crtSquashY->setEasingCurve(QEasingCurve::InCubic);
|
||||
m_crtSquashY->setDuration(AnimationTokens::DurationMs::kFast);
|
||||
m_crtSquashY->setEasingCurve(AnimationTokens::Curve::exit());
|
||||
|
||||
m_crtSquashX = new QPropertyAnimation(this, "geometry", this);
|
||||
m_crtSquashX->setDuration(120);
|
||||
m_crtSquashX->setEasingCurve(QEasingCurve::InExpo);
|
||||
m_crtSquashX->setDuration(AnimationTokens::DurationMs::kMicro);
|
||||
m_crtSquashX->setEasingCurve(AnimationTokens::Curve::exit());
|
||||
|
||||
m_hideOpacity = new QPropertyAnimation(this, "windowOpacity", this);
|
||||
m_hideOpacity->setDuration(120);
|
||||
m_hideOpacity->setEasingCurve(QEasingCurve::InCubic);
|
||||
m_hideOpacity->setDuration(AnimationTokens::DurationMs::kMicro);
|
||||
m_hideOpacity->setEasingCurve(AnimationTokens::Curve::exit());
|
||||
|
||||
m_crtPhase2Group = new QParallelAnimationGroup(this);
|
||||
m_crtPhase2Group->addAnimation(m_crtSquashX);
|
||||
@@ -116,6 +118,19 @@ ModernToast::ModernToast(QWidget *parent)
|
||||
m_stayTimer = new QTimer(this);
|
||||
m_stayTimer->setSingleShot(true);
|
||||
connect(m_stayTimer, &QTimer::timeout, this, [this]() {
|
||||
if (MotionPrefs::reduceMotion()) {
|
||||
// Reduce-motion: dismiss instantly, no CRT squash.
|
||||
m_hideGroup->stop();
|
||||
m_isCrtFadingOut = false;
|
||||
m_comboCount = 0;
|
||||
setWindowOpacity(0.0);
|
||||
hide();
|
||||
if (graphicsEffect()) {
|
||||
graphicsEffect()->setEnabled(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
m_isCrtFadingOut = true;
|
||||
update();
|
||||
|
||||
@@ -143,29 +158,29 @@ ModernToast::ModernToast(QWidget *parent)
|
||||
});
|
||||
|
||||
|
||||
QEasingCurve expandCurve(QEasingCurve::OutBack);
|
||||
QEasingCurve expandCurve(AnimationTokens::Curve::bounce());
|
||||
expandCurve.setOvershoot(2.5);
|
||||
|
||||
m_shellExpand = new QPropertyAnimation(this, "geometry", this);
|
||||
m_shellExpand->setDuration(160);
|
||||
m_shellExpand->setDuration(AnimationTokens::DurationMs::kFast);
|
||||
m_shellExpand->setEasingCurve(expandCurve);
|
||||
|
||||
m_textExpand = new QPropertyAnimation(this, "textScale", this);
|
||||
m_textExpand->setDuration(160);
|
||||
m_textExpand->setDuration(AnimationTokens::DurationMs::kFast);
|
||||
m_textExpand->setEasingCurve(expandCurve);
|
||||
|
||||
m_expandGroup = new QParallelAnimationGroup(this);
|
||||
m_expandGroup->addAnimation(m_shellExpand);
|
||||
m_expandGroup->addAnimation(m_textExpand);
|
||||
|
||||
QEasingCurve shrinkCurve(QEasingCurve::OutCubic);
|
||||
QEasingCurve shrinkCurve(AnimationTokens::Curve::enter());
|
||||
|
||||
m_shellShrink = new QPropertyAnimation(this, "geometry", this);
|
||||
m_shellShrink->setDuration(220);
|
||||
m_shellShrink->setDuration(AnimationTokens::DurationMs::kNormal);
|
||||
m_shellShrink->setEasingCurve(shrinkCurve);
|
||||
|
||||
m_textShrink = new QPropertyAnimation(this, "textScale", this);
|
||||
m_textShrink->setDuration(220);
|
||||
m_textShrink->setDuration(AnimationTokens::DurationMs::kNormal);
|
||||
m_textShrink->setEasingCurve(shrinkCurve);
|
||||
|
||||
m_shrinkGroup = new QParallelAnimationGroup(this);
|
||||
@@ -284,6 +299,23 @@ void ModernToast::showWithAnimation(const QString &msg, int durationMs) {
|
||||
|
||||
show();
|
||||
|
||||
if (MotionPrefs::reduceMotion()) {
|
||||
// Reduce-motion: present instantly at final geometry, no slide/bounce/combo pop.
|
||||
m_showGroup->stop();
|
||||
m_popSequentialGroup->stop();
|
||||
m_hideGroup->stop();
|
||||
m_isCrtFadingOut = false;
|
||||
m_comboCount = 0;
|
||||
if (graphicsEffect()) {
|
||||
graphicsEffect()->setEnabled(true);
|
||||
}
|
||||
setTextScale(1.0);
|
||||
setGeometry(m_baseGeometry);
|
||||
setWindowOpacity(1.0);
|
||||
m_stayTimer->start(durationMs);
|
||||
return;
|
||||
}
|
||||
|
||||
bool isFullyHidden = (windowOpacity() == 0.0 && m_showGroup->state() != QAbstractAnimation::Running);
|
||||
bool isFadingOut = (m_hideGroup->state() == QAbstractAnimation::Running);
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ bool MpvController::init() {
|
||||
|
||||
mpv_set_option_string(m_mpv, "sub-hinting", "none");
|
||||
mpv_set_option_string(m_mpv, "sub-ass-hinting", "none");
|
||||
mpv_request_log_messages(m_mpv, "warn");
|
||||
mpv_request_log_messages(m_mpv, "debug");
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#include <cmath>
|
||||
#include <config/config_keys.h>
|
||||
#include <config/configstore.h>
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
#include "../utils/motionprefs.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -98,14 +100,14 @@ QWidget *PlayerLongPressHandler::createIndicator(
|
||||
overlay->setGraphicsEffect(opacityEffect);
|
||||
|
||||
opacityAnim = new QPropertyAnimation(opacityEffect, "opacity", this);
|
||||
opacityAnim->setEasingCurve(QEasingCurve::OutCubic);
|
||||
opacityAnim->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
|
||||
geometryAnim = new QPropertyAnimation(overlay, "geometry", this);
|
||||
geometryAnim->setEasingCurve(QEasingCurve::OutCubic);
|
||||
geometryAnim->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
|
||||
iconAnim = new QPropertyAnimation(iconLabel, "pos", this);
|
||||
iconAnim->setDuration(220);
|
||||
iconAnim->setEasingCurve(QEasingCurve::InOutSine);
|
||||
iconAnim->setDuration(AnimationTokens::DurationMs::kNormal);
|
||||
iconAnim->setEasingCurve(AnimationTokens::Curve::oscillate());
|
||||
|
||||
connect(opacityAnim, &QPropertyAnimation::finished, overlay,
|
||||
[overlay, opacityEffect]()
|
||||
@@ -151,7 +153,7 @@ double PlayerLongPressHandler::nextPlaybackRate(double currentRate) const
|
||||
|
||||
bool PlayerLongPressHandler::canStartPropertyAnimation(const QPropertyAnimation *animation) const
|
||||
{
|
||||
return animation && animation->targetObject();
|
||||
return animation && animation->targetObject() && !MotionPrefs::reduceMotion();
|
||||
}
|
||||
|
||||
bool PlayerLongPressHandler::isMouseEdgeLongPressEnabled() const
|
||||
@@ -545,10 +547,14 @@ void PlayerLongPressHandler::animateIndicator(
|
||||
overlay->raise();
|
||||
opacityEffect->setOpacity(startOpacity);
|
||||
|
||||
opacityAnim->setDuration(wasVisible ? 120 : 180);
|
||||
opacityAnim->setDuration(wasVisible ? AnimationTokens::DurationMs::kMicro
|
||||
: AnimationTokens::DurationMs::kFast);
|
||||
opacityAnim->setStartValue(startOpacity);
|
||||
opacityAnim->setEndValue(targetOpacity);
|
||||
|
||||
// Geometry lags the opacity slightly (140/200 vs 120/180) so the pill
|
||||
// reads as sliding into place — tuned choreography, intentionally off
|
||||
// the shared duration scale.
|
||||
geometryAnim->setDuration(wasVisible ? 140 : 200);
|
||||
geometryAnim->setStartValue(startGeometry);
|
||||
geometryAnim->setEndValue(targetGeometry);
|
||||
@@ -564,11 +570,11 @@ void PlayerLongPressHandler::animateIndicator(
|
||||
const QRect currentGeometry = overlay->geometry();
|
||||
overlay->setProperty("indicatorVisible", false);
|
||||
|
||||
opacityAnim->setDuration(120);
|
||||
opacityAnim->setDuration(AnimationTokens::DurationMs::kMicro);
|
||||
opacityAnim->setStartValue(opacityEffect->opacity());
|
||||
opacityAnim->setEndValue(0.0);
|
||||
|
||||
geometryAnim->setDuration(120);
|
||||
geometryAnim->setDuration(AnimationTokens::DurationMs::kMicro);
|
||||
geometryAnim->setStartValue(currentGeometry);
|
||||
geometryAnim->setEndValue(
|
||||
currentGeometry.translated(direction < 0 ? -10 : 10, 0));
|
||||
@@ -581,6 +587,12 @@ void PlayerLongPressHandler::animateIndicator(
|
||||
else if (opacityEffect)
|
||||
{
|
||||
opacityEffect->setOpacity(visible ? (active ? 1.0 : 0.62) : 0.0);
|
||||
if (!visible)
|
||||
{
|
||||
// The animated path hides via the opacityAnim finished handler;
|
||||
// when snapping we must hide directly.
|
||||
overlay->hide();
|
||||
}
|
||||
}
|
||||
|
||||
if (canStartPropertyAnimation(geometryAnim))
|
||||
@@ -755,7 +767,7 @@ void PlayerLongPressHandler::updateIndicators()
|
||||
m_rightIndicatorText, 1, rightActive,
|
||||
rightActive ? indicatorText : QString());
|
||||
|
||||
if (m_pulseTimer && !m_pulseTimer->isActive())
|
||||
if (m_pulseTimer && !m_pulseTimer->isActive() && !MotionPrefs::reduceMotion())
|
||||
{
|
||||
m_pulseForward = false;
|
||||
updateIndicatorPulse();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "playerosdlayer.h"
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
#include "../utils/motionprefs.h"
|
||||
#include <QAbstractAnimation>
|
||||
#include <QGraphicsOpacityEffect>
|
||||
#include <QIcon>
|
||||
@@ -56,7 +58,8 @@ PlayerOsdLayer::PlayerOsdLayer(QWidget *parent)
|
||||
|
||||
|
||||
m_fadeAnim = new QPropertyAnimation(m_opacity, "opacity", this);
|
||||
m_fadeAnim->setDuration(250);
|
||||
m_fadeAnim->setDuration(AnimationTokens::DurationMs::kNormal);
|
||||
m_fadeAnim->setEasingCurve(AnimationTokens::Curve::fade());
|
||||
|
||||
|
||||
m_hideTimer = new QTimer(this);
|
||||
@@ -276,7 +279,7 @@ void PlayerOsdLayer::fadeIn()
|
||||
|
||||
bool PlayerOsdLayer::canStartAnimation() const
|
||||
{
|
||||
return m_fadeAnim && m_fadeAnim->targetObject();
|
||||
return m_fadeAnim && m_fadeAnim->targetObject() && !MotionPrefs::reduceMotion();
|
||||
}
|
||||
|
||||
void PlayerOsdLayer::updateSeekLayout()
|
||||
|
||||
@@ -134,6 +134,9 @@ QWidget *PlayerOverlayDialog::surfaceWidget() const
|
||||
return m_surface;
|
||||
}
|
||||
|
||||
// NOTE: no FadeHelper here — m_surface carries a QGraphicsDropShadowEffect,
|
||||
// and nesting a QGraphicsOpacityEffect on the parent breaks rendering
|
||||
// ("QPainter not active" spam, invisible surface). Open/close stay instant.
|
||||
void PlayerOverlayDialog::open()
|
||||
{
|
||||
syncOverlayGeometry();
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <QPainterPath>
|
||||
#include <QVariantAnimation>
|
||||
#include <utility>
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -30,8 +31,8 @@ constexpr int kChipBadgeHeight = 12;
|
||||
constexpr int kChipRemoveTop = 0;
|
||||
constexpr int kChipRemoveButtonSize = 12;
|
||||
constexpr int kChipRemoveHoverSize = 12;
|
||||
constexpr int kChipHoverDecorFadeInDurationMs = 150;
|
||||
constexpr int kChipHoverDecorFadeOutDurationMs = 120;
|
||||
constexpr int kChipHoverDecorFadeInDurationMs = AnimationTokens::DurationMs::kQuick;
|
||||
constexpr int kChipHoverDecorFadeOutDurationMs = AnimationTokens::DurationMs::kMicro;
|
||||
constexpr qreal kChipHoverDecorMinScale = 0.9;
|
||||
constexpr qreal kChipHoverDecorMaxShiftY = 1.8;
|
||||
constexpr qreal kChipRemoveDecorDelayProgress = 0.18;
|
||||
@@ -259,7 +260,7 @@ SearchHistoryChip::SearchHistoryChip(QWidget *parent)
|
||||
m_hoverDecorAnimation->setStartValue(0.0);
|
||||
m_hoverDecorAnimation->setEndValue(1.0);
|
||||
m_hoverDecorAnimation->setDuration(kChipHoverDecorFadeInDurationMs);
|
||||
m_hoverDecorAnimation->setEasingCurve(QEasingCurve::OutCubic);
|
||||
m_hoverDecorAnimation->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
connect(m_hoverDecorAnimation, &QVariantAnimation::valueChanged, this,
|
||||
[this](const QVariant &value) {
|
||||
m_hoverDecorProgress = value.toReal();
|
||||
@@ -559,8 +560,8 @@ void SearchHistoryChip::syncHoverDecorationVisibility()
|
||||
m_hoverDecorAnimation->stop();
|
||||
m_hoverDecorAnimation->setDuration(visible ? kChipHoverDecorFadeInDurationMs
|
||||
: kChipHoverDecorFadeOutDurationMs);
|
||||
m_hoverDecorAnimation->setEasingCurve(visible ? QEasingCurve::OutCubic
|
||||
: QEasingCurve::InCubic);
|
||||
m_hoverDecorAnimation->setEasingCurve(visible ? AnimationTokens::Curve::enter()
|
||||
: AnimationTokens::Curve::exit());
|
||||
m_hoverDecorAnimation->setStartValue(m_hoverDecorProgress);
|
||||
m_hoverDecorAnimation->setEndValue(targetProgress);
|
||||
m_hoverDecorAnimation->start();
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <QVBoxLayout>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -37,12 +38,15 @@ constexpr int kSearchHistoryPopupEdgeScrollBarWidth = 4;
|
||||
constexpr int kSearchHistoryPopupContentRightInset = 6;
|
||||
constexpr int kSearchHistoryPopupCollapsedBodyMaxHeight = 228;
|
||||
constexpr int kSearchHistoryPopupExpandedBodyMaxHeight = 320;
|
||||
// Expand/collapse/sort/hide are a deliberately-tuned multi-stage choreography for
|
||||
// this popup and intentionally sit off the shared duration scale; only the show
|
||||
// duration lines up with a design tier (kFast).
|
||||
constexpr int kSearchHistoryPopupExpandDurationMs = 240;
|
||||
constexpr int kSearchHistoryPopupCollapseDurationMs = 200;
|
||||
constexpr int kSearchHistoryPopupSortTransitionDurationMs = 190;
|
||||
constexpr qreal kSearchHistoryPopupSortTransitionMidpoint = 0.44;
|
||||
constexpr qreal kSearchHistoryPopupSortTransitionMinOpacity = 0.18;
|
||||
constexpr int kSearchHistoryPopupShowDurationMs = 180;
|
||||
constexpr int kSearchHistoryPopupShowDurationMs = AnimationTokens::DurationMs::kFast;
|
||||
constexpr int kSearchHistoryPopupHideDurationMs = 140;
|
||||
constexpr int kSearchHistoryPopupVisibilityOffsetY = 8;
|
||||
|
||||
@@ -122,6 +126,13 @@ SearchHistoryPopup::SearchHistoryPopup(QWidget *parent)
|
||||
m_card->setObjectName("SearchHistoryPopupCard");
|
||||
m_card->setAttribute(Qt::WA_StyledBackground, true);
|
||||
|
||||
// Drives the show/hide opacity fade. Without this the effect is never created
|
||||
// and every `if (m_cardOpacityEffect)` branch is dead — the popup pops instead
|
||||
// of fading. m_card owns no other graphics effect, so this is safe.
|
||||
m_cardOpacityEffect = new QGraphicsOpacityEffect(m_card);
|
||||
m_cardOpacityEffect->setOpacity(1.0);
|
||||
m_card->setGraphicsEffect(m_cardOpacityEffect);
|
||||
|
||||
auto *mainLayout = new QVBoxLayout(m_card);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
mainLayout->setSpacing(0);
|
||||
@@ -246,7 +257,7 @@ SearchHistoryPopup::SearchHistoryPopup(QWidget *parent)
|
||||
m_heightAnimation->setStartValue(0.0);
|
||||
m_heightAnimation->setEndValue(1.0);
|
||||
m_heightAnimation->setDuration(kSearchHistoryPopupExpandDurationMs);
|
||||
m_heightAnimation->setEasingCurve(QEasingCurve::InOutCubic);
|
||||
m_heightAnimation->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
connect(m_heightAnimation, &QVariantAnimation::valueChanged, this,
|
||||
[this](const QVariant &value) {
|
||||
const qreal progress = value.toReal();
|
||||
@@ -278,7 +289,7 @@ SearchHistoryPopup::SearchHistoryPopup(QWidget *parent)
|
||||
m_sortTransitionAnimation->setStartValue(0.0);
|
||||
m_sortTransitionAnimation->setEndValue(1.0);
|
||||
m_sortTransitionAnimation->setDuration(kSearchHistoryPopupSortTransitionDurationMs);
|
||||
m_sortTransitionAnimation->setEasingCurve(QEasingCurve::InOutCubic);
|
||||
m_sortTransitionAnimation->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
connect(m_sortTransitionAnimation, &QVariantAnimation::valueChanged, this,
|
||||
[this](const QVariant &value) {
|
||||
if (!m_flowOpacityEffect)
|
||||
@@ -331,7 +342,7 @@ SearchHistoryPopup::SearchHistoryPopup(QWidget *parent)
|
||||
m_visibilityAnimation->setStartValue(0.0);
|
||||
m_visibilityAnimation->setEndValue(1.0);
|
||||
m_visibilityAnimation->setDuration(kSearchHistoryPopupShowDurationMs);
|
||||
m_visibilityAnimation->setEasingCurve(QEasingCurve::OutCubic);
|
||||
m_visibilityAnimation->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
connect(m_visibilityAnimation, &QVariantAnimation::valueChanged, this,
|
||||
[this](const QVariant &value) {
|
||||
const qreal progress = value.toReal();
|
||||
@@ -451,7 +462,7 @@ void SearchHistoryPopup::showBelow(QWidget *anchor) {
|
||||
m_visibilityTargetOpacity = 1.0;
|
||||
m_visibilityTargetOffsetY = 0;
|
||||
m_visibilityAnimation->setDuration(kSearchHistoryPopupShowDurationMs);
|
||||
m_visibilityAnimation->setEasingCurve(QEasingCurve::OutCubic);
|
||||
m_visibilityAnimation->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
m_visibilityAnimation->setStartValue(0.0);
|
||||
m_visibilityAnimation->setEndValue(1.0);
|
||||
m_visibilityAnimation->start();
|
||||
@@ -507,7 +518,7 @@ void SearchHistoryPopup::dismiss(bool immediate)
|
||||
}
|
||||
|
||||
m_visibilityAnimation->setDuration(kSearchHistoryPopupHideDurationMs);
|
||||
m_visibilityAnimation->setEasingCurve(QEasingCurve::InCubic);
|
||||
m_visibilityAnimation->setEasingCurve(AnimationTokens::Curve::exit());
|
||||
m_visibilityAnimation->setStartValue(0.0);
|
||||
m_visibilityAnimation->setEndValue(1.0);
|
||||
m_visibilityAnimation->start();
|
||||
@@ -1116,8 +1127,7 @@ void SearchHistoryPopup::setExpanded(bool expanded)
|
||||
{
|
||||
m_heightAnimation->setDuration(expanded ? kSearchHistoryPopupExpandDurationMs
|
||||
: kSearchHistoryPopupCollapseDurationMs);
|
||||
m_heightAnimation->setEasingCurve(expanded ? QEasingCurve::OutCubic
|
||||
: QEasingCurve::InOutCubic);
|
||||
m_heightAnimation->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#include "seektimebubble.h"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QPainter>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "../utils/fadehelper.h"
|
||||
|
||||
SeekTimeBubble::SeekTimeBubble(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
setVisible(false);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(10, 4, 10, 4);
|
||||
layout->setSpacing(4);
|
||||
|
||||
m_thumbSlot = new QLabel(this);
|
||||
m_thumbSlot->setObjectName("seekBubbleThumb");
|
||||
m_thumbSlot->setVisible(false);
|
||||
layout->addWidget(m_thumbSlot, 0, Qt::AlignHCenter);
|
||||
|
||||
m_timeLabel = new QLabel(this);
|
||||
m_timeLabel->setObjectName("seekBubbleTime");
|
||||
m_timeLabel->setAlignment(Qt::AlignCenter);
|
||||
layout->addWidget(m_timeLabel, 0, Qt::AlignHCenter);
|
||||
}
|
||||
|
||||
void SeekTimeBubble::setTimeText(const QString &text)
|
||||
{
|
||||
if (m_timeLabel->text() == text)
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_timeLabel->setText(text);
|
||||
adjustSize();
|
||||
}
|
||||
|
||||
void SeekTimeBubble::showAt(const QPoint &anchor)
|
||||
{
|
||||
adjustSize();
|
||||
|
||||
QPoint topLeft(anchor.x() - width() / 2, anchor.y() - height());
|
||||
if (parentWidget())
|
||||
{
|
||||
const int maxX = parentWidget()->width() - width();
|
||||
topLeft.setX(qBound(0, topLeft.x(), qMax(0, maxX)));
|
||||
topLeft.setY(qMax(0, topLeft.y()));
|
||||
}
|
||||
move(topLeft);
|
||||
|
||||
if (!m_shown)
|
||||
{
|
||||
m_shown = true;
|
||||
raise();
|
||||
FadeHelper::fadeIn(this, AnimationTokens::DurationMs::kQuick);
|
||||
}
|
||||
}
|
||||
|
||||
void SeekTimeBubble::hideBubble()
|
||||
{
|
||||
if (!m_shown)
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_shown = false;
|
||||
FadeHelper::fadeOut(this, AnimationTokens::DurationMs::kMicro,
|
||||
AnimationTokens::Curve::exit(), [this]() { hide(); });
|
||||
}
|
||||
|
||||
void SeekTimeBubble::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
Q_UNUSED(event);
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.setPen(QPen(QColor(148, 163, 184, 60), 1));
|
||||
painter.setBrush(QColor(15, 23, 42, 235));
|
||||
painter.drawRoundedRect(QRectF(rect()).adjusted(0.5, 0.5, -0.5, -0.5), 6, 6);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#ifndef SEEKTIMEBUBBLE_H
|
||||
#define SEEKTIMEBUBBLE_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class QLabel;
|
||||
|
||||
// Small floating bubble shown above the player seek bar while hovering,
|
||||
// displaying the timestamp under the cursor. Fades in kQuick / out kMicro
|
||||
// (via FadeHelper, so reduce-motion snaps). The layout reserves a hidden
|
||||
// thumbnail slot above the time label so a trickplay preview image can be
|
||||
// added later without re-layouting.
|
||||
//
|
||||
// The bubble is an always-dark player surface, styled by objectName
|
||||
// "seekBubbleTime" in the QSS plus a self-painted panel.
|
||||
class SeekTimeBubble : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SeekTimeBubble(QWidget *parent);
|
||||
|
||||
void setTimeText(const QString &text);
|
||||
// Anchor = bottom-center of the bubble, in parent coordinates. Clamped to
|
||||
// stay inside the parent. Fades in on the first call after a hide.
|
||||
void showAt(const QPoint &anchor);
|
||||
void hideBubble();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
private:
|
||||
QLabel *m_thumbSlot;
|
||||
QLabel *m_timeLabel;
|
||||
bool m_shown = false;
|
||||
};
|
||||
|
||||
#endif // SEEKTIMEBUBBLE_H
|
||||
@@ -10,6 +10,8 @@
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QFont>
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
#include "../utils/motionprefs.h"
|
||||
|
||||
ServerWheelView::ServerWheelView(QWidget* parent) : QWidget(parent) {
|
||||
setAttribute(Qt::WA_StyledBackground, true);
|
||||
@@ -17,8 +19,8 @@ ServerWheelView::ServerWheelView(QWidget* parent) : QWidget(parent) {
|
||||
|
||||
m_scrollAnim = new QVariantAnimation(this);
|
||||
|
||||
m_scrollAnim->setEasingCurve(QEasingCurve::OutExpo);
|
||||
m_scrollAnim->setDuration(450);
|
||||
m_scrollAnim->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
m_scrollAnim->setDuration(AnimationTokens::DurationMs::kSlow);
|
||||
connect(m_scrollAnim, &QVariantAnimation::valueChanged, this, [this](const QVariant& val){
|
||||
m_scrollOffset = val.toReal();
|
||||
updateLayout();
|
||||
@@ -186,6 +188,11 @@ void ServerWheelView::installFilterRecursively(QWidget* w) {
|
||||
|
||||
void ServerWheelView::scrollTo(qreal target) {
|
||||
m_scrollAnim->stop();
|
||||
if (MotionPrefs::reduceMotion()) {
|
||||
m_scrollOffset = target;
|
||||
updateLayout();
|
||||
return;
|
||||
}
|
||||
m_scrollAnim->setStartValue(m_scrollOffset);
|
||||
m_scrollAnim->setEndValue(target);
|
||||
m_scrollAnim->start();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "settingssubpanel.h"
|
||||
#include "../managers/thememanager.h"
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
#include <config/configstore.h>
|
||||
#include <config/config_keys.h>
|
||||
#include <QEasingCurve>
|
||||
#include <QScrollArea>
|
||||
#include <QScrollBar>
|
||||
#include <QTimer>
|
||||
@@ -39,8 +39,8 @@ SettingsSubPanel::SettingsSubPanel(const QString &iconPath, QWidget *parent)
|
||||
|
||||
|
||||
m_animation = new QPropertyAnimation(this, "panelHeight", this);
|
||||
m_animation->setDuration(250);
|
||||
m_animation->setEasingCurve(QEasingCurve::OutCubic);
|
||||
m_animation->setDuration(AnimationTokens::DurationMs::kNormal);
|
||||
m_animation->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
|
||||
|
||||
connect(m_animation, &QPropertyAnimation::finished, this, [this]() {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "shimmerwidget.h"
|
||||
#include "../managers/thememanager.h"
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
#include "../utils/motionprefs.h"
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPropertyAnimation>
|
||||
@@ -14,9 +16,9 @@ ShimmerWidget::ShimmerWidget(QWidget* parent)
|
||||
m_animation = new QPropertyAnimation(this, "shimmerPhase", this);
|
||||
m_animation->setStartValue(0.0);
|
||||
m_animation->setEndValue(1.0);
|
||||
m_animation->setDuration(1800);
|
||||
m_animation->setDuration(AnimationTokens::DurationMs::kShimmer);
|
||||
m_animation->setLoopCount(-1);
|
||||
m_animation->setEasingCurve(QEasingCurve::Linear);
|
||||
m_animation->setEasingCurve(AnimationTokens::Curve::loop());
|
||||
}
|
||||
|
||||
ShimmerWidget::~ShimmerWidget()
|
||||
@@ -63,10 +65,16 @@ void ShimmerWidget::setShimmerPhase(qreal phase)
|
||||
|
||||
void ShimmerWidget::startAnimation()
|
||||
{
|
||||
if (m_animation && m_animation->state() != QAbstractAnimation::Running) {
|
||||
m_shimmerPhase = 0.0;
|
||||
m_animation->start();
|
||||
if (!m_animation || m_animation->state() == QAbstractAnimation::Running) {
|
||||
return;
|
||||
}
|
||||
m_shimmerPhase = 0.0;
|
||||
if (MotionPrefs::reduceMotion()) {
|
||||
// Reduce-motion: show a static skeleton (base shapes) with no moving sweep.
|
||||
update();
|
||||
return;
|
||||
}
|
||||
m_animation->start();
|
||||
}
|
||||
|
||||
void ShimmerWidget::stopAnimation()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "slidingstackedwidget.h"
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
#include <QWidget>
|
||||
#include <QLabel>
|
||||
#include <QScreen>
|
||||
@@ -17,8 +18,8 @@ QPoint interpolatePoint(const QPoint &start, const QPoint &end, qreal progress)
|
||||
|
||||
SlidingStackedWidget::SlidingStackedWidget(QWidget *parent)
|
||||
: QStackedWidget(parent),
|
||||
m_speed(350),
|
||||
m_animationType(QEasingCurve::OutCubic),
|
||||
m_speed(AnimationTokens::DurationMs::kSlow),
|
||||
m_animationType(AnimationTokens::Curve::enter().type()),
|
||||
m_isAnimating(false),
|
||||
m_nextIndex(0)
|
||||
{
|
||||
@@ -45,6 +46,35 @@ void SlidingStackedWidget::slideInWgt(QWidget *widget, SlideDirection direction)
|
||||
}
|
||||
}
|
||||
|
||||
void SlidingStackedWidget::jumpToWgt(QWidget *widget)
|
||||
{
|
||||
int idx = indexOf(widget);
|
||||
if (idx != -1) {
|
||||
jumpToIdx(idx);
|
||||
}
|
||||
}
|
||||
|
||||
void SlidingStackedWidget::jumpToIdx(int idx)
|
||||
{
|
||||
if (idx < 0 || idx >= count())
|
||||
return;
|
||||
|
||||
if (currentIndex() == idx && !m_isAnimating)
|
||||
return;
|
||||
|
||||
if (m_isAnimating) {
|
||||
m_slideTimer.stop();
|
||||
animationDoneSlot();
|
||||
}
|
||||
|
||||
m_nextIndex = idx;
|
||||
setCurrentIndex(idx);
|
||||
QWidget *w = widget(idx);
|
||||
if (w) w->raise();
|
||||
flushPendingWidgetDisposals();
|
||||
Q_EMIT animationFinished();
|
||||
}
|
||||
|
||||
void SlidingStackedWidget::slideInIdx(int idx, SlideDirection direction)
|
||||
{
|
||||
if (idx < 0 || idx >= count())
|
||||
@@ -64,12 +94,7 @@ void SlidingStackedWidget::slideInIdx(int idx, SlideDirection direction)
|
||||
|
||||
bool reduceAnimations = ConfigStore::instance()->get<bool>(ConfigKeys::UiAnimations, false);
|
||||
if (reduceAnimations) {
|
||||
m_nextIndex = idx;
|
||||
setCurrentIndex(idx);
|
||||
QWidget *w = widget(idx);
|
||||
if (w) w->raise();
|
||||
flushPendingWidgetDisposals();
|
||||
Q_EMIT animationFinished();
|
||||
jumpToIdx(idx);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@ public:
|
||||
|
||||
void slideInIdx(int idx, SlideDirection direction = Automatic);
|
||||
void slideInWgt(QWidget *widget, SlideDirection direction = Automatic);
|
||||
// Instant switch (no slide), safely finishing any in-flight animation.
|
||||
// For targets with their own entrance animation (e.g. the player view).
|
||||
void jumpToIdx(int idx);
|
||||
void jumpToWgt(QWidget *widget);
|
||||
void disposeWidgetWhenSafe(QWidget *widget);
|
||||
|
||||
Q_SIGNALS:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "splitplayerbutton.h"
|
||||
#include "../managers/externalplayerdetector.h"
|
||||
#include "../utils/popupfade.h"
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QMouseEvent>
|
||||
@@ -26,6 +27,7 @@ SplitPlayerButton::SplitPlayerButton(QWidget *parent)
|
||||
m_menu->setWindowFlags(Qt::Popup | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint);
|
||||
m_menu->setAttribute(Qt::WA_TranslucentBackground, true);
|
||||
m_menu->setAutoFillBackground(false);
|
||||
PopupFade::attach(m_menu);
|
||||
}
|
||||
|
||||
void SplitPlayerButton::setPlayers(const QList<DetectedPlayer> &players,
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <QStyle>
|
||||
#include <QStringList>
|
||||
#include <QVBoxLayout>
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -115,7 +116,7 @@ TaskEditDialog::TaskEditDialog(QEmbyCore *core, const QString &taskId,
|
||||
|
||||
m_pageStack = new SlidingStackedWidget(this);
|
||||
m_pageStack->setSpeed(260);
|
||||
m_pageStack->setEasingCurve(QEasingCurve::OutCubic);
|
||||
m_pageStack->setEasingCurve(AnimationTokens::Curve::enter().type());
|
||||
|
||||
setupOverviewPage();
|
||||
setupTriggersPage();
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include <QScrollArea>
|
||||
#include <QScrollBar>
|
||||
#include <QTimer>
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
#include "../utils/motionprefs.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -148,11 +150,15 @@ void TranscodingCodecListWidget::onRowDragFinish(TranscodingCodecRowWidget* row)
|
||||
m_dragHotSpotY = 0;
|
||||
|
||||
stopPosAnimations(row);
|
||||
if (MotionPrefs::reduceMotion()) {
|
||||
row->move(QPoint(0, rowTopForIndex(finalIndex)));
|
||||
} else {
|
||||
auto* animation = new QPropertyAnimation(row, "pos", row);
|
||||
animation->setDuration(180);
|
||||
animation->setEasingCurve(QEasingCurve::OutCubic);
|
||||
animation->setDuration(AnimationTokens::DurationMs::kFast);
|
||||
animation->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
animation->setEndValue(QPoint(0, rowTopForIndex(finalIndex)));
|
||||
animation->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
emit orderChanged(codecOrder());
|
||||
@@ -276,12 +282,12 @@ void TranscodingCodecListWidget::relayout(bool animate) {
|
||||
}
|
||||
|
||||
const QPoint targetPos(0, top);
|
||||
if (animate && row->pos() != targetPos) {
|
||||
if (animate && !MotionPrefs::reduceMotion() && row->pos() != targetPos) {
|
||||
stopPosAnimations(row);
|
||||
|
||||
auto* animation = new QPropertyAnimation(row, "pos", row);
|
||||
animation->setDuration(220);
|
||||
animation->setEasingCurve(QEasingCurve::OutCubic);
|
||||
animation->setDuration(AnimationTokens::DurationMs::kNormal);
|
||||
animation->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
animation->setEndValue(targetPos);
|
||||
animation->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
} else {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <QStyle>
|
||||
#include <QTime>
|
||||
#include <QVBoxLayout>
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -165,7 +166,7 @@ UserEditDialog::UserEditDialog(QEmbyCore *core, const QString &userId,
|
||||
m_pageStack = new SlidingStackedWidget(this);
|
||||
m_pageStack->setObjectName("UserEditPageStack");
|
||||
m_pageStack->setSpeed(260);
|
||||
m_pageStack->setEasingCurve(QEasingCurve::OutCubic);
|
||||
m_pageStack->setEasingCurve(AnimationTokens::Curve::enter().type());
|
||||
|
||||
setupProfileTab();
|
||||
setupAccessTab();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "mainwindow.h"
|
||||
#include "tokens/AnimationTokens.h"
|
||||
#include <qembycore.h>
|
||||
#include "views/public/loginview.h"
|
||||
#include "views/media/playerview.h"
|
||||
@@ -566,11 +567,11 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
|
||||
|
||||
auto anim = new QPropertyAnimation(transition, "radius", transition);
|
||||
anim->setDuration(450);
|
||||
anim->setDuration(AnimationTokens::DurationMs::kSlow);
|
||||
anim->setStartValue(0);
|
||||
anim->setEndValue(maxRadius);
|
||||
|
||||
anim->setEasingCurve(QEasingCurve::InOutCubic);
|
||||
anim->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
|
||||
|
||||
connect(anim, &QPropertyAnimation::finished, transition, &QObject::deleteLater);
|
||||
@@ -662,10 +663,10 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
|
||||
|
||||
auto anim = new QPropertyAnimation(transition, "radius", transition);
|
||||
anim->setDuration(450);
|
||||
anim->setDuration(AnimationTokens::DurationMs::kSlow);
|
||||
anim->setStartValue(0);
|
||||
anim->setEndValue(maxRadius);
|
||||
anim->setEasingCurve(QEasingCurve::InOutCubic);
|
||||
anim->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
connect(anim, &QPropertyAnimation::finished, transition, &QObject::deleteLater);
|
||||
anim->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
} else {
|
||||
@@ -957,10 +958,10 @@ void MainWindow::navigateToHome()
|
||||
m_viewStack->setCurrentWidget(m_homeView);
|
||||
|
||||
auto *fadeIn = new QPropertyAnimation(homeOpacity, "opacity", this);
|
||||
fadeIn->setDuration(250);
|
||||
fadeIn->setDuration(AnimationTokens::DurationMs::kNormal);
|
||||
fadeIn->setStartValue(0.0);
|
||||
fadeIn->setEndValue(1.0);
|
||||
fadeIn->setEasingCurve(QEasingCurve::OutQuad);
|
||||
fadeIn->setEasingCurve(AnimationTokens::Curve::fade());
|
||||
|
||||
connect(fadeIn, &QPropertyAnimation::finished, this, [this]() {
|
||||
m_homeView->setGraphicsEffect(nullptr);
|
||||
@@ -1017,10 +1018,10 @@ void MainWindow::navigateToLogin() {
|
||||
m_viewStack->setCurrentWidget(m_loginView);
|
||||
|
||||
auto *fadeIn = new QPropertyAnimation(loginOpacity, "opacity", this);
|
||||
fadeIn->setDuration(250);
|
||||
fadeIn->setDuration(AnimationTokens::DurationMs::kNormal);
|
||||
fadeIn->setStartValue(0.0);
|
||||
fadeIn->setEndValue(1.0);
|
||||
fadeIn->setEasingCurve(QEasingCurve::OutQuad);
|
||||
fadeIn->setEasingCurve(AnimationTokens::Curve::fade());
|
||||
|
||||
connect(fadeIn, &QPropertyAnimation::finished, this, [this]() {
|
||||
m_loginView->setGraphicsEffect(nullptr);
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
/* =========================================================
|
||||
全局基础设定 & Tailwind Dark 色板映射
|
||||
qEmby Dark Theme — QSS Style Sheet
|
||||
Design tokens: see src/qEmbyApp/tokens/DesignTokens.h
|
||||
Animation tokens: see src/qEmbyApp/tokens/AnimationTokens.h
|
||||
|
||||
Border-radius scale: 2(kXs) 4(kSm) 6(kMd) 8(kLg) 14(kXl)
|
||||
Spacing scale: 4(kXs) 8(kSm) 12(kMd) 16(kLg) 24(kXl)
|
||||
Font scale: 10 11 12 13 14 16 20 24 28
|
||||
|
||||
Color palette (Tailwind Dark):
|
||||
Primary Blue: #3B82F6, Hover: #2563EB
|
||||
Danger Red: #EF4444, Hover: #DC2626
|
||||
Slate-900 (主背景): #0F172A
|
||||
@@ -252,7 +260,7 @@ QListView#SearchCompleterPopup,
|
||||
QListView#SearchHistoryCompleterPopup {
|
||||
background-color: #0F172A;
|
||||
border: 1px solid rgba(148, 163, 184, 0.20);
|
||||
border-radius: 20px;
|
||||
border-radius: 14px;
|
||||
padding: 0px;
|
||||
outline: none;
|
||||
}
|
||||
@@ -450,7 +458,7 @@ QMenu::separator {
|
||||
QMenu#AppTextContextMenu {
|
||||
background-color: #0F172A;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
padding: 8px 0px;
|
||||
}
|
||||
|
||||
@@ -532,7 +540,7 @@ QWidget#login-add-page QLineEdit:focus {
|
||||
QWidget#login-add-page QWidget#login-ssl-card {
|
||||
background-color: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.10);
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
QWidget#login-add-page QWidget#login-ssl-card:hover {
|
||||
@@ -558,7 +566,7 @@ QWidget#login-add-page QLabel#login-ssl-description {
|
||||
QWidget#server-card {
|
||||
background-color: #1E293B;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
QWidget#server-card:hover {
|
||||
@@ -578,7 +586,7 @@ QLabel#server-url-label {
|
||||
QLabel#server-fallback-icon {
|
||||
background-color: #1E3A8A;
|
||||
color: #60A5FA;
|
||||
border-radius: 20px;
|
||||
border-radius: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -613,7 +621,7 @@ QPushButton#add-server-btn {
|
||||
qproperty-iconSize: 16px 16px;
|
||||
background-color: transparent;
|
||||
border: 2px dashed #334155;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
color: #60A5FA;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
@@ -999,7 +1007,7 @@ ModernMessageBox {
|
||||
QWidget[playerDialogSurface="true"] {
|
||||
background-color: rgba(15, 23, 42, 0.94);
|
||||
border: 1px solid rgba(71, 85, 105, 0.88);
|
||||
border-radius: 18px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
QWidget#dialog-titlebar {
|
||||
@@ -1168,7 +1176,7 @@ QWidget#playerDanmakuSettingsDialog {
|
||||
}
|
||||
|
||||
#playerDanmakuSettingsDialog QLabel#playerDanmakuTileIcon[compactIcon="true"] {
|
||||
border-radius: 11px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
#playerDanmakuSettingsDialog QLabel#playerDanmakuTileTitle {
|
||||
@@ -1489,7 +1497,7 @@ QListWidget#modernComboEmbeddedList QScrollBar:vertical {
|
||||
|
||||
QListWidget#modernComboEmbeddedList QScrollBar::handle:vertical {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
@@ -2312,7 +2320,7 @@ QLabel#identify-preview-label {
|
||||
QLabel#identify-preview-label[media-image-editor="true"] {
|
||||
background-color: rgba(15, 23, 42, 0.84);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
@@ -2377,13 +2385,13 @@ QProgressBar#detail-progress-bar {
|
||||
background-color: rgba(255, 255, 255, 0.12);
|
||||
border: 1px solid transparent;
|
||||
/* 核心:用透明边框强制覆盖原生样式引擎 */
|
||||
border-radius: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
QProgressBar#detail-progress-bar::chunk {
|
||||
background-color: #3B82F6;
|
||||
/* 如果你要苹果绿,请替换为 #30D158 */
|
||||
border-radius: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* =========================================
|
||||
@@ -2470,7 +2478,7 @@ QPushButton#hud-close-btn:hover {
|
||||
QPushButton.player-hud-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
QPushButton.player-hud-btn:hover {
|
||||
@@ -2516,7 +2524,7 @@ QPushButton#playerSpeedBtn {
|
||||
/* 彻底抹除边框 */
|
||||
outline: none;
|
||||
/* 彻底抹除焦点虚线 */
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
@@ -2524,7 +2532,7 @@ QPushButton#playerMediaSwitchBtn {
|
||||
min-width: 0px;
|
||||
padding: 0px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
QPushButton#playerMediaSwitchBtn:hover {
|
||||
@@ -2749,7 +2757,7 @@ QPushButton#immersive-stream-btn {
|
||||
border: none;
|
||||
padding: 4px 10px;
|
||||
/* 【调优】上下内边距收紧,打造细长的精致胶囊 */
|
||||
border-radius: 5px;
|
||||
border-radius: 6px;
|
||||
/* 圆角微调,配合更小的高度 */
|
||||
text-align: left;
|
||||
}
|
||||
@@ -2774,7 +2782,7 @@ QLineEdit#detail-episode-jump-edit {
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
border-radius: 5px;
|
||||
border-radius: 6px;
|
||||
selection-background-color: rgba(59, 130, 246, 0.55);
|
||||
selection-color: #FFFFFF;
|
||||
}
|
||||
@@ -2814,7 +2822,7 @@ QMenu#immersive-stream-menu {
|
||||
QMenu#immersive-stream-menu::item {
|
||||
color: #E0E0E0;
|
||||
padding: 6px 28px 6px 14px;
|
||||
border-radius: 5px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -2850,7 +2858,7 @@ QWidget#stream-menu-item-host {
|
||||
|
||||
QWidget#stream-menu-item {
|
||||
background: transparent;
|
||||
border-radius: 5px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
QWidget#stream-menu-item:hover,
|
||||
@@ -2915,15 +2923,24 @@ QLabel#osdSeekTimeLabel {
|
||||
border: 1px solid rgba(255, 255, 255, 28);
|
||||
}
|
||||
|
||||
/* 进度条悬停时间气泡(面板底色由 SeekTimeBubble 自绘) */
|
||||
QLabel#seekBubbleTime {
|
||||
background: transparent;
|
||||
color: #F8FAFC;
|
||||
font-family: "Segoe UI", "Microsoft YaHei UI", "Microsoft YaHei", sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
QWidget#osdSeekStem {
|
||||
background-color: rgba(96, 165, 250, 180);
|
||||
border-radius: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
QWidget#osdSeekMarker {
|
||||
background-color: rgba(248, 250, 252, 220);
|
||||
border: 1px solid rgba(96, 165, 250, 230);
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
QWidget#osdVolumePanel {
|
||||
@@ -2961,7 +2978,7 @@ QLabel#osdVolumeLabel {
|
||||
QWidget#playerMouseEdgeIndicator {
|
||||
background-color: rgba(255, 255, 255, 18);
|
||||
border: 1px solid rgba(255, 255, 255, 52);
|
||||
border-radius: 22px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
QWidget#playerMouseEdgeIndicator[gestureActive="true"] {
|
||||
@@ -3053,7 +3070,7 @@ QScrollBar#modernMenuScrollBar:vertical {
|
||||
|
||||
QScrollBar#modernMenuScrollBar::handle:vertical {
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
min-height: 28px;
|
||||
/* 显式归零,覆盖全局规则的 margin: 0px 3px,否则会被压缩成 0 宽不可见 */
|
||||
margin: 0px;
|
||||
@@ -3155,7 +3172,7 @@ LoadingOverlay {
|
||||
#SettingsCard {
|
||||
background-color: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
#SettingsCard:hover {
|
||||
@@ -3546,7 +3563,7 @@ QLineEdit#TagInputInlineEdit {
|
||||
QToolButton#TagInputClearBtn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
qproperty-icon: url(":/svg/dark/close.svg");
|
||||
qproperty-iconSize: 8px 8px;
|
||||
}
|
||||
@@ -3595,7 +3612,7 @@ QMenu#TagPresetMenu::indicator {
|
||||
background: rgba(255, 255, 255, 0.025);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-left: 3px solid #3B82F6;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
@@ -3801,7 +3818,7 @@ QLabel#AboutThanksAvatar {
|
||||
color: #FFFFFF;
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
|
||||
stop:0 #F97316, stop:0.48 #DB2777, stop:1 #2563EB);
|
||||
border-radius: 11px;
|
||||
border-radius: 14px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
@@ -3965,7 +3982,7 @@ QLabel#SettingsPageTitle {
|
||||
/* 左右宽阔的热区,保证不拥挤 */
|
||||
margin: 2px 6px;
|
||||
/* 【核心】:让 hover 高亮区不贴边,形成独立胶囊 */
|
||||
border-radius: 5px;
|
||||
border-radius: 6px;
|
||||
/* 胶囊状内圆角 */
|
||||
font-size: 13px;
|
||||
}
|
||||
@@ -3997,14 +4014,14 @@ QLabel#SettingsPageTitle {
|
||||
#ManageInfoCard {
|
||||
background-color: rgba(30, 41, 59, 0.5);
|
||||
border: 1px solid rgba(71, 85, 105, 0.4);
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#ManageActivityCard {
|
||||
background-color: rgba(30, 41, 59, 0.5);
|
||||
border: 1px solid rgba(71, 85, 105, 0.4);
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
@@ -4062,7 +4079,7 @@ QFrame#TranscodingGroupBlock,
|
||||
QFrame#TranscodingCodecItem {
|
||||
background-color: rgba(30, 41, 59, 0.6);
|
||||
border: 1px solid rgba(71, 85, 105, 0.28);
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
QFrame#TranscodingCodecItem[dragging="true"] {
|
||||
@@ -4295,12 +4312,12 @@ QProgressBar#NowPlayingProgressBar {
|
||||
max-height: 2px;
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
QProgressBar#NowPlayingProgressBar::chunk {
|
||||
background-color: #3B82F6;
|
||||
border-radius: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
QLabel#NowPlayingUser {
|
||||
@@ -4756,7 +4773,7 @@ QCheckBox#LibraryMetadataLockCheck::indicator:checked {
|
||||
QLineEdit#PlaylistSearchEdit {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.09);
|
||||
border-radius: 7px;
|
||||
border-radius: 6px;
|
||||
color: #CBD5E1;
|
||||
font-size: 12px;
|
||||
font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
@@ -5286,12 +5303,12 @@ QProgressBar#ManageLibScanProgress {
|
||||
max-height: 2px;
|
||||
background-color: rgba(255, 255, 255, 0.12);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
QProgressBar#ManageLibScanProgress::chunk {
|
||||
background-color: #3B82F6;
|
||||
border-radius: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* 取消扫描按钮 */
|
||||
@@ -5383,7 +5400,7 @@ QToolButton#TagInputClearBtn {
|
||||
border: none;
|
||||
qproperty-icon: url(:/svg/dark/close.svg);
|
||||
qproperty-iconSize: 10px 10px;
|
||||
border-radius: 5px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
QToolButton#TagInputClearBtn:hover {
|
||||
@@ -5441,7 +5458,7 @@ QLineEdit#TagPresetSearchEdit:focus {
|
||||
/* 弹出面板内复选框 */
|
||||
QFrame#TagPresetOptionRow {
|
||||
background-color: transparent;
|
||||
border-radius: 5px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
QFrame#TagPresetOptionRow:hover {
|
||||
@@ -5464,7 +5481,7 @@ QCheckBox#TagPresetCheckbox {
|
||||
font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
spacing: 8px;
|
||||
padding: 3px 6px;
|
||||
border-radius: 5px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
QCheckBox#TagPresetCheckbox:hover {
|
||||
@@ -5606,7 +5623,7 @@ QPushButton#ManageTabBtn:checked:hover {
|
||||
QFrame#ManageUserCard {
|
||||
background-color: rgba(30, 41, 59, 0.5);
|
||||
border: 1px solid rgba(71, 85, 105, 0.4);
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
QFrame#ManageUserCard:hover {
|
||||
@@ -5618,7 +5635,7 @@ QFrame#ManageUserCard:hover {
|
||||
QLabel#ManageUserAvatar {
|
||||
background-color: rgba(59, 130, 246, 0.2);
|
||||
border: 1px solid rgba(59, 130, 246, 0.35);
|
||||
border-radius: 24px;
|
||||
border-radius: 14px;
|
||||
color: #93C5FD;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
@@ -5813,7 +5830,7 @@ QPlainTextEdit#ManageLogViewer {
|
||||
QFrame#UserEditNavCard {
|
||||
background-color: rgba(30, 41, 59, 0.62);
|
||||
border: 1px solid rgba(148, 163, 184, 0.12);
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
QTabBar#UserEditTabBar {
|
||||
@@ -5846,7 +5863,7 @@ QTabBar#UserEditTabBar::tab:hover:!selected {
|
||||
QFrame#UserEditSectionCard {
|
||||
background-color: rgba(30, 41, 59, 0.58);
|
||||
border: 1px solid rgba(148, 163, 184, 0.10);
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
QLabel#UserEditSectionTitle {
|
||||
@@ -5875,7 +5892,7 @@ QWidget#UserEditFolderListContainer {
|
||||
QFrame#UserEditFooterBar {
|
||||
background-color: rgba(15, 23, 42, 0.78);
|
||||
border: 1px solid rgba(148, 163, 184, 0.12);
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
/* UserEdit 滚动区 */
|
||||
@@ -6056,7 +6073,7 @@ QScrollArea#DownloadRecordsArea QScrollBar:vertical {
|
||||
QScrollArea#DownloadRecordsArea QScrollBar::handle:vertical {
|
||||
background-color: rgba(148, 163, 184, 0.28);
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
@@ -6148,7 +6165,7 @@ QLabel#DownloadSectionTitle {
|
||||
QLabel#DownloadLocationValue {
|
||||
background-color: rgba(15, 23, 42, 0.52);
|
||||
border: 1px solid rgba(100, 116, 139, 0.14);
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
color: #F8FAFC;
|
||||
padding: 10px 12px;
|
||||
font-size: 12px;
|
||||
@@ -6180,7 +6197,7 @@ QLabel#DownloadEmptyHint {
|
||||
QPushButton#DownloadIconButton {
|
||||
background-color: rgba(15, 23, 42, 0.76);
|
||||
border: 1px solid rgba(100, 116, 139, 0.24);
|
||||
border-radius: 11px;
|
||||
border-radius: 14px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
@@ -6252,7 +6269,7 @@ QPushButton#DownloadIconButton:disabled {
|
||||
#webdav-sync-dialog QLineEdit {
|
||||
background-color: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.10);
|
||||
border-radius: 7px;
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
font-size: 13px;
|
||||
color: #F8FAFC;
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
/* =========================================================
|
||||
全局基础设定 & Tailwind 色板变量映射 (供参考)
|
||||
qEmby Light Theme — QSS Style Sheet
|
||||
Design tokens: see src/qEmbyApp/tokens/DesignTokens.h
|
||||
Animation tokens: see src/qEmbyApp/tokens/AnimationTokens.h
|
||||
|
||||
Border-radius scale: 2(kXs) 4(kSm) 6(kMd) 8(kLg) 14(kXl)
|
||||
Spacing scale: 4(kXs) 8(kSm) 12(kMd) 16(kLg) 24(kXl)
|
||||
Font scale: 10 11 12 13 14 16 20 24 28
|
||||
|
||||
Color palette (Tailwind Light):
|
||||
Primary Blue: #3B82F6, Hover: #2563EB
|
||||
Danger Red: #EF4444, Hover: #DC2626
|
||||
Gray-800 (标题): #1F2937
|
||||
@@ -257,7 +265,7 @@ QListView#SearchCompleterPopup,
|
||||
QListView#SearchHistoryCompleterPopup {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E4EBF3;
|
||||
border-radius: 20px;
|
||||
border-radius: 14px;
|
||||
padding: 0px;
|
||||
outline: none;
|
||||
}
|
||||
@@ -458,7 +466,7 @@ QMenu::separator {
|
||||
QMenu#AppTextContextMenu {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
padding: 8px 0px;
|
||||
}
|
||||
|
||||
@@ -542,7 +550,7 @@ QWidget#login-add-page QLineEdit:focus {
|
||||
QWidget#login-add-page QWidget#login-ssl-card {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
QWidget#login-add-page QWidget#login-ssl-card:hover {
|
||||
@@ -568,7 +576,7 @@ QWidget#login-add-page QLabel#login-ssl-description {
|
||||
QWidget#server-card {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
QWidget#server-card:hover {
|
||||
@@ -588,7 +596,7 @@ QLabel#server-url-label {
|
||||
QLabel#server-fallback-icon {
|
||||
background-color: #EFF6FF;
|
||||
color: #3B82F6;
|
||||
border-radius: 20px;
|
||||
border-radius: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -623,7 +631,7 @@ QPushButton#add-server-btn {
|
||||
qproperty-iconSize: 16px 16px;
|
||||
background-color: transparent;
|
||||
border: 2px dashed #BFDBFE;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
color: #3B82F6;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
@@ -1014,7 +1022,7 @@ ModernMessageBox {
|
||||
QWidget[playerDialogSurface="true"] {
|
||||
background-color: rgba(255, 255, 255, 0.96);
|
||||
border: 1px solid #D1D5DB;
|
||||
border-radius: 18px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
QWidget#dialog-titlebar {
|
||||
@@ -1190,7 +1198,7 @@ QWidget#playerDanmakuSettingsDialog {
|
||||
}
|
||||
|
||||
#playerDanmakuSettingsDialog QLabel#playerDanmakuTileIcon[compactIcon="true"] {
|
||||
border-radius: 11px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
#playerDanmakuSettingsDialog QLabel#playerDanmakuTileTitle {
|
||||
@@ -1512,7 +1520,7 @@ QListWidget#modernComboEmbeddedList QScrollBar:vertical {
|
||||
|
||||
QListWidget#modernComboEmbeddedList QScrollBar::handle:vertical {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
@@ -2411,7 +2419,7 @@ QLabel#identify-preview-label {
|
||||
QLabel#identify-preview-label[media-image-editor="true"] {
|
||||
background-color: #EEF2F7;
|
||||
border: 1px solid #D8E0EA;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
@@ -2451,13 +2459,13 @@ QProgressBar#detail-progress-bar {
|
||||
background-color: rgba(0, 0, 0, 0.12);
|
||||
border: 1px solid transparent;
|
||||
/* 核心:用透明边框强制覆盖原生样式引擎 */
|
||||
border-radius: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
QProgressBar#detail-progress-bar::chunk {
|
||||
background-color: #3B82F6;
|
||||
/* 主题蓝 */
|
||||
border-radius: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* =========================================
|
||||
@@ -2544,7 +2552,7 @@ QPushButton#hud-close-btn:hover {
|
||||
QPushButton.player-hud-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
QPushButton.player-hud-btn:hover {
|
||||
@@ -2590,7 +2598,7 @@ QPushButton#playerSpeedBtn {
|
||||
/* 彻底抹除边框 */
|
||||
outline: none;
|
||||
/* 彻底抹除焦点虚线 */
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
@@ -2598,7 +2606,7 @@ QPushButton#playerMediaSwitchBtn {
|
||||
min-width: 0px;
|
||||
padding: 0px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
QPushButton#playerMediaSwitchBtn:hover {
|
||||
@@ -2822,7 +2830,7 @@ QPushButton#immersive-stream-btn {
|
||||
border: none;
|
||||
padding: 4px 10px;
|
||||
/* 【调优】上下内边距收紧,打造细长的精致胶囊 */
|
||||
border-radius: 5px;
|
||||
border-radius: 6px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@@ -2846,7 +2854,7 @@ QLineEdit#detail-episode-jump-edit {
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
border-radius: 5px;
|
||||
border-radius: 6px;
|
||||
selection-background-color: rgba(59, 130, 246, 0.22);
|
||||
selection-color: #1D4ED8;
|
||||
}
|
||||
@@ -2884,7 +2892,7 @@ QMenu#immersive-stream-menu {
|
||||
QMenu#immersive-stream-menu::item {
|
||||
color: #333333;
|
||||
padding: 6px 28px 6px 14px;
|
||||
border-radius: 5px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -2921,7 +2929,7 @@ QWidget#stream-menu-item-host {
|
||||
QWidget#stream-menu-item {
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 5px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
QWidget#stream-menu-item:hover {
|
||||
@@ -2999,15 +3007,24 @@ QLabel#osdSeekTimeLabel {
|
||||
border: 1px solid rgba(255, 255, 255, 28);
|
||||
}
|
||||
|
||||
/* 进度条悬停时间气泡(播放器恒暗,与 dark 一致;面板底色由 SeekTimeBubble 自绘) */
|
||||
QLabel#seekBubbleTime {
|
||||
background: transparent;
|
||||
color: #F8FAFC;
|
||||
font-family: "Segoe UI", "Microsoft YaHei UI", "Microsoft YaHei", sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
QWidget#osdSeekStem {
|
||||
background-color: rgba(96, 165, 250, 180);
|
||||
border-radius: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
QWidget#osdSeekMarker {
|
||||
background-color: rgba(248, 250, 252, 220);
|
||||
border: 1px solid rgba(96, 165, 250, 230);
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
QWidget#osdVolumePanel {
|
||||
@@ -3045,7 +3062,7 @@ QLabel#osdVolumeLabel {
|
||||
QWidget#playerMouseEdgeIndicator {
|
||||
background-color: rgba(255, 255, 255, 18);
|
||||
border: 1px solid rgba(255, 255, 255, 52);
|
||||
border-radius: 22px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
QWidget#playerMouseEdgeIndicator[gestureActive="true"] {
|
||||
@@ -3140,7 +3157,7 @@ QScrollBar#modernMenuScrollBar:vertical {
|
||||
|
||||
QScrollBar#modernMenuScrollBar::handle:vertical {
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
min-height: 28px;
|
||||
/* 显式归零,覆盖全局规则的 margin: 0px 3px,否则会被压缩成 0 宽不可见 */
|
||||
margin: 0px;
|
||||
@@ -3343,7 +3360,7 @@ QLabel#AboutThanksAvatar {
|
||||
color: #FFFFFF;
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
|
||||
stop:0 #FB923C, stop:0.48 #DB2777, stop:1 #2563EB);
|
||||
border-radius: 11px;
|
||||
border-radius: 14px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
@@ -3488,7 +3505,7 @@ QLabel#SettingsPageTitle {
|
||||
#SettingsCard {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
#SettingsCard:hover {
|
||||
@@ -3874,7 +3891,7 @@ QLineEdit#TagInputInlineEdit {
|
||||
QToolButton#TagInputClearBtn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
qproperty-icon: url(":/svg/light/close.svg");
|
||||
qproperty-iconSize: 8px 8px;
|
||||
}
|
||||
@@ -3923,7 +3940,7 @@ QMenu#TagPresetMenu::indicator {
|
||||
background: rgba(0, 0, 0, 0.018);
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border-left: 3px solid #4A90D9;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
@@ -4030,7 +4047,7 @@ QLabel#SettingsSubTitle {
|
||||
/* Tailwind Gray 700 */
|
||||
padding: 6px 32px 6px 14px;
|
||||
margin: 2px 6px;
|
||||
border-radius: 5px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@@ -4060,14 +4077,14 @@ QLabel#SettingsSubTitle {
|
||||
#ManageInfoCard {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#ManageActivityCard {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
@@ -4125,7 +4142,7 @@ QFrame#TranscodingGroupBlock,
|
||||
QFrame#TranscodingCodecItem {
|
||||
background-color: #F8FAFC;
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
QFrame#TranscodingCodecItem[dragging="true"] {
|
||||
@@ -4358,12 +4375,12 @@ QProgressBar#NowPlayingProgressBar {
|
||||
max-height: 2px;
|
||||
background-color: rgba(0, 0, 0, 0.12);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
QProgressBar#NowPlayingProgressBar::chunk {
|
||||
background-color: #3B82F6;
|
||||
border-radius: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
QLabel#NowPlayingUser {
|
||||
@@ -4818,7 +4835,7 @@ QCheckBox#LibraryMetadataLockCheck::indicator:checked {
|
||||
QLineEdit#PlaylistSearchEdit {
|
||||
background-color: #F8FAFC;
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 7px;
|
||||
border-radius: 6px;
|
||||
color: #334155;
|
||||
font-size: 12px;
|
||||
font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
@@ -5347,12 +5364,12 @@ QProgressBar#ManageLibScanProgress {
|
||||
max-height: 2px;
|
||||
background-color: rgba(0, 0, 0, 0.12);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
QProgressBar#ManageLibScanProgress::chunk {
|
||||
background-color: #3B82F6;
|
||||
border-radius: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* 取消扫描按钮 */
|
||||
@@ -5444,7 +5461,7 @@ QToolButton#TagInputClearBtn {
|
||||
border: none;
|
||||
qproperty-icon: url(:/svg/light/close.svg);
|
||||
qproperty-iconSize: 10px 10px;
|
||||
border-radius: 5px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
QToolButton#TagInputClearBtn:hover {
|
||||
@@ -5502,7 +5519,7 @@ QLineEdit#TagPresetSearchEdit:focus {
|
||||
/* 弹出面板内复选框 */
|
||||
QFrame#TagPresetOptionRow {
|
||||
background-color: transparent;
|
||||
border-radius: 5px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
QFrame#TagPresetOptionRow:hover {
|
||||
@@ -5525,7 +5542,7 @@ QCheckBox#TagPresetCheckbox {
|
||||
font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
spacing: 8px;
|
||||
padding: 3px 6px;
|
||||
border-radius: 5px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
QCheckBox#TagPresetCheckbox:hover {
|
||||
@@ -5667,7 +5684,7 @@ QPushButton#ManageTabBtn:checked:hover {
|
||||
QFrame#ManageUserCard {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
QFrame#ManageUserCard:hover {
|
||||
@@ -5679,7 +5696,7 @@ QFrame#ManageUserCard:hover {
|
||||
QLabel#ManageUserAvatar {
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
border: 1px solid rgba(59, 130, 246, 0.25);
|
||||
border-radius: 24px;
|
||||
border-radius: 14px;
|
||||
color: #2563EB;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
@@ -5875,7 +5892,7 @@ QPlainTextEdit#ManageLogViewer {
|
||||
QFrame#UserEditNavCard {
|
||||
background-color: rgba(248, 250, 252, 0.88);
|
||||
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
QTabBar#UserEditTabBar {
|
||||
@@ -5908,7 +5925,7 @@ QTabBar#UserEditTabBar::tab:hover:!selected {
|
||||
QFrame#UserEditSectionCard {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
QLabel#UserEditSectionTitle {
|
||||
@@ -5937,7 +5954,7 @@ QWidget#UserEditFolderListContainer {
|
||||
QFrame#UserEditFooterBar {
|
||||
background-color: rgba(255, 255, 255, 0.96);
|
||||
border: 1px solid rgba(148, 163, 184, 0.16);
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
/* UserEdit 滚动区 */
|
||||
@@ -6118,7 +6135,7 @@ QScrollArea#DownloadRecordsArea QScrollBar:vertical {
|
||||
QScrollArea#DownloadRecordsArea QScrollBar::handle:vertical {
|
||||
background-color: rgba(148, 163, 184, 0.42);
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
@@ -6210,7 +6227,7 @@ QLabel#DownloadSectionTitle {
|
||||
QLabel#DownloadLocationValue {
|
||||
background-color: rgba(248, 250, 252, 0.72);
|
||||
border: 1px solid rgba(148, 163, 184, 0.10);
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
color: #0F172A;
|
||||
padding: 10px 12px;
|
||||
font-size: 12px;
|
||||
@@ -6242,7 +6259,7 @@ QLabel#DownloadEmptyHint {
|
||||
QPushButton#DownloadIconButton {
|
||||
background-color: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
border-radius: 11px;
|
||||
border-radius: 14px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
@@ -6303,7 +6320,7 @@ QPushButton#DownloadIconButton:disabled {
|
||||
#webdav-sync-dialog QLineEdit {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #CBD5E1;
|
||||
border-radius: 7px;
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
font-size: 13px;
|
||||
color: #1F2937;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#ifndef QEMBYAPP_ANIMATION_TOKENS_H
|
||||
#define QEMBYAPP_ANIMATION_TOKENS_H
|
||||
|
||||
#include <QEasingCurve>
|
||||
|
||||
namespace AnimationTokens {
|
||||
|
||||
namespace DurationMs {
|
||||
constexpr int kMicro = 120;
|
||||
constexpr int kQuick = 150; // between kMicro and kFast (small hover/decoration fades)
|
||||
constexpr int kFast = 180;
|
||||
constexpr int kNormal = 300;
|
||||
constexpr int kSlow = 400;
|
||||
constexpr int kGentle = 800;
|
||||
constexpr int kShimmer = 1800;
|
||||
}
|
||||
|
||||
namespace Curve {
|
||||
inline QEasingCurve enter() { return QEasingCurve::OutCubic; }
|
||||
inline QEasingCurve exit() { return QEasingCurve::InCubic; }
|
||||
inline QEasingCurve bounce() { return QEasingCurve::OutBack; }
|
||||
inline QEasingCurve loop() { return QEasingCurve::Linear; }
|
||||
inline QEasingCurve fade() { return QEasingCurve::OutQuad; }
|
||||
inline QEasingCurve oscillate() { return QEasingCurve::InOutSine; }
|
||||
}
|
||||
|
||||
} // namespace AnimationTokens
|
||||
|
||||
#endif // QEMBYAPP_ANIMATION_TOKENS_H
|
||||
@@ -0,0 +1,36 @@
|
||||
#ifndef QEMBYAPP_DESIGN_TOKENS_H
|
||||
#define QEMBYAPP_DESIGN_TOKENS_H
|
||||
|
||||
namespace DesignTokens {
|
||||
|
||||
namespace Radius {
|
||||
constexpr int kXs = 2;
|
||||
constexpr int kSm = 4;
|
||||
constexpr int kMd = 6;
|
||||
constexpr int kLg = 8;
|
||||
constexpr int kXl = 14;
|
||||
}
|
||||
|
||||
namespace Spacing {
|
||||
constexpr int kXs = 4;
|
||||
constexpr int kSm = 8;
|
||||
constexpr int kMd = 12;
|
||||
constexpr int kLg = 16;
|
||||
constexpr int kXl = 24;
|
||||
}
|
||||
|
||||
namespace FontSize {
|
||||
constexpr int kCaption = 10;
|
||||
constexpr int kSmall = 11;
|
||||
constexpr int kBody = 12;
|
||||
constexpr int kBodyPlus = 13;
|
||||
constexpr int kHeading = 14;
|
||||
constexpr int kSubtitle = 16;
|
||||
constexpr int kTitle = 20;
|
||||
constexpr int kDisplay = 24;
|
||||
constexpr int kHero = 28;
|
||||
}
|
||||
|
||||
} // namespace DesignTokens
|
||||
|
||||
#endif // QEMBYAPP_DESIGN_TOKENS_H
|
||||
@@ -0,0 +1,121 @@
|
||||
#include "fadehelper.h"
|
||||
|
||||
#include <QGraphicsOpacityEffect>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QWidget>
|
||||
|
||||
#include "motionprefs.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// Returns the opacity effect to drive, creating one if the widget has no effect.
|
||||
// Sets `usable` to false when the widget already owns a *different* effect (e.g. a
|
||||
// drop shadow) that we must not replace.
|
||||
QGraphicsOpacityEffect *resolveOpacityEffect(QWidget *target, bool &usable)
|
||||
{
|
||||
if (QGraphicsEffect *existing = target->graphicsEffect())
|
||||
{
|
||||
auto *opacity = qobject_cast<QGraphicsOpacityEffect *>(existing);
|
||||
usable = (opacity != nullptr);
|
||||
return opacity;
|
||||
}
|
||||
|
||||
auto *opacity = new QGraphicsOpacityEffect(target);
|
||||
opacity->setOpacity(1.0);
|
||||
target->setGraphicsEffect(opacity);
|
||||
usable = true;
|
||||
return opacity;
|
||||
}
|
||||
|
||||
// Cancel any fade already in flight on this effect so overlapping calls don't fight.
|
||||
void stopInFlight(QGraphicsOpacityEffect *effect)
|
||||
{
|
||||
const auto running = effect->findChildren<QPropertyAnimation *>();
|
||||
for (QPropertyAnimation *anim : running)
|
||||
{
|
||||
anim->stop();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QPropertyAnimation *FadeHelper::fadeIn(QWidget *target, int durationMs,
|
||||
const QEasingCurve &curve)
|
||||
{
|
||||
if (!target)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool usable = false;
|
||||
QGraphicsOpacityEffect *effect = resolveOpacityEffect(target, usable);
|
||||
target->show();
|
||||
|
||||
if (!usable || !effect)
|
||||
{
|
||||
return nullptr; // foreign effect present — leave it alone
|
||||
}
|
||||
|
||||
stopInFlight(effect); // a new request always supersedes an in-flight fade
|
||||
|
||||
if (durationMs <= 0 || MotionPrefs::reduceMotion())
|
||||
{
|
||||
effect->setOpacity(1.0);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto *anim = new QPropertyAnimation(effect, "opacity", effect);
|
||||
anim->setDuration(durationMs);
|
||||
anim->setEasingCurve(curve);
|
||||
anim->setStartValue(effect->opacity());
|
||||
anim->setEndValue(1.0);
|
||||
anim->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
return anim;
|
||||
}
|
||||
|
||||
QPropertyAnimation *FadeHelper::fadeOut(QWidget *target, int durationMs,
|
||||
const QEasingCurve &curve,
|
||||
std::function<void()> onDone)
|
||||
{
|
||||
if (!target)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool usable = false;
|
||||
QGraphicsOpacityEffect *effect = resolveOpacityEffect(target, usable);
|
||||
|
||||
if (!usable || !effect)
|
||||
{
|
||||
if (onDone)
|
||||
{
|
||||
onDone(); // can't fade a foreign-effect widget — just finish
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
stopInFlight(effect); // a new request always supersedes an in-flight fade
|
||||
|
||||
if (durationMs <= 0 || MotionPrefs::reduceMotion())
|
||||
{
|
||||
effect->setOpacity(0.0);
|
||||
if (onDone)
|
||||
{
|
||||
onDone();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto *anim = new QPropertyAnimation(effect, "opacity", effect);
|
||||
anim->setDuration(durationMs);
|
||||
anim->setEasingCurve(curve);
|
||||
anim->setStartValue(effect->opacity());
|
||||
anim->setEndValue(0.0);
|
||||
if (onDone)
|
||||
{
|
||||
// `target` as context: if it's destroyed mid-fade, the callback is dropped.
|
||||
QObject::connect(anim, &QAbstractAnimation::finished, target, onDone);
|
||||
}
|
||||
anim->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
return anim;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#ifndef QEMBYAPP_FADE_HELPER_H
|
||||
#define QEMBYAPP_FADE_HELPER_H
|
||||
|
||||
#include <QEasingCurve>
|
||||
#include <functional>
|
||||
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
|
||||
class QWidget;
|
||||
class QPropertyAnimation;
|
||||
|
||||
// Reusable opacity fade built on QGraphicsOpacityEffect + AnimationTokens.
|
||||
// Both helpers honor the "Reduce Animations" preference (MotionPrefs::reduceMotion):
|
||||
// when reduce-motion is on (or durationMs <= 0) they snap to the end state instead
|
||||
// of animating.
|
||||
//
|
||||
// One-effect-per-widget guard: if `target` already owns a non-opacity QGraphicsEffect
|
||||
// (e.g. a drop shadow), the helper will NOT clobber it — it snaps to the end state and
|
||||
// returns nullptr, leaving the foreign effect intact. Callers that need to fade such a
|
||||
// widget must wrap it in a container.
|
||||
//
|
||||
// CAUTION — the guard cannot see CHILD effects: fading a widget whose descendant
|
||||
// carries its own QGraphicsEffect nests effects, which breaks rendering
|
||||
// ("QPainter not active" spam, invisible content — see PlayerOverlayDialog's
|
||||
// shadowed surface). Don't fade such parents; fade an effect-free ancestor level
|
||||
// or skip the fade.
|
||||
namespace FadeHelper {
|
||||
|
||||
// Ensure `target` is visible and animate its opacity up to 1.0.
|
||||
// Returns the running animation, or nullptr when snapped / not applicable.
|
||||
QPropertyAnimation *fadeIn(QWidget *target,
|
||||
int durationMs = AnimationTokens::DurationMs::kNormal,
|
||||
const QEasingCurve &curve = AnimationTokens::Curve::fade());
|
||||
|
||||
// Animate `target` opacity down to 0.0, invoking `onDone` when finished (or
|
||||
// immediately when snapped). Returns the running animation, or nullptr when snapped.
|
||||
QPropertyAnimation *fadeOut(QWidget *target,
|
||||
int durationMs = AnimationTokens::DurationMs::kNormal,
|
||||
const QEasingCurve &curve = AnimationTokens::Curve::fade(),
|
||||
std::function<void()> onDone = {});
|
||||
|
||||
} // namespace FadeHelper
|
||||
|
||||
#endif // QEMBYAPP_FADE_HELPER_H
|
||||
@@ -0,0 +1,50 @@
|
||||
#include "hoveranimator.h"
|
||||
|
||||
#include <QVariantAnimation>
|
||||
|
||||
#include "motionprefs.h"
|
||||
|
||||
HoverAnimator::HoverAnimator(QObject *parent, int inMs, int outMs)
|
||||
: QObject(parent), m_inMs(inMs), m_outMs(outMs)
|
||||
{
|
||||
m_anim = new QVariantAnimation(this);
|
||||
connect(m_anim, &QVariantAnimation::valueChanged, this,
|
||||
[this](const QVariant &value) { applyProgress(value.toReal()); });
|
||||
}
|
||||
|
||||
void HoverAnimator::setHovered(bool hovered)
|
||||
{
|
||||
if (m_hovered == hovered)
|
||||
return;
|
||||
m_hovered = hovered;
|
||||
|
||||
const qreal target = hovered ? 1.0 : 0.0;
|
||||
m_anim->stop();
|
||||
|
||||
if (MotionPrefs::reduceMotion() || m_progress == target) {
|
||||
applyProgress(target);
|
||||
return;
|
||||
}
|
||||
|
||||
m_anim->setStartValue(m_progress);
|
||||
m_anim->setEndValue(target);
|
||||
m_anim->setDuration(hovered ? m_inMs : m_outMs);
|
||||
m_anim->setEasingCurve(hovered ? AnimationTokens::Curve::enter()
|
||||
: AnimationTokens::Curve::exit());
|
||||
m_anim->start();
|
||||
}
|
||||
|
||||
void HoverAnimator::setCallback(std::function<void(qreal)> callback)
|
||||
{
|
||||
m_callback = std::move(callback);
|
||||
}
|
||||
|
||||
void HoverAnimator::applyProgress(qreal progress)
|
||||
{
|
||||
if (m_progress == progress)
|
||||
return;
|
||||
m_progress = progress;
|
||||
if (m_callback)
|
||||
m_callback(m_progress);
|
||||
Q_EMIT progressChanged(m_progress);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#ifndef QEMBYAPP_HOVER_ANIMATOR_H
|
||||
#define QEMBYAPP_HOVER_ANIMATOR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <functional>
|
||||
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
|
||||
class QVariantAnimation;
|
||||
|
||||
// Interpolated 0..1 hover progress for paint-time hover effects, replacing
|
||||
// binary isHovered repaints. Asymmetric rhythm by convention: hover-in is
|
||||
// slower (kQuick / enter) than hover-out (kMicro / exit).
|
||||
//
|
||||
// Honors the "Reduce Animations" preference (MotionPrefs::reduceMotion):
|
||||
// when reduce-motion is on, setHovered() snaps progress straight to 0/1.
|
||||
// Retargeting mid-flight restarts from the current progress, so rapid
|
||||
// enter/leave never jumps.
|
||||
//
|
||||
// Typical use in a QWidget:
|
||||
// m_hover = new HoverAnimator(this);
|
||||
// m_hover->setCallback([this](qreal) { update(); });
|
||||
// // enterEvent -> m_hover->setHovered(true), leaveEvent -> false
|
||||
// // paintEvent -> interpolate with m_hover->progress()
|
||||
class HoverAnimator : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit HoverAnimator(QObject *parent = nullptr,
|
||||
int inMs = AnimationTokens::DurationMs::kQuick,
|
||||
int outMs = AnimationTokens::DurationMs::kMicro);
|
||||
|
||||
void setHovered(bool hovered);
|
||||
bool isHovered() const { return m_hovered; }
|
||||
qreal progress() const { return m_progress; }
|
||||
|
||||
// Invoked on every progress change (typically [w] { w->update(); }).
|
||||
void setCallback(std::function<void(qreal)> callback);
|
||||
|
||||
Q_SIGNALS:
|
||||
void progressChanged(qreal progress);
|
||||
|
||||
private:
|
||||
void applyProgress(qreal progress);
|
||||
|
||||
QVariantAnimation *m_anim = nullptr;
|
||||
std::function<void(qreal)> m_callback;
|
||||
qreal m_progress = 0.0;
|
||||
bool m_hovered = false;
|
||||
int m_inMs;
|
||||
int m_outMs;
|
||||
};
|
||||
|
||||
#endif // QEMBYAPP_HOVER_ANIMATOR_H
|
||||
@@ -0,0 +1,132 @@
|
||||
#include "indexhoveranimator.h"
|
||||
|
||||
#include <QVariantAnimation>
|
||||
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
#include "motionprefs.h"
|
||||
|
||||
IndexHoverAnimator::IndexHoverAnimator(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void IndexHoverAnimator::setHoveredIndex(const QPersistentModelIndex &index)
|
||||
{
|
||||
if (m_hovered == index)
|
||||
return;
|
||||
|
||||
const QPersistentModelIndex previous = m_hovered;
|
||||
m_hovered = index;
|
||||
|
||||
if (MotionPrefs::reduceMotion()) {
|
||||
for (Entry &entry : m_entries) {
|
||||
if (entry.anim)
|
||||
entry.anim->stop();
|
||||
}
|
||||
const QList<Entry> old = m_entries;
|
||||
m_entries.clear();
|
||||
for (const Entry &entry : old) {
|
||||
if (entry.anim)
|
||||
entry.anim->deleteLater();
|
||||
repaint(entry.index);
|
||||
}
|
||||
if (index.isValid()) {
|
||||
Entry snapped;
|
||||
snapped.index = index;
|
||||
snapped.progress = 1.0;
|
||||
m_entries.append(snapped);
|
||||
repaint(index);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (previous.isValid()) {
|
||||
if (Entry *entry = entryFor(previous))
|
||||
animateEntry(*entry, false);
|
||||
}
|
||||
if (index.isValid()) {
|
||||
Entry *entry = entryFor(index);
|
||||
if (!entry) {
|
||||
Entry fresh;
|
||||
fresh.index = index;
|
||||
m_entries.append(fresh);
|
||||
entry = &m_entries.last();
|
||||
}
|
||||
animateEntry(*entry, true);
|
||||
}
|
||||
}
|
||||
|
||||
qreal IndexHoverAnimator::progressFor(const QModelIndex &index) const
|
||||
{
|
||||
for (const Entry &entry : m_entries) {
|
||||
if (entry.index.isValid() && entry.index == index)
|
||||
return entry.progress;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
void IndexHoverAnimator::setRepaintCallback(
|
||||
std::function<void(const QModelIndex &)> callback)
|
||||
{
|
||||
m_repaintCallback = std::move(callback);
|
||||
}
|
||||
|
||||
IndexHoverAnimator::Entry *IndexHoverAnimator::entryFor(
|
||||
const QPersistentModelIndex &index)
|
||||
{
|
||||
for (Entry &entry : m_entries) {
|
||||
if (entry.index == index)
|
||||
return &entry;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void IndexHoverAnimator::animateEntry(Entry &entry, bool entering)
|
||||
{
|
||||
const QPersistentModelIndex index = entry.index;
|
||||
if (!entry.anim) {
|
||||
auto *anim = new QVariantAnimation(this);
|
||||
entry.anim = anim;
|
||||
connect(anim, &QVariantAnimation::valueChanged, this,
|
||||
[this, index](const QVariant &value) {
|
||||
if (Entry *e = entryFor(index)) {
|
||||
e->progress = value.toReal();
|
||||
repaint(index);
|
||||
}
|
||||
});
|
||||
connect(anim, &QVariantAnimation::finished, this,
|
||||
[this]() { prune(); });
|
||||
}
|
||||
|
||||
entry.anim->stop();
|
||||
entry.anim->setStartValue(entry.progress);
|
||||
entry.anim->setEndValue(entering ? 1.0 : 0.0);
|
||||
entry.anim->setDuration(entering ? AnimationTokens::DurationMs::kQuick
|
||||
: AnimationTokens::DurationMs::kMicro);
|
||||
entry.anim->setEasingCurve(entering ? AnimationTokens::Curve::enter()
|
||||
: AnimationTokens::Curve::exit());
|
||||
entry.anim->start();
|
||||
}
|
||||
|
||||
void IndexHoverAnimator::repaint(const QPersistentModelIndex &index)
|
||||
{
|
||||
if (m_repaintCallback && index.isValid())
|
||||
m_repaintCallback(index);
|
||||
}
|
||||
|
||||
void IndexHoverAnimator::prune()
|
||||
{
|
||||
for (int i = m_entries.size() - 1; i >= 0; --i) {
|
||||
Entry &entry = m_entries[i];
|
||||
const bool finished =
|
||||
!entry.anim || entry.anim->state() != QAbstractAnimation::Running;
|
||||
const bool dead = !entry.index.isValid() ||
|
||||
(finished && entry.progress <= 0.0 &&
|
||||
entry.index != m_hovered);
|
||||
if (dead) {
|
||||
if (entry.anim)
|
||||
entry.anim->deleteLater();
|
||||
m_entries.removeAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#ifndef QEMBYAPP_INDEX_HOVER_ANIMATOR_H
|
||||
#define QEMBYAPP_INDEX_HOVER_ANIMATOR_H
|
||||
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QPersistentModelIndex>
|
||||
#include <functional>
|
||||
|
||||
class QVariantAnimation;
|
||||
|
||||
// Per-index hover progress for delegate-painted item views: tracks which
|
||||
// QModelIndex is hovered and interpolates a 0..1 progress per index, so a
|
||||
// stateless delegate can paint smooth hover transitions. When the hover
|
||||
// moves between items the old index animates out while the new one animates
|
||||
// in, concurrently. Same asymmetric rhythm as HoverAnimator (in kQuick/enter,
|
||||
// out kMicro/exit) and the same reduce-motion snapping.
|
||||
//
|
||||
// The repaint callback is invoked with every index whose progress changed;
|
||||
// callers should invalidate visualRect(idx) expanded by repaintMargin()
|
||||
// (lift + shadow overflow). Entries are pruned automatically once they fade
|
||||
// back to 0 or their index is invalidated by the model.
|
||||
class IndexHoverAnimator : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit IndexHoverAnimator(QObject *parent = nullptr);
|
||||
|
||||
// Pass an invalid index (or call clear()) when the cursor leaves.
|
||||
void setHoveredIndex(const QPersistentModelIndex &index);
|
||||
void clear() { setHoveredIndex(QPersistentModelIndex()); }
|
||||
|
||||
QPersistentModelIndex hoveredIndex() const { return m_hovered; }
|
||||
// 0.0 for untracked/invalid indexes.
|
||||
qreal progressFor(const QModelIndex &index) const;
|
||||
|
||||
// Extra pixels the repaint region must extend beyond the item rect.
|
||||
void setRepaintMargin(int px) { m_repaintMargin = px; }
|
||||
int repaintMargin() const { return m_repaintMargin; }
|
||||
|
||||
void setRepaintCallback(std::function<void(const QModelIndex &)> callback);
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
QPersistentModelIndex index;
|
||||
QVariantAnimation *anim = nullptr;
|
||||
qreal progress = 0.0;
|
||||
};
|
||||
|
||||
Entry *entryFor(const QPersistentModelIndex &index);
|
||||
void animateEntry(Entry &entry, bool entering);
|
||||
void repaint(const QPersistentModelIndex &index);
|
||||
void prune();
|
||||
|
||||
QList<Entry> m_entries;
|
||||
QPersistentModelIndex m_hovered;
|
||||
std::function<void(const QModelIndex &)> m_repaintCallback;
|
||||
int m_repaintMargin = 0;
|
||||
};
|
||||
|
||||
#endif // QEMBYAPP_INDEX_HOVER_ANIMATOR_H
|
||||
@@ -0,0 +1,19 @@
|
||||
#ifndef QEMBYAPP_MOTION_PREFS_H
|
||||
#define QEMBYAPP_MOTION_PREFS_H
|
||||
|
||||
#include "../../qEmbyCore/config/config_keys.h"
|
||||
#include "../../qEmbyCore/config/configstore.h"
|
||||
|
||||
// Single source of truth for the "Reduce Animations" accessibility preference
|
||||
// (ConfigKeys::UiAnimations, default false). reduceMotion() == true means the
|
||||
// user wants motion suppressed: skip/snap animations to their end state.
|
||||
namespace MotionPrefs {
|
||||
|
||||
inline bool reduceMotion()
|
||||
{
|
||||
return ConfigStore::instance()->get<bool>(ConfigKeys::UiAnimations, false);
|
||||
}
|
||||
|
||||
} // namespace MotionPrefs
|
||||
|
||||
#endif // QEMBYAPP_MOTION_PREFS_H
|
||||
@@ -0,0 +1,37 @@
|
||||
#include "popfeedback.h"
|
||||
|
||||
#include <QVariantAnimation>
|
||||
|
||||
#include "motionprefs.h"
|
||||
|
||||
namespace PopFeedback {
|
||||
|
||||
QVariantAnimation *play(QObject *context, std::function<void(qreal)> callback,
|
||||
qreal peak, int durationMs)
|
||||
{
|
||||
if (!callback)
|
||||
return nullptr;
|
||||
|
||||
if (MotionPrefs::reduceMotion() || durationMs <= 0) {
|
||||
callback(1.0);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto *anim = new QVariantAnimation(context);
|
||||
anim->setStartValue(0.85);
|
||||
anim->setKeyValueAt(0.55, peak);
|
||||
anim->setEndValue(1.0);
|
||||
anim->setDuration(durationMs);
|
||||
anim->setEasingCurve(AnimationTokens::Curve::fade());
|
||||
|
||||
QObject::connect(anim, &QVariantAnimation::valueChanged, context,
|
||||
[callback](const QVariant &value) {
|
||||
callback(value.toReal());
|
||||
});
|
||||
QObject::connect(anim, &QVariantAnimation::finished, context,
|
||||
[callback]() { callback(1.0); });
|
||||
anim->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
return anim;
|
||||
}
|
||||
|
||||
} // namespace PopFeedback
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef QEMBYAPP_POP_FEEDBACK_H
|
||||
#define QEMBYAPP_POP_FEEDBACK_H
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
|
||||
class QObject;
|
||||
class QVariantAnimation;
|
||||
|
||||
// One-shot scale pulse confirming a state toggle (favorite, played, ...):
|
||||
// dips to ~0.85, overshoots to `peak`, settles at 1.0. The callback receives
|
||||
// the current scale factor per tick and is guaranteed a final cb(1.0), so
|
||||
// painting keyed on the scale always ends at rest.
|
||||
//
|
||||
// Honors the "Reduce Animations" preference: when reduce-motion is on the
|
||||
// callback is invoked once with 1.0 and nullptr is returned. The animation
|
||||
// parents to `context` and self-deletes when finished.
|
||||
namespace PopFeedback {
|
||||
|
||||
QVariantAnimation *play(QObject *context, std::function<void(qreal)> callback,
|
||||
qreal peak = 1.15,
|
||||
int durationMs = AnimationTokens::DurationMs::kNormal);
|
||||
|
||||
} // namespace PopFeedback
|
||||
|
||||
#endif // QEMBYAPP_POP_FEEDBACK_H
|
||||
@@ -0,0 +1,71 @@
|
||||
#ifndef QEMBYAPP_POPUP_FADE_H
|
||||
#define QEMBYAPP_POPUP_FADE_H
|
||||
|
||||
#include <QEvent>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QVariantAnimation>
|
||||
#include <QWidget>
|
||||
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
#include "motionprefs.h"
|
||||
|
||||
// Entrance fade for top-level popups (frameless QMenus, combo popups):
|
||||
// windowOpacity 0 -> 1 over kQuick on every Show. Closing stays instant —
|
||||
// menus dismiss on action and must not linger. Best-effort: compositors that
|
||||
// ignore windowOpacity simply show the popup fully opaque.
|
||||
//
|
||||
// Honors the "Reduce Animations" preference. For child-widget overlays use
|
||||
// FadeHelper instead (windowOpacity only works on top-level windows).
|
||||
namespace PopupFade {
|
||||
|
||||
// Fade the (already shown) top-level popup in right now. Used directly when
|
||||
// the popup is opened by code that can call it (e.g. after
|
||||
// QComboBox::showPopup()); attach() automates it for widgets with a Show event.
|
||||
inline void run(QWidget *target)
|
||||
{
|
||||
if (!target || !target->isWindow() || MotionPrefs::reduceMotion())
|
||||
return;
|
||||
|
||||
auto *anim = new QVariantAnimation(target);
|
||||
anim->setStartValue(0.0);
|
||||
anim->setEndValue(1.0);
|
||||
anim->setDuration(AnimationTokens::DurationMs::kQuick);
|
||||
anim->setEasingCurve(AnimationTokens::Curve::fade());
|
||||
QObject::connect(anim, &QVariantAnimation::valueChanged, target,
|
||||
[target = QPointer<QWidget>(target)](const QVariant &value) {
|
||||
if (target)
|
||||
target->setWindowOpacity(value.toReal());
|
||||
});
|
||||
target->setWindowOpacity(0.0);
|
||||
anim->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
}
|
||||
|
||||
class Filter : public QObject
|
||||
{
|
||||
public:
|
||||
explicit Filter(QWidget *target) : QObject(target), m_target(target) {}
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *object, QEvent *event) override
|
||||
{
|
||||
if (object == m_target && event->type() == QEvent::Show && m_target)
|
||||
{
|
||||
run(m_target);
|
||||
}
|
||||
return QObject::eventFilter(object, event);
|
||||
}
|
||||
|
||||
private:
|
||||
QPointer<QWidget> m_target;
|
||||
};
|
||||
|
||||
inline void attach(QWidget *popup)
|
||||
{
|
||||
if (popup)
|
||||
popup->installEventFilter(new Filter(popup));
|
||||
}
|
||||
|
||||
} // namespace PopupFade
|
||||
|
||||
#endif // QEMBYAPP_POPUP_FADE_H
|
||||
@@ -0,0 +1,50 @@
|
||||
#include "refreshspin.h"
|
||||
|
||||
#include <QAbstractButton>
|
||||
#include <QIcon>
|
||||
#include <QPixmap>
|
||||
#include <QTransform>
|
||||
#include <QVariantAnimation>
|
||||
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
#include "motionprefs.h"
|
||||
|
||||
namespace RefreshSpin {
|
||||
|
||||
QVariantAnimation *start(QAbstractButton *button)
|
||||
{
|
||||
if (!button || MotionPrefs::reduceMotion())
|
||||
return nullptr;
|
||||
|
||||
const QIcon originalIcon = button->icon();
|
||||
const QSize iconSize = button->iconSize();
|
||||
const QPixmap basePixmap = originalIcon.pixmap(iconSize);
|
||||
if (iconSize.isEmpty() || basePixmap.isNull())
|
||||
return nullptr;
|
||||
|
||||
auto *animation = new QVariantAnimation(button);
|
||||
animation->setStartValue(0.0);
|
||||
animation->setEndValue(360.0);
|
||||
animation->setDuration(AnimationTokens::DurationMs::kGentle);
|
||||
animation->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
|
||||
QObject::connect(animation, &QVariantAnimation::valueChanged, button,
|
||||
[button, basePixmap, iconSize](const QVariant &value) {
|
||||
QTransform transform;
|
||||
transform.translate(iconSize.width() / 2.0, iconSize.height() / 2.0);
|
||||
transform.rotate(value.toReal());
|
||||
transform.translate(-iconSize.width() / 2.0, -iconSize.height() / 2.0);
|
||||
const QPixmap rotated =
|
||||
basePixmap.transformed(transform, Qt::SmoothTransformation);
|
||||
const int x = (rotated.width() - iconSize.width()) / 2;
|
||||
const int y = (rotated.height() - iconSize.height()) / 2;
|
||||
button->setIcon(QIcon(
|
||||
rotated.copy(x, y, iconSize.width(), iconSize.height())));
|
||||
});
|
||||
QObject::connect(animation, &QVariantAnimation::finished, button,
|
||||
[button, originalIcon]() { button->setIcon(originalIcon); });
|
||||
animation->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
return animation;
|
||||
}
|
||||
|
||||
} // namespace RefreshSpin
|
||||
@@ -0,0 +1,18 @@
|
||||
#ifndef QEMBYAPP_REFRESH_SPIN_H
|
||||
#define QEMBYAPP_REFRESH_SPIN_H
|
||||
|
||||
class QAbstractButton;
|
||||
class QVariantAnimation;
|
||||
|
||||
// One-shot 360° icon spin for manual-refresh buttons (kGentle / enter curve).
|
||||
// Restores the button's original icon when the spin finishes. Honors the
|
||||
// "Reduce Animations" preference (MotionPrefs::reduceMotion): when reduced,
|
||||
// no animation starts, the icon stays static and nullptr is returned.
|
||||
// The animation is owned by the button and self-deletes when stopped.
|
||||
namespace RefreshSpin {
|
||||
|
||||
QVariantAnimation *start(QAbstractButton *button);
|
||||
|
||||
} // namespace RefreshSpin
|
||||
|
||||
#endif // QEMBYAPP_REFRESH_SPIN_H
|
||||
@@ -105,4 +105,41 @@ NinePatchShadow make(int blurRadius, int cornerRadius, const QColor &color, qrea
|
||||
baked.save(path, "PNG"); // best-effort cache; failure just re-bakes next time
|
||||
return result;
|
||||
}
|
||||
|
||||
void drawNinePatch(QPainter *painter, const QRect &target, const NinePatchShadow &patch)
|
||||
{
|
||||
if (!painter || patch.isNull())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const int b = patch.border;
|
||||
const int t = patch.total;
|
||||
const qreal s = patch.dpr;
|
||||
const QRect r = target;
|
||||
|
||||
// Degenerate target (smaller than the two fixed borders): nothing sensible
|
||||
// to slice, skip rather than draw garbage.
|
||||
if (r.width() < 2 * b || r.height() < 2 * b)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 3x3 grid: corners fixed, edges/centre stretched. Target columns/rows are
|
||||
// logical; source columns/rows are physical (pixmap carries no dpr).
|
||||
const int xt[4] = {r.left(), r.left() + b, r.right() + 1 - b, r.right() + 1};
|
||||
const int yt[4] = {r.top(), r.top() + b, r.bottom() + 1 - b, r.bottom() + 1};
|
||||
const qreal xs[4] = {0.0, b * s, (t - b) * s, t * s};
|
||||
const qreal ys[4] = {0.0, b * s, (t - b) * s, t * s};
|
||||
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
for (int j = 0; j < 3; ++j)
|
||||
{
|
||||
const QRectF targetCell(xt[i], yt[j], xt[i + 1] - xt[i], yt[j + 1] - yt[j]);
|
||||
const QRectF sourceCell(xs[i], ys[j], xs[i + 1] - xs[i], ys[j + 1] - ys[j]);
|
||||
painter->drawPixmap(targetCell, patch.pixmap, sourceCell);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace ShadowPixmap
|
||||
|
||||
@@ -27,6 +27,8 @@ struct NinePatchShadow
|
||||
bool isNull() const { return pixmap.isNull(); }
|
||||
};
|
||||
|
||||
class QPainter;
|
||||
|
||||
namespace ShadowPixmap
|
||||
{
|
||||
// Build (or load from the on-disk cache) a 9-patch shadow for a rounded rect.
|
||||
@@ -34,6 +36,11 @@ namespace ShadowPixmap
|
||||
// its alpha controls overall strength. Results are cached per
|
||||
// (blurRadius, cornerRadius, color, dpr) under CacheLocation/shadows.
|
||||
NinePatchShadow make(int blurRadius, int cornerRadius, const QColor &color, qreal dpr);
|
||||
|
||||
// 9-slice the baked shadow onto `target` (the shadowed body rect grown by
|
||||
// patch.border on every edge): corners fixed, edges/centre stretched. Cheap
|
||||
// enough for per-frame painting — no blur happens at draw time.
|
||||
void drawNinePatch(QPainter *painter, const QRect &target, const NinePatchShadow &patch);
|
||||
}
|
||||
|
||||
#endif // SHADOWPIXMAP_H
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <QWheelEvent>
|
||||
#include <cmath>
|
||||
|
||||
#include "motionprefs.h"
|
||||
|
||||
SmoothScrollController::SmoothScrollController(QScrollBar *scrollBar,
|
||||
QObject *parent)
|
||||
: QObject(parent), m_scrollBar(scrollBar)
|
||||
@@ -179,6 +181,12 @@ void SmoothScrollController::startTicker()
|
||||
return;
|
||||
}
|
||||
|
||||
if (MotionPrefs::reduceMotion()) {
|
||||
m_ticker.stop();
|
||||
m_scrollBar->setValue(m_targetValue);
|
||||
return;
|
||||
}
|
||||
|
||||
m_ticker.setInterval(frameIntervalMs());
|
||||
if (!m_ticker.isActive()) {
|
||||
m_frameClock.restart();
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "tweenedvalue.h"
|
||||
|
||||
#include <QVariantAnimation>
|
||||
|
||||
#include "motionprefs.h"
|
||||
|
||||
TweenedValue::TweenedValue(QObject *parent, int durationMs)
|
||||
: QObject(parent), m_durationMs(durationMs)
|
||||
{
|
||||
m_anim = new QVariantAnimation(this);
|
||||
m_anim->setDuration(m_durationMs);
|
||||
m_anim->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
connect(m_anim, &QVariantAnimation::valueChanged, this,
|
||||
[this](const QVariant &value) { applyValue(value.toReal()); });
|
||||
}
|
||||
|
||||
void TweenedValue::setTarget(qreal target)
|
||||
{
|
||||
if (m_target == target)
|
||||
return;
|
||||
m_target = target;
|
||||
|
||||
m_anim->stop();
|
||||
if (MotionPrefs::reduceMotion() || m_value == target) {
|
||||
applyValue(target);
|
||||
return;
|
||||
}
|
||||
|
||||
m_anim->setStartValue(m_value);
|
||||
m_anim->setEndValue(target);
|
||||
m_anim->start();
|
||||
}
|
||||
|
||||
void TweenedValue::snapTo(qreal value)
|
||||
{
|
||||
m_anim->stop();
|
||||
m_target = value;
|
||||
applyValue(value);
|
||||
}
|
||||
|
||||
void TweenedValue::applyValue(qreal value)
|
||||
{
|
||||
if (m_value == value)
|
||||
return;
|
||||
m_value = value;
|
||||
Q_EMIT valueChanged(m_value);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#ifndef QEMBYAPP_TWEENED_VALUE_H
|
||||
#define QEMBYAPP_TWEENED_VALUE_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "../tokens/AnimationTokens.h"
|
||||
|
||||
class QVariantAnimation;
|
||||
|
||||
// Glides a displayed value toward its data target (progress bars, arcs,
|
||||
// counters) instead of jumping. setTarget() retargets mid-flight from the
|
||||
// current value; snapTo() sets without animating (initialization / resets).
|
||||
//
|
||||
// Honors the "Reduce Animations" preference: when reduce-motion is on,
|
||||
// setTarget() snaps and emits immediately.
|
||||
class TweenedValue : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TweenedValue(QObject *parent = nullptr,
|
||||
int durationMs = AnimationTokens::DurationMs::kNormal);
|
||||
|
||||
void setTarget(qreal target);
|
||||
void snapTo(qreal value);
|
||||
qreal value() const { return m_value; }
|
||||
qreal target() const { return m_target; }
|
||||
|
||||
Q_SIGNALS:
|
||||
void valueChanged(qreal value);
|
||||
|
||||
private:
|
||||
void applyValue(qreal value);
|
||||
|
||||
QVariantAnimation *m_anim = nullptr;
|
||||
qreal m_value = 0.0;
|
||||
qreal m_target = 0.0;
|
||||
int m_durationMs;
|
||||
};
|
||||
|
||||
#endif // QEMBYAPP_TWEENED_VALUE_H
|
||||
@@ -2,6 +2,8 @@
|
||||
#include "librarygrid.h"
|
||||
#include "../../components/libraryactionmenu.h"
|
||||
#include "../../managers/thememanager.h"
|
||||
#include "../../utils/hoveranimator.h"
|
||||
#include "../../utils/tweenedvalue.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
@@ -10,6 +12,16 @@
|
||||
#include <QApplication>
|
||||
#include <QHBoxLayout>
|
||||
|
||||
namespace {
|
||||
QColor lerpColor(const QColor& from, const QColor& to, qreal t)
|
||||
{
|
||||
return QColor(qRound(from.red() + (to.red() - from.red()) * t),
|
||||
qRound(from.green() + (to.green() - from.green()) * t),
|
||||
qRound(from.blue() + (to.blue() - from.blue()) * t),
|
||||
qRound(from.alpha() + (to.alpha() - from.alpha()) * t));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -37,6 +49,13 @@ LibraryCard::LibraryCard(const VirtualFolder& folder, int index, QWidget* parent
|
||||
m_btnScan->setToolTip(tr("Scan"));
|
||||
m_btnDelete->setToolTip(tr("Delete"));
|
||||
|
||||
m_hover = new HoverAnimator(this);
|
||||
m_hover->setCallback([this](qreal) { update(); });
|
||||
|
||||
m_scanTween = new TweenedValue(this);
|
||||
connect(m_scanTween, &TweenedValue::valueChanged, this,
|
||||
[this](qreal) { update(); });
|
||||
|
||||
updateButtonLayout();
|
||||
|
||||
connect(m_btnEdit, &QPushButton::clicked, this, [this]() { emit editClicked(m_folderIdx); });
|
||||
@@ -75,13 +94,21 @@ void LibraryCard::setSelected(bool selected)
|
||||
|
||||
void LibraryCard::setScanProgress(int percent)
|
||||
{
|
||||
const bool wasActive = m_scanProgress >= 0;
|
||||
m_scanProgress = qBound(0, percent, 100);
|
||||
// First appearance snaps; later server updates glide the arc.
|
||||
if (wasActive) {
|
||||
m_scanTween->setTarget(m_scanProgress);
|
||||
} else {
|
||||
m_scanTween->snapTo(m_scanProgress);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void LibraryCard::clearScanProgress()
|
||||
{
|
||||
m_scanProgress = -1;
|
||||
m_scanTween->snapTo(0);
|
||||
update();
|
||||
}
|
||||
|
||||
@@ -135,6 +162,14 @@ void LibraryCard::paintEvent(QPaintEvent* event)
|
||||
QColor typeFg = isDark ? QColor("#94A3B8") : QColor("#64748B");
|
||||
const QColor accent("#3B82F6");
|
||||
|
||||
const qreal hp = m_hover->progress();
|
||||
if (hp > 0.0 && !m_selected && !m_dragging) {
|
||||
const QColor hoverBorder = isDark ? QColor(96, 165, 250, 190) : QColor(59, 130, 246, 190);
|
||||
const QColor hoverBg = isDark ? QColor(51, 65, 85, 190) : QColor(255, 255, 255, 255);
|
||||
border = lerpColor(border, hoverBorder, hp);
|
||||
cardBg = lerpColor(cardBg, hoverBg, hp);
|
||||
}
|
||||
|
||||
if (m_selected) {
|
||||
cardBg = isDark ? QColor(30, 41, 59, 220) : QColor(239, 246, 255, 245);
|
||||
border = isDark ? QColor(96, 165, 250, 220) : QColor(59, 130, 246, 220);
|
||||
@@ -257,7 +292,8 @@ void LibraryCard::paintEvent(QPaintEvent* event)
|
||||
p.setPen(arcPen);
|
||||
|
||||
|
||||
int spanAngle = -static_cast<int>(m_scanProgress * 3.6 * 16);
|
||||
// Arc glides via the tween; the % text shows the target value.
|
||||
int spanAngle = -static_cast<int>(m_scanTween->value() * 3.6 * 16);
|
||||
p.drawArc(arcRect, 90 * 16, spanAngle);
|
||||
|
||||
|
||||
@@ -277,6 +313,18 @@ void LibraryCard::resizeEvent(QResizeEvent* event)
|
||||
updateButtonLayout();
|
||||
}
|
||||
|
||||
void LibraryCard::enterEvent(QEnterEvent* event)
|
||||
{
|
||||
QFrame::enterEvent(event);
|
||||
m_hover->setHovered(true);
|
||||
}
|
||||
|
||||
void LibraryCard::leaveEvent(QEvent* event)
|
||||
{
|
||||
QFrame::leaveEvent(event);
|
||||
m_hover->setHovered(false);
|
||||
}
|
||||
|
||||
void LibraryCard::mousePressEvent(QMouseEvent* event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
|
||||
@@ -7,8 +7,11 @@
|
||||
#include "../../components/librarycontextmenurequest.h"
|
||||
#include <models/admin/adminmodels.h>
|
||||
|
||||
class HoverAnimator;
|
||||
class LibraryGrid;
|
||||
class TweenedValue;
|
||||
class QContextMenuEvent;
|
||||
class QEnterEvent;
|
||||
class QMouseEvent;
|
||||
class QPaintEvent;
|
||||
class QResizeEvent;
|
||||
@@ -52,6 +55,8 @@ protected:
|
||||
void mousePressEvent(QMouseEvent* event) override;
|
||||
void mouseMoveEvent(QMouseEvent* event) override;
|
||||
void mouseReleaseEvent(QMouseEvent* event) override;
|
||||
void enterEvent(QEnterEvent* event) override;
|
||||
void leaveEvent(QEvent* event) override;
|
||||
|
||||
private:
|
||||
void updateButtonLayout();
|
||||
@@ -71,6 +76,8 @@ private:
|
||||
QPushButton* m_btnEdit;
|
||||
QPushButton* m_btnScan;
|
||||
QPushButton* m_btnDelete;
|
||||
HoverAnimator* m_hover = nullptr;
|
||||
TweenedValue* m_scanTween = nullptr;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <QScrollBar>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QTimer>
|
||||
#include "../../tokens/AnimationTokens.h"
|
||||
#include "../../utils/motionprefs.h"
|
||||
|
||||
|
||||
static void stopPosAnimations(LibraryCard* card)
|
||||
@@ -210,13 +212,13 @@ void LibraryGrid::relayout(bool animate)
|
||||
if (card == m_dragCard) continue;
|
||||
|
||||
QPoint target = posForIndex(i, m_columns);
|
||||
if (animate && card->pos() != target) {
|
||||
if (animate && !MotionPrefs::reduceMotion() && card->pos() != target) {
|
||||
|
||||
stopPosAnimations(card);
|
||||
|
||||
auto* anim = new QPropertyAnimation(card, "pos", card);
|
||||
anim->setDuration(250);
|
||||
anim->setEasingCurve(QEasingCurve::OutCubic);
|
||||
anim->setDuration(AnimationTokens::DurationMs::kNormal);
|
||||
anim->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
anim->setEndValue(target);
|
||||
anim->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
} else {
|
||||
@@ -274,11 +276,15 @@ void LibraryGrid::onCardDragEnd(LibraryCard* card)
|
||||
stopPosAnimations(dragCard);
|
||||
|
||||
QPoint target = posForIndex(finalIndex, m_columns);
|
||||
if (MotionPrefs::reduceMotion()) {
|
||||
dragCard->move(target);
|
||||
} else {
|
||||
auto* anim = new QPropertyAnimation(dragCard, "pos", dragCard);
|
||||
anim->setDuration(200);
|
||||
anim->setEasingCurve(QEasingCurve::OutCubic);
|
||||
anim->setDuration(AnimationTokens::DurationMs::kFast);
|
||||
anim->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
anim->setEndValue(target);
|
||||
anim->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
QStringList newOrder;
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "pageusers.h"
|
||||
#include "../../components/slidingstackedwidget.h"
|
||||
#include "../../managers/thememanager.h"
|
||||
#include "../../tokens/AnimationTokens.h"
|
||||
#include "../../utils/motionprefs.h"
|
||||
#include <qembycore.h>
|
||||
#include <services/manager/servermanager.h>
|
||||
#include <QApplication>
|
||||
@@ -185,8 +187,8 @@ void ManageView::setupUi() {
|
||||
m_scrollTargets.resize(m_scrollAreas.size(), 0);
|
||||
for (auto *sa : m_scrollAreas) {
|
||||
auto *anim = new QPropertyAnimation(sa->verticalScrollBar(), "value", this);
|
||||
anim->setEasingCurve(QEasingCurve::OutCubic);
|
||||
anim->setDuration(450);
|
||||
anim->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
anim->setDuration(AnimationTokens::DurationMs::kSlow);
|
||||
m_scrollAnims.append(anim);
|
||||
}
|
||||
|
||||
@@ -196,7 +198,10 @@ void ManageView::setupUi() {
|
||||
void ManageView::setupConnections() {
|
||||
connect(m_navMenu, &QListWidget::currentRowChanged, this, [this](int row) {
|
||||
if (row >= 0) {
|
||||
m_stack->setCurrentIndex(row);
|
||||
// Vertical slide following the nav list's direction of travel.
|
||||
m_stack->slideInIdx(row, row > m_stack->currentIndex()
|
||||
? SlidingStackedWidget::BottomToTop
|
||||
: SlidingStackedWidget::TopToBottom);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -236,11 +241,15 @@ bool ManageView::eventFilter(QObject *obj, QEvent *event) {
|
||||
if (newTarget != vBar->value()) {
|
||||
m_scrollTargets[i] = newTarget;
|
||||
anim->stop();
|
||||
if (MotionPrefs::reduceMotion()) {
|
||||
vBar->setValue(newTarget);
|
||||
} else {
|
||||
anim->setStartValue(vBar->value());
|
||||
anim->setEndValue(newTarget);
|
||||
anim->start();
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <services/media/mediaservice.h>
|
||||
#include "../../managers/thememanager.h"
|
||||
#include "../../utils/imageutils.h"
|
||||
#include "../../utils/tweenedvalue.h"
|
||||
|
||||
#include <QFontMetrics>
|
||||
#include <QHBoxLayout>
|
||||
@@ -86,6 +87,10 @@ void NowPlayingCard::setupUi(const SessionInfo& session) {
|
||||
m_progressBar->setRange(0, 1000);
|
||||
infoLayout->addWidget(m_progressBar);
|
||||
|
||||
m_progressTween = new TweenedValue(this);
|
||||
connect(m_progressTween, &TweenedValue::valueChanged, this,
|
||||
[this](qreal value) { m_progressBar->setValue(qRound(value)); });
|
||||
|
||||
infoLayout->addSpacing(2);
|
||||
|
||||
|
||||
@@ -151,7 +156,7 @@ void NowPlayingCard::updateSession(const SessionInfo& session) {
|
||||
m_stateIcon->setPixmap(stateIcon.pixmap(QSize(12, 12)));
|
||||
|
||||
m_progressText->setText(posStr + " / " + durStr);
|
||||
m_progressBar->setValue(static_cast<int>(session.playbackProgress() * 1000));
|
||||
m_progressTween->setTarget(session.playbackProgress() * 1000);
|
||||
m_progressText->show();
|
||||
m_stateIcon->show();
|
||||
m_progressBar->show();
|
||||
|
||||
@@ -10,6 +10,7 @@ struct SessionInfo;
|
||||
class QEmbyCore;
|
||||
class QLabel;
|
||||
class QProgressBar;
|
||||
class TweenedValue;
|
||||
|
||||
class NowPlayingCard : public QFrame {
|
||||
Q_OBJECT
|
||||
@@ -38,6 +39,7 @@ private:
|
||||
QProgressBar* m_progressBar;
|
||||
QLabel* m_userLabel;
|
||||
QLabel* m_streamLabel;
|
||||
TweenedValue* m_progressTween = nullptr;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
#include <QTimeZone>
|
||||
#include <QTransform>
|
||||
#include <QVariantAnimation>
|
||||
#include "../../tokens/AnimationTokens.h"
|
||||
#include "../../utils/refreshspin.h"
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace {
|
||||
@@ -348,33 +350,7 @@ QCoro::Task<void> PageCollections::loadData(bool isManual) {
|
||||
setLoadingState(true);
|
||||
if (isManual) {
|
||||
m_btnRefresh->setEnabled(false);
|
||||
QIcon originalIcon = m_btnRefresh->icon();
|
||||
QSize iconSz = m_btnRefresh->iconSize();
|
||||
QPixmap basePx = originalIcon.pixmap(iconSz);
|
||||
|
||||
auto *rotAnim = new QVariantAnimation(this);
|
||||
rotAnim->setStartValue(0.0);
|
||||
rotAnim->setEndValue(360.0);
|
||||
rotAnim->setDuration(800);
|
||||
rotAnim->setEasingCurve(QEasingCurve::InOutCubic);
|
||||
|
||||
connect(rotAnim, &QVariantAnimation::valueChanged, this,
|
||||
[this, basePx, iconSz](const QVariant &val) {
|
||||
qreal angle = val.toReal();
|
||||
QTransform t;
|
||||
t.translate(iconSz.width() / 2.0, iconSz.height() / 2.0);
|
||||
t.rotate(angle);
|
||||
t.translate(-iconSz.width() / 2.0, -iconSz.height() / 2.0);
|
||||
QPixmap rotated = basePx.transformed(t, Qt::SmoothTransformation);
|
||||
int xOff = (rotated.width() - iconSz.width()) / 2;
|
||||
int yOff = (rotated.height() - iconSz.height()) / 2;
|
||||
QPixmap cropped =
|
||||
rotated.copy(xOff, yOff, iconSz.width(), iconSz.height());
|
||||
m_btnRefresh->setIcon(QIcon(cropped));
|
||||
});
|
||||
connect(rotAnim, &QVariantAnimation::finished, this,
|
||||
[this, originalIcon]() { m_btnRefresh->setIcon(originalIcon); });
|
||||
rotAnim->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
RefreshSpin::start(m_btnRefresh);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
#include <QTransform>
|
||||
#include <QVBoxLayout>
|
||||
#include <QVariantAnimation>
|
||||
#include "../../tokens/AnimationTokens.h"
|
||||
#include "../../utils/refreshspin.h"
|
||||
|
||||
PageDashboard::PageDashboard(QEmbyCore *core, QWidget *parent) : ManagePageBase(core, tr("Dashboard"), parent)
|
||||
{
|
||||
@@ -222,41 +224,7 @@ QCoro::Task<void> PageDashboard::loadData(bool isManual)
|
||||
if (isManual)
|
||||
{
|
||||
m_btnRefresh->setEnabled(false);
|
||||
|
||||
|
||||
QIcon originalIcon = m_btnRefresh->icon();
|
||||
QSize iconSz = m_btnRefresh->iconSize();
|
||||
QPixmap basePx = originalIcon.pixmap(iconSz);
|
||||
|
||||
auto *rotAnim = new QVariantAnimation(this);
|
||||
rotAnim->setStartValue(0.0);
|
||||
rotAnim->setEndValue(360.0);
|
||||
rotAnim->setDuration(800);
|
||||
rotAnim->setEasingCurve(QEasingCurve::InOutCubic);
|
||||
|
||||
connect(rotAnim, &QVariantAnimation::valueChanged, this,
|
||||
[this, basePx, iconSz](const QVariant &val)
|
||||
{
|
||||
qreal angle = val.toReal();
|
||||
QTransform t;
|
||||
t.translate(iconSz.width() / 2.0, iconSz.height() / 2.0);
|
||||
t.rotate(angle);
|
||||
t.translate(-iconSz.width() / 2.0, -iconSz.height() / 2.0);
|
||||
QPixmap rotated = basePx.transformed(t, Qt::SmoothTransformation);
|
||||
|
||||
int xOff = (rotated.width() - iconSz.width()) / 2;
|
||||
int yOff = (rotated.height() - iconSz.height()) / 2;
|
||||
QPixmap cropped = rotated.copy(xOff, yOff, iconSz.width(), iconSz.height());
|
||||
m_btnRefresh->setIcon(QIcon(cropped));
|
||||
});
|
||||
|
||||
connect(rotAnim, &QVariantAnimation::finished, this,
|
||||
[this, originalIcon]()
|
||||
{
|
||||
m_btnRefresh->setIcon(originalIcon);
|
||||
});
|
||||
|
||||
rotAnim->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
RefreshSpin::start(m_btnRefresh);
|
||||
}
|
||||
|
||||
try
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
#include <QVariant>
|
||||
#include <QVBoxLayout>
|
||||
#include <QVariantAnimation>
|
||||
#include "../../tokens/AnimationTokens.h"
|
||||
#include "../../utils/refreshspin.h"
|
||||
|
||||
|
||||
|
||||
@@ -502,33 +504,7 @@ QCoro::Task<void> PageLibraries::loadData(bool isManual) {
|
||||
setLoadingState(true);
|
||||
if (isManual) {
|
||||
m_btnRefresh->setEnabled(false);
|
||||
QIcon originalIcon = m_btnRefresh->icon();
|
||||
QSize iconSz = m_btnRefresh->iconSize();
|
||||
QPixmap basePx = originalIcon.pixmap(iconSz);
|
||||
|
||||
auto *rotAnim = new QVariantAnimation(this);
|
||||
rotAnim->setStartValue(0.0);
|
||||
rotAnim->setEndValue(360.0);
|
||||
rotAnim->setDuration(800);
|
||||
rotAnim->setEasingCurve(QEasingCurve::InOutCubic);
|
||||
|
||||
connect(rotAnim, &QVariantAnimation::valueChanged, this,
|
||||
[this, basePx, iconSz](const QVariant &val) {
|
||||
qreal angle = val.toReal();
|
||||
QTransform t;
|
||||
t.translate(iconSz.width() / 2.0, iconSz.height() / 2.0);
|
||||
t.rotate(angle);
|
||||
t.translate(-iconSz.width() / 2.0, -iconSz.height() / 2.0);
|
||||
QPixmap rotated = basePx.transformed(t, Qt::SmoothTransformation);
|
||||
int xOff = (rotated.width() - iconSz.width()) / 2;
|
||||
int yOff = (rotated.height() - iconSz.height()) / 2;
|
||||
QPixmap cropped =
|
||||
rotated.copy(xOff, yOff, iconSz.width(), iconSz.height());
|
||||
m_btnRefresh->setIcon(QIcon(cropped));
|
||||
});
|
||||
connect(rotAnim, &QVariantAnimation::finished, this,
|
||||
[this, originalIcon]() { m_btnRefresh->setIcon(originalIcon); });
|
||||
rotAnim->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
RefreshSpin::start(m_btnRefresh);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
#include <QTransform>
|
||||
#include <QVBoxLayout>
|
||||
#include <QVariantAnimation>
|
||||
#include "../../tokens/AnimationTokens.h"
|
||||
#include "../../utils/refreshspin.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
@@ -529,33 +531,7 @@ QCoro::Task<void> PageLogs::loadFileList(bool isManual) {
|
||||
}
|
||||
|
||||
if (isManual) {
|
||||
const QIcon originalIcon = m_btnRefresh->icon();
|
||||
const QSize iconSize = m_btnRefresh->iconSize();
|
||||
const QPixmap basePixmap = originalIcon.pixmap(iconSize);
|
||||
|
||||
auto* rotationAnimation = new QVariantAnimation(this);
|
||||
rotationAnimation->setStartValue(0.0);
|
||||
rotationAnimation->setEndValue(360.0);
|
||||
rotationAnimation->setDuration(800);
|
||||
rotationAnimation->setEasingCurve(QEasingCurve::InOutCubic);
|
||||
|
||||
connect(rotationAnimation, &QVariantAnimation::valueChanged, this,
|
||||
[this, basePixmap, iconSize](const QVariant& value) {
|
||||
const qreal angle = value.toReal();
|
||||
QTransform transform;
|
||||
transform.translate(iconSize.width() / 2.0, iconSize.height() / 2.0);
|
||||
transform.rotate(angle);
|
||||
transform.translate(-iconSize.width() / 2.0, -iconSize.height() / 2.0);
|
||||
|
||||
const QPixmap rotated = basePixmap.transformed(transform, Qt::SmoothTransformation);
|
||||
const int xOffset = (rotated.width() - iconSize.width()) / 2;
|
||||
const int yOffset = (rotated.height() - iconSize.height()) / 2;
|
||||
m_btnRefresh->setIcon(
|
||||
QIcon(rotated.copy(xOffset, yOffset, iconSize.width(), iconSize.height())));
|
||||
});
|
||||
connect(rotationAnimation, &QVariantAnimation::finished, this,
|
||||
[this, originalIcon]() { m_btnRefresh->setIcon(originalIcon); });
|
||||
rotationAnimation->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
RefreshSpin::start(m_btnRefresh);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include <QTransform>
|
||||
#include <QVBoxLayout>
|
||||
#include <QVariantAnimation>
|
||||
#include "../../tokens/AnimationTokens.h"
|
||||
#include "../../utils/refreshspin.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -155,33 +157,7 @@ QCoro::Task<void> PageTasks::loadData(bool isManual)
|
||||
|
||||
if (isManual)
|
||||
{
|
||||
QIcon originalIcon = m_btnRefresh->icon();
|
||||
QSize iconSz = m_btnRefresh->iconSize();
|
||||
QPixmap basePx = originalIcon.pixmap(iconSz);
|
||||
|
||||
auto *rotAnim = new QVariantAnimation(this);
|
||||
rotAnim->setStartValue(0.0);
|
||||
rotAnim->setEndValue(360.0);
|
||||
rotAnim->setDuration(800);
|
||||
rotAnim->setEasingCurve(QEasingCurve::InOutCubic);
|
||||
|
||||
connect(rotAnim, &QVariantAnimation::valueChanged, this,
|
||||
[this, basePx, iconSz](const QVariant &val)
|
||||
{
|
||||
qreal angle = val.toReal();
|
||||
QTransform t;
|
||||
t.translate(iconSz.width() / 2.0, iconSz.height() / 2.0);
|
||||
t.rotate(angle);
|
||||
t.translate(-iconSz.width() / 2.0, -iconSz.height() / 2.0);
|
||||
QPixmap rotated = basePx.transformed(t, Qt::SmoothTransformation);
|
||||
int xOff = (rotated.width() - iconSz.width()) / 2;
|
||||
int yOff = (rotated.height() - iconSz.height()) / 2;
|
||||
QPixmap cropped = rotated.copy(xOff, yOff, iconSz.width(), iconSz.height());
|
||||
m_btnRefresh->setIcon(QIcon(cropped));
|
||||
});
|
||||
connect(rotAnim, &QVariantAnimation::finished, this,
|
||||
[this, originalIcon]() { m_btnRefresh->setIcon(originalIcon); });
|
||||
rotAnim->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
RefreshSpin::start(m_btnRefresh);
|
||||
}
|
||||
|
||||
try
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
#include <QTransform>
|
||||
#include <QVariantAnimation>
|
||||
#include <QVBoxLayout>
|
||||
#include "../../tokens/AnimationTokens.h"
|
||||
#include "../../utils/refreshspin.h"
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
@@ -2483,33 +2485,5 @@ bool PageTranscoding::isEmbyAdvancedMode(const QJsonObject& config) const {
|
||||
}
|
||||
|
||||
void PageTranscoding::playRefreshAnimation() {
|
||||
const QIcon originalIcon = m_btnRefresh->icon();
|
||||
const QSize iconSize = m_btnRefresh->iconSize();
|
||||
const QPixmap basePixmap = originalIcon.pixmap(iconSize);
|
||||
if (iconSize.isEmpty() || basePixmap.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* animation = new QVariantAnimation(this);
|
||||
animation->setStartValue(0.0);
|
||||
animation->setEndValue(360.0);
|
||||
animation->setDuration(800);
|
||||
animation->setEasingCurve(QEasingCurve::InOutCubic);
|
||||
|
||||
connect(animation, &QVariantAnimation::valueChanged, this,
|
||||
[this, basePixmap, iconSize](const QVariant& value) {
|
||||
QTransform transform;
|
||||
transform.translate(iconSize.width() / 2.0, iconSize.height() / 2.0);
|
||||
transform.rotate(value.toReal());
|
||||
transform.translate(-iconSize.width() / 2.0, -iconSize.height() / 2.0);
|
||||
const QPixmap rotated =
|
||||
basePixmap.transformed(transform, Qt::SmoothTransformation);
|
||||
const int x = (rotated.width() - iconSize.width()) / 2;
|
||||
const int y = (rotated.height() - iconSize.height()) / 2;
|
||||
m_btnRefresh->setIcon(
|
||||
QIcon(rotated.copy(x, y, iconSize.width(), iconSize.height())));
|
||||
});
|
||||
connect(animation, &QVariantAnimation::finished, this,
|
||||
[this, originalIcon]() { m_btnRefresh->setIcon(originalIcon); });
|
||||
animation->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
RefreshSpin::start(m_btnRefresh);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
#include <QTransform>
|
||||
#include <QVBoxLayout>
|
||||
#include <QVariantAnimation>
|
||||
#include "../../tokens/AnimationTokens.h"
|
||||
#include "../../utils/refreshspin.h"
|
||||
|
||||
|
||||
|
||||
@@ -145,34 +147,7 @@ QCoro::Task<void> PageUsers::loadData(bool isManual) {
|
||||
m_btnRefresh->setEnabled(false);
|
||||
|
||||
if (isManual) {
|
||||
QIcon originalIcon = m_btnRefresh->icon();
|
||||
QSize iconSz = m_btnRefresh->iconSize();
|
||||
QPixmap basePx = originalIcon.pixmap(iconSz);
|
||||
|
||||
auto *rotAnim = new QVariantAnimation(this);
|
||||
rotAnim->setStartValue(0.0);
|
||||
rotAnim->setEndValue(360.0);
|
||||
rotAnim->setDuration(800);
|
||||
rotAnim->setEasingCurve(QEasingCurve::InOutCubic);
|
||||
|
||||
connect(rotAnim, &QVariantAnimation::valueChanged, this,
|
||||
[this, basePx, iconSz](const QVariant &val) {
|
||||
qreal angle = val.toReal();
|
||||
QTransform t;
|
||||
t.translate(iconSz.width() / 2.0, iconSz.height() / 2.0);
|
||||
t.rotate(angle);
|
||||
t.translate(-iconSz.width() / 2.0, -iconSz.height() / 2.0);
|
||||
QPixmap rotated =
|
||||
basePx.transformed(t, Qt::SmoothTransformation);
|
||||
int xOff = (rotated.width() - iconSz.width()) / 2;
|
||||
int yOff = (rotated.height() - iconSz.height()) / 2;
|
||||
QPixmap cropped =
|
||||
rotated.copy(xOff, yOff, iconSz.width(), iconSz.height());
|
||||
m_btnRefresh->setIcon(QIcon(cropped));
|
||||
});
|
||||
connect(rotAnim, &QVariantAnimation::finished, this,
|
||||
[this, originalIcon]() { m_btnRefresh->setIcon(originalIcon); });
|
||||
rotAnim->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
RefreshSpin::start(m_btnRefresh);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "detailview.h"
|
||||
#include "../../tokens/AnimationTokens.h"
|
||||
#include "../../utils/fadehelper.h"
|
||||
#include "../../components/detailactionwidget.h"
|
||||
#include "../../components/detailbottominfowidget.h"
|
||||
#include "../../components/detailcontentwidget.h"
|
||||
@@ -463,7 +465,7 @@ void DetailView::setupUi() {
|
||||
|
||||
m_vScrollController =
|
||||
new SmoothScrollController(m_mainScrollArea->verticalScrollBar(), this);
|
||||
m_vScrollController->setDuration(170);
|
||||
m_vScrollController->setDuration(AnimationTokens::DurationMs::kFast);
|
||||
|
||||
m_contentWidget = new DetailContentWidget(m_mainScrollArea);
|
||||
m_contentWidget->setObjectName("detail-content-container");
|
||||
@@ -1037,7 +1039,9 @@ QCoro::Task<void> DetailView::loadItem(const QString &itemId,
|
||||
|
||||
|
||||
|
||||
QTimer::singleShot(380, this, [safeThis, itemId]() {
|
||||
// Guard tracks the page-slide duration (kSlow) so the poster shadow / deferred
|
||||
// content reveal lands as the incoming slide settles; kept in sync via the token.
|
||||
QTimer::singleShot(AnimationTokens::DurationMs::kSlow, this, [safeThis, itemId]() {
|
||||
if (!safeThis || safeThis->m_pendingDeferredFetchId != itemId)
|
||||
return;
|
||||
if (safeThis->m_posterShadow)
|
||||
@@ -3253,7 +3257,8 @@ QCoro::Task<void> DetailView::executeLoadImages(QPointer<DetailView> safeThis,
|
||||
if (cachedImages.logo.has_value()) {
|
||||
safeThis->m_logoLabel->setPixmap(
|
||||
QPixmap::fromImage(cachedImages.logo.value()));
|
||||
safeThis->m_logoLabel->show();
|
||||
FadeHelper::fadeIn(safeThis->m_logoLabel,
|
||||
AnimationTokens::DurationMs::kNormal);
|
||||
qDebug() << "[DetailView] Detail logo image cache hit"
|
||||
<< "itemId=" << item.id;
|
||||
}
|
||||
@@ -3357,7 +3362,8 @@ QCoro::Task<void> DetailView::executeLoadImages(QPointer<DetailView> safeThis,
|
||||
if (!result.isNull()) {
|
||||
safeThis->m_logoLabel->setPixmap(
|
||||
QPixmap::fromImage(result));
|
||||
safeThis->m_logoLabel->show();
|
||||
FadeHelper::fadeIn(safeThis->m_logoLabel,
|
||||
AnimationTokens::DurationMs::kNormal);
|
||||
saveDetailImageAsync(
|
||||
cacheServerId, cacheUserId, currentItemId,
|
||||
QStringLiteral("logo"), logoId, logoType, logoTag,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#include "medialistmodel.h"
|
||||
|
||||
#include "../../managers/thememanager.h"
|
||||
#include "../../utils/indexhoveranimator.h"
|
||||
#include "../../utils/popfeedback.h"
|
||||
#include <QAbstractItemView>
|
||||
#include <QEvent>
|
||||
#include <QFontMetrics>
|
||||
@@ -11,6 +13,7 @@
|
||||
#include <QPainterPath>
|
||||
#include <QPixmapCache>
|
||||
#include <QStyle>
|
||||
#include <QVariantAnimation>
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -87,11 +90,19 @@ QRect baseImageRectForCard(MediaCardDelegate::CardStyle style, const QRect &rect
|
||||
return QRect(rect.x() + padding, rect.y() + padding, imgWidth, imgHeight);
|
||||
}
|
||||
|
||||
QRect hoverImageRect(const QRect &baseImgRect)
|
||||
QRect episodeImageRect(const QRect &rect)
|
||||
{
|
||||
const int expandW = qRound(baseImgRect.width() * 0.035);
|
||||
const int expandH = qRound(baseImgRect.height() * 0.035);
|
||||
return baseImgRect.adjusted(-expandW, -expandH, expandW, expandH);
|
||||
const int padding = 10;
|
||||
const int imgWidth = 250;
|
||||
const int imgHeight = 140;
|
||||
return QRect(rect.x() + padding, rect.y() + (rect.height() - imgHeight) / 2, imgWidth, imgHeight);
|
||||
}
|
||||
|
||||
QRect episodeFavoriteRect(const QRect &rect)
|
||||
{
|
||||
const int favBtnSize = 24;
|
||||
const int rightMargin = 20;
|
||||
return QRect(rect.right() - rightMargin - favBtnSize, rect.center().y() - favBtnSize / 2, favBtnSize, favBtnSize);
|
||||
}
|
||||
|
||||
QRect centerPlayButtonRect(const QRect &targetImgRect)
|
||||
@@ -201,6 +212,154 @@ MediaCardThemeHelper::MediaCardThemeHelper(QWidget *parent)
|
||||
MediaCardDelegate::MediaCardDelegate(CardStyle style, QObject *parent)
|
||||
: QStyledItemDelegate(parent), m_style(style), m_tileSize(160, 270), m_themeHelper(nullptr)
|
||||
{
|
||||
m_hover = new IndexHoverAnimator(this);
|
||||
// Stale-region reach: 3px lift + shadow falloff + 5px downward offset.
|
||||
m_hover->setRepaintMargin(24);
|
||||
m_hover->setRepaintCallback([this](const QModelIndex &index) { repaintIndex(index); });
|
||||
}
|
||||
|
||||
void MediaCardDelegate::repaintIndex(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
const int margin = m_hover->repaintMargin();
|
||||
for (QWidget *viewport : m_installedViewports)
|
||||
{
|
||||
auto *view = qobject_cast<QAbstractItemView *>(viewport->parentWidget());
|
||||
if (view && view->model() == index.model())
|
||||
{
|
||||
viewport->update(view->visualRect(index).adjusted(-margin, -margin, margin, margin));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MediaCardDelegate::HoverControl MediaCardDelegate::hoverButtonAt(const QRect &itemRect, const MediaItem &item,
|
||||
const QPoint &pos) const
|
||||
{
|
||||
const HoverOverlayMode overlayMode = hoverOverlayModeForItem(item, m_style, m_showMoreButtonForNonPlayableTiles);
|
||||
if (overlayMode == HoverOverlayMode::None)
|
||||
{
|
||||
return HoverControlNone;
|
||||
}
|
||||
|
||||
if (m_style == EpisodeList)
|
||||
{
|
||||
if (m_hoverControls.testFlag(HoverControlFavorite) && episodeFavoriteRect(itemRect).contains(pos))
|
||||
{
|
||||
return HoverControlFavorite;
|
||||
}
|
||||
if (m_hoverControls.testFlag(HoverControlPlay) &&
|
||||
centerPlayButtonRect(episodeImageRect(itemRect)).contains(pos))
|
||||
{
|
||||
return HoverControlPlay;
|
||||
}
|
||||
return HoverControlNone;
|
||||
}
|
||||
|
||||
const QRect baseImgRect = baseImageRectForCard(m_style, itemRect, m_contentPadding);
|
||||
const bool showPlayButton = shouldShowPlayButton(overlayMode, m_hoverControls);
|
||||
const bool showFavoriteButton = shouldShowFavoriteButton(overlayMode, m_hoverControls);
|
||||
const bool showMoreButton = shouldShowMoreButton(overlayMode, m_hoverControls);
|
||||
if (!showPlayButton && !showFavoriteButton && !showMoreButton)
|
||||
{
|
||||
return HoverControlNone;
|
||||
}
|
||||
|
||||
const HoverButtonLayout layout =
|
||||
buildHoverButtonLayout(baseImgRect, showPlayButton, showFavoriteButton, showMoreButton);
|
||||
if (showPlayButton && layout.playRect.contains(pos))
|
||||
{
|
||||
return HoverControlPlay;
|
||||
}
|
||||
if (showFavoriteButton && layout.favoriteRect.contains(pos))
|
||||
{
|
||||
return HoverControlFavorite;
|
||||
}
|
||||
if (showMoreButton && layout.moreRect.contains(pos))
|
||||
{
|
||||
return HoverControlMore;
|
||||
}
|
||||
return HoverControlNone;
|
||||
}
|
||||
|
||||
void MediaCardDelegate::setHoveredButton(const QPersistentModelIndex &index, HoverControl button) const
|
||||
{
|
||||
if (m_hoverButtonIndex == index && m_hoveredButton == button)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const QPersistentModelIndex previous = m_hoverButtonIndex;
|
||||
m_hoverButtonIndex = index;
|
||||
m_hoveredButton = button;
|
||||
if (previous.isValid() && previous != index)
|
||||
{
|
||||
repaintIndex(previous);
|
||||
}
|
||||
repaintIndex(index);
|
||||
}
|
||||
|
||||
void MediaCardDelegate::setPressedIndex(const QPersistentModelIndex &index) const
|
||||
{
|
||||
if (m_pressedIndex == index)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const QPersistentModelIndex previous = m_pressedIndex;
|
||||
m_pressedIndex = index;
|
||||
if (previous.isValid())
|
||||
{
|
||||
repaintIndex(previous);
|
||||
}
|
||||
repaintIndex(index);
|
||||
}
|
||||
|
||||
void MediaCardDelegate::playFavoritePop(const QPersistentModelIndex &index)
|
||||
{
|
||||
if (!index.isValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
auto *anim = PopFeedback::play(this, [this, index](qreal scale) {
|
||||
m_popScale.insert(index, scale);
|
||||
repaintIndex(index);
|
||||
});
|
||||
if (anim)
|
||||
{
|
||||
connect(anim, &QObject::destroyed, this, [this, index]() {
|
||||
m_popScale.remove(index);
|
||||
repaintIndex(index);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
m_popScale.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
qreal MediaCardDelegate::hoverProgressFor(const QStyleOptionViewItem &option, const QModelIndex &index) const
|
||||
{
|
||||
// MouseMove on the viewport drives m_hover; State_MouseOver only bootstraps
|
||||
// states the filter cannot see (view shown under the cursor, hover moved by
|
||||
// scrolling). Never mutate animation state synchronously inside paint().
|
||||
if ((option.state & QStyle::State_MouseOver) && m_hover->hoveredIndex() != index)
|
||||
{
|
||||
const QPersistentModelIndex persistent(index);
|
||||
QMetaObject::invokeMethod(
|
||||
m_hover, [hover = m_hover, persistent]() { hover->setHoveredIndex(persistent); },
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
return m_hover->progressFor(index);
|
||||
}
|
||||
|
||||
const NinePatchShadow &MediaCardDelegate::liftShadow(qreal dpr, int cornerRadius) const
|
||||
{
|
||||
if (m_liftShadow.isNull() || !qFuzzyCompare(m_liftShadow.dpr, dpr))
|
||||
{
|
||||
m_liftShadow = ShadowPixmap::make(14, cornerRadius, QColor(0, 0, 0, 110), dpr);
|
||||
}
|
||||
return m_liftShadow;
|
||||
}
|
||||
|
||||
QSize MediaCardDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
|
||||
@@ -214,112 +373,84 @@ QSize MediaCardDelegate::sizeHint(const QStyleOptionViewItem &option, const QMod
|
||||
|
||||
bool MediaCardDelegate::eventFilter(QObject *object, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonRelease ||
|
||||
event->type() == QEvent::MouseButtonDblClick)
|
||||
{
|
||||
|
||||
QWidget *viewport = qobject_cast<QWidget *>(object);
|
||||
if (viewport)
|
||||
{
|
||||
|
||||
QAbstractItemView *view = qobject_cast<QAbstractItemView *>(viewport->parentWidget());
|
||||
if (view)
|
||||
{
|
||||
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
|
||||
QPoint pos = mouseEvent->pos();
|
||||
QModelIndex index = view->indexAt(pos);
|
||||
|
||||
if (index.isValid())
|
||||
{
|
||||
MediaItem item = index.data(MediaListModel::ItemDataRole).value<MediaItem>();
|
||||
const HoverOverlayMode overlayMode =
|
||||
hoverOverlayModeForItem(item, m_style, m_showMoreButtonForNonPlayableTiles);
|
||||
|
||||
if (overlayMode != HoverOverlayMode::None)
|
||||
{
|
||||
|
||||
QRect rect = view->visualRect(index);
|
||||
|
||||
|
||||
|
||||
|
||||
if (m_style == EpisodeList)
|
||||
{
|
||||
int padding = 10;
|
||||
int imgWidth = 250;
|
||||
int imgHeight = 140;
|
||||
QRect baseImgRect(rect.x() + padding, rect.y() + (rect.height() - imgHeight) / 2, imgWidth,
|
||||
imgHeight);
|
||||
|
||||
|
||||
int favBtnSize = 24;
|
||||
int rightMargin = 20;
|
||||
QRect favRect(rect.right() - rightMargin - favBtnSize, rect.center().y() - favBtnSize / 2,
|
||||
favBtnSize, favBtnSize);
|
||||
|
||||
const bool showFavoriteButton = m_hoverControls.testFlag(HoverControlFavorite);
|
||||
bool inFav = showFavoriteButton && favRect.contains(pos);
|
||||
|
||||
|
||||
int playSize = 48;
|
||||
QRect playRectCenter(baseImgRect.center().x() - playSize / 2,
|
||||
baseImgRect.center().y() - playSize / 2, playSize, playSize);
|
||||
const bool showPlayButton = m_hoverControls.testFlag(HoverControlPlay);
|
||||
bool inThumbPlay = showPlayButton && playRectCenter.contains(pos);
|
||||
|
||||
if (inFav || inThumbPlay)
|
||||
{
|
||||
if (event->type() == QEvent::MouseButtonRelease)
|
||||
{
|
||||
if (inThumbPlay)
|
||||
emit playRequested(item);
|
||||
else if (inFav)
|
||||
emit favoriteRequested(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const QRect baseImgRect = baseImageRectForCard(m_style, rect, m_contentPadding);
|
||||
const QRect targetImgRect = hoverImageRect(baseImgRect);
|
||||
const bool showPlayButton = shouldShowPlayButton(overlayMode, m_hoverControls);
|
||||
const bool showFavoriteButton = shouldShowFavoriteButton(overlayMode, m_hoverControls);
|
||||
const bool showMoreButton = shouldShowMoreButton(overlayMode, m_hoverControls);
|
||||
|
||||
if (!showPlayButton && !showFavoriteButton && !showMoreButton)
|
||||
QAbstractItemView *view = viewport ? qobject_cast<QAbstractItemView *>(viewport->parentWidget()) : nullptr;
|
||||
if (!view)
|
||||
{
|
||||
return QStyledItemDelegate::eventFilter(object, event);
|
||||
}
|
||||
|
||||
const HoverButtonLayout buttonLayout = buildHoverButtonLayout(
|
||||
targetImgRect, showPlayButton, showFavoriteButton, showMoreButton);
|
||||
const QEvent::Type type = event->type();
|
||||
|
||||
|
||||
const bool inPlay = showPlayButton && buttonLayout.playRect.contains(pos);
|
||||
const bool inFav = showFavoriteButton && buttonLayout.favoriteRect.contains(pos);
|
||||
const bool inMore = showMoreButton && buttonLayout.moreRect.contains(pos);
|
||||
|
||||
if (inPlay || inFav || inMore)
|
||||
if (type == QEvent::Leave)
|
||||
{
|
||||
m_hover->clear();
|
||||
setHoveredButton(QPersistentModelIndex(), HoverControlNone);
|
||||
setPressedIndex(QPersistentModelIndex());
|
||||
return QStyledItemDelegate::eventFilter(object, event);
|
||||
}
|
||||
|
||||
if (event->type() == QEvent::MouseButtonRelease)
|
||||
if (type == QEvent::MouseMove)
|
||||
{
|
||||
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
|
||||
const QPoint pos = mouseEvent->pos();
|
||||
const QModelIndex index = view->indexAt(pos);
|
||||
const QPersistentModelIndex persistent(index);
|
||||
m_hover->setHoveredIndex(persistent);
|
||||
|
||||
HoverControl button = HoverControlNone;
|
||||
if (index.isValid())
|
||||
{
|
||||
const MediaItem item = index.data(MediaListModel::ItemDataRole).value<MediaItem>();
|
||||
button = hoverButtonAt(view->visualRect(index), item, pos);
|
||||
}
|
||||
setHoveredButton(button == HoverControlNone ? QPersistentModelIndex() : persistent, button);
|
||||
return QStyledItemDelegate::eventFilter(object, event);
|
||||
}
|
||||
|
||||
if (type == QEvent::MouseButtonPress || type == QEvent::MouseButtonRelease ||
|
||||
type == QEvent::MouseButtonDblClick)
|
||||
{
|
||||
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
|
||||
const QPoint pos = mouseEvent->pos();
|
||||
const QModelIndex index = view->indexAt(pos);
|
||||
|
||||
// Press feedback is instant; only the release animates.
|
||||
if (type == QEvent::MouseButtonPress && mouseEvent->button() == Qt::LeftButton)
|
||||
{
|
||||
setPressedIndex(QPersistentModelIndex(index));
|
||||
}
|
||||
else if (type == QEvent::MouseButtonRelease)
|
||||
{
|
||||
setPressedIndex(QPersistentModelIndex());
|
||||
}
|
||||
|
||||
if (index.isValid())
|
||||
{
|
||||
const MediaItem item = index.data(MediaListModel::ItemDataRole).value<MediaItem>();
|
||||
const HoverControl button = hoverButtonAt(view->visualRect(index), item, pos);
|
||||
if (button != HoverControlNone)
|
||||
{
|
||||
if (type == QEvent::MouseButtonRelease)
|
||||
{
|
||||
if (button == HoverControlPlay)
|
||||
{
|
||||
if (inPlay)
|
||||
emit playRequested(item);
|
||||
else if (inFav)
|
||||
}
|
||||
else if (button == HoverControlFavorite)
|
||||
{
|
||||
playFavoritePop(QPersistentModelIndex(index));
|
||||
emit favoriteRequested(item);
|
||||
else if (inMore)
|
||||
}
|
||||
else if (button == HoverControlMore)
|
||||
{
|
||||
emit moreMenuRequested(item, mouseEvent->globalPosition().toPoint());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return QStyledItemDelegate::eventFilter(object, event);
|
||||
}
|
||||
@@ -372,7 +503,9 @@ void MediaCardDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opt
|
||||
QRect rect = option.rect;
|
||||
MediaItem item = index.data(MediaListModel::ItemDataRole).value<MediaItem>();
|
||||
QPixmap poster = index.data(MediaListModel::PosterPixmapRole).value<QPixmap>();
|
||||
bool isHovered = option.state & QStyle::State_MouseOver;
|
||||
const qreal hp = hoverProgressFor(option, index);
|
||||
const bool isPressed = m_pressedIndex.isValid() && m_pressedIndex == index;
|
||||
const qreal popScale = m_popScale.value(QPersistentModelIndex(index), 1.0);
|
||||
const HoverOverlayMode overlayMode = hoverOverlayModeForItem(item, m_style, m_showMoreButtonForNonPlayableTiles);
|
||||
const bool showPlayButton = shouldShowPlayButton(overlayMode, m_hoverControls);
|
||||
const bool showFavoriteButton = shouldShowFavoriteButton(overlayMode, m_hoverControls);
|
||||
@@ -384,18 +517,22 @@ void MediaCardDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opt
|
||||
|
||||
if (m_style == EpisodeList)
|
||||
{
|
||||
if (isHovered)
|
||||
if (hp > 0.01)
|
||||
{
|
||||
|
||||
QColor rowBg = isDarkTheme ? QColor(255, 255, 255, 20) : QColor(0, 0, 0, 15);
|
||||
rowBg.setAlpha(qRound(rowBg.alpha() * hp));
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(isDarkTheme ? QColor(255, 255, 255, 20) : QColor(0, 0, 0, 15));
|
||||
painter->setBrush(rowBg);
|
||||
painter->drawRoundedRect(rect.adjusted(5, 5, -5, -5), 8, 8);
|
||||
}
|
||||
if (isPressed)
|
||||
{
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(isDarkTheme ? QColor(255, 255, 255, 14) : QColor(0, 0, 0, 20));
|
||||
painter->drawRoundedRect(rect.adjusted(5, 5, -5, -5), 8, 8);
|
||||
}
|
||||
|
||||
int padding = 10;
|
||||
int imgWidth = 250;
|
||||
int imgHeight = 140;
|
||||
QRect baseImgRect(rect.x() + padding, rect.y() + (rect.height() - imgHeight) / 2, imgWidth, imgHeight);
|
||||
const QRect baseImgRect = episodeImageRect(rect);
|
||||
|
||||
int imgRadius = 8;
|
||||
painter->setPen(Qt::NoPen);
|
||||
@@ -419,17 +556,24 @@ void MediaCardDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opt
|
||||
painter->drawText(baseImgRect, Qt::AlignCenter, tr("No Image"));
|
||||
}
|
||||
|
||||
if (isHovered && showPlayButton)
|
||||
if (hp > 0.01 && showPlayButton)
|
||||
{
|
||||
painter->setOpacity(hp);
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(QColor(0, 0, 0, 70));
|
||||
painter->drawRoundedRect(baseImgRect, imgRadius, imgRadius);
|
||||
|
||||
int playSize = 48;
|
||||
QRect playRectCenter(baseImgRect.center().x() - playSize / 2, baseImgRect.center().y() - playSize / 2,
|
||||
playSize, playSize);
|
||||
painter->setBrush(QColor(0, 0, 0, 160));
|
||||
const QRect playRectCenter = centerPlayButtonRect(baseImgRect);
|
||||
const bool playHovered = m_hoverButtonIndex == index && m_hoveredButton == HoverControlPlay;
|
||||
painter->setBrush(QColor(0, 0, 0, playHovered ? 200 : 160));
|
||||
painter->drawEllipse(playRectCenter);
|
||||
if (playHovered)
|
||||
{
|
||||
painter->setPen(QPen(QColor(255, 255, 255, 90), 1));
|
||||
painter->setBrush(Qt::NoBrush);
|
||||
painter->drawEllipse(playRectCenter);
|
||||
painter->setPen(Qt::NoPen);
|
||||
}
|
||||
QIcon playIcon(":/svg/player/play.svg");
|
||||
if (!playIcon.isNull())
|
||||
{
|
||||
@@ -445,6 +589,7 @@ void MediaCardDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opt
|
||||
painter->setBrush(Qt::white);
|
||||
painter->drawPath(playPath);
|
||||
}
|
||||
painter->setOpacity(1.0);
|
||||
}
|
||||
|
||||
|
||||
@@ -506,15 +651,22 @@ void MediaCardDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opt
|
||||
|
||||
if (showFavoriteButton)
|
||||
{
|
||||
QRect favRect(rect.right() - rightMargin - favBtnSize, rect.center().y() - favBtnSize / 2, favBtnSize,
|
||||
favBtnSize);
|
||||
QRectF favRect(episodeFavoriteRect(rect));
|
||||
const bool favHovered = m_hoverButtonIndex == index && m_hoveredButton == HoverControlFavorite;
|
||||
const qreal favScale = popScale * (favHovered ? 1.1 : 1.0);
|
||||
if (!qFuzzyCompare(favScale, 1.0))
|
||||
{
|
||||
const QPointF center = favRect.center();
|
||||
favRect.setSize(favRect.size() * favScale);
|
||||
favRect.moveCenter(center);
|
||||
}
|
||||
QString themeFolder = isDarkTheme ? "dark" : "light";
|
||||
QString favIconPath = item.isFavorite() ? QString(":/svg/%1/heart-fill.svg").arg(themeFolder)
|
||||
: QString(":/svg/%1/heart-outline.svg").arg(themeFolder);
|
||||
QIcon favIcon(favIconPath);
|
||||
if (!favIcon.isNull())
|
||||
{
|
||||
favIcon.paint(painter, favRect.adjusted(2, 2, -2, -2));
|
||||
favIcon.paint(painter, favRect.toAlignedRect().adjusted(2, 2, -2, -2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,25 +718,24 @@ void MediaCardDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opt
|
||||
yearRect = QRect(rect.x() + padding, titleRect.bottom() + 1, imgWidth, subTitleHeight);
|
||||
}
|
||||
|
||||
// Ring + lift + fade hover: the image block rises slightly and gains a
|
||||
// pre-baked soft shadow; paint and hit-testing share baseImgRect.
|
||||
const int imgRadius = 8;
|
||||
const int lift = qRound(3.0 * hp);
|
||||
const QRect targetImgRect = baseImgRect.translated(0, -lift);
|
||||
|
||||
QRect targetImgRect = baseImgRect;
|
||||
|
||||
if (isHovered)
|
||||
if (hp > 0.01)
|
||||
{
|
||||
targetImgRect = hoverImageRect(baseImgRect);
|
||||
const int expandH = targetImgRect.bottom() - baseImgRect.bottom();
|
||||
titleRect.translate(0, expandH);
|
||||
yearRect.translate(0, expandH);
|
||||
|
||||
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(QColor(0, 0, 0, 60));
|
||||
|
||||
painter->drawRoundedRect(targetImgRect.translated(0, 6), 12, 12);
|
||||
const qreal dpr = option.widget ? option.widget->devicePixelRatioF() : 1.0;
|
||||
const NinePatchShadow &shadow = liftShadow(dpr, imgRadius);
|
||||
painter->setOpacity(hp);
|
||||
ShadowPixmap::drawNinePatch(
|
||||
painter,
|
||||
targetImgRect.adjusted(-shadow.border, -shadow.border, shadow.border, shadow.border).translated(0, 5),
|
||||
shadow);
|
||||
painter->setOpacity(1.0);
|
||||
}
|
||||
|
||||
|
||||
int imgRadius = 8;
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(placeholderBg);
|
||||
painter->drawRoundedRect(targetImgRect, imgRadius, imgRadius);
|
||||
@@ -681,20 +832,30 @@ void MediaCardDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opt
|
||||
|
||||
|
||||
|
||||
if (isHovered && hasHoverButtons)
|
||||
if (hp > 0.01 && hasHoverButtons)
|
||||
{
|
||||
|
||||
painter->setOpacity(hp);
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(QColor(0, 0, 0, 70));
|
||||
painter->drawRoundedRect(targetImgRect, imgRadius, imgRadius);
|
||||
|
||||
const HoverButtonLayout buttonLayout =
|
||||
buildHoverButtonLayout(targetImgRect, showPlayButton, showFavoriteButton, showMoreButton);
|
||||
const bool buttonStateHere = m_hoverButtonIndex == index;
|
||||
const QPen buttonHoverPen(QColor(255, 255, 255, 90), 1);
|
||||
|
||||
if (showPlayButton)
|
||||
{
|
||||
painter->setBrush(QColor(0, 0, 0, 160));
|
||||
const bool playHovered = buttonStateHere && m_hoveredButton == HoverControlPlay;
|
||||
painter->setBrush(QColor(0, 0, 0, playHovered ? 200 : 160));
|
||||
painter->drawEllipse(buttonLayout.playRect);
|
||||
if (playHovered)
|
||||
{
|
||||
painter->setPen(buttonHoverPen);
|
||||
painter->setBrush(Qt::NoBrush);
|
||||
painter->drawEllipse(buttonLayout.playRect);
|
||||
painter->setPen(Qt::NoPen);
|
||||
}
|
||||
|
||||
QIcon playIcon(":/svg/player/play.svg");
|
||||
if (!playIcon.isNull())
|
||||
@@ -715,8 +876,16 @@ void MediaCardDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opt
|
||||
|
||||
if (showMoreButton)
|
||||
{
|
||||
painter->setBrush(QColor(0, 0, 0, 160));
|
||||
const bool moreHovered = buttonStateHere && m_hoveredButton == HoverControlMore;
|
||||
painter->setBrush(QColor(0, 0, 0, moreHovered ? 200 : 160));
|
||||
painter->drawRoundedRect(buttonLayout.moreRect, 6, 6);
|
||||
if (moreHovered)
|
||||
{
|
||||
painter->setPen(buttonHoverPen);
|
||||
painter->setBrush(Qt::NoBrush);
|
||||
painter->drawRoundedRect(buttonLayout.moreRect, 6, 6);
|
||||
painter->setPen(Qt::NoPen);
|
||||
}
|
||||
|
||||
QIcon moreIcon(":/svg/dark/more-line.svg");
|
||||
if (!moreIcon.isNull())
|
||||
@@ -736,27 +905,61 @@ void MediaCardDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opt
|
||||
|
||||
if (showFavoriteButton)
|
||||
{
|
||||
painter->setBrush(QColor(0, 0, 0, 160));
|
||||
painter->drawRoundedRect(buttonLayout.favoriteRect, 6, 6);
|
||||
const bool favHovered = buttonStateHere && m_hoveredButton == HoverControlFavorite;
|
||||
QRectF favRect(buttonLayout.favoriteRect);
|
||||
if (!qFuzzyCompare(popScale, 1.0))
|
||||
{
|
||||
const QPointF center = favRect.center();
|
||||
favRect.setSize(favRect.size() * popScale);
|
||||
favRect.moveCenter(center);
|
||||
}
|
||||
painter->setBrush(QColor(0, 0, 0, favHovered ? 200 : 160));
|
||||
painter->drawRoundedRect(favRect, 6, 6);
|
||||
if (favHovered)
|
||||
{
|
||||
painter->setPen(buttonHoverPen);
|
||||
painter->setBrush(Qt::NoBrush);
|
||||
painter->drawRoundedRect(favRect, 6, 6);
|
||||
painter->setPen(Qt::NoPen);
|
||||
}
|
||||
|
||||
QString favIconPath = item.isFavorite() ? ":/svg/dark/heart-fill.svg" : ":/svg/dark/heart-outline.svg";
|
||||
QIcon favIcon(favIconPath);
|
||||
if (!favIcon.isNull())
|
||||
{
|
||||
favIcon.paint(painter, buttonLayout.favoriteRect.adjusted(5, 5, -5, -5), Qt::AlignCenter);
|
||||
favIcon.paint(painter, favRect.toAlignedRect().adjusted(5, 5, -5, -5), Qt::AlignCenter);
|
||||
}
|
||||
else
|
||||
{
|
||||
painter->setBrush(Qt::white);
|
||||
painter->drawEllipse(buttonLayout.favoriteRect.center(), 4, 4);
|
||||
painter->drawEllipse(favRect.center(), 4, 4);
|
||||
}
|
||||
}
|
||||
|
||||
painter->setOpacity(1.0);
|
||||
}
|
||||
|
||||
// Press feedback: instant dim, no animation.
|
||||
if (isPressed)
|
||||
{
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(QColor(0, 0, 0, 45));
|
||||
painter->drawRoundedRect(targetImgRect, imgRadius, imgRadius);
|
||||
}
|
||||
|
||||
const bool isHighlighted = !m_highlightedItemId.isEmpty() && item.id == m_highlightedItemId;
|
||||
if (hp > 0.01 && !isHighlighted)
|
||||
{
|
||||
// Hover ring; yields to the highlight ring below.
|
||||
QColor ringColor = isDarkTheme ? QColor(96, 165, 250) : QColor(59, 130, 246);
|
||||
ringColor.setAlpha(qRound(200 * hp));
|
||||
painter->setPen(QPen(ringColor, 1.5));
|
||||
painter->setBrush(Qt::NoBrush);
|
||||
painter->drawRoundedRect(QRectF(targetImgRect).adjusted(-1.5, -1.5, 1.5, 1.5), 9.5, 9.5);
|
||||
painter->setPen(Qt::NoPen);
|
||||
}
|
||||
|
||||
|
||||
if (!m_highlightedItemId.isEmpty() && item.id == m_highlightedItemId)
|
||||
if (isHighlighted)
|
||||
{
|
||||
QColor borderColor = isDarkTheme ? QColor(96, 165, 250, 220)
|
||||
: QColor(59, 130, 246, 220);
|
||||
|
||||
@@ -5,10 +5,15 @@
|
||||
#include <QWidget>
|
||||
#include <QColor>
|
||||
#include <QFlags>
|
||||
#include <QHash>
|
||||
#include <QPersistentModelIndex>
|
||||
#include <QSet>
|
||||
|
||||
#include "../../utils/shadowpixmap.h"
|
||||
|
||||
struct MediaItem;
|
||||
class IndexHoverAnimator;
|
||||
class QAbstractItemView;
|
||||
|
||||
|
||||
class MediaCardThemeHelper : public QWidget {
|
||||
@@ -109,11 +114,26 @@ protected:
|
||||
bool eventFilter(QObject *object, QEvent *event) override;
|
||||
|
||||
private:
|
||||
// Hover button hit-testing shared by paint highlights and mouse handling.
|
||||
HoverControl hoverButtonAt(const QRect &itemRect, const MediaItem &item, const QPoint &pos) const;
|
||||
void setHoveredButton(const QPersistentModelIndex &index, HoverControl button) const;
|
||||
void setPressedIndex(const QPersistentModelIndex &index) const;
|
||||
void repaintIndex(const QModelIndex &index) const;
|
||||
void playFavoritePop(const QPersistentModelIndex &index);
|
||||
qreal hoverProgressFor(const QStyleOptionViewItem &option, const QModelIndex &index) const;
|
||||
const NinePatchShadow &liftShadow(qreal dpr, int cornerRadius) const;
|
||||
|
||||
CardStyle m_style;
|
||||
QSize m_tileSize;
|
||||
QString m_highlightedItemId;
|
||||
mutable MediaCardThemeHelper *m_themeHelper;
|
||||
mutable QSet<QWidget*> m_installedViewports;
|
||||
IndexHoverAnimator *m_hover = nullptr;
|
||||
mutable QPersistentModelIndex m_pressedIndex;
|
||||
mutable QPersistentModelIndex m_hoverButtonIndex;
|
||||
mutable HoverControl m_hoveredButton = HoverControlNone;
|
||||
QHash<QPersistentModelIndex, qreal> m_popScale;
|
||||
mutable NinePatchShadow m_liftShadow;
|
||||
bool m_showMoreButtonForNonPlayableTiles = false;
|
||||
HoverControls m_hoverControls = HoverControlAll;
|
||||
int m_titleFontPixelSize = 13;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "../../components/playerosdlayer.h"
|
||||
#include "../../components/playerstatisticsoverlay.h"
|
||||
#include "../../components/playersubtitlesettingsdialog.h"
|
||||
#include "../../components/seektimebubble.h"
|
||||
#include "../../components/shadowoverlay.h"
|
||||
#include "../../utils/mediaitemutils.h"
|
||||
#include "../../utils/mediasourcepreferenceutils.h"
|
||||
@@ -42,6 +43,8 @@
|
||||
#include <QVBoxLayout>
|
||||
#include <QWheelEvent>
|
||||
#include <fileutils.h>
|
||||
#include "../../tokens/AnimationTokens.h"
|
||||
#include "../../utils/motionprefs.h"
|
||||
|
||||
#include <QVector>
|
||||
#include <QWindow>
|
||||
@@ -1369,16 +1372,20 @@ void PlayerView::setupUi()
|
||||
m_fadeGroup = new QParallelAnimationGroup(this);
|
||||
|
||||
auto *topAnim = new QPropertyAnimation(m_topOpacity, "opacity", this);
|
||||
topAnim->setDuration(250);
|
||||
topAnim->setDuration(AnimationTokens::DurationMs::kNormal);
|
||||
topAnim->setEasingCurve(AnimationTokens::Curve::fade());
|
||||
|
||||
auto *bottomAnim = new QPropertyAnimation(m_bottomOpacity, "opacity", this);
|
||||
bottomAnim->setDuration(250);
|
||||
bottomAnim->setDuration(AnimationTokens::DurationMs::kNormal);
|
||||
bottomAnim->setEasingCurve(AnimationTokens::Curve::fade());
|
||||
|
||||
auto *logoAnim = new QPropertyAnimation(m_logoOpacity, "opacity", this);
|
||||
logoAnim->setDuration(250);
|
||||
logoAnim->setDuration(AnimationTokens::DurationMs::kNormal);
|
||||
logoAnim->setEasingCurve(AnimationTokens::Curve::fade());
|
||||
|
||||
auto *speedAnim = new QPropertyAnimation(m_speedOpacity, "opacity", this);
|
||||
speedAnim->setDuration(250);
|
||||
speedAnim->setDuration(AnimationTokens::DurationMs::kNormal);
|
||||
speedAnim->setEasingCurve(AnimationTokens::Curve::fade());
|
||||
|
||||
m_fadeGroup->addAnimation(topAnim);
|
||||
m_fadeGroup->addAnimation(bottomAnim);
|
||||
@@ -1389,6 +1396,34 @@ void PlayerView::setupUi()
|
||||
connect(m_playPauseBtn, &QPushButton::clicked, this, &PlayerView::togglePlayPause);
|
||||
connect(m_progressSlider, &QSlider::sliderMoved, this, &PlayerView::onSliderMoved);
|
||||
|
||||
m_seekBubble = new SeekTimeBubble(this);
|
||||
connect(m_progressSlider, &ModernSlider::hoverMoved, this,
|
||||
[this](const QPoint &pos)
|
||||
{
|
||||
if (m_totalDuration <= 0)
|
||||
{
|
||||
m_seekBubble->hideBubble();
|
||||
return;
|
||||
}
|
||||
// Mirror ModernSlider's track math so the bubble time matches
|
||||
// what a click at this x would seek to.
|
||||
const int edgePadding = qMax(m_progressSlider->normalHandleRadius(),
|
||||
m_progressSlider->activeHandleRadius());
|
||||
const double trackWidth = m_progressSlider->width() - 2.0 * edgePadding;
|
||||
if (trackWidth <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
double ratio = (pos.x() - edgePadding) / trackWidth;
|
||||
ratio = qBound(0.0, ratio, 1.0);
|
||||
const double hoverSeconds = ratio * m_totalDuration;
|
||||
m_seekBubble->setTimeText(formatTime(hoverSeconds, m_totalDuration));
|
||||
const QPoint anchor = m_progressSlider->mapTo(this, QPoint(pos.x(), 0));
|
||||
m_seekBubble->showAt(anchor - QPoint(0, 8));
|
||||
});
|
||||
connect(m_progressSlider, &ModernSlider::hoverLeft, this,
|
||||
[this]() { m_seekBubble->hideBubble(); });
|
||||
|
||||
|
||||
connect(m_rewindBtn, &QPushButton::pressed, this,
|
||||
[this]()
|
||||
@@ -2197,8 +2232,8 @@ void PlayerView::setupRightSidebar()
|
||||
});
|
||||
|
||||
m_rightSidebarAnim = new QPropertyAnimation(m_rightSidebar, "pos", this);
|
||||
m_rightSidebarAnim->setDuration(350);
|
||||
m_rightSidebarAnim->setEasingCurve(QEasingCurve::OutCubic);
|
||||
m_rightSidebarAnim->setDuration(AnimationTokens::DurationMs::kSlow);
|
||||
m_rightSidebarAnim->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
connect(m_rightSidebarAnim, &QPropertyAnimation::finished, this,
|
||||
[this]()
|
||||
{
|
||||
@@ -2224,7 +2259,8 @@ QCoro::Task<void> PlayerView::showRightSidebar()
|
||||
const int sidebarY = m_topHUD->height();
|
||||
const QPoint hiddenPos(width() + 30, sidebarY);
|
||||
|
||||
if (!m_rightSidebar || !m_rightSidebarAnim || !m_rightSidebarAnim->targetObject())
|
||||
if (!m_rightSidebar || !m_rightSidebarAnim || !m_rightSidebarAnim->targetObject() ||
|
||||
MotionPrefs::reduceMotion())
|
||||
{
|
||||
if (m_rightSidebar)
|
||||
{
|
||||
@@ -2285,7 +2321,8 @@ void PlayerView::hideRightSidebar(bool immediate)
|
||||
const int sidebarY = m_topHUD ? m_topHUD->height() : 0;
|
||||
const QPoint hiddenPos(width() + 30, sidebarY);
|
||||
|
||||
if (immediate || m_hasReportedStop || !m_rightSidebarAnim || !m_rightSidebarAnim->targetObject())
|
||||
if (immediate || m_hasReportedStop || !m_rightSidebarAnim || !m_rightSidebarAnim->targetObject() ||
|
||||
MotionPrefs::reduceMotion())
|
||||
{
|
||||
if (m_rightSidebarAnim)
|
||||
{
|
||||
@@ -3141,7 +3178,8 @@ void PlayerView::showControls()
|
||||
stopFadeGroupSafely(m_fadeGroup);
|
||||
}
|
||||
|
||||
bool canAnimate = true;
|
||||
// Reduce-motion routes through the existing snap-to-target fallback below.
|
||||
bool canAnimate = !MotionPrefs::reduceMotion();
|
||||
for (int i = 0; i < m_fadeGroup->animationCount(); ++i)
|
||||
{
|
||||
auto *anim = qobject_cast<QPropertyAnimation *>(m_fadeGroup->animationAt(i));
|
||||
@@ -3207,6 +3245,10 @@ void PlayerView::hideControls()
|
||||
hideRightSidebar();
|
||||
hideHudMediaSwitcher();
|
||||
setPlayerChromeVisible(false);
|
||||
if (m_seekBubble)
|
||||
{
|
||||
m_seekBubble->hideBubble();
|
||||
}
|
||||
|
||||
if (m_activePopup)
|
||||
{
|
||||
@@ -3226,7 +3268,8 @@ void PlayerView::hideControls()
|
||||
stopFadeGroupSafely(m_fadeGroup);
|
||||
}
|
||||
|
||||
bool canAnimate = true;
|
||||
// Reduce-motion routes through the existing snap-to-target fallback below.
|
||||
bool canAnimate = !MotionPrefs::reduceMotion();
|
||||
for (int i = 0; i < m_fadeGroup->animationCount(); ++i)
|
||||
{
|
||||
auto *anim = qobject_cast<QPropertyAnimation *>(m_fadeGroup->animationAt(i));
|
||||
|
||||
@@ -35,6 +35,7 @@ class PlayerOsdLayer;
|
||||
class PlayerLongPressHandler;
|
||||
class PlayerStatisticsOverlay;
|
||||
class NativeDanmakuOverlay;
|
||||
class SeekTimeBubble;
|
||||
|
||||
class PlayerView : public BaseView {
|
||||
Q_OBJECT
|
||||
@@ -229,6 +230,7 @@ private:
|
||||
QLabel *m_currentTimeLabel;
|
||||
ModernSlider *m_progressSlider;
|
||||
QLabel *m_totalTimeLabel;
|
||||
SeekTimeBubble *m_seekBubble = nullptr;
|
||||
|
||||
|
||||
QPushButton *m_prevMediaBtn;
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
#include <QStandardPaths>
|
||||
#include <QTextStream>
|
||||
#include <QVariantAnimation>
|
||||
#include "../../tokens/AnimationTokens.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
#include <memory>
|
||||
@@ -1047,8 +1048,8 @@ PagePlayer::PagePlayer(QEmbyCore *core, QWidget *parent)
|
||||
auto *rotAnim = new QVariantAnimation(searchBtn);
|
||||
rotAnim->setStartValue(0.0);
|
||||
rotAnim->setEndValue(360.0);
|
||||
rotAnim->setDuration(500);
|
||||
rotAnim->setEasingCurve(QEasingCurve::InOutCubic);
|
||||
rotAnim->setDuration(AnimationTokens::DurationMs::kSlow);
|
||||
rotAnim->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
|
||||
connect(rotAnim, &QVariantAnimation::valueChanged, searchBtn,
|
||||
[searchBtn, originalPix](const QVariant &val) {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "settingsview.h"
|
||||
#include "../../tokens/AnimationTokens.h"
|
||||
#include "../../utils/motionprefs.h"
|
||||
#include "../../components/slidingstackedwidget.h"
|
||||
#include "../../managers/thememanager.h"
|
||||
#include "pageabout.h"
|
||||
@@ -148,8 +150,8 @@ QScrollArea *SettingsView::wrapInScrollArea(QWidget *page, int row) {
|
||||
|
||||
auto *anim =
|
||||
new QPropertyAnimation(scroll->verticalScrollBar(), "value", this);
|
||||
anim->setEasingCurve(QEasingCurve::OutCubic);
|
||||
anim->setDuration(450);
|
||||
anim->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
anim->setDuration(AnimationTokens::DurationMs::kSlow);
|
||||
m_scrollAnims[row] = anim;
|
||||
m_scrollTargets[row] = 0;
|
||||
}
|
||||
@@ -198,7 +200,7 @@ void SettingsView::ensurePageAt(int row) {
|
||||
m_stack->insertWidget(row, scroll);
|
||||
m_stack->removeWidget(placeholder);
|
||||
m_stack->blockSignals(wasBlocked);
|
||||
placeholder->deleteLater();
|
||||
m_stack->disposeWidgetWhenSafe(placeholder);
|
||||
|
||||
m_pages[row] = scroll;
|
||||
}
|
||||
@@ -208,7 +210,10 @@ void SettingsView::setupConnections() {
|
||||
connect(m_navMenu, &QListWidget::currentRowChanged, this, [this](int row) {
|
||||
if (row >= 0) {
|
||||
ensurePageAt(row);
|
||||
m_stack->setCurrentIndex(row);
|
||||
// Vertical slide following the nav list's direction of travel.
|
||||
m_stack->slideInIdx(row, row > m_stack->currentIndex()
|
||||
? SlidingStackedWidget::BottomToTop
|
||||
: SlidingStackedWidget::TopToBottom);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -261,11 +266,15 @@ bool SettingsView::eventFilter(QObject *obj, QEvent *event) {
|
||||
if (newTarget != vBar->value()) {
|
||||
m_scrollTargets[i] = newTarget;
|
||||
anim->stop();
|
||||
if (MotionPrefs::reduceMotion()) {
|
||||
vBar->setValue(newTarget);
|
||||
} else {
|
||||
anim->setStartValue(vBar->value());
|
||||
anim->setEndValue(newTarget);
|
||||
anim->start();
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "categoryview.h"
|
||||
#include "../../tokens/AnimationTokens.h"
|
||||
#include "../../components/elidedlabel.h"
|
||||
#include "../../components/mediagridwidget.h"
|
||||
#include "../../components/modernsortbutton.h"
|
||||
@@ -144,10 +145,10 @@ void CategoryView::setupTopBar(QHBoxLayout *headerLayout) {
|
||||
|
||||
|
||||
m_refreshAnimation = new QPropertyAnimation(this, QByteArray());
|
||||
m_refreshAnimation->setDuration(800);
|
||||
m_refreshAnimation->setDuration(AnimationTokens::DurationMs::kGentle);
|
||||
m_refreshAnimation->setStartValue(0.0);
|
||||
m_refreshAnimation->setEndValue(360.0);
|
||||
m_refreshAnimation->setEasingCurve(QEasingCurve::Linear);
|
||||
m_refreshAnimation->setEasingCurve(AnimationTokens::Curve::loop());
|
||||
|
||||
connect(m_refreshAnimation, &QPropertyAnimation::valueChanged, this,
|
||||
[this, refreshIcon](const QVariant &value) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "dashboardview.h"
|
||||
#include "../../tokens/AnimationTokens.h"
|
||||
|
||||
#include "../../components/horizontallistviewgallery.h"
|
||||
#include "../../components/libraryactionmenu.h"
|
||||
@@ -276,7 +277,7 @@ void DashboardView::setupUi()
|
||||
|
||||
m_vScrollController =
|
||||
new SmoothScrollController(m_mainScrollArea->verticalScrollBar(), this);
|
||||
m_vScrollController->setDuration(160);
|
||||
m_vScrollController->setDuration(AnimationTokens::DurationMs::kFast);
|
||||
|
||||
m_mainScrollArea->viewport()->installEventFilter(this);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "favoritesview.h"
|
||||
#include "../../tokens/AnimationTokens.h"
|
||||
#include "../../utils/smoothscrollcontroller.h"
|
||||
#include <qembycore.h>
|
||||
#include <services/media/mediaservice.h>
|
||||
@@ -118,7 +119,7 @@ void FavoritesView::setupUi()
|
||||
|
||||
m_vScrollController =
|
||||
new SmoothScrollController(m_mainScrollArea->verticalScrollBar(), this);
|
||||
m_vScrollController->setDuration(160);
|
||||
m_vScrollController->setDuration(AnimationTokens::DurationMs::kFast);
|
||||
|
||||
m_mainScrollArea->viewport()->installEventFilter(this);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "../../managers/searchhistorymanager.h"
|
||||
#include "../../managers/playbackmanager.h"
|
||||
#include "../../utils/smoothscrollcontroller.h"
|
||||
#include "../../tokens/AnimationTokens.h"
|
||||
#include "../admin/manageview.h"
|
||||
#include "../media/detailview.h"
|
||||
#include "../media/libraryview.h"
|
||||
@@ -713,7 +714,7 @@ void HomeView::setupSidebar()
|
||||
m_libraryList->setWordWrap(false);
|
||||
m_sidebarLibraryScrollController =
|
||||
new SmoothScrollController(m_libraryList->verticalScrollBar(), this);
|
||||
m_sidebarLibraryScrollController->setDuration(160);
|
||||
m_sidebarLibraryScrollController->setDuration(AnimationTokens::DurationMs::kFast);
|
||||
m_libraryList->viewport()->installEventFilter(this);
|
||||
layout->addWidget(m_libraryList, 1);
|
||||
|
||||
@@ -877,8 +878,8 @@ void HomeView::setupSidebar()
|
||||
});
|
||||
|
||||
m_sidebarAnim = new QPropertyAnimation(m_sidebar, "pos", this);
|
||||
m_sidebarAnim->setDuration(350);
|
||||
m_sidebarAnim->setEasingCurve(QEasingCurve::OutCubic);
|
||||
m_sidebarAnim->setDuration(AnimationTokens::DurationMs::kSlow);
|
||||
m_sidebarAnim->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
|
||||
m_sidebar->installEventFilter(this);
|
||||
m_sidebar->setMouseTracking(true);
|
||||
@@ -1113,7 +1114,15 @@ void HomeView::pushView(QWidget *view)
|
||||
|
||||
QMetaObject::invokeMethod(view, "scrollToTop", Qt::QueuedConnection);
|
||||
|
||||
if (view->property("routeType").toString() == QLatin1String("PlayerView"))
|
||||
{
|
||||
// 播放器有自己的入场动画(HUD 淡入等),瞬切避免与翻页动画叠加
|
||||
m_contentSwitcher->jumpToWgt(view);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_contentSwitcher->slideInWgt(view, SlidingStackedWidget::RightToLeft);
|
||||
}
|
||||
Q_EMIT canNavigateBackChanged(!m_navStack.isEmpty());
|
||||
}
|
||||
|
||||
@@ -1201,7 +1210,15 @@ void HomeView::navigateBack()
|
||||
|
||||
|
||||
|
||||
if (current && current->property("routeType").toString() == QLatin1String("PlayerView"))
|
||||
{
|
||||
// 与进入播放器对称:退出时瞬切,避免黑色播放器表面滑走
|
||||
m_contentSwitcher->jumpToWgt(targetView);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_contentSwitcher->slideInWgt(targetView, SlidingStackedWidget::LeftToRight);
|
||||
}
|
||||
Q_EMIT canNavigateBackChanged(!m_navStack.isEmpty());
|
||||
|
||||
|
||||
@@ -1672,6 +1689,8 @@ void HomeView::showSidebar()
|
||||
|
||||
if (m_sidebarAnim->state() == QAbstractAnimation::Running)
|
||||
m_sidebarAnim->stop();
|
||||
m_sidebarAnim->setDuration(AnimationTokens::DurationMs::kSlow);
|
||||
m_sidebarAnim->setEasingCurve(AnimationTokens::Curve::enter());
|
||||
m_sidebarAnim->setStartValue(m_sidebar->pos());
|
||||
|
||||
if (m_sidebarOnRight)
|
||||
@@ -1710,6 +1729,9 @@ void HomeView::hideSidebar()
|
||||
m_sidebarAutoHideTimer->stop();
|
||||
if (m_sidebarAnim->state() == QAbstractAnimation::Running)
|
||||
m_sidebarAnim->stop();
|
||||
// Dismiss reads snappier and more natural with an ease-in exit over kNormal.
|
||||
m_sidebarAnim->setDuration(AnimationTokens::DurationMs::kNormal);
|
||||
m_sidebarAnim->setEasingCurve(AnimationTokens::Curve::exit());
|
||||
m_sidebarAnim->setStartValue(m_sidebar->pos());
|
||||
|
||||
if (m_sidebarOnRight)
|
||||
|
||||
Reference in New Issue
Block a user