chore: baseline usable version

This commit is contained in:
dela
2026-06-10 11:17:51 +08:00
commit a2f82a86b6
932 changed files with 172551 additions and 0 deletions

View 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);
}