Compare commits

...

6 Commits

Author SHA1 Message Date
7164a5a299 fix(LrcBar): use availableGeometry instead for window positioning 2026-01-05 00:01:03 +08:00
a60f430334 chore: update copyright to 2026, add version info to about dialog 2026-01-04 00:06:44 +08:00
0a4c1fbe88 fix(CI): should no longer deploy core5compat 2026-01-03 19:47:53 +08:00
6b691a282b feat: adds a lyrics widget instead of just a floating lrcbar 2026-01-03 19:27:55 +08:00
Codeberg Translate
11f79a9b98 i18n: Translations update from Codeberg Translate
Co-authored-by: Dirk <dirk@noreply.codeberg.org>
Translate-URL: https://translate.codeberg.org/projects/pineapple-apps/pineapple-music/de/
Translation: Pineapple Applications/Pineapple Music
2025-12-06 04:37:13 +00:00
fbcda7a401 chore(CI): use Qt 6.10.1 and disable USE_QTEXTCODEC
Note: The ffmpeg version used by Qt's official binary and our CI
doesn't match, but that's okay because their micro version bumps
are ABI compatible.
2025-12-06 11:52:39 +08:00
12 changed files with 366 additions and 135 deletions

View File

@@ -8,7 +8,7 @@ jobs:
strategy: strategy:
matrix: matrix:
include: include:
- qt_ver: '6.9.3' - qt_ver: '6.10.1'
vs: '2022' vs: '2022'
aqt_arch: 'win64_msvc2022_64' aqt_arch: 'win64_msvc2022_64'
msvc_arch: 'x64' msvc_arch: 'x64'
@@ -22,7 +22,7 @@ jobs:
with: with:
arch: ${{ matrix.aqt_arch }} arch: ${{ matrix.aqt_arch }}
version: ${{ matrix.qt_ver }} version: ${{ matrix.qt_ver }}
modules: 'qtmultimedia qt5compat' modules: 'qtmultimedia'
- name: Build - name: Build
shell: cmd shell: cmd
run: | run: |
@@ -59,11 +59,11 @@ jobs:
cmake --build build_dependencies/taglib --config Release --target=install -j || goto :error cmake --build build_dependencies/taglib --config Release --target=install -j || goto :error
echo ::endgroup:: echo ::endgroup::
:: ------ app ------ :: ------ app ------
cmake -Bbuild . -DUSE_QTEXTCODEC=ON -DCMAKE_INSTALL_PREFIX='%PWD%\build\' || goto :error cmake -Bbuild . -DUSE_QTEXTCODEC=OFF -DCMAKE_INSTALL_PREFIX='%PWD%\build\' || goto :error
cmake --build build --config Release -j || goto :error cmake --build build --config Release -j || goto :error
cmake --build build --config Release --target=install || goto :error cmake --build build --config Release --target=install || goto :error
:: ------ pkg ------ :: ------ pkg ------
windeployqt --verbose=2 --no-quick-import --no-translations --no-opengl-sw --no-system-d3d-compiler --no-system-dxc-compiler --multimedia --core5compat --skip-plugin-types tls,networkinformation build\bin\pmusic.exe windeployqt --verbose=2 --no-quick-import --no-translations --no-opengl-sw --no-system-d3d-compiler --no-system-dxc-compiler --multimedia --skip-plugin-types tls,networkinformation build\bin\pmusic.exe
robocopy ./dependencies_bin/bin build/bin *.dll robocopy ./dependencies_bin/bin build/bin *.dll
if ErrorLevel 8 (exit /B 1) if ErrorLevel 8 (exit /B 1)
copy LICENSE build/bin/ copy LICENSE build/bin/

29
.gitignore vendored
View File

@@ -1,13 +1,16 @@
# Common build folder # Common build folder
[Bb]uild/ [Bb]uild/
build-*/ build-*/
# IDE folder # IDE folder
.vscode/ .vscode/
.idea/ .idea/
# User config file # Clangd cache
CMakeLists.txt.user* .cache/
# Why, macOS, why? # User config file
.DS_Store CMakeLists.txt.user*
# Why, macOS, why?
.DS_Store

View File

