From 6b691a282b19f14772fc9b359f76dd1bdeb3fcc7 Mon Sep 17 00:00:00 2001 From: Gary Wang Date: Sat, 3 Jan 2026 19:27:55 +0800 Subject: [PATCH] feat: adds a lyrics widget instead of just a floating lrcbar --- .gitignore | 29 +++++----- CMakeLists.txt | 2 + lrcbar.cpp | 16 +++--- lrcbar.h | 71 +++++++++++++------------ lyricsmanager.cpp | 25 ++++++++- lyricsmanager.h | 95 ++++++++++++++++++--------------- lyricswidget.cpp | 131 ++++++++++++++++++++++++++++++++++++++++++++++ lyricswidget.h | 37 +++++++++++++ mainwindow.cpp | 29 ++++++++-- mainwindow.h | 4 ++ 10 files changed, 333 insertions(+), 106 deletions(-) create mode 100644 lyricswidget.cpp create mode 100644 lyricswidget.h diff --git a/.gitignore b/.gitignore index 2ad1ce1..3c24423 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,16 @@ -# Common build folder -[Bb]uild/ -build-*/ - -# IDE folder -.vscode/ -.idea/ - -# User config file -CMakeLists.txt.user* - -# Why, macOS, why? -.DS_Store +# Common build folder +[Bb]uild/ +build-*/ + +# IDE folder +.vscode/ +.idea/ + +# Clangd cache +.cache/ + +# User config file +CMakeLists.txt.user* + +# Why, macOS, why? +.DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt index c0cd1a4..5dad2ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,6 +47,7 @@ set (PMUSIC_CPP_FILES lyricsmanager.cpp fftspectrum.cpp playbackprogressindicator.cpp + lyricswidget.cpp ) set (PMUSIC_HEADER_FILES @@ -59,6 +60,7 @@ set (PMUSIC_HEADER_FILES fftspectrum.h playbackprogressindicator.h taskbarmanager.h + lyricswidget.h ) set (PMUSIC_UI_FILES diff --git a/lrcbar.cpp b/lrcbar.cpp index b6c4866..45cc636 100644 --- a/lrcbar.cpp +++ b/lrcbar.cpp @@ -10,9 +10,9 @@ #include #include -LrcBar::LrcBar(QWidget *parent) +LrcBar::LrcBar(QWidget *parent, LyricsManager *mgr) : QWidget(parent, Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool) - , m_lrcMgr(new LyricsManager(this)) + , m_lrcMgr(mgr) { m_font.setPointSize(30); m_font.setStyleStrategy(QFont::PreferAntialias); @@ -35,6 +35,11 @@ LrcBar::LrcBar(QWidget *parent) setGeometry(QRect(QPoint((qApp->primaryScreen()->geometry().width() - windowSize.width()) / 2, qApp->primaryScreen()->geometry().height() - windowSize.height() - 50), windowSize)); + + connect(m_lrcMgr, &LyricsManager::lyricsLoaded, this, [this](bool){ + m_currentTimeMs = 0; + update(); + }); } LrcBar::~LrcBar() @@ -42,18 +47,11 @@ LrcBar::~LrcBar() } -bool LrcBar::loadLyrics(QString filepath) -{ - m_currentTimeMs = 0; - return m_lrcMgr->loadLyrics(filepath); -} - void LrcBar::playbackPositionChanged(int timestampMs, int totalTimeMs) { if (!isVisible()) return; m_currentTimeMs = timestampMs; - m_lrcMgr->updateCurrentTimeMs(timestampMs, totalTimeMs); update(); } diff --git a/lrcbar.h b/lrcbar.h index 0c29f8a..06e4b68 100644 --- a/lrcbar.h +++ b/lrcbar.h @@ -1,36 +1,35 @@ -// SPDX-FileCopyrightText: 2024 Gary Wang -// -// SPDX-License-Identifier: MIT - -#pragma once - -#include -#include -#include - -class LyricsManager; -class LrcBar : public QWidget -{ - Q_OBJECT -public: - explicit LrcBar(QWidget *parent); - ~LrcBar(); - - bool loadLyrics(QString filepath); - void playbackPositionChanged(int timestampMs, int totalTimeMs); - -protected: - QSize sizeHint() const override; - void mouseMoveEvent(QMouseEvent *event) override; - void paintEvent(QPaintEvent *) override; - void enterEvent(QEnterEvent *) override; - void leaveEvent(QEvent *) override; - -private: - int m_currentTimeMs = 0; - LyricsManager * m_lrcMgr; - QFont m_font; - QLinearGradient m_baseLinearGradient; - QLinearGradient m_maskLinearGradient; - bool m_underMouse = false; -}; +// SPDX-FileCopyrightText: 2024 Gary Wang +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include + +class LyricsManager; +class LrcBar : public QWidget +{ + Q_OBJECT +public: + explicit LrcBar(QWidget *parent, LyricsManager *mgr); + ~LrcBar(); + + void playbackPositionChanged(int timestampMs, int totalTimeMs); + +protected: + QSize sizeHint() const override; + void mouseMoveEvent(QMouseEvent *event) override; + void paintEvent(QPaintEvent *) override; + void enterEvent(QEnterEvent *) override; + void leaveEvent(QEvent *) override; + +private: + int m_currentTimeMs = 0; + LyricsManager * m_lrcMgr; + QFont m_font; + QLinearGradient m_baseLinearGradient; + QLinearGradient m_maskLinearGradient; + bool m_underMouse = false; +}; diff --git a/lyricsmanager.cpp b/lyricsmanager.cpp index f336ce2..2fa6073 100644 --- a/lyricsmanager.cpp +++ b/lyricsmanager.cpp @@ -43,10 +43,14 @@ bool LyricsManager::loadLyrics(QString filepath) if (!filepath.endsWith(".lrc", Qt::CaseInsensitive)) { fileInfo.setFile(fileInfo.dir().filePath(fileInfo.completeBaseName() + ".lrc")); } - if (!fileInfo.exists()) return false; + if (!fileInfo.exists()) { + emit lyricsLoaded(false); + return false; + } QFile file(fileInfo.absoluteFilePath()); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + emit lyricsLoaded(false); return false; } QByteArray fileContent(file.readAll()); @@ -126,9 +130,11 @@ bool LyricsManager::loadLyrics(QString filepath) if (!m_lyricsMap.isEmpty()) { m_timestampList = m_lyricsMap.keys(); std::sort(m_timestampList.begin(), m_timestampList.end()); + emit lyricsLoaded(true); return true; } + emit lyricsLoaded(false); return false; } @@ -151,6 +157,22 @@ void LyricsManager::updateCurrentTimeMs(int curTimeMs, int totalTimeMs) iter--; } m_currentLyricsTime = *iter; + m_currentLineIndex = std::distance(m_timestampList.begin(), iter); +} + +const QList& LyricsManager::timestamps() const +{ + return m_timestampList; +} + +const QHash& LyricsManager::lyricsMap() const +{ + return m_lyricsMap; +} + +int LyricsManager::currentLineIndex() const +{ + return m_currentLineIndex; } QString LyricsManager::lyrics(int lineOffset) const @@ -197,6 +219,7 @@ void LyricsManager::reset() { m_currentLyricsTime = 0; m_nextLyricsTime = 0; + m_currentLineIndex = -1; m_timeOffset = 0; m_lyricsMap.clear(); m_timestampList.clear(); diff --git a/lyricsmanager.h b/lyricsmanager.h index 5383fd1..01f9540 100644 --- a/lyricsmanager.h +++ b/lyricsmanager.h @@ -1,43 +1,52 @@ -// SPDX-FileCopyrightText: 2024 Gary Wang -// -// SPDX-License-Identifier: MIT - -#pragma once - -#include -#include -#include -#include - -Q_DECLARE_LOGGING_CATEGORY(lcLyrics) -Q_DECLARE_LOGGING_CATEGORY(lcLyricsParser) - -class LyricsManager : public QObject -{ - Q_OBJECT -public: - explicit LyricsManager(QObject *parent); - ~LyricsManager(); - - bool loadLyrics(QString filepath); - bool hasLyrics() const; - void updateCurrentTimeMs(int curTimeMs, int totalTimeMs); - QString lyrics(int lineOffset = 0) const; - double maskPercent(int curTimeMs); - - static int parseTimeToMilliseconds(const QString& timeString); - -protected: - - -private: - void reset(); - int currentLyricsTime() const; - int nextLyricsTime() const; - - QHash m_lyricsMap; - QList m_timestampList; - int m_currentLyricsTime = 0; - int m_nextLyricsTime = 0; - int m_timeOffset = 0; -}; +// SPDX-FileCopyrightText: 2024 Gary Wang +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(lcLyrics) +Q_DECLARE_LOGGING_CATEGORY(lcLyricsParser) + +class LyricsManager : public QObject +{ + Q_OBJECT +public: + explicit LyricsManager(QObject *parent); + ~LyricsManager(); + + bool loadLyrics(QString filepath); + bool hasLyrics() const; + void updateCurrentTimeMs(int curTimeMs, int totalTimeMs); + QString lyrics(int lineOffset = 0) const; + double maskPercent(int curTimeMs); + + const QList& timestamps() const; + const QHash& lyricsMap() const; + int currentLineIndex() const; + + static int parseTimeToMilliseconds(const QString& timeString); + +signals: + void lyricsLoaded(bool success); + + +protected: + + +private: + void reset(); + int currentLyricsTime() const; + int nextLyricsTime() const; + + QHash m_lyricsMap; + QList m_timestampList; + int m_currentLyricsTime = 0; + int m_nextLyricsTime = 0; + int m_currentLineIndex = -1; + int m_timeOffset = 0; +}; diff --git a/lyricswidget.cpp b/lyricswidget.cpp new file mode 100644 index 0000000..59c0023 --- /dev/null +++ b/lyricswidget.cpp @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2026 Gary Wang +// +// SPDX-License-Identifier: MIT + +#include "lyricswidget.h" +#include "lyricsmanager.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +LyricsWidget::LyricsWidget(QWidget *parent) + : QWidget(parent) + , m_listWidget(new QListWidget(this)) +{ + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(m_listWidget); + + m_listWidget->setFrameShape(QFrame::NoFrame); + m_listWidget->setStyleSheet("background: transparent;"); + m_listWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + m_listWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_listWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Hide scrollbar for cleaner look? Or Keep it? User didn't specify. Hiding it looks more like "Lyrics Mode" + // Enable word wrap + m_listWidget->setWordWrap(true); + m_listWidget->setTextElideMode(Qt::ElideNone); + m_listWidget->setSelectionMode(QAbstractItemView::NoSelection); + m_listWidget->setFocusPolicy(Qt::NoFocus); +} + +void LyricsWidget::setLyricsManager(LyricsManager *mgr) +{ + if (m_lyricsManager) { + disconnect(m_lyricsManager, nullptr, this, nullptr); + } + m_lyricsManager = mgr; + if (m_lyricsManager) { + connect(m_lyricsManager, &LyricsManager::lyricsLoaded, this, &LyricsWidget::onLyricsLoaded); + // Load initial state if any + onLyricsLoaded(m_lyricsManager->hasLyrics()); + } +} + +void LyricsWidget::updatePosition(qint64 position) +{ + if (!m_lyricsManager || !m_lyricsManager->hasLyrics() || !isVisible()) return; + + // Note: rely on LyricsManager::updateCurrentTimeMs() + int index = m_lyricsManager->currentLineIndex(); + + if (index != m_lastHighlightIndex) { + highlightCurrentLine(index); + } +} + +void LyricsWidget::onLyricsLoaded(bool success) +{ + m_listWidget->clear(); + m_lastHighlightIndex = -1; + + if (!success || !m_lyricsManager) return; + + const QList& timestamps = m_lyricsManager->timestamps(); + const QHash& lyricsMap = m_lyricsManager->lyricsMap(); + + for (int timestamp : timestamps) { + QListWidgetItem *item = new QListWidgetItem(lyricsMap.value(timestamp)); + item->setTextAlignment(Qt::AlignCenter); + + // Default style + QFont font = item->font(); + font.setPointSize(10); + item->setFont(font); + item->setForeground(QColor(255, 255, 255, 150)); // Dimmed + + m_listWidget->addItem(item); + } +} + +void LyricsWidget::highlightCurrentLine(int index) +{ + if (index < 0 || index >= m_listWidget->count()) return; + + // Reset old highlight + if (m_lastHighlightIndex >= 0 && m_lastHighlightIndex < m_listWidget->count()) { + QListWidgetItem *oldItem = m_listWidget->item(m_lastHighlightIndex); + QFont font = oldItem->font(); + font.setPointSize(10); + font.setBold(false); + oldItem->setFont(font); + oldItem->setForeground(QColor(255, 255, 255, 150)); + } + + // Set new highlight + QListWidgetItem *newItem = m_listWidget->item(index); + QFont font = newItem->font(); + font.setPointSize(14); + font.setBold(true); + newItem->setFont(font); + newItem->setForeground(QColor(255, 255, 255, 255)); + + // Smooth scroll + QScrollBar *vBar = m_listWidget->verticalScrollBar(); + int startScroll = vBar->value(); + + m_listWidget->scrollToItem(newItem, QAbstractItemView::PositionAtCenter); + int endScroll = vBar->value(); + + // Restore and animate + if (startScroll != endScroll) { + vBar->setValue(startScroll); + + if (!m_scrollAnimation) { + m_scrollAnimation = new QPropertyAnimation(vBar, "value", this); + m_scrollAnimation->setDuration(400); + m_scrollAnimation->setEasingCurve(QEasingCurve::OutCubic); + } + m_scrollAnimation->stop(); + m_scrollAnimation->setStartValue(startScroll); + m_scrollAnimation->setEndValue(endScroll); + m_scrollAnimation->start(); + } + + m_lastHighlightIndex = index; +} diff --git a/lyricswidget.h b/lyricswidget.h new file mode 100644 index 0000000..6c4d4a0 --- /dev/null +++ b/lyricswidget.h @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2026 Gary Wang +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include + +QT_BEGIN_NAMESPACE +class QListWidget; +class QListWidgetItem; +class QPropertyAnimation; +QT_END_NAMESPACE + +class LyricsManager; +class LyricsWidget : public QWidget +{ + Q_OBJECT +public: + explicit LyricsWidget(QWidget *parent = nullptr); + + void setLyricsManager(LyricsManager *mgr); + void updatePosition(qint64 position); + +private slots: + void onLyricsLoaded(bool success); + +private: + QListWidget *m_listWidget; + LyricsManager *m_lyricsManager = nullptr; + int m_lastHighlightIndex = -1; + QPropertyAnimation *m_scrollAnimation = nullptr; + + void updateLyrics(); + void highlightCurrentLine(int index); +}; + diff --git a/mainwindow.cpp b/mainwindow.cpp index 3a8eb00..62b2e49 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -9,6 +9,8 @@ #include "fftspectrum.h" #include "lrcbar.h" #include "taskbarmanager.h" +#include "lyricsmanager.h" +#include "lyricswidget.h" // taglib #ifndef NO_TAGLIB @@ -48,11 +50,16 @@ MainWindow::MainWindow(QWidget *parent) , m_mediaPlayer(new QMediaPlayer(this)) , m_audioOutput(new QAudioOutput(this)) , m_fftSpectrum(new FFTSpectrum(this)) - , m_lrcbar(new LrcBar(nullptr)) + , m_lyricsManager(new LyricsManager(this)) + , m_lrcbar(new LrcBar(nullptr, m_lyricsManager)) + , m_lyricsWidget(new LyricsWidget(this)) , m_playlistManager(new PlaylistManager(this)) , m_taskbarManager(new TaskBarManager(this)) { ui->setupUi(this); + m_lyricsWidget->setLyricsManager(m_lyricsManager); + ui->pluginStackedWidget->addWidget(m_lyricsWidget); + m_playlistManager->setAutoLoadFilterSuffixes({ "*.mp3", "*.wav", "*.aiff", "*.ape", "*.flac", "*.ogg", "*.oga", "*.mpga", "*.aac", "*.tta" }); @@ -255,7 +262,7 @@ void MainWindow::dropEvent(QDropEvent *e) } if (fileName.endsWith(".lrc")) { - m_lrcbar->loadLyrics(fileName); + m_lyricsManager->loadLyrics(fileName); return; } @@ -313,7 +320,7 @@ void MainWindow::loadFile(const QUrl &url) { const QString filePath = url.toLocalFile(); m_mediaPlayer->setSource(url); - m_lrcbar->loadLyrics(filePath); + m_lyricsManager->loadLyrics(filePath); QList> chapters(PlaybackProgressIndicator::tryLoadChapters(filePath)); ui->playbackProgressIndicator->setChapters(chapters); } @@ -519,7 +526,12 @@ void MainWindow::initConnections() ui->playbackProgressIndicator->setPosition(pos); m_taskbarManager->setProgressValue(pos); } - m_lrcbar->playbackPositionChanged(pos, m_mediaPlayer->duration()); + + if (m_lrcbar->isVisible() || m_lyricsWidget->isVisible()) { + m_lyricsManager->updateCurrentTimeMs(pos, m_mediaPlayer->duration()); + m_lrcbar->playbackPositionChanged(pos, m_mediaPlayer->duration()); + m_lyricsWidget->updatePosition(pos); + } static QString lastChapterName; if (ui->playbackProgressIndicator->chapterModel()->rowCount() > 0) { @@ -718,6 +730,15 @@ void MainWindow::on_playlistView_activated(const QModelIndex &index) void MainWindow::on_lrcBtn_clicked() { + if (size().height() < 200) { + setFixedSize(fullSize); + } + + if (ui->pluginStackedWidget->currentWidget() != m_lyricsWidget) { + ui->pluginStackedWidget->setCurrentWidget(m_lyricsWidget); + return; + } + if (m_lrcbar->isVisible()) { m_lrcbar->hide(); } else { diff --git a/mainwindow.h b/mainwindow.h index 3586714..3ce30e1 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -21,6 +21,8 @@ QT_END_NAMESPACE class FFTSpectrum; class LrcBar; +class LyricsManager; +class LyricsWidget; class PlaylistManager; class TaskBarManager; class MainWindow : public QMainWindow @@ -104,7 +106,9 @@ private: QMediaPlayer *m_mediaPlayer; QAudioOutput *m_audioOutput; FFTSpectrum* m_fftSpectrum; + LyricsManager *m_lyricsManager; LrcBar *m_lrcbar; + LyricsWidget *m_lyricsWidget; QPropertyAnimation *m_fadeOutAnimation; PlaylistManager *m_playlistManager; TaskBarManager *m_taskbarManager;