chore: baseline usable version
This commit is contained in:
450
src/qEmbyApp/components/detailactionwidget.cpp
Normal file
450
src/qEmbyApp/components/detailactionwidget.cpp
Normal file
@@ -0,0 +1,450 @@
|
||||
#include "detailactionwidget.h"
|
||||
#include "../managers/externalplayerdetector.h"
|
||||
#include "../managers/thememanager.h"
|
||||
#include "../utils/mediasourcepreferenceutils.h"
|
||||
#include "../utils/playerpreferenceutils.h"
|
||||
#include "modernmenubutton.h"
|
||||
#include "splitplayerbutton.h"
|
||||
#include <QFileInfo>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QProgressBar>
|
||||
#include <QPushButton>
|
||||
#include <QSet>
|
||||
#include <QStyle>
|
||||
#include <QVBoxLayout>
|
||||
#include <config/config_keys.h>
|
||||
#include <config/configstore.h>
|
||||
|
||||
DetailActionWidget::DetailActionWidget(QWidget *parent) : QWidget(parent) {
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
mainLayout->setSpacing(4);
|
||||
|
||||
|
||||
auto *actionsLayout = new QHBoxLayout();
|
||||
actionsLayout->setSpacing(12);
|
||||
actionsLayout->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||
|
||||
m_resumeBtn = new QPushButton(tr("▶ Resume"), this);
|
||||
m_resumeBtn->setObjectName("detail-resume-btn");
|
||||
m_resumeBtn->setCursor(Qt::PointingHandCursor);
|
||||
m_resumeBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
|
||||
m_playBtn = new QPushButton(tr("▶ Play"), this);
|
||||
m_playBtn->setObjectName("detail-play-btn");
|
||||
m_playBtn->setCursor(Qt::PointingHandCursor);
|
||||
m_playBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
|
||||
m_favBtn = new QPushButton(this);
|
||||
m_favBtn->setObjectName("detail-fav-btn");
|
||||
m_favBtn->setIcon(QIcon(":/svg/light/heart.svg"));
|
||||
m_favBtn->setIconSize(QSize(20, 20));
|
||||
m_favBtn->setFixedSize(36, 36);
|
||||
m_favBtn->setCursor(Qt::PointingHandCursor);
|
||||
|
||||
m_playedBtn = new QPushButton(this);
|
||||
m_playedBtn->setObjectName("detail-played-btn");
|
||||
m_playedBtn->setIconSize(QSize(20, 20));
|
||||
m_playedBtn->setFixedSize(36, 36);
|
||||
m_playedBtn->setCursor(Qt::PointingHandCursor);
|
||||
|
||||
m_progressWidget = new QWidget(this);
|
||||
m_progressWidget->setMaximumWidth(450);
|
||||
auto *progressLayout = new QHBoxLayout(m_progressWidget);
|
||||
progressLayout->setContentsMargins(0, 0, 0, 0);
|
||||
progressLayout->setSpacing(10);
|
||||
|
||||
m_progressBar = new QProgressBar(m_progressWidget);
|
||||
m_progressBar->setObjectName("detail-progress-bar");
|
||||
m_progressBar->setTextVisible(false);
|
||||
|
||||
m_remainingTimeLabel = new QLabel(m_progressWidget);
|
||||
m_remainingTimeLabel->setObjectName("detail-remaining-time");
|
||||
|
||||
progressLayout->addWidget(m_progressBar, 1);
|
||||
progressLayout->addWidget(m_remainingTimeLabel);
|
||||
|
||||
|
||||
m_extPlayerBtn = new SplitPlayerButton(this);
|
||||
m_extPlayerBtn->setObjectName("detail-ext-player-btn");
|
||||
m_extPlayerBtn->setIconOnly(true);
|
||||
m_extPlayerBtn->hide();
|
||||
|
||||
connect(m_extPlayerBtn, &SplitPlayerButton::playRequested, this,
|
||||
&DetailActionWidget::externalPlayRequested);
|
||||
connect(m_extPlayerBtn, &SplitPlayerButton::playerSelected, this,
|
||||
&DetailActionWidget::externalPlayRequested);
|
||||
|
||||
actionsLayout->addWidget(m_resumeBtn);
|
||||
actionsLayout->addWidget(m_playBtn);
|
||||
actionsLayout->addWidget(m_extPlayerBtn);
|
||||
actionsLayout->addWidget(m_favBtn);
|
||||
actionsLayout->addWidget(m_playedBtn);
|
||||
actionsLayout->addWidget(m_progressWidget);
|
||||
actionsLayout->addStretch();
|
||||
|
||||
|
||||
auto *streamSelectorsLayout = new QHBoxLayout();
|
||||
streamSelectorsLayout->setSpacing(6);
|
||||
streamSelectorsLayout->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||
|
||||
m_versionComboBox = new ModernMenuButton(this);
|
||||
m_audioComboBox = new ModernMenuButton(this);
|
||||
m_subtitleComboBox = new ModernMenuButton(this);
|
||||
|
||||
streamSelectorsLayout->addWidget(m_versionComboBox);
|
||||
streamSelectorsLayout->addWidget(m_audioComboBox);
|
||||
streamSelectorsLayout->addWidget(m_subtitleComboBox);
|
||||
streamSelectorsLayout->addStretch();
|
||||
|
||||
mainLayout->addLayout(actionsLayout);
|
||||
mainLayout->addLayout(streamSelectorsLayout);
|
||||
|
||||
connect(m_playBtn, &QPushButton::clicked, this,
|
||||
&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_versionComboBox, &ModernMenuButton::currentIndexChanged, this,
|
||||
&DetailActionWidget::sourceVersionChanged);
|
||||
|
||||
clear();
|
||||
}
|
||||
|
||||
void DetailActionWidget::clear() {
|
||||
m_resumeBtn->hide();
|
||||
m_playBtn->setText(tr("▶ Play"));
|
||||
m_progressWidget->hide();
|
||||
|
||||
m_versionComboBox->blockSignals(true);
|
||||
m_versionComboBox->clear();
|
||||
m_versionComboBox->hide();
|
||||
m_versionComboBox->blockSignals(false);
|
||||
|
||||
m_audioComboBox->blockSignals(true);
|
||||
m_audioComboBox->clear();
|
||||
m_audioComboBox->hide();
|
||||
m_audioComboBox->blockSignals(false);
|
||||
|
||||
m_subtitleComboBox->blockSignals(true);
|
||||
m_subtitleComboBox->clear();
|
||||
m_subtitleComboBox->hide();
|
||||
m_subtitleComboBox->blockSignals(false);
|
||||
}
|
||||
|
||||
void DetailActionWidget::setupNormalMode(const MediaItem &item) {
|
||||
m_playBtn->setEnabled(true);
|
||||
if (item.userData.playedPercentage > 0 &&
|
||||
item.userData.playedPercentage < 100) {
|
||||
m_resumeBtn->show();
|
||||
m_playBtn->setText(tr("↺ Play from Beginning"));
|
||||
|
||||
m_progressBar->setValue(static_cast<int>(item.userData.playedPercentage));
|
||||
long long remainingTicks =
|
||||
item.runTimeTicks - item.userData.playbackPositionTicks;
|
||||
if (remainingTicks > 0) {
|
||||
m_remainingTimeLabel->setText(
|
||||
tr("%1 remaining").arg(formatRunTime(remainingTicks)));
|
||||
} else {
|
||||
m_remainingTimeLabel->clear();
|
||||
}
|
||||
m_progressWidget->show();
|
||||
} else {
|
||||
m_resumeBtn->hide();
|
||||
m_playBtn->setText(tr("▶ Play"));
|
||||
m_progressWidget->hide();
|
||||
}
|
||||
}
|
||||
|
||||
void DetailActionWidget::setSeriesLoadingMode() {
|
||||
m_resumeBtn->hide();
|
||||
m_playBtn->setText(tr("▶ Play"));
|
||||
m_playBtn->setEnabled(false);
|
||||
m_progressWidget->hide();
|
||||
}
|
||||
|
||||
void DetailActionWidget::setupSeriesMode(const MediaItem &nextUpItem,
|
||||
const QString &epTag) {
|
||||
m_playBtn->setEnabled(true);
|
||||
if (nextUpItem.userData.playbackPositionTicks > 0) {
|
||||
m_resumeBtn->show();
|
||||
m_resumeBtn->setText(tr("▶ Resume %1").arg(epTag));
|
||||
m_playBtn->setText(tr("↺ Restart %1").arg(epTag));
|
||||
|
||||
if (nextUpItem.userData.playedPercentage > 0) {
|
||||
m_progressBar->setValue(
|
||||
static_cast<int>(nextUpItem.userData.playedPercentage));
|
||||
long long remainingTicks =
|
||||
nextUpItem.runTimeTicks - nextUpItem.userData.playbackPositionTicks;
|
||||
if (remainingTicks > 0) {
|
||||
m_remainingTimeLabel->setText(
|
||||
tr("%1 remaining").arg(formatRunTime(remainingTicks)));
|
||||
} else {
|
||||
m_remainingTimeLabel->clear();
|
||||
}
|
||||
m_progressWidget->show();
|
||||
}
|
||||
} else {
|
||||
m_resumeBtn->hide();
|
||||
m_playBtn->setText(tr("▶ Play %1").arg(epTag));
|
||||
m_progressWidget->hide();
|
||||
}
|
||||
}
|
||||
|
||||
void DetailActionWidget::setFavoriteState(bool isFavorite) {
|
||||
m_favBtn->setProperty("isFavorite", isFavorite);
|
||||
m_favBtn->setIcon(QIcon(isFavorite ? ":/svg/light/heart-fill.svg"
|
||||
: ":/svg/light/heart-outline.svg"));
|
||||
m_favBtn->style()->unpolish(m_favBtn);
|
||||
m_favBtn->style()->polish(m_favBtn);
|
||||
}
|
||||
|
||||
void DetailActionWidget::setPlayedState(bool played) {
|
||||
m_playedBtn->setProperty("played", played);
|
||||
const QString themeDir =
|
||||
ThemeManager::instance()->isDarkMode() ? "dark" : "light";
|
||||
if (played) {
|
||||
m_playedBtn->setIcon(QIcon(":/svg/dark/played-check.svg"));
|
||||
} else {
|
||||
m_playedBtn->setIcon(
|
||||
QIcon(QString(":/svg/%1/unplayed-check.svg").arg(themeDir)));
|
||||
}
|
||||
m_playedBtn->style()->unpolish(m_playedBtn);
|
||||
m_playedBtn->style()->polish(m_playedBtn);
|
||||
}
|
||||
|
||||
void DetailActionWidget::setSources(const QList<MediaSourceInfo> &sources,
|
||||
int currentIndex) {
|
||||
m_versionComboBox->blockSignals(true);
|
||||
m_versionComboBox->clear();
|
||||
|
||||
if (!sources.isEmpty()) {
|
||||
for (int i = 0; i < sources.size(); ++i) {
|
||||
const auto &src = sources[i];
|
||||
QString versionName =
|
||||
src.name.isEmpty() ? tr("Version %1").arg(i + 1) : src.name;
|
||||
QString videoInfo = tr("Unknown Video");
|
||||
for (const auto &stream : src.mediaStreams) {
|
||||
if (stream.type == "Video") {
|
||||
QStringList parts;
|
||||
if (stream.width > 0)
|
||||
parts << QString("%1x%2").arg(stream.width).arg(stream.height);
|
||||
if (!stream.codec.isEmpty())
|
||||
parts << stream.codec.toUpper();
|
||||
if (stream.realFrameRate > 0)
|
||||
parts << QString::number(stream.realFrameRate, 'f', 1) + " fps";
|
||||
if (stream.bitRate > 0)
|
||||
parts << QString("%1 Mbps").arg(stream.bitRate / 1000000.0, 0, 'f',
|
||||
1);
|
||||
if (!parts.isEmpty())
|
||||
videoInfo = parts.join(" · ");
|
||||
break;
|
||||
}
|
||||
}
|
||||
m_versionComboBox->addItem("🎞 " + versionName, videoInfo, QString(),
|
||||
src.id);
|
||||
}
|
||||
|
||||
int bestIndex = currentIndex;
|
||||
if (sources.size() > 1) {
|
||||
bestIndex = MediaSourcePreferenceUtils::resolvePreferredMediaSourceIndex(
|
||||
sources,
|
||||
ConfigStore::instance()
|
||||
->get<QString>(ConfigKeys::PlayerPreferredVersion)
|
||||
.trimmed());
|
||||
}
|
||||
|
||||
m_versionComboBox->setCurrentIndex(bestIndex);
|
||||
m_versionComboBox->show();
|
||||
} else {
|
||||
m_versionComboBox->hide();
|
||||
}
|
||||
m_versionComboBox->blockSignals(false);
|
||||
}
|
||||
|
||||
void DetailActionWidget::setStreams(const MediaSourceInfo &source) {
|
||||
m_audioComboBox->blockSignals(true);
|
||||
m_subtitleComboBox->blockSignals(true);
|
||||
m_audioComboBox->clear();
|
||||
m_subtitleComboBox->clear();
|
||||
|
||||
m_subtitleComboBox->addItem("🚫 " + tr("No Subtitles"), QString(),
|
||||
tr("Disable subtitle display"), -1);
|
||||
|
||||
|
||||
QString prefAudioLang = ConfigStore::instance()->get<QString>(
|
||||
ConfigKeys::PlayerAudioLang, "auto");
|
||||
QString prefSubLang =
|
||||
ConfigStore::instance()->get<QString>(ConfigKeys::PlayerSubLang, "auto");
|
||||
const int preferredAudioStreamIndex =
|
||||
PlayerPreferenceUtils::findPreferredStreamIndex(
|
||||
source.mediaStreams, "Audio", prefAudioLang);
|
||||
const int preferredSubtitleStreamIndex =
|
||||
PlayerPreferenceUtils::findPreferredStreamIndex(
|
||||
source.mediaStreams, "Subtitle", prefSubLang);
|
||||
const bool subtitleDisabled =
|
||||
PlayerPreferenceUtils::isSubtitleDisabled(prefSubLang);
|
||||
|
||||
int defaultAudioIdx = 0;
|
||||
int defaultSubIdx = 0;
|
||||
bool subMatchedByPref = false;
|
||||
bool subMatchedByDefault = false;
|
||||
|
||||
for (const auto &stream : source.mediaStreams) {
|
||||
if (stream.type == "Audio") {
|
||||
QString title =
|
||||
stream.displayTitle.isEmpty() ? stream.language : stream.displayTitle;
|
||||
if (title.isEmpty())
|
||||
title = tr("Audio Track");
|
||||
QStringList parts;
|
||||
if (!stream.codec.isEmpty())
|
||||
parts << stream.codec.toUpper();
|
||||
if (stream.channels > 0)
|
||||
parts << QString("%1 ch").arg(stream.channels);
|
||||
if (stream.sampleRate > 0)
|
||||
parts << QString("%1 kHz").arg(stream.sampleRate / 1000.0, 0, 'f', 1);
|
||||
if (stream.bitRate > 0)
|
||||
parts << QString("%1 kbps").arg(stream.bitRate / 1000);
|
||||
|
||||
|
||||
QStringList secondLineParts;
|
||||
if (!stream.title.isEmpty())
|
||||
secondLineParts << stream.title;
|
||||
if (!parts.isEmpty())
|
||||
secondLineParts << parts.join(" · ");
|
||||
m_audioComboBox->addItem("🔊 " + title, QString(),
|
||||
secondLineParts.join(" · "), stream.index);
|
||||
|
||||
int curIdx = m_audioComboBox->count() - 1;
|
||||
if (preferredAudioStreamIndex >= 0 &&
|
||||
stream.index == preferredAudioStreamIndex) {
|
||||
defaultAudioIdx = curIdx;
|
||||
} else if (preferredAudioStreamIndex < 0 && stream.isDefault) {
|
||||
defaultAudioIdx = curIdx;
|
||||
}
|
||||
} else if (stream.type == "Subtitle") {
|
||||
QString title =
|
||||
stream.displayTitle.isEmpty() ? stream.language : stream.displayTitle;
|
||||
if (title.isEmpty())
|
||||
title = tr("Subtitle");
|
||||
QStringList parts;
|
||||
if (!stream.codec.isEmpty())
|
||||
parts << stream.codec.toUpper();
|
||||
if (stream.isForced)
|
||||
parts << tr("Forced");
|
||||
if (stream.isExternal)
|
||||
parts << tr("External");
|
||||
if (stream.isHearingImpaired)
|
||||
parts << tr("SDH");
|
||||
|
||||
|
||||
QStringList secondLineParts;
|
||||
if (!stream.title.isEmpty())
|
||||
secondLineParts << stream.title;
|
||||
if (!parts.isEmpty())
|
||||
secondLineParts << parts.join(" · ");
|
||||
m_subtitleComboBox->addItem("💬 " + title, QString(),
|
||||
secondLineParts.join(" · "), stream.index);
|
||||
|
||||
int curIdx = m_subtitleComboBox->count() - 1;
|
||||
if (preferredSubtitleStreamIndex >= 0 &&
|
||||
stream.index == preferredSubtitleStreamIndex) {
|
||||
defaultSubIdx = curIdx;
|
||||
subMatchedByPref = true;
|
||||
} else if (preferredSubtitleStreamIndex < 0 && !subMatchedByPref &&
|
||||
stream.isDefault) {
|
||||
defaultSubIdx = curIdx;
|
||||
subMatchedByDefault = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (subtitleDisabled) {
|
||||
defaultSubIdx = 0;
|
||||
} else if (!subMatchedByPref && !subMatchedByDefault &&
|
||||
m_subtitleComboBox->count() > 1) {
|
||||
|
||||
|
||||
defaultSubIdx = 1;
|
||||
}
|
||||
|
||||
if (m_audioComboBox->count() > 0) {
|
||||
m_audioComboBox->setCurrentIndex(defaultAudioIdx);
|
||||
m_audioComboBox->show();
|
||||
} else {
|
||||
m_audioComboBox->hide();
|
||||
}
|
||||
|
||||
if (m_subtitleComboBox->count() > 1) {
|
||||
m_subtitleComboBox->setCurrentIndex(defaultSubIdx);
|
||||
m_subtitleComboBox->show();
|
||||
} else {
|
||||
m_subtitleComboBox->hide();
|
||||
}
|
||||
|
||||
m_audioComboBox->blockSignals(false);
|
||||
m_subtitleComboBox->blockSignals(false);
|
||||
}
|
||||
|
||||
int DetailActionWidget::currentSourceIndex() const {
|
||||
return qMax(0, m_versionComboBox->currentIndex());
|
||||
}
|
||||
int DetailActionWidget::currentAudioIndex() const {
|
||||
return m_audioComboBox->isVisible() ? m_audioComboBox->currentData().toInt()
|
||||
: -1;
|
||||
}
|
||||
int DetailActionWidget::currentSubtitleIndex() const {
|
||||
return m_subtitleComboBox->isVisible()
|
||||
? m_subtitleComboBox->currentData().toInt()
|
||||
: -1;
|
||||
}
|
||||
|
||||
QString DetailActionWidget::formatRunTime(long long ticks) {
|
||||
long long totalSeconds = ticks / 10000000;
|
||||
long long hours = totalSeconds / 3600;
|
||||
long long minutes = (totalSeconds % 3600) / 60;
|
||||
if (hours > 0)
|
||||
return QString(tr("%1 hr %2 min")).arg(hours).arg(minutes);
|
||||
return QString(tr("%1 min")).arg(minutes);
|
||||
}
|
||||
|
||||
void DetailActionWidget::refreshExtPlayerButton() {
|
||||
bool extEnabled =
|
||||
ConfigStore::instance()->get<bool>(ConfigKeys::ExtPlayerEnable, false);
|
||||
if (!extEnabled) {
|
||||
m_extPlayerBtn->hide();
|
||||
return;
|
||||
}
|
||||
|
||||
QString currentPath =
|
||||
ConfigStore::instance()->get<QString>(ConfigKeys::ExtPlayerPath);
|
||||
|
||||
|
||||
QList<DetectedPlayer> allPlayers = ExternalPlayerDetector::loadFromConfig();
|
||||
|
||||
|
||||
QSet<QString> knownPaths;
|
||||
for (const auto &p : allPlayers)
|
||||
knownPaths.insert(p.path);
|
||||
if (!currentPath.isEmpty() && currentPath != "custom" &&
|
||||
!knownPaths.contains(currentPath) && QFileInfo::exists(currentPath)) {
|
||||
allPlayers.prepend({QFileInfo(currentPath).baseName(), currentPath});
|
||||
}
|
||||
|
||||
if (allPlayers.isEmpty()) {
|
||||
m_extPlayerBtn->hide();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
QString activePlayerPath = currentPath;
|
||||
if (activePlayerPath.isEmpty() || activePlayerPath == "custom") {
|
||||
activePlayerPath = allPlayers.first().path;
|
||||
}
|
||||
m_extPlayerBtn->setPlayers(allPlayers, activePlayerPath);
|
||||
}
|
||||
Reference in New Issue
Block a user