@@ -47,6 +47,7 @@ set (PMUSIC_CPP_FILES
lyricsmanager.cpp lyricsmanager.cpp
fftspectrum.cpp fftspectrum.cpp
playbackprogressindicator.cpp playbackprogressindicator.cpp
lyricswidget.cpp
) )
set (PMUSIC_HEADER_FILES set (PMUSIC_HEADER_FILES
@@ -59,6 +60,7 @@ set (PMUSIC_HEADER_FILES
fftspectrum.h fftspectrum.h
playbackprogressindicator.h playbackprogressindicator.h
taskbarmanager.h taskbarmanager.h
lyricswidget.h
) )
set (PMUSIC_UI_FILES set (PMUSIC_UI_FILES
@@ -113,6 +115,10 @@ if (USE_QTEXTCODEC)
target_link_libraries(${EXE_NAME} PRIVATE Qt6::Core5Compat) target_link_libraries(${EXE_NAME} PRIVATE Qt6::Core5Compat)
endif() endif()
target_compile_definitions(${EXE_NAME} PRIVATE
PMUSIC_VERSION_STRING="${CMAKE_PROJECT_VERSION}"
)
# Install settings # Install settings
if (WIN32) if (WIN32)
set_target_properties(${EXE_NAME} PROPERTIES set_target_properties(${EXE_NAME} PROPERTIES

View File

@@ -6,7 +6,7 @@
<message> <message>
<location filename="../lrcbar.cpp" line="88"/> <location filename="../lrcbar.cpp" line="88"/>
<source>(Interlude...)</source> <source>(Interlude...)</source>
<translation type="unfinished"></translation> <translation>(Zwischenspiel )</translation>
</message> </message>
</context> </context>
<context> <context>
@@ -14,92 +14,92 @@
<message> <message>
<location filename="../mainwindow.cpp" line="125"/> <location filename="../mainwindow.cpp" line="125"/>
<source>Mono</source> <source>Mono</source>
<translation type="unfinished"></translation> <translation>Mono</translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="127"/> <location filename="../mainwindow.cpp" line="127"/>
<source>Stereo</source> <source>Stereo</source>
<translation type="unfinished"></translation> <translation>Stereo</translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="129"/> <location filename="../mainwindow.cpp" line="129"/>
<source>%1 Channels</source> <source>%1 Channels</source>
<translation type="unfinished"></translation> <translation>%1 Kanäle</translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="298"/> <location filename="../mainwindow.cpp" line="298"/>
<source>Select songs to play</source> <source>Select songs to play</source>
<translation type="unfinished"></translation> <translation>Songs zum Spielen auswählen</translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="300"/> <location filename="../mainwindow.cpp" line="300"/>
<source>Audio Files</source> <source>Audio Files</source>
<translation type="unfinished"></translation> <translation>Audiodateien</translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="685"/> <location filename="../mainwindow.cpp" line="685"/>
<source>Select image as background skin</source> <source>Select image as background skin</source>
<translation type="unfinished"></translation> <translation>Bild als Hintergrundskin auswählen</translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="687"/> <location filename="../mainwindow.cpp" line="687"/>
<source>Image files (*.jpg *.jpeg *.png *.gif)</source> <source>Image files (*.jpg *.jpeg *.png *.gif)</source>
<translation type="unfinished"></translation> <translation>Bilddateien (*.jpg *.jpeg *.png *.gif)</translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="764"/> <location filename="../mainwindow.cpp" line="764"/>
<source>Based on the following free software libraries:</source> <source>Based on the following free software libraries:</source>
<translation type="unfinished"></translation> <translation>Basiert auf den folgenden freien Programmbibliotheken:</translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.ui" line="23"/> <location filename="../mainwindow.ui" line="23"/>
<location filename="../mainwindow.cpp" line="762"/> <location filename="../mainwindow.cpp" line="762"/>
<location filename="../lrcbar.cpp" line="89"/> <location filename="../lrcbar.cpp" line="89"/>
<source>Pineapple Music</source> <source>Pineapple Music</source>
<translation type="unfinished"></translation> <translation>Pineapple Music</translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.ui" line="327"/> <location filename="../mainwindow.ui" line="327"/>
<source>No song loaded...</source> <source>No song loaded...</source>
<translation type="unfinished"></translation> <translation>Kein Song geladen </translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.ui" line="339"/> <location filename="../mainwindow.ui" line="339"/>
<source>Drag and drop file to load</source> <source>Drag and drop file to load</source>
<translation type="unfinished"></translation> <translation>Datei zum Laden ziehen und ablegen</translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.ui" line="352"/> <location filename="../mainwindow.ui" line="352"/>
<source>Lrc</source> <source>Lrc</source>
<comment>Lyrics</comment> <comment>Lyrics</comment>
<translation type="unfinished"></translation> <translation>Lrc</translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.ui" line="711"/> <location filename="../mainwindow.ui" line="711"/>
<location filename="../mainwindow.ui" line="714"/> <location filename="../mainwindow.ui" line="714"/>
<location filename="../mainwindow.cpp" line="759"/> <location filename="../mainwindow.cpp" line="759"/>
<source>About</source> <source>About</source>
<translation type="unfinished"></translation> <translation>Über</translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.ui" line="728"/> <location filename="../mainwindow.ui" line="728"/>
<source>Open</source> <source>Open</source>
<translation type="unfinished"></translation> <translation>Öffnen</translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="135"/> <location filename="../mainwindow.cpp" line="135"/>
<source>Sample Rate: %1 Hz</source> <source>Sample Rate: %1 Hz</source>
<translation type="unfinished"></translation> <translation>Abtastrate: %1 Hz</translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="140"/> <location filename="../mainwindow.cpp" line="140"/>
<source>Bitrate: %1 Kbps</source> <source>Bitrate: %1 Kbps</source>
<translation type="unfinished"></translation> <translation>Bitrate: %1 kbps</translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="145"/> <location filename="../mainwindow.cpp" line="145"/>
<source>Channel Count: %1</source> <source>Channel Count: %1</source>
<translation type="unfinished"></translation> <translation>Kanalanzahl: %1</translation>
</message> </message>
</context> </context>
<context> <context>
@@ -107,12 +107,12 @@
<message> <message>
<location filename="../playbackprogressindicator.cpp" line="85"/> <location filename="../playbackprogressindicator.cpp" line="85"/>
<source>Time</source> <source>Time</source>
<translation type="unfinished"></translation> <translation>Zeit</translation>
</message> </message>
<message> <message>
<location filename="../playbackprogressindicator.cpp" line="85"/> <location filename="../playbackprogressindicator.cpp" line="85"/>
<source>Chapter Name</source> <source>Chapter Name</source>
<translation type="unfinished"></translation> <translation>Kapitelname</translation>
</message> </message>
</context> </context>
<context> <context>
@@ -120,7 +120,7 @@
<message> <message>
<location filename="../main.cpp" line="27"/> <location filename="../main.cpp" line="27"/>
<source>File list.</source> <source>File list.</source>
<translation type="unfinished"></translation> <translation>Dateiliste.</translation>
</message> </message>
</context> </context>
</TS> </TS>

View File

@@ -10,9 +10,9 @@
#include <QPainter> #include <QPainter>
#include <QWindow> #include <QWindow>
LrcBar::LrcBar(QWidget *parent) LrcBar::LrcBar(QWidget *parent, LyricsManager *mgr)
: QWidget(parent, Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool) : QWidget(parent, Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool)
, m_lrcMgr(new LyricsManager(this)) , m_lrcMgr(mgr)
{ {
m_font.setPointSize(30); m_font.setPointSize(30);
m_font.setStyleStrategy(QFont::PreferAntialias); m_font.setStyleStrategy(QFont::PreferAntialias);
@@ -32,9 +32,14 @@ LrcBar::LrcBar(QWidget *parent)
setAttribute(Qt::WA_TranslucentBackground); setAttribute(Qt::WA_TranslucentBackground);
setMouseTracking(true); setMouseTracking(true);
setGeometry(QRect(QPoint((qApp->primaryScreen()->geometry().width() - windowSize.width()) / 2, setGeometry(QRect(QPoint((qApp->primaryScreen()->availableGeometry().width() - windowSize.width()) / 2,
qApp->primaryScreen()->geometry().height() - windowSize.height() - 50), qApp->primaryScreen()->availableGeometry().bottom() - windowSize.height() - 5),
windowSize)); windowSize));
connect(m_lrcMgr, &LyricsManager::lyricsLoaded, this, [this](bool){
m_currentTimeMs = 0;
update();
});
} }
LrcBar::~LrcBar() 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) void LrcBar::playbackPositionChanged(int timestampMs, int totalTimeMs)
{ {
if (!isVisible()) return; if (!isVisible()) return;
m_currentTimeMs = timestampMs; m_currentTimeMs = timestampMs;
m_lrcMgr->updateCurrentTimeMs(timestampMs, totalTimeMs);
update(); update();
} }

View File

@@ -1,36 +1,35 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net> // SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
#pragma once #pragma once
#include <QLinearGradient> #include <QLinearGradient>
#include <QMediaPlayer> #include <QMediaPlayer>
#include <QWidget> #include <QWidget>
class LyricsManager; class LyricsManager;
class LrcBar : public QWidget class LrcBar : public QWidget
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit LrcBar(QWidget *parent); explicit LrcBar(QWidget *parent, LyricsManager *mgr);
~LrcBar(); ~LrcBar();
bool loadLyrics(QString filepath); void playbackPositionChanged(int timestampMs, int totalTimeMs);
void playbackPositionChanged(int timestampMs, int totalTimeMs);
protected:
protected: QSize sizeHint() const override;
QSize sizeHint() const override; void mouseMoveEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override; void paintEvent(QPaintEvent *) override;
void paintEvent(QPaintEvent *) override; void enterEvent(QEnterEvent *) override;
void enterEvent(QEnterEvent *) override; void leaveEvent(QEvent *) override;
void leaveEvent(QEvent *) override;
private:
private: int m_currentTimeMs = 0;
int m_currentTimeMs = 0; LyricsManager * m_lrcMgr;
LyricsManager * m_lrcMgr; QFont m_font;
QFont m_font; QLinearGradient m_baseLinearGradient;
QLinearGradient m_baseLinearGradient; QLinearGradient m_maskLinearGradient;
QLinearGradient m_maskLinearGradient; bool m_underMouse = false;
bool m_underMouse = false; };
};

View File

@@ -43,10 +43,14 @@ bool LyricsManager::loadLyrics(QString filepath)
if (!filepath.endsWith(".lrc", Qt::CaseInsensitive)) { if (!filepath.endsWith(".lrc", Qt::CaseInsensitive)) {
fileInfo.setFile(fileInfo.dir().filePath(fileInfo.completeBaseName() + ".lrc")); 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()); QFile file(fileInfo.absoluteFilePath());
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
emit lyricsLoaded(false);
return false; return false;
} }
QByteArray fileContent(file.readAll()); QByteArray fileContent(file.readAll());
@@ -126,9 +130,11 @@ bool LyricsManager::loadLyrics(QString filepath)
if (!m_lyricsMap.isEmpty()) { if (!m_lyricsMap.isEmpty()) {
m_timestampList = m_lyricsMap.keys(); m_timestampList = m_lyricsMap.keys();
std::sort(m_timestampList.begin(), m_timestampList.end()); std::sort(m_timestampList.begin(), m_timestampList.end());
emit lyricsLoaded(true);
return true; return true;
} }
emit lyricsLoaded(false);
return false; return false;
} }
@@ -151,6 +157,22 @@ void LyricsManager::updateCurrentTimeMs(int curTimeMs, int totalTimeMs)
iter--; iter--;
} }
m_currentLyricsTime = *iter; m_currentLyricsTime = *iter;
m_currentLineIndex = std::distance(m_timestampList.begin(), iter);
}
const QList<int>& LyricsManager::timestamps() const
{
return m_timestampList;
}
const QHash<int, QString>& LyricsManager::lyricsMap() const
{
return m_lyricsMap;
}
int LyricsManager::currentLineIndex() const
{
return m_currentLineIndex;
} }
QString LyricsManager::lyrics(int lineOffset) const QString LyricsManager::lyrics(int lineOffset) const
@@ -197,6 +219,7 @@ void LyricsManager::reset()
{ {
m_currentLyricsTime = 0; m_currentLyricsTime = 0;
m_nextLyricsTime = 0; m_nextLyricsTime = 0;
m_currentLineIndex = -1;
m_timeOffset = 0; m_timeOffset = 0;
m_lyricsMap.clear(); m_lyricsMap.clear();
m_timestampList.clear(); m_timestampList.clear();

View File

@@ -1,43 +1,52 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net> // SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
#pragma once #pragma once
#include <QHash> #include <QHash>
#include <QList> #include <QList>
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QObject> #include <QObject>
Q_DECLARE_LOGGING_CATEGORY(lcLyrics) Q_DECLARE_LOGGING_CATEGORY(lcLyrics)
Q_DECLARE_LOGGING_CATEGORY(lcLyricsParser) Q_DECLARE_LOGGING_CATEGORY(lcLyricsParser)
class LyricsManager : public QObject class LyricsManager : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit LyricsManager(QObject *parent); explicit LyricsManager(QObject *parent);
~LyricsManager(); ~LyricsManager();
bool loadLyrics(QString filepath); bool loadLyrics(QString filepath);
bool hasLyrics() const; bool hasLyrics() const;
void updateCurrentTimeMs(int curTimeMs, int totalTimeMs); void updateCurrentTimeMs(int curTimeMs, int totalTimeMs);
QString lyrics(int lineOffset = 0) const; QString lyrics(int lineOffset = 0) const;
double maskPercent(int curTimeMs); double maskPercent(int curTimeMs);
static int parseTimeToMilliseconds(const QString& timeString); const QList<int>& timestamps() const;
const QHash<int, QString>& lyricsMap() const;
protected: int currentLineIndex() const;
static int parseTimeToMilliseconds(const QString& timeString);
private:
void reset(); signals:
int currentLyricsTime() const; void lyricsLoaded(bool success);
int nextLyricsTime() const;
QHash<int, QString> m_lyricsMap; protected:
QList<int> m_timestampList;
int m_currentLyricsTime = 0;
int m_nextLyricsTime = 0; private:
int m_timeOffset = 0; void reset();
}; int currentLyricsTime() const;
int nextLyricsTime() const;
QHash<int, QString> m_lyricsMap;
QList<int> m_timestampList;
int m_currentLyricsTime = 0;
int m_nextLyricsTime = 0;
int m_currentLineIndex = -1;
int m_timeOffset = 0;
};

131
lyricswidget.cpp Normal file
View File

@@ -0,0 +1,131 @@
// SPDX-FileCopyrightText: 2026 Gary Wang <opensource@blumia.net>
//
// SPDX-License-Identifier: MIT
#include "lyricswidget.h"
#include "lyricsmanager.h"
#include <QListWidget>
#include <QVBoxLayout>
#include <QListWidgetItem>
#include <QListWidgetItem>
#include <QFont>
#include <QPropertyAnimation>
#include <QScrollBar>
#include <QEasingCurve>
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<int>& timestamps = m_lyricsManager->timestamps();
const QHash<int, QString>& 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;
}

37
lyricswidget.h Normal file
View File

@@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2026 Gary Wang <opensource@blumia.net>
//
// SPDX-License-Identifier: MIT
#pragma once
#include <QWidget>
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);
};

View File

@@ -9,6 +9,8 @@
#include "fftspectrum.h" #include "fftspectrum.h"
#include "lrcbar.h" #include "lrcbar.h"
#include "taskbarmanager.h" #include "taskbarmanager.h"
#include "lyricsmanager.h"
#include "lyricswidget.h"
// taglib // taglib
#ifndef NO_TAGLIB #ifndef NO_TAGLIB
@@ -48,11 +50,16 @@ MainWindow::MainWindow(QWidget *parent)
, m_mediaPlayer(new QMediaPlayer(this)) , m_mediaPlayer(new QMediaPlayer(this))
, m_audioOutput(new QAudioOutput(this)) , m_audioOutput(new QAudioOutput(this))
, m_fftSpectrum(new FFTSpectrum(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_playlistManager(new PlaylistManager(this))
, m_taskbarManager(new TaskBarManager(this)) , m_taskbarManager(new TaskBarManager(this))
{ {
ui->setupUi(this); ui->setupUi(this);
m_lyricsWidget->setLyricsManager(m_lyricsManager);
ui->pluginStackedWidget->addWidget(m_lyricsWidget);
m_playlistManager->setAutoLoadFilterSuffixes({ m_playlistManager->setAutoLoadFilterSuffixes({
"*.mp3", "*.wav", "*.aiff", "*.ape", "*.flac", "*.ogg", "*.oga", "*.mpga", "*.aac", "*.tta" "*.mp3", "*.wav", "*.aiff", "*.ape", "*.flac", "*.ogg", "*.oga", "*.mpga", "*.aac", "*.tta"
}); });
@@ -255,7 +262,7 @@ void MainWindow::dropEvent(QDropEvent *e)
} }
if (fileName.endsWith(".lrc")) { if (fileName.endsWith(".lrc")) {
m_lrcbar->loadLyrics(fileName); m_lyricsManager->loadLyrics(fileName);
return; return;
} }
@@ -313,7 +320,7 @@ void MainWindow::loadFile(const QUrl &url)
{ {
const QString filePath = url.toLocalFile(); const QString filePath = url.toLocalFile();
m_mediaPlayer->setSource(url); m_mediaPlayer->setSource(url);
m_lrcbar->loadLyrics(filePath); m_lyricsManager->loadLyrics(filePath);
QList<std::pair<qint64, QString>> chapters(PlaybackProgressIndicator::tryLoadChapters(filePath)); QList<std::pair<qint64, QString>> chapters(PlaybackProgressIndicator::tryLoadChapters(filePath));
ui->playbackProgressIndicator->setChapters(chapters); ui->playbackProgressIndicator->setChapters(chapters);
} }
@@ -519,7 +526,12 @@ void MainWindow::initConnections()
ui->playbackProgressIndicator->setPosition(pos); ui->playbackProgressIndicator->setPosition(pos);
m_taskbarManager->setProgressValue(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; static QString lastChapterName;
if (ui->playbackProgressIndicator->chapterModel()->rowCount() > 0) { if (ui->playbackProgressIndicator->chapterModel()->rowCount() > 0) {
@@ -718,6 +730,15 @@ void MainWindow::on_playlistView_activated(const QModelIndex &index)
void MainWindow::on_lrcBtn_clicked() 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()) { if (m_lrcbar->isVisible()) {
m_lrcbar->hide(); m_lrcbar->hide();
} else { } else {
@@ -764,7 +785,7 @@ void MainWindow::on_actionHelp_triggered()
infoBox.setWindowTitle(tr("About")); infoBox.setWindowTitle(tr("About"));
infoBox.setStandardButtons(QMessageBox::Ok); infoBox.setStandardButtons(QMessageBox::Ok);
infoBox.setText( infoBox.setText(
tr("Pineapple Music") % QString("%1 %2").arg(tr("Pineapple Music")).arg(PMUSIC_VERSION_STRING) %
"\n\n" % "\n\n" %
tr("Based on the following free software libraries:") % tr("Based on the following free software libraries:") %
"\n\n" % "\n\n" %
@@ -785,7 +806,7 @@ void MainWindow::on_actionHelp_triggered()
"\n" "\n"
"[Source Code](https://github.com/BLumia/pineapple-music)\n" "[Source Code](https://github.com/BLumia/pineapple-music)\n"
"\n" "\n"
"Copyright &copy; 2025 [BLumia](https://github.com/BLumia/)" "Copyright &copy; 2026 [BLumia](https://github.com/BLumia/)"
); );
infoBox.setTextFormat(Qt::MarkdownText); infoBox.setTextFormat(Qt::MarkdownText);
infoBox.exec(); infoBox.exec();

View File

@@ -21,6 +21,8 @@ QT_END_NAMESPACE
class FFTSpectrum; class FFTSpectrum;
class LrcBar; class LrcBar;
class LyricsManager;
class LyricsWidget;
class PlaylistManager; class PlaylistManager;
class TaskBarManager; class TaskBarManager;
class MainWindow : public QMainWindow class MainWindow : public QMainWindow
@@ -104,7 +106,9 @@ private:
QMediaPlayer *m_mediaPlayer; QMediaPlayer *m_mediaPlayer;
QAudioOutput *m_audioOutput; QAudioOutput *m_audioOutput;
FFTSpectrum* m_fftSpectrum; FFTSpectrum* m_fftSpectrum;
LyricsManager *m_lyricsManager;
LrcBar *m_lrcbar; LrcBar *m_lrcbar;
LyricsWidget *m_lyricsWidget;
QPropertyAnimation *m_fadeOutAnimation; QPropertyAnimation *m_fadeOutAnimation;
PlaylistManager *m_playlistManager; PlaylistManager *m_playlistManager;
TaskBarManager *m_taskbarManager; TaskBarManager *m_taskbarManager;