feat: basic lyrics support
This commit is contained in:
parent
f4374a0768
commit
2a92f4ea7f
|
@ -26,6 +26,8 @@ set (PMUSIC_CPP_FILES
|
||||||
seekableslider.cpp
|
seekableslider.cpp
|
||||||
playlistmanager.cpp
|
playlistmanager.cpp
|
||||||
singleapplicationmanager.cpp
|
singleapplicationmanager.cpp
|
||||||
|
lrcbar.cpp
|
||||||
|
lyricsmanager.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set (PMUSIC_HEADER_FILES
|
set (PMUSIC_HEADER_FILES
|
||||||
|
@ -33,6 +35,8 @@ set (PMUSIC_HEADER_FILES
|
||||||
seekableslider.h
|
seekableslider.h
|
||||||
playlistmanager.h
|
playlistmanager.h
|
||||||
singleapplicationmanager.h
|
singleapplicationmanager.h
|
||||||
|
lrcbar.h
|
||||||
|
lyricsmanager.h
|
||||||
)
|
)
|
||||||
|
|
||||||
set (PMUSIC_UI_FILES
|
set (PMUSIC_UI_FILES
|
||||||
|
|
|
@ -7,6 +7,7 @@ Since **I** just need a simple player which *just works* right now, so I did man
|
||||||
- File format support will be limited by the [FFmpeg version that Qt 6 uses](https://doc.qt.io/qt-6/qtmultimedia-attribution-ffmpeg.html).
|
- File format support will be limited by the [FFmpeg version that Qt 6 uses](https://doc.qt.io/qt-6/qtmultimedia-attribution-ffmpeg.html).
|
||||||
- ...which if you use Qt's official binary, only contains the LGPLv2.1+ part. (already good enough, tho)
|
- ...which if you use Qt's official binary, only contains the LGPLv2.1+ part. (already good enough, tho)
|
||||||
- No music library management support and there won't be one!
|
- No music library management support and there won't be one!
|
||||||
|
- It'll auto-load music files in the same folder of the file that you attempted to play, so organize your music files on a folder-basis.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
|
|
|
@ -1,71 +1,85 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!DOCTYPE TS>
|
<!DOCTYPE TS>
|
||||||
<TS version="2.1">
|
<TS version="2.1">
|
||||||
|
<context>
|
||||||
|
<name>LrcBar</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../lrcbar.cpp" line="88"/>
|
||||||
|
<source>(Interlude...)</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>MainWindow</name>
|
<name>MainWindow</name>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="71"/>
|
<location filename="../mainwindow.cpp" line="93"/>
|
||||||
<source>Mono</source>
|
<source>Mono</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="73"/>
|
<location filename="../mainwindow.cpp" line="95"/>
|
||||||
<source>Stereo</source>
|
<source>Stereo</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="75"/>
|
<location filename="../mainwindow.cpp" line="97"/>
|
||||||
<source>%1 Channels</source>
|
<source>%1 Channels</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="200"/>
|
<location filename="../mainwindow.cpp" line="241"/>
|
||||||
<source>Select songs to play</source>
|
<source>Select songs to play</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="202"/>
|
<location filename="../mainwindow.cpp" line="243"/>
|
||||||
<source>Audio Files</source>
|
<source>Audio Files</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="576"/>
|
||||||
|
<source>Select image as background skin</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="578"/>
|
||||||
|
<source>Image files (*.jpg *.jpeg *.png *.gif)</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.ui" line="23"/>
|
<location filename="../mainwindow.ui" line="23"/>
|
||||||
<source>Pineapple Player</source>
|
<location filename="../lrcbar.cpp" line="89"/>
|
||||||
<translation type="unfinished"></translation>
|
<source>Pineapple Music</source>
|
||||||
</message>
|
|
||||||
<message>
|
|
||||||
<location filename="../mainwindow.ui" line="231"/>
|
|
||||||
<source>^</source>
|
|
||||||
<translation type="unfinished"></translation>
|
|
||||||
</message>
|
|
||||||
<message>
|
|
||||||
<location filename="../mainwindow.ui" line="297"/>
|
|
||||||
<source>No song loaded...</source>
|
|
||||||
<translation type="unfinished"></translation>
|
|
||||||
</message>
|
|
||||||
<message>
|
|
||||||
<location filename="../mainwindow.ui" line="304"/>
|
|
||||||
<source>Drag and drop file to load</source>
|
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.ui" line="313"/>
|
<location filename="../mainwindow.ui" line="313"/>
|
||||||
<location filename="../mainwindow.ui" line="320"/>
|
<source>No song loaded...</source>
|
||||||
<source>0:00</source>
|
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="81"/>
|
<location filename="../mainwindow.ui" line="325"/>
|
||||||
|
<source>Drag and drop file to load</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="338"/>
|
||||||
|
<source>Lrc</source>
|
||||||
|
<comment>Lyrics</comment>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="103"/>
|
||||||
<source>Sample Rate: %1 Hz</source>
|
<source>Sample Rate: %1 Hz</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="86"/>
|
<location filename="../mainwindow.cpp" line="108"/>
|
||||||
<source>Bitrate: %1 Kbps</source>
|
<source>Bitrate: %1 Kbps</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="91"/>
|
<location filename="../mainwindow.cpp" line="113"/>
|
||||||
<source>Channel Count: %1</source>
|
<source>Channel Count: %1</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
|
|
|
@ -1,71 +1,85 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!DOCTYPE TS>
|
<!DOCTYPE TS>
|
||||||
<TS version="2.1" language="zh_CN">
|
<TS version="2.1" language="zh_CN">
|
||||||
|
<context>
|
||||||
|
<name>LrcBar</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../lrcbar.cpp" line="88"/>
|
||||||
|
<source>(Interlude...)</source>
|
||||||
|
<translation>(间奏…)</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>MainWindow</name>
|
<name>MainWindow</name>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="71"/>
|
<location filename="../mainwindow.cpp" line="93"/>
|
||||||
<source>Mono</source>
|
<source>Mono</source>
|
||||||
<translation>单声道</translation>
|
<translation>单声道</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="73"/>
|
<location filename="../mainwindow.cpp" line="95"/>
|
||||||
<source>Stereo</source>
|
<source>Stereo</source>
|
||||||
<translation>立体声</translation>
|
<translation>立体声</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="75"/>
|
<location filename="../mainwindow.cpp" line="97"/>
|
||||||
<source>%1 Channels</source>
|
<source>%1 Channels</source>
|
||||||
<translation>%1 声道</translation>
|
<translation>%1 声道</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="200"/>
|
<location filename="../mainwindow.cpp" line="241"/>
|
||||||
<source>Select songs to play</source>
|
<source>Select songs to play</source>
|
||||||
<translation>选择要播放的曲目</translation>
|
<translation>选择要播放的曲目</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="202"/>
|
<location filename="../mainwindow.cpp" line="243"/>
|
||||||
<source>Audio Files</source>
|
<source>Audio Files</source>
|
||||||
<translation>音频文件</translation>
|
<translation>音频文件</translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="576"/>
|
||||||
|
<source>Select image as background skin</source>
|
||||||
|
<translation>选择图片作为背景皮肤</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="578"/>
|
||||||
|
<source>Image files (*.jpg *.jpeg *.png *.gif)</source>
|
||||||
|
<translation>图片文件 (*.jpg *.jpeg *.png *.gif)</translation>
|
||||||
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.ui" line="23"/>
|
<location filename="../mainwindow.ui" line="23"/>
|
||||||
<source>Pineapple Player</source>
|
<location filename="../lrcbar.cpp" line="89"/>
|
||||||
<translation type="unfinished"></translation>
|
<source>Pineapple Music</source>
|
||||||
|
<translation>菠萝音乐</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.ui" line="231"/>
|
<location filename="../mainwindow.ui" line="313"/>
|
||||||
<source>^</source>
|
|
||||||
<translation type="unfinished"></translation>
|
|
||||||
</message>
|
|
||||||
<message>
|
|
||||||
<location filename="../mainwindow.ui" line="297"/>
|
|
||||||
<source>No song loaded...</source>
|
<source>No song loaded...</source>
|
||||||
<translation>未加载曲目...</translation>
|
<translation>未加载曲目...</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.ui" line="304"/>
|
<location filename="../mainwindow.ui" line="325"/>
|
||||||
<source>Drag and drop file to load</source>
|
<source>Drag and drop file to load</source>
|
||||||
<translation>拖放文件来播放</translation>
|
<translation>拖放文件来播放</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.ui" line="313"/>
|
<location filename="../mainwindow.ui" line="338"/>
|
||||||
<location filename="../mainwindow.ui" line="320"/>
|
<source>Lrc</source>
|
||||||
<source>0:00</source>
|
<comment>Lyrics</comment>
|
||||||
<translation type="unfinished"></translation>
|
<translation>歌词</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="81"/>
|
<location filename="../mainwindow.cpp" line="103"/>
|
||||||
<source>Sample Rate: %1 Hz</source>
|
<source>Sample Rate: %1 Hz</source>
|
||||||
<translation>采样率: %1 Hz</translation>
|
<translation>采样率: %1 Hz</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="86"/>
|
<location filename="../mainwindow.cpp" line="108"/>
|
||||||
<source>Bitrate: %1 Kbps</source>
|
<source>Bitrate: %1 Kbps</source>
|
||||||
<translation>比特率: %1 Kbps</translation>
|
<translation>比特率: %1 Kbps</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="91"/>
|
<location filename="../mainwindow.cpp" line="113"/>
|
||||||
<source>Channel Count: %1</source>
|
<source>Channel Count: %1</source>
|
||||||
<translation>声道数: %1</translation>
|
<translation>声道数: %1</translation>
|
||||||
</message>
|
</message>
|
||||||
|
@ -75,7 +89,7 @@
|
||||||
<message>
|
<message>
|
||||||
<location filename="../main.cpp" line="28"/>
|
<location filename="../main.cpp" line="28"/>
|
||||||
<source>File list.</source>
|
<source>File list.</source>
|
||||||
<translation>文件列表</translation>
|
<translation>文件列表。</translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
</TS>
|
</TS>
|
||||||
|
|
128
lrcbar.cpp
Normal file
128
lrcbar.cpp
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "lrcbar.h"
|
||||||
|
|
||||||
|
#include "lyricsmanager.h"
|
||||||
|
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QWindow>
|
||||||
|
|
||||||
|
LrcBar::LrcBar(QWidget *parent)
|
||||||
|
: QWidget(parent, Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool)
|
||||||
|
, m_lrcMgr(new LyricsManager(this))
|
||||||
|
{
|
||||||
|
m_font.setPointSize(30);
|
||||||
|
m_font.setStyleStrategy(QFont::PreferAntialias);
|
||||||
|
|
||||||
|
QSize windowSize(sizeHint());
|
||||||
|
|
||||||
|
QFontMetrics fm(m_font);
|
||||||
|
m_baseLinearGradient.setColorAt(0, QColor(20, 100, 200));
|
||||||
|
m_baseLinearGradient.setColorAt(1, QColor(0, 80, 255));
|
||||||
|
m_baseLinearGradient.setStart(0, (windowSize.height() - fm.height()) / 2);
|
||||||
|
m_baseLinearGradient.setFinalStop(0, (windowSize.height() + fm.height()) / 2);
|
||||||
|
m_maskLinearGradient.setColorAt(0, QColor(255, 128, 0));
|
||||||
|
m_maskLinearGradient.setColorAt(0.5, QColor(255, 255, 0));
|
||||||
|
m_maskLinearGradient.setColorAt(1, QColor(255, 128, 0));
|
||||||
|
m_maskLinearGradient.setStart(0, (windowSize.height() - fm.height()) / 2);
|
||||||
|
m_maskLinearGradient.setFinalStop(0, (windowSize.height() + fm.height()) / 2);
|
||||||
|
|
||||||
|
setAttribute(Qt::WA_TranslucentBackground);
|
||||||
|
setMouseTracking(true);
|
||||||
|
setGeometry(QRect(QPoint((qApp->primaryScreen()->geometry().width() - windowSize.width()) / 2,
|
||||||
|
qApp->primaryScreen()->geometry().height() - windowSize.height() - 50),
|
||||||
|
windowSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
QSize LrcBar::sizeHint() const
|
||||||
|
{
|
||||||
|
return QSize(1000, 88);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LrcBar::mouseMoveEvent(QMouseEvent *event)
|
||||||
|
{
|
||||||
|
if (event->buttons() & Qt::LeftButton) {
|
||||||
|
window()->windowHandle()->startSystemMove();
|
||||||
|
event->accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
return QWidget::mouseMoveEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LrcBar::paintEvent(QPaintEvent *)
|
||||||
|
{
|
||||||
|
QPainter painter(this);
|
||||||
|
if (underMouse()) {
|
||||||
|
painter.setBrush(QBrush(QColor(255, 255, 255, 120)));
|
||||||
|
painter.setPen(Qt::NoPen);
|
||||||
|
painter.drawRect(0, 0, width(), height());
|
||||||
|
}
|
||||||
|
painter.setFont(m_font);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
|
||||||
|
QString curLrc(m_lrcMgr->lyrics().trimmed());
|
||||||
|
if (curLrc.isEmpty()) {
|
||||||
|
curLrc = m_lrcMgr->hasLyrics() ? tr("(Interlude...)")
|
||||||
|
: QCoreApplication::translate("MainWindow", "Pineapple Music", nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
QFontMetrics fm(m_font);
|
||||||
|
int lrcWidth = fm.horizontalAdvance(curLrc);
|
||||||
|
int maskWidth = lrcWidth * m_lrcMgr->maskPercent(m_currentTimeMs);
|
||||||
|
int startOffsetX = 0;
|
||||||
|
|
||||||
|
if (fm.horizontalAdvance(curLrc) < width()) {
|
||||||
|
startOffsetX = (width() - lrcWidth) / 2;
|
||||||
|
} else {
|
||||||
|
if (maskWidth < width() / 2) {
|
||||||
|
startOffsetX = 0;
|
||||||
|
} else if (lrcWidth - maskWidth < width() / 2) {
|
||||||
|
startOffsetX = width() - lrcWidth;
|
||||||
|
} else {
|
||||||
|
startOffsetX = 0 - (maskWidth - width() / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// shadow
|
||||||
|
painter.setPen(QColor(0, 0, 0, 80));
|
||||||
|
painter.drawText(startOffsetX + 2, 2, lrcWidth, this->height(), Qt::AlignVCenter, curLrc);
|
||||||
|
// text itself
|
||||||
|
painter.setPen(QPen(m_baseLinearGradient, 0));
|
||||||
|
painter.drawText(startOffsetX, 0, lrcWidth, this->height(), Qt::AlignVCenter, curLrc);
|
||||||
|
// mask
|
||||||
|
painter.setPen(QPen(m_maskLinearGradient, 0));
|
||||||
|
painter.drawText(startOffsetX, 0, maskWidth, this->height(), Qt::AlignVCenter, curLrc);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LrcBar::enterEvent(QEnterEvent *)
|
||||||
|
{
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LrcBar::leaveEvent(QEvent *)
|
||||||
|
{
|
||||||
|
update();
|
||||||
|
}
|
36
lrcbar.h
Normal file
36
lrcbar.h
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QLinearGradient>
|
||||||
|
#include <QMediaPlayer>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
143
lyricsmanager.cpp
Normal file
143
lyricsmanager.cpp
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "lyricsmanager.h"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
|
LyricsManager::LyricsManager(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
LyricsManager::~LyricsManager()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LyricsManager::loadLyrics(QString filepath)
|
||||||
|
{
|
||||||
|
// reset state
|
||||||
|
reset();
|
||||||
|
|
||||||
|
// check and load file
|
||||||
|
QFileInfo fileInfo(filepath);
|
||||||
|
if (!filepath.endsWith(".lrc", Qt::CaseInsensitive)) {
|
||||||
|
fileInfo.setFile(fileInfo.dir().filePath(fileInfo.completeBaseName() + ".lrc"));
|
||||||
|
}
|
||||||
|
if (!fileInfo.exists()) return false;
|
||||||
|
|
||||||
|
QFile file(fileInfo.absoluteFilePath());
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QTextStream stream(&file);
|
||||||
|
QStringList lines = QString(stream.readAll()).split('\n');
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
// parse lyrics timestamp
|
||||||
|
QRegularExpression tagRegex(R"regex(\[(ti|ar|al|au|length|by|offset|tool|re|ve|#):\s?([^\]]*)\]$)regex");
|
||||||
|
QRegularExpression lrcRegex(R"regex(\[(\d{2,3}:\d{2}\.\d{2})\](.*))regex");
|
||||||
|
bool tagSectionPassed = false;
|
||||||
|
|
||||||
|
for (QString line : std::as_const(lines)) {
|
||||||
|
line = line.trimmed();
|
||||||
|
if (line.isEmpty()) continue;
|
||||||
|
|
||||||
|
if (!tagSectionPassed) {
|
||||||
|
QRegularExpressionMatch tagMatch = tagRegex.match(line);
|
||||||
|
if (tagMatch.hasMatch()) {
|
||||||
|
QString tag(tagMatch.captured(1));
|
||||||
|
if (tag == QLatin1String("offset")) {
|
||||||
|
// The value is prefixed with either + or -, with + causing lyrics to appear sooner
|
||||||
|
m_timeOffset = -tagMatch.captured(2).toInt();
|
||||||
|
qDebug() << m_timeOffset;
|
||||||
|
}
|
||||||
|
qDebug() << "[tag]" << tagMatch.captured(1) << tagMatch.captured(2);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QRegularExpressionMatch match = lrcRegex.match(line);
|
||||||
|
if (match.hasMatch()) {
|
||||||
|
tagSectionPassed = true;
|
||||||
|
QTime timestamp(QTime::fromString(match.captured(1), "m:s.zz"));
|
||||||
|
m_lyricsMap.insert(timestamp.msecsSinceStartOfDay(), match.captured(2));
|
||||||
|
qDebug() << "[lrc]" << match.captured(1) << match.captured(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!m_lyricsMap.isEmpty()) {
|
||||||
|
m_timestampList = m_lyricsMap.keys();
|
||||||
|
std::sort(m_timestampList.begin(), m_timestampList.end());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LyricsManager::hasLyrics() const
|
||||||
|
{
|
||||||
|
return !m_lyricsMap.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LyricsManager::updateCurrentTimeMs(int curTimeMs, int totalTimeMs)
|
||||||
|
{
|
||||||
|
if (!hasLyrics()) return;
|
||||||
|
|
||||||
|
// TODO: we don't need to find from the top everytime the time is updated
|
||||||
|
auto iter = std::find_if(m_timestampList.begin(), m_timestampList.end() + 1, [&curTimeMs, this](int timestamp) -> bool {
|
||||||
|
return (timestamp + m_timeOffset) > curTimeMs;
|
||||||
|
});
|
||||||
|
|
||||||
|
m_nextLyricsTime = (iter == m_timestampList.end() + 1) ? totalTimeMs : *iter;
|
||||||
|
if (iter != m_timestampList.begin() && iter != (m_timestampList.end() + 1)) {
|
||||||
|
iter--;
|
||||||
|
}
|
||||||
|
m_currentLyricsTime = *iter;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString LyricsManager::lyrics(int lineOffset) const
|
||||||
|
{
|
||||||
|
if (!hasLyrics()) return QString();
|
||||||
|
|
||||||
|
int index = m_timestampList.indexOf(m_currentLyricsTime) + lineOffset;
|
||||||
|
if (index >= 0 && index < m_timestampList.count()) {
|
||||||
|
int timestamp = m_timestampList.at(index);
|
||||||
|
return m_lyricsMap.value(timestamp);
|
||||||
|
} else {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double LyricsManager::maskPercent(int curTimeMs)
|
||||||
|
{
|
||||||
|
if (!hasLyrics()) return 0;
|
||||||
|
if (curTimeMs <= currentLyricsTime()) return 0;
|
||||||
|
if (curTimeMs >= nextLyricsTime()) return 1;
|
||||||
|
if (m_nextLyricsTime == currentLyricsTime()) return 1;
|
||||||
|
|
||||||
|
return (double)(curTimeMs - currentLyricsTime()) / (m_nextLyricsTime - m_currentLyricsTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LyricsManager::reset()
|
||||||
|
{
|
||||||
|
m_currentLyricsTime = 0;
|
||||||
|
m_nextLyricsTime = 0;
|
||||||
|
m_timeOffset = 0;
|
||||||
|
m_lyricsMap.clear();
|
||||||
|
m_timestampList.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
int LyricsManager::currentLyricsTime() const
|
||||||
|
{
|
||||||
|
return m_currentLyricsTime + m_timeOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
int LyricsManager::nextLyricsTime() const
|
||||||
|
{
|
||||||
|
return m_nextLyricsTime + m_timeOffset;
|
||||||
|
}
|
37
lyricsmanager.h
Normal file
37
lyricsmanager.h
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QList>
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
|
|
||||||
|
private:
|
||||||
|
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_timeOffset = 0;
|
||||||
|
};
|
|
@ -6,6 +6,7 @@
|
||||||
#include "./ui_mainwindow.h"
|
#include "./ui_mainwindow.h"
|
||||||
|
|
||||||
#include "playlistmanager.h"
|
#include "playlistmanager.h"
|
||||||
|
#include "lrcbar.h"
|
||||||
|
|
||||||
// taglib
|
// taglib
|
||||||
#ifndef NO_TAGLIB
|
#ifndef NO_TAGLIB
|
||||||
|
@ -38,6 +39,7 @@ MainWindow::MainWindow(QWidget *parent)
|
||||||
, m_mediaDevices(new QMediaDevices(this))
|
, m_mediaDevices(new QMediaDevices(this))
|
||||||
, m_mediaPlayer(new QMediaPlayer(this))
|
, m_mediaPlayer(new QMediaPlayer(this))
|
||||||
, m_audioOutput(new QAudioOutput(this))
|
, m_audioOutput(new QAudioOutput(this))
|
||||||
|
, m_lrcbar(new LrcBar(nullptr))
|
||||||
, m_playlistManager(new PlaylistManager(this))
|
, m_playlistManager(new PlaylistManager(this))
|
||||||
{
|
{
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
|
@ -48,8 +50,8 @@ MainWindow::MainWindow(QWidget *parent)
|
||||||
m_mediaPlayer->setLoops(QMediaPlayer::Infinite);
|
m_mediaPlayer->setLoops(QMediaPlayer::Infinite);
|
||||||
ui->playlistView->setModel(m_playlistManager->model());
|
ui->playlistView->setModel(m_playlistManager->model());
|
||||||
|
|
||||||
this->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint | Qt::WindowMinimizeButtonHint);
|
setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint | Qt::WindowMinimizeButtonHint);
|
||||||
this->setAttribute(Qt::WA_TranslucentBackground, true);
|
setAttribute(Qt::WA_TranslucentBackground, true);
|
||||||
|
|
||||||
loadSkinData();
|
loadSkinData();
|
||||||
initConnections();
|
initConnections();
|
||||||
|
@ -60,6 +62,7 @@ MainWindow::MainWindow(QWidget *parent)
|
||||||
|
|
||||||
MainWindow::~MainWindow()
|
MainWindow::~MainWindow()
|
||||||
{
|
{
|
||||||
|
delete m_lrcbar;
|
||||||
delete ui;
|
delete ui;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,6 +218,11 @@ void MainWindow::dropEvent(QDropEvent *e)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fileName.endsWith(".lrc")) {
|
||||||
|
m_lrcbar->loadLyrics(fileName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const QModelIndex & modelIndex = m_playlistManager->loadPlaylist(urls);
|
const QModelIndex & modelIndex = m_playlistManager->loadPlaylist(urls);
|
||||||
if (modelIndex.isValid()) {
|
if (modelIndex.isValid()) {
|
||||||
loadByModelIndex(modelIndex);
|
loadByModelIndex(modelIndex);
|
||||||
|
@ -238,11 +246,13 @@ void MainWindow::loadFile()
|
||||||
|
|
||||||
m_playlistManager->loadPlaylist(urlList);
|
m_playlistManager->loadPlaylist(urlList);
|
||||||
m_mediaPlayer->setSource(urlList.first());
|
m_mediaPlayer->setSource(urlList.first());
|
||||||
|
m_lrcbar->loadLyrics(urlList.first().toLocalFile());
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::loadByModelIndex(const QModelIndex & index)
|
void MainWindow::loadByModelIndex(const QModelIndex & index)
|
||||||
{
|
{
|
||||||
m_mediaPlayer->setSource(m_playlistManager->urlByIndex(index));
|
m_mediaPlayer->setSource(m_playlistManager->urlByIndex(index));
|
||||||
|
m_lrcbar->loadLyrics(m_playlistManager->localFileByIndex(index));
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::play()
|
void MainWindow::play()
|
||||||
|
@ -441,6 +451,7 @@ void MainWindow::initConnections()
|
||||||
if (m_mediaPlayer->duration() != 0) {
|
if (m_mediaPlayer->duration() != 0) {
|
||||||
ui->playbackSlider->setSliderPosition(ui->playbackSlider->maximum() * pos / m_mediaPlayer->duration());
|
ui->playbackSlider->setSliderPosition(ui->playbackSlider->maximum() * pos / m_mediaPlayer->duration());
|
||||||
}
|
}
|
||||||
|
m_lrcbar->playbackPositionChanged(pos, m_mediaPlayer->duration());
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(m_audioOutput, &QAudioOutput::mutedChanged, this, [=](bool muted) {
|
connect(m_audioOutput, &QAudioOutput::mutedChanged, this, [=](bool muted) {
|
||||||
|
@ -578,3 +589,13 @@ void MainWindow::on_playlistView_activated(const QModelIndex &index)
|
||||||
loadByModelIndex(index);
|
loadByModelIndex(index);
|
||||||
play();
|
play();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_lrcBtn_clicked()
|
||||||
|
{
|
||||||
|
if (m_lrcbar->isVisible()) {
|
||||||
|
m_lrcbar->hide();
|
||||||
|
} else {
|
||||||
|
m_lrcbar->show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ class QAudioOutput;
|
||||||
class QPropertyAnimation;
|
class QPropertyAnimation;
|
||||||
QT_END_NAMESPACE
|
QT_END_NAMESPACE
|
||||||
|
|
||||||
|
class LrcBar;
|
||||||
class PlaylistManager;
|
class PlaylistManager;
|
||||||
class MainWindow : public QMainWindow
|
class MainWindow : public QMainWindow
|
||||||
{
|
{
|
||||||
|
@ -73,6 +74,7 @@ private slots:
|
||||||
void on_setSkinBtn_clicked();
|
void on_setSkinBtn_clicked();
|
||||||
void on_playListBtn_clicked();
|
void on_playListBtn_clicked();
|
||||||
void on_playlistView_activated(const QModelIndex &index);
|
void on_playlistView_activated(const QModelIndex &index);
|
||||||
|
void on_lrcBtn_clicked();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void playbackModeChanged(enum PlaybackMode mode);
|
void playbackModeChanged(enum PlaybackMode mode);
|
||||||
|
@ -89,6 +91,7 @@ private:
|
||||||
QMediaDevices *m_mediaDevices;
|
QMediaDevices *m_mediaDevices;
|
||||||
QMediaPlayer *m_mediaPlayer;
|
QMediaPlayer *m_mediaPlayer;
|
||||||
QAudioOutput *m_audioOutput;
|
QAudioOutput *m_audioOutput;
|
||||||
|
LrcBar *m_lrcbar;
|
||||||
QPropertyAnimation *m_fadeOutAnimation;
|
QPropertyAnimation *m_fadeOutAnimation;
|
||||||
PlaylistManager *m_playlistManager;
|
PlaylistManager *m_playlistManager;
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
<string>Pineapple Player</string>
|
<string>Pineapple Music</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowIcon">
|
<property name="windowIcon">
|
||||||
<iconset resource="resources.qrc">
|
<iconset resource="resources.qrc">
|
||||||
|
@ -314,6 +314,11 @@ QListView {
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="propLabel">
|
<widget class="QLabel" name="propLabel">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -321,19 +326,40 @@ QListView {
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="lrcBtn">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string comment="Lyrics">Lrc</string>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="playbackTimeLayout">
|
<layout class="QHBoxLayout" name="playbackTimeLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="nowTimeLabel">
|
<widget class="QLabel" name="nowTimeLabel">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>0:00</string>
|
<string notr="true">0:00</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="totalTimeLabel">
|
<widget class="QLabel" name="totalTimeLabel">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>0:00</string>
|
<string notr="true">0:00</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="alignment">
|
<property name="alignment">
|
||||||
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
|
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user