#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 #include #include #include #include #include #include #include #include #include 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(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(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 &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(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( ConfigKeys::PlayerAudioLang, "auto"); QString prefSubLang = ConfigStore::instance()->get(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(ConfigKeys::ExtPlayerEnable, false); if (!extEnabled) { m_extPlayerBtn->hide(); return; } QString currentPath = ConfigStore::instance()->get(ConfigKeys::ExtPlayerPath); QList allPlayers = ExternalPlayerDetector::loadFromConfig(); QSet 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); }