Reformat
This commit is contained in:
parent
10769eef09
commit
58e8345542
20 changed files with 2063 additions and 1901 deletions
|
|
@ -55,3 +55,13 @@ target_include_directories(${PROJECT_NAME} PRIVATE
|
||||||
)
|
)
|
||||||
|
|
||||||
# Note: acestep.cpp binaries (ace-qwen3, dit-vae) and models should be provided at runtime
|
# Note: acestep.cpp binaries (ace-qwen3, dit-vae) and models should be provided at runtime
|
||||||
|
|
||||||
|
# Install targets
|
||||||
|
install(TARGETS ${PROJECT_NAME} DESTINATION bin)
|
||||||
|
|
||||||
|
# Install .desktop file
|
||||||
|
install(FILES aceradio.desktop DESTINATION share/applications)
|
||||||
|
|
||||||
|
# Install icon files
|
||||||
|
install(FILES res/xyz.uvos.aceradio.png DESTINATION share/icons/hicolor/256x256/apps RENAME xyz.uvos.aceradio.png)
|
||||||
|
install(FILES res/xyz.uvos.aceradio.svg DESTINATION share/icons/hicolor/scalable/apps RENAME xyz.uvos.aceradio.svg)
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,13 @@ void AceStepWorker::cancelGeneration()
|
||||||
bool AceStepWorker::songGenerateing(SongItem* song)
|
bool AceStepWorker::songGenerateing(SongItem* song)
|
||||||
{
|
{
|
||||||
workerMutex.lock();
|
workerMutex.lock();
|
||||||
if(!currentWorker) {
|
if(!currentWorker)
|
||||||
|
{
|
||||||
workerMutex.unlock();
|
workerMutex.unlock();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
SongItem workerSong = currentWorker->getSong();
|
SongItem workerSong = currentWorker->getSong();
|
||||||
workerMutex.unlock();
|
workerMutex.unlock();
|
||||||
if(song)
|
if(song)
|
||||||
|
|
@ -68,7 +70,8 @@ void AceStepWorker::Worker::run()
|
||||||
// Parse and modify the template
|
// Parse and modify the template
|
||||||
QJsonParseError parseError;
|
QJsonParseError parseError;
|
||||||
QJsonDocument templateDoc = QJsonDocument::fromJson(jsonTemplate.toUtf8(), &parseError);
|
QJsonDocument templateDoc = QJsonDocument::fromJson(jsonTemplate.toUtf8(), &parseError);
|
||||||
if (!templateDoc.isObject()) {
|
if (!templateDoc.isObject())
|
||||||
|
{
|
||||||
emit parent->generationError("Invalid JSON template: " + QString(parseError.errorString()));
|
emit parent->generationError("Invalid JSON template: " + QString(parseError.errorString()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -76,21 +79,26 @@ void AceStepWorker::Worker::run()
|
||||||
QJsonObject requestObj = templateDoc.object();
|
QJsonObject requestObj = templateDoc.object();
|
||||||
requestObj["caption"] = song.caption;
|
requestObj["caption"] = song.caption;
|
||||||
|
|
||||||
if (!song.lyrics.isEmpty()) {
|
if (!song.lyrics.isEmpty())
|
||||||
|
{
|
||||||
requestObj["lyrics"] = song.lyrics;
|
requestObj["lyrics"] = song.lyrics;
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
// Remove lyrics field if empty to let the LLM generate them
|
// Remove lyrics field if empty to let the LLM generate them
|
||||||
requestObj.remove("lyrics");
|
requestObj.remove("lyrics");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply vocal language override if set
|
// Apply vocal language override if set
|
||||||
if (!song.vocalLanguage.isEmpty()) {
|
if (!song.vocalLanguage.isEmpty())
|
||||||
|
{
|
||||||
requestObj["vocal_language"] = song.vocalLanguage;
|
requestObj["vocal_language"] = song.vocalLanguage;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the request file
|
// Write the request file
|
||||||
QFile requestFileHandle(requestFile);
|
QFile requestFileHandle(requestFile);
|
||||||
if (!requestFileHandle.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
if (!requestFileHandle.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||||
|
{
|
||||||
emit parent->generationError("Failed to create request file: " + requestFileHandle.errorString());
|
emit parent->generationError("Failed to create request file: " + requestFileHandle.errorString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -106,12 +114,14 @@ void AceStepWorker::Worker::run()
|
||||||
QFileInfo qwen3Info(qwen3Binary);
|
QFileInfo qwen3Info(qwen3Binary);
|
||||||
QFileInfo ditVaeInfo(ditVaeBinary);
|
QFileInfo ditVaeInfo(ditVaeBinary);
|
||||||
|
|
||||||
if (!qwen3Info.exists() || !qwen3Info.isExecutable()) {
|
if (!qwen3Info.exists() || !qwen3Info.isExecutable())
|
||||||
|
{
|
||||||
emit parent->generationError("ace-qwen3 binary not found at: " + qwen3Binary);
|
emit parent->generationError("ace-qwen3 binary not found at: " + qwen3Binary);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ditVaeInfo.exists() || !ditVaeInfo.isExecutable()) {
|
if (!ditVaeInfo.exists() || !ditVaeInfo.isExecutable())
|
||||||
|
{
|
||||||
emit parent->generationError("dit-vae binary not found at: " + ditVaeBinary);
|
emit parent->generationError("dit-vae binary not found at: " + ditVaeBinary);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -122,22 +132,26 @@ void AceStepWorker::Worker::run()
|
||||||
QString ditModel = this->ditModelPath;
|
QString ditModel = this->ditModelPath;
|
||||||
QString vaeModel = this->vaeModelPath;
|
QString vaeModel = this->vaeModelPath;
|
||||||
|
|
||||||
if (!QFileInfo::exists(qwen3Model)) {
|
if (!QFileInfo::exists(qwen3Model))
|
||||||
|
{
|
||||||
emit parent->generationError("Qwen3 model not found: " + qwen3Model);
|
emit parent->generationError("Qwen3 model not found: " + qwen3Model);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!QFileInfo::exists(textEncoderModel)) {
|
if (!QFileInfo::exists(textEncoderModel))
|
||||||
|
{
|
||||||
emit parent->generationError("Text encoder model not found: " + textEncoderModel);
|
emit parent->generationError("Text encoder model not found: " + textEncoderModel);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!QFileInfo::exists(ditModel)) {
|
if (!QFileInfo::exists(ditModel))
|
||||||
|
{
|
||||||
emit parent->generationError("DiT model not found: " + ditModel);
|
emit parent->generationError("DiT model not found: " + ditModel);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!QFileInfo::exists(vaeModel)) {
|
if (!QFileInfo::exists(vaeModel))
|
||||||
|
{
|
||||||
emit parent->generationError("VAE model not found: " + vaeModel);
|
emit parent->generationError("VAE model not found: " + vaeModel);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -151,12 +165,14 @@ void AceStepWorker::Worker::run()
|
||||||
emit parent->progressUpdate(20);
|
emit parent->progressUpdate(20);
|
||||||
|
|
||||||
qwen3Process.start(qwen3Binary, qwen3Args);
|
qwen3Process.start(qwen3Binary, qwen3Args);
|
||||||
if (!qwen3Process.waitForStarted()) {
|
if (!qwen3Process.waitForStarted())
|
||||||
|
{
|
||||||
emit parent->generationError("Failed to start ace-qwen3: " + qwen3Process.errorString());
|
emit parent->generationError("Failed to start ace-qwen3: " + qwen3Process.errorString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!qwen3Process.waitForFinished(60000)) { // 60 second timeout
|
if (!qwen3Process.waitForFinished(60000)) // 60 second timeout
|
||||||
|
{
|
||||||
qwen3Process.terminate();
|
qwen3Process.terminate();
|
||||||
qwen3Process.waitForFinished(5000);
|
qwen3Process.waitForFinished(5000);
|
||||||
emit parent->generationError("ace-qwen3 timed out after 60 seconds");
|
emit parent->generationError("ace-qwen3 timed out after 60 seconds");
|
||||||
|
|
@ -164,29 +180,34 @@ void AceStepWorker::Worker::run()
|
||||||
}
|
}
|
||||||
|
|
||||||
int exitCode = qwen3Process.exitCode();
|
int exitCode = qwen3Process.exitCode();
|
||||||
if (exitCode != 0) {
|
if (exitCode != 0)
|
||||||
|
{
|
||||||
QString errorOutput = qwen3Process.readAllStandardError();
|
QString errorOutput = qwen3Process.readAllStandardError();
|
||||||
emit parent->generationError("ace-qwen3 exited with code " + QString::number(exitCode) + ": " + errorOutput);
|
emit parent->generationError("ace-qwen3 exited with code " + QString::number(exitCode) + ": " + errorOutput);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString requestLmOutputFile = tempDir + "/request_" + QString::number(uid) + "0.json";
|
QString requestLmOutputFile = tempDir + "/request_" + QString::number(uid) + "0.json";
|
||||||
if (!QFileInfo::exists(requestLmOutputFile)) {
|
if (!QFileInfo::exists(requestLmOutputFile))
|
||||||
|
{
|
||||||
emit parent->generationError("ace-qwen3 failed to create enhaced request file "+requestLmOutputFile);
|
emit parent->generationError("ace-qwen3 failed to create enhaced request file "+requestLmOutputFile);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load lyrics from the enhanced request file
|
// Load lyrics from the enhanced request file
|
||||||
QFile lmOutputFile(requestLmOutputFile);
|
QFile lmOutputFile(requestLmOutputFile);
|
||||||
if (lmOutputFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
if (lmOutputFile.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||||
|
{
|
||||||
QJsonParseError parseError;
|
QJsonParseError parseError;
|
||||||
song.json = lmOutputFile.readAll();
|
song.json = lmOutputFile.readAll();
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(song.json.toUtf8(), &parseError);
|
QJsonDocument doc = QJsonDocument::fromJson(song.json.toUtf8(), &parseError);
|
||||||
lmOutputFile.close();
|
lmOutputFile.close();
|
||||||
|
|
||||||
if (doc.isObject() && !parseError.error) {
|
if (doc.isObject() && !parseError.error)
|
||||||
|
{
|
||||||
QJsonObject obj = doc.object();
|
QJsonObject obj = doc.object();
|
||||||
if (obj.contains("lyrics") && obj["lyrics"].isString()) {
|
if (obj.contains("lyrics") && obj["lyrics"].isString())
|
||||||
|
{
|
||||||
song.lyrics = obj["lyrics"].toString();
|
song.lyrics = obj["lyrics"].toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -205,12 +226,14 @@ void AceStepWorker::Worker::run()
|
||||||
emit parent->progressUpdate(60);
|
emit parent->progressUpdate(60);
|
||||||
|
|
||||||
ditVaeProcess.start(ditVaeBinary, ditVaeArgs);
|
ditVaeProcess.start(ditVaeBinary, ditVaeArgs);
|
||||||
if (!ditVaeProcess.waitForStarted()) {
|
if (!ditVaeProcess.waitForStarted())
|
||||||
|
{
|
||||||
emit parent->generationError("Failed to start dit-vae: " + ditVaeProcess.errorString());
|
emit parent->generationError("Failed to start dit-vae: " + ditVaeProcess.errorString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ditVaeProcess.waitForFinished(120000)) { // 2 minute timeout
|
if (!ditVaeProcess.waitForFinished(120000)) // 2 minute timeout
|
||||||
|
{
|
||||||
ditVaeProcess.terminate();
|
ditVaeProcess.terminate();
|
||||||
ditVaeProcess.waitForFinished(5000);
|
ditVaeProcess.waitForFinished(5000);
|
||||||
emit parent->generationError("dit-vae timed out after 2 minutes");
|
emit parent->generationError("dit-vae timed out after 2 minutes");
|
||||||
|
|
@ -218,7 +241,8 @@ void AceStepWorker::Worker::run()
|
||||||
}
|
}
|
||||||
|
|
||||||
exitCode = ditVaeProcess.exitCode();
|
exitCode = ditVaeProcess.exitCode();
|
||||||
if (exitCode != 0) {
|
if (exitCode != 0)
|
||||||
|
{
|
||||||
QString errorOutput = ditVaeProcess.readAllStandardError();
|
QString errorOutput = ditVaeProcess.readAllStandardError();
|
||||||
emit parent->generationError("dit-vae exited with code " + QString::number(exitCode) + ": " + errorOutput);
|
emit parent->generationError("dit-vae exited with code " + QString::number(exitCode) + ": " + errorOutput);
|
||||||
return;
|
return;
|
||||||
|
|
@ -228,7 +252,8 @@ void AceStepWorker::Worker::run()
|
||||||
|
|
||||||
// Find the generated WAV file
|
// Find the generated WAV file
|
||||||
QString wavFile = QFileInfo(requestFile).absolutePath()+"/request_" + QString::number(uid) + "00.wav";
|
QString wavFile = QFileInfo(requestFile).absolutePath()+"/request_" + QString::number(uid) + "00.wav";
|
||||||
if (!QFileInfo::exists(wavFile)) {
|
if (!QFileInfo::exists(wavFile))
|
||||||
|
{
|
||||||
emit parent->generationError("No WAV file generated at "+wavFile);
|
emit parent->generationError("No WAV file generated at "+wavFile);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@ signals:
|
||||||
void progressUpdate(int percent);
|
void progressUpdate(int percent);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
class Worker : public QRunnable {
|
class Worker : public QRunnable
|
||||||
|
{
|
||||||
public:
|
public:
|
||||||
Worker(AceStepWorker *parent, const SongItem& song, const QString &jsonTemplate,
|
Worker(AceStepWorker *parent, const SongItem& song, const QString &jsonTemplate,
|
||||||
const QString &aceStepPath, const QString &qwen3ModelPath,
|
const QString &aceStepPath, const QString &qwen3ModelPath,
|
||||||
|
|
|
||||||
|
|
@ -80,23 +80,28 @@ void AdvancedSettingsDialog::setVAEModelPath(const QString &path)
|
||||||
void AdvancedSettingsDialog::on_aceStepBrowseButton_clicked()
|
void AdvancedSettingsDialog::on_aceStepBrowseButton_clicked()
|
||||||
{
|
{
|
||||||
QString dir = QFileDialog::getExistingDirectory(this, "Select AceStep Build Directory", ui->aceStepPathEdit->text());
|
QString dir = QFileDialog::getExistingDirectory(this, "Select AceStep Build Directory", ui->aceStepPathEdit->text());
|
||||||
if (!dir.isEmpty()) {
|
if (!dir.isEmpty())
|
||||||
|
{
|
||||||
ui->aceStepPathEdit->setText(dir);
|
ui->aceStepPathEdit->setText(dir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AdvancedSettingsDialog::on_qwen3BrowseButton_clicked()
|
void AdvancedSettingsDialog::on_qwen3BrowseButton_clicked()
|
||||||
{
|
{
|
||||||
QString file = QFileDialog::getOpenFileName(this, "Select Qwen3 Model", ui->qwen3ModelEdit->text(), "GGUF Files (*.gguf)");
|
QString file = QFileDialog::getOpenFileName(this, "Select Qwen3 Model", ui->qwen3ModelEdit->text(),
|
||||||
if (!file.isEmpty()) {
|
"GGUF Files (*.gguf)");
|
||||||
|
if (!file.isEmpty())
|
||||||
|
{
|
||||||
ui->qwen3ModelEdit->setText(file);
|
ui->qwen3ModelEdit->setText(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AdvancedSettingsDialog::on_textEncoderBrowseButton_clicked()
|
void AdvancedSettingsDialog::on_textEncoderBrowseButton_clicked()
|
||||||
{
|
{
|
||||||
QString file = QFileDialog::getOpenFileName(this, "Select Text Encoder Model", ui->textEncoderEdit->text(), "GGUF Files (*.gguf)");
|
QString file = QFileDialog::getOpenFileName(this, "Select Text Encoder Model", ui->textEncoderEdit->text(),
|
||||||
if (!file.isEmpty()) {
|
"GGUF Files (*.gguf)");
|
||||||
|
if (!file.isEmpty())
|
||||||
|
{
|
||||||
ui->textEncoderEdit->setText(file);
|
ui->textEncoderEdit->setText(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -104,7 +109,8 @@ void AdvancedSettingsDialog::on_textEncoderBrowseButton_clicked()
|
||||||
void AdvancedSettingsDialog::on_ditBrowseButton_clicked()
|
void AdvancedSettingsDialog::on_ditBrowseButton_clicked()
|
||||||
{
|
{
|
||||||
QString file = QFileDialog::getOpenFileName(this, "Select DiT Model", ui->ditModelEdit->text(), "GGUF Files (*.gguf)");
|
QString file = QFileDialog::getOpenFileName(this, "Select DiT Model", ui->ditModelEdit->text(), "GGUF Files (*.gguf)");
|
||||||
if (!file.isEmpty()) {
|
if (!file.isEmpty())
|
||||||
|
{
|
||||||
ui->ditModelEdit->setText(file);
|
ui->ditModelEdit->setText(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +118,8 @@ void AdvancedSettingsDialog::on_ditBrowseButton_clicked()
|
||||||
void AdvancedSettingsDialog::on_vaeBrowseButton_clicked()
|
void AdvancedSettingsDialog::on_vaeBrowseButton_clicked()
|
||||||
{
|
{
|
||||||
QString file = QFileDialog::getOpenFileName(this, "Select VAE Model", ui->vaeModelEdit->text(), "GGUF Files (*.gguf)");
|
QString file = QFileDialog::getOpenFileName(this, "Select VAE Model", ui->vaeModelEdit->text(), "GGUF Files (*.gguf)");
|
||||||
if (!file.isEmpty()) {
|
if (!file.isEmpty())
|
||||||
|
{
|
||||||
ui->vaeModelEdit->setText(file);
|
ui->vaeModelEdit->setText(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,7 +4,8 @@
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui
|
||||||
|
{
|
||||||
class AdvancedSettingsDialog;
|
class AdvancedSettingsDialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<ui version="4.0">
|
<ui version="4.0">
|
||||||
<class>AdvancedSettingsDialog</class>
|
<class>AdvancedSettingsDialog</class>
|
||||||
<widget class="QDialog" name="AdvancedSettingsDialog">
|
<widget class="QDialog" name="AdvancedSettingsDialog">
|
||||||
<property name="geometry">
|
<property name="geometry">
|
||||||
|
|
@ -200,4 +200,4 @@
|
||||||
</hints>
|
</hints>
|
||||||
</connection>
|
</connection>
|
||||||
</connections>
|
</connections>
|
||||||
</ui>
|
</ui>
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,10 @@ AudioPlayer::AudioPlayer(QObject *parent)
|
||||||
|
|
||||||
// Set up position timer for updating playback position
|
// Set up position timer for updating playback position
|
||||||
positionTimer->setInterval(500); // Update every 500ms
|
positionTimer->setInterval(500); // Update every 500ms
|
||||||
connect(positionTimer, &QTimer::timeout, [this]() {
|
connect(positionTimer, &QTimer::timeout, [this]()
|
||||||
if (isPlaying()) {
|
{
|
||||||
|
if (isPlaying())
|
||||||
|
{
|
||||||
emit positionChanged(mediaPlayer->position());
|
emit positionChanged(mediaPlayer->position());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -31,7 +33,8 @@ AudioPlayer::~AudioPlayer()
|
||||||
|
|
||||||
void AudioPlayer::play(const QString &filePath)
|
void AudioPlayer::play(const QString &filePath)
|
||||||
{
|
{
|
||||||
if (isPlaying()) {
|
if (isPlaying())
|
||||||
|
{
|
||||||
stop();
|
stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,7 +47,8 @@ void AudioPlayer::play(const QString &filePath)
|
||||||
|
|
||||||
void AudioPlayer::play()
|
void AudioPlayer::play()
|
||||||
{
|
{
|
||||||
if (!isPlaying()) {
|
if (!isPlaying())
|
||||||
|
{
|
||||||
mediaPlayer->play();
|
mediaPlayer->play();
|
||||||
positionTimer->start();
|
positionTimer->start();
|
||||||
}
|
}
|
||||||
|
|
@ -52,7 +56,8 @@ void AudioPlayer::play()
|
||||||
|
|
||||||
void AudioPlayer::pause()
|
void AudioPlayer::pause()
|
||||||
{
|
{
|
||||||
if (isPlaying()) {
|
if (isPlaying())
|
||||||
|
{
|
||||||
mediaPlayer->pause();
|
mediaPlayer->pause();
|
||||||
positionTimer->stop();
|
positionTimer->stop();
|
||||||
}
|
}
|
||||||
|
|
@ -86,12 +91,16 @@ int AudioPlayer::position() const
|
||||||
|
|
||||||
void AudioPlayer::handlePlaybackStateChanged(QMediaPlayer::PlaybackState state)
|
void AudioPlayer::handlePlaybackStateChanged(QMediaPlayer::PlaybackState state)
|
||||||
{
|
{
|
||||||
if (state == QMediaPlayer::PlayingState) {
|
if (state == QMediaPlayer::PlayingState)
|
||||||
|
{
|
||||||
emit playbackStarted();
|
emit playbackStarted();
|
||||||
} else if (state == QMediaPlayer::StoppedState ||
|
}
|
||||||
state == QMediaPlayer::PausedState) {
|
else if (state == QMediaPlayer::StoppedState ||
|
||||||
|
state == QMediaPlayer::PausedState)
|
||||||
|
{
|
||||||
// Check if we reached the end
|
// Check if we reached the end
|
||||||
if (mediaPlayer->position() >= mediaPlayer->duration() - 100) {
|
if (mediaPlayer->position() >= mediaPlayer->duration() - 100)
|
||||||
|
{
|
||||||
emit playbackFinished();
|
emit playbackFinished();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -99,16 +108,22 @@ void AudioPlayer::handlePlaybackStateChanged(QMediaPlayer::PlaybackState state)
|
||||||
|
|
||||||
void AudioPlayer::handleMediaStatusChanged(QMediaPlayer::MediaStatus status)
|
void AudioPlayer::handleMediaStatusChanged(QMediaPlayer::MediaStatus status)
|
||||||
{
|
{
|
||||||
if (status == QMediaPlayer::EndOfMedia) {
|
if (status == QMediaPlayer::EndOfMedia)
|
||||||
|
{
|
||||||
emit playbackFinished();
|
emit playbackFinished();
|
||||||
} else if (status == QMediaPlayer::LoadedMedia ||
|
}
|
||||||
status == QMediaPlayer::BufferedMedia) {
|
else if (status == QMediaPlayer::LoadedMedia ||
|
||||||
|
status == QMediaPlayer::BufferedMedia)
|
||||||
|
{
|
||||||
// Media loaded successfully, emit duration
|
// Media loaded successfully, emit duration
|
||||||
int duration = mediaPlayer->duration();
|
int duration = mediaPlayer->duration();
|
||||||
if (duration > 0) {
|
if (duration > 0)
|
||||||
|
{
|
||||||
emit durationChanged(duration);
|
emit durationChanged(duration);
|
||||||
}
|
}
|
||||||
} else if (status == QMediaPlayer::InvalidMedia) {
|
}
|
||||||
|
else if (status == QMediaPlayer::InvalidMedia)
|
||||||
|
{
|
||||||
emit playbackError(mediaPlayer->errorString());
|
emit playbackError(mediaPlayer->errorString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,14 @@ MainWindow::MainWindow(QWidget *parent)
|
||||||
connect(ui->actionLoadPlaylist, &QAction::triggered, this, &MainWindow::on_actionLoadPlaylist);
|
connect(ui->actionLoadPlaylist, &QAction::triggered, this, &MainWindow::on_actionLoadPlaylist);
|
||||||
connect(ui->actionAppendPlaylist, &QAction::triggered, this, &MainWindow::on_actionAppendPlaylist);
|
connect(ui->actionAppendPlaylist, &QAction::triggered, this, &MainWindow::on_actionAppendPlaylist);
|
||||||
connect(ui->actionSaveSong, &QAction::triggered, this, &MainWindow::on_actionSaveSong);
|
connect(ui->actionSaveSong, &QAction::triggered, this, &MainWindow::on_actionSaveSong);
|
||||||
connect(ui->actionQuit, &QAction::triggered, this, [this](){close();});
|
connect(ui->actionQuit, &QAction::triggered, this, [this]()
|
||||||
connect(ui->actionClearPlaylist, &QAction::triggered, this, [this](){songModel->clear();});
|
{
|
||||||
|
close();
|
||||||
|
});
|
||||||
|
connect(ui->actionClearPlaylist, &QAction::triggered, this, [this]()
|
||||||
|
{
|
||||||
|
songModel->clear();
|
||||||
|
});
|
||||||
connect(audioPlayer, &AudioPlayer::playbackFinished, this, &MainWindow::playNextSong);
|
connect(audioPlayer, &AudioPlayer::playbackFinished, this, &MainWindow::playNextSong);
|
||||||
connect(audioPlayer, &AudioPlayer::playbackStarted, this, &MainWindow::playbackStarted);
|
connect(audioPlayer, &AudioPlayer::playbackStarted, this, &MainWindow::playbackStarted);
|
||||||
connect(audioPlayer, &AudioPlayer::positionChanged, this, &MainWindow::updatePosition);
|
connect(audioPlayer, &AudioPlayer::positionChanged, this, &MainWindow::updatePosition);
|
||||||
|
|
@ -59,12 +65,14 @@ MainWindow::MainWindow(QWidget *parent)
|
||||||
connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked);
|
connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked);
|
||||||
|
|
||||||
// Connect audio player error signal
|
// Connect audio player error signal
|
||||||
connect(audioPlayer, &AudioPlayer::playbackError, [this](const QString &error) {
|
connect(audioPlayer, &AudioPlayer::playbackError, [this](const QString &error)
|
||||||
|
{
|
||||||
QMessageBox::warning(this, "Playback Error", "Failed to play audio: " + error);
|
QMessageBox::warning(this, "Playback Error", "Failed to play audio: " + error);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add some default songs
|
// Add some default songs
|
||||||
if(songModel->songCount() == 0) {
|
if(songModel->songCount() == 0)
|
||||||
|
{
|
||||||
SongItem defaultSong1("Upbeat pop rock anthem with driving electric guitars", "");
|
SongItem defaultSong1("Upbeat pop rock anthem with driving electric guitars", "");
|
||||||
SongItem defaultSong2("Chill electronic music with smooth synths and relaxing beats", "");
|
SongItem defaultSong2("Chill electronic music with smooth synths and relaxing beats", "");
|
||||||
SongItem defaultSong3("Jazz fusion with saxophone solos and complex rhythms", "");
|
SongItem defaultSong3("Jazz fusion with saxophone solos and complex rhythms", "");
|
||||||
|
|
@ -75,7 +83,8 @@ MainWindow::MainWindow(QWidget *parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select first item
|
// Select first item
|
||||||
if (songModel->rowCount() > 0) {
|
if (songModel->rowCount() > 0)
|
||||||
|
{
|
||||||
QModelIndex firstIndex = songModel->index(0, 0);
|
QModelIndex firstIndex = songModel->index(0, 0);
|
||||||
ui->songListView->setCurrentIndex(firstIndex);
|
ui->songListView->setCurrentIndex(firstIndex);
|
||||||
}
|
}
|
||||||
|
|
@ -117,7 +126,8 @@ void MainWindow::loadSettings()
|
||||||
QSettings settings("MusicGenerator", "AceStepGUI");
|
QSettings settings("MusicGenerator", "AceStepGUI");
|
||||||
|
|
||||||
// Load JSON template (default to simple configuration)
|
// Load JSON template (default to simple configuration)
|
||||||
jsonTemplate = settings.value("jsonTemplate", "{\n\t\"inference_steps\": 8,\n\t\"shift\": 3.0,\n\t\"vocal_language\": \"en\"\n}").toString();
|
jsonTemplate = settings.value("jsonTemplate",
|
||||||
|
"{\n\t\"inference_steps\": 8,\n\t\"shift\": 3.0,\n\t\"vocal_language\": \"en\"\n}").toString();
|
||||||
|
|
||||||
// Load shuffle mode
|
// Load shuffle mode
|
||||||
shuffleMode = settings.value("shuffleMode", false).toBool();
|
shuffleMode = settings.value("shuffleMode", false).toBool();
|
||||||
|
|
@ -126,8 +136,10 @@ void MainWindow::loadSettings()
|
||||||
// Load path settings with defaults based on application directory
|
// Load path settings with defaults based on application directory
|
||||||
QString appDir = QCoreApplication::applicationDirPath();
|
QString appDir = QCoreApplication::applicationDirPath();
|
||||||
aceStepPath = settings.value("aceStepPath", appDir + "/acestep.cpp").toString();
|
aceStepPath = settings.value("aceStepPath", appDir + "/acestep.cpp").toString();
|
||||||
qwen3ModelPath = settings.value("qwen3ModelPath", appDir + "/acestep.cpp/models/acestep-5Hz-lm-4B-Q8_0.gguf").toString();
|
qwen3ModelPath = settings.value("qwen3ModelPath",
|
||||||
textEncoderModelPath = settings.value("textEncoderModelPath", appDir + "/acestep.cpp/models/Qwen3-Embedding-0.6B-BF16.gguf").toString();
|
appDir + "/acestep.cpp/models/acestep-5Hz-lm-4B-Q8_0.gguf").toString();
|
||||||
|
textEncoderModelPath = settings.value("textEncoderModelPath",
|
||||||
|
appDir + "/acestep.cpp/models/Qwen3-Embedding-0.6B-BF16.gguf").toString();
|
||||||
ditModelPath = settings.value("ditModelPath", appDir + "/acestep.cpp/models/acestep-v15-turbo-Q8_0.gguf").toString();
|
ditModelPath = settings.value("ditModelPath", appDir + "/acestep.cpp/models/acestep-v15-turbo-Q8_0.gguf").toString();
|
||||||
vaeModelPath = settings.value("vaeModelPath", appDir + "/acestep.cpp/models/vae-BF16.gguf").toString();
|
vaeModelPath = settings.value("vaeModelPath", appDir + "/acestep.cpp/models/vae-BF16.gguf").toString();
|
||||||
}
|
}
|
||||||
|
|
@ -197,7 +209,8 @@ void MainWindow::updateControls()
|
||||||
|
|
||||||
void MainWindow::on_playButton_clicked()
|
void MainWindow::on_playButton_clicked()
|
||||||
{
|
{
|
||||||
if (isPaused) {
|
if (isPaused)
|
||||||
|
{
|
||||||
// Resume playback
|
// Resume playback
|
||||||
audioPlayer->play();
|
audioPlayer->play();
|
||||||
isPaused = false;
|
isPaused = false;
|
||||||
|
|
@ -217,7 +230,8 @@ void MainWindow::on_playButton_clicked()
|
||||||
|
|
||||||
void MainWindow::on_pauseButton_clicked()
|
void MainWindow::on_pauseButton_clicked()
|
||||||
{
|
{
|
||||||
if (isPlaying && !isPaused) {
|
if (isPlaying && !isPaused)
|
||||||
|
{
|
||||||
// Pause playback
|
// Pause playback
|
||||||
audioPlayer->pause();
|
audioPlayer->pause();
|
||||||
isPaused = true;
|
isPaused = true;
|
||||||
|
|
@ -227,7 +241,8 @@ void MainWindow::on_pauseButton_clicked()
|
||||||
|
|
||||||
void MainWindow::on_skipButton_clicked()
|
void MainWindow::on_skipButton_clicked()
|
||||||
{
|
{
|
||||||
if (isPlaying) {
|
if (isPlaying)
|
||||||
|
{
|
||||||
audioPlayer->stop();
|
audioPlayer->stop();
|
||||||
isPaused = false;
|
isPaused = false;
|
||||||
playNextSong();
|
playNextSong();
|
||||||
|
|
@ -236,7 +251,8 @@ void MainWindow::on_skipButton_clicked()
|
||||||
|
|
||||||
void MainWindow::on_stopButton_clicked()
|
void MainWindow::on_stopButton_clicked()
|
||||||
{
|
{
|
||||||
if (isPlaying) {
|
if (isPlaying)
|
||||||
|
{
|
||||||
// Stop current playback completely
|
// Stop current playback completely
|
||||||
audioPlayer->stop();
|
audioPlayer->stop();
|
||||||
ui->nowPlayingLabel->setText("Now Playing:");
|
ui->nowPlayingLabel->setText("Now Playing:");
|
||||||
|
|
@ -257,7 +273,8 @@ void MainWindow::on_addSongButton_clicked()
|
||||||
{
|
{
|
||||||
SongDialog dialog(this);
|
SongDialog dialog(this);
|
||||||
|
|
||||||
if (dialog.exec() == QDialog::Accepted) {
|
if (dialog.exec() == QDialog::Accepted)
|
||||||
|
{
|
||||||
QString caption = dialog.getCaption();
|
QString caption = dialog.getCaption();
|
||||||
QString lyrics = dialog.getLyrics();
|
QString lyrics = dialog.getLyrics();
|
||||||
QString vocalLanguage = dialog.getVocalLanguage();
|
QString vocalLanguage = dialog.getVocalLanguage();
|
||||||
|
|
@ -274,7 +291,8 @@ void MainWindow::on_addSongButton_clicked()
|
||||||
|
|
||||||
void MainWindow::on_songListView_doubleClicked(const QModelIndex &index)
|
void MainWindow::on_songListView_doubleClicked(const QModelIndex &index)
|
||||||
{
|
{
|
||||||
if (!index.isValid()) return;
|
if (!index.isValid())
|
||||||
|
return;
|
||||||
|
|
||||||
// Temporarily disconnect the signal to prevent multiple invocations
|
// Temporarily disconnect the signal to prevent multiple invocations
|
||||||
// This happens when the dialog closes and triggers another double-click event
|
// This happens when the dialog closes and triggers another double-click event
|
||||||
|
|
@ -283,11 +301,15 @@ void MainWindow::on_songListView_doubleClicked(const QModelIndex &index)
|
||||||
int row = index.row();
|
int row = index.row();
|
||||||
|
|
||||||
// Different behavior based on which column was clicked
|
// Different behavior based on which column was clicked
|
||||||
if (index.column() == 0) {
|
if (index.column() == 0)
|
||||||
|
{
|
||||||
// Column 0 (play indicator): Stop current playback and play this song
|
// Column 0 (play indicator): Stop current playback and play this song
|
||||||
if (isPlaying) {
|
if (isPlaying)
|
||||||
|
{
|
||||||
audioPlayer->stop();
|
audioPlayer->stop();
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
isPlaying = true;
|
isPlaying = true;
|
||||||
updateControls();
|
updateControls();
|
||||||
}
|
}
|
||||||
|
|
@ -296,13 +318,16 @@ void MainWindow::on_songListView_doubleClicked(const QModelIndex &index)
|
||||||
flushGenerationQueue();
|
flushGenerationQueue();
|
||||||
currentSong = songModel->getSong(row);
|
currentSong = songModel->getSong(row);
|
||||||
ensureSongsInQueue(true);
|
ensureSongsInQueue(true);
|
||||||
} else if (index.column() == 1 || index.column() == 2) {
|
}
|
||||||
|
else if (index.column() == 1 || index.column() == 2)
|
||||||
|
{
|
||||||
// Column 1 (caption): Edit the song
|
// Column 1 (caption): Edit the song
|
||||||
SongItem song = songModel->getSong(row);
|
SongItem song = songModel->getSong(row);
|
||||||
|
|
||||||
SongDialog dialog(this, song.caption, song.lyrics, song.vocalLanguage);
|
SongDialog dialog(this, song.caption, song.lyrics, song.vocalLanguage);
|
||||||
|
|
||||||
if (dialog.exec() == QDialog::Accepted) {
|
if (dialog.exec() == QDialog::Accepted)
|
||||||
|
{
|
||||||
QString caption = dialog.getCaption();
|
QString caption = dialog.getCaption();
|
||||||
QString lyrics = dialog.getLyrics();
|
QString lyrics = dialog.getLyrics();
|
||||||
QString vocalLanguage = dialog.getVocalLanguage();
|
QString vocalLanguage = dialog.getVocalLanguage();
|
||||||
|
|
@ -321,7 +346,8 @@ void MainWindow::on_songListView_doubleClicked(const QModelIndex &index)
|
||||||
void MainWindow::on_removeSongButton_clicked()
|
void MainWindow::on_removeSongButton_clicked()
|
||||||
{
|
{
|
||||||
QModelIndex currentIndex = ui->songListView->currentIndex();
|
QModelIndex currentIndex = ui->songListView->currentIndex();
|
||||||
if (!currentIndex.isValid()) return;
|
if (!currentIndex.isValid())
|
||||||
|
return;
|
||||||
|
|
||||||
// Get the row from the current selection (works with table view)
|
// Get the row from the current selection (works with table view)
|
||||||
int row = currentIndex.row();
|
int row = currentIndex.row();
|
||||||
|
|
@ -330,7 +356,8 @@ void MainWindow::on_removeSongButton_clicked()
|
||||||
|
|
||||||
// Select next item or previous if at end
|
// Select next item or previous if at end
|
||||||
int newRow = qMin(row, songModel->rowCount() - 1);
|
int newRow = qMin(row, songModel->rowCount() - 1);
|
||||||
if (newRow >= 0) {
|
if (newRow >= 0)
|
||||||
|
{
|
||||||
QModelIndex newIndex = songModel->index(newRow, 0);
|
QModelIndex newIndex = songModel->index(newRow, 0);
|
||||||
ui->songListView->setCurrentIndex(newIndex);
|
ui->songListView->setCurrentIndex(newIndex);
|
||||||
}
|
}
|
||||||
|
|
@ -348,11 +375,13 @@ void MainWindow::on_advancedSettingsButton_clicked()
|
||||||
dialog.setDiTModelPath(ditModelPath);
|
dialog.setDiTModelPath(ditModelPath);
|
||||||
dialog.setVAEModelPath(vaeModelPath);
|
dialog.setVAEModelPath(vaeModelPath);
|
||||||
|
|
||||||
if (dialog.exec() == QDialog::Accepted) {
|
if (dialog.exec() == QDialog::Accepted)
|
||||||
|
{
|
||||||
// Validate JSON template
|
// Validate JSON template
|
||||||
QJsonParseError parseError;
|
QJsonParseError parseError;
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(dialog.getJsonTemplate().toUtf8(), &parseError);
|
QJsonDocument doc = QJsonDocument::fromJson(dialog.getJsonTemplate().toUtf8(), &parseError);
|
||||||
if (!doc.isObject()) {
|
if (!doc.isObject())
|
||||||
|
{
|
||||||
QMessageBox::warning(this, "Invalid JSON", "Please enter valid JSON: " + QString(parseError.errorString()));
|
QMessageBox::warning(this, "Invalid JSON", "Please enter valid JSON: " + QString(parseError.errorString()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -389,10 +418,12 @@ void MainWindow::songGenerated(const SongItem& song)
|
||||||
{
|
{
|
||||||
isGeneratingNext = false;
|
isGeneratingNext = false;
|
||||||
|
|
||||||
if (!isPaused && isPlaying && !audioPlayer->isPlaying()) {
|
if (!isPaused && isPlaying && !audioPlayer->isPlaying())
|
||||||
|
{
|
||||||
playSong(song);
|
playSong(song);
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
generatedSongQueue.enqueue(song);
|
generatedSongQueue.enqueue(song);
|
||||||
}
|
}
|
||||||
ui->statusLabel->setText("idle");
|
ui->statusLabel->setText("idle");
|
||||||
|
|
@ -406,10 +437,13 @@ void MainWindow::playNextSong()
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Check if we have a pre-generated next song in the queue
|
// Check if we have a pre-generated next song in the queue
|
||||||
if (!generatedSongQueue.isEmpty()) {
|
if (!generatedSongQueue.isEmpty())
|
||||||
|
{
|
||||||
SongItem generatedSong = generatedSongQueue.dequeue();
|
SongItem generatedSong = generatedSongQueue.dequeue();
|
||||||
playSong(generatedSong);
|
playSong(generatedSong);
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
ui->nowPlayingLabel->setText("Now Playing: Waiting for generation...");
|
ui->nowPlayingLabel->setText("Now Playing: Waiting for generation...");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -460,7 +494,8 @@ void MainWindow::updatePlaybackStatus(bool playing)
|
||||||
|
|
||||||
void MainWindow::on_positionSlider_sliderMoved(int position)
|
void MainWindow::on_positionSlider_sliderMoved(int position)
|
||||||
{
|
{
|
||||||
if (isPlaying && audioPlayer->isPlaying()) {
|
if (isPlaying && audioPlayer->isPlaying())
|
||||||
|
{
|
||||||
audioPlayer->setPosition(position);
|
audioPlayer->setPosition(position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -468,7 +503,8 @@ void MainWindow::on_positionSlider_sliderMoved(int position)
|
||||||
void MainWindow::ensureSongsInQueue(bool enqeueCurrent)
|
void MainWindow::ensureSongsInQueue(bool enqeueCurrent)
|
||||||
{
|
{
|
||||||
// Only generate more songs if we're playing and not already at capacity
|
// Only generate more songs if we're playing and not already at capacity
|
||||||
if (!isPlaying || isGeneratingNext || generatedSongQueue.size() >= generationTresh) {
|
if (!isPlaying || isGeneratingNext || generatedSongQueue.size() >= generationTresh)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -482,10 +518,12 @@ void MainWindow::ensureSongsInQueue(bool enqeueCurrent)
|
||||||
lastSong = currentSong;
|
lastSong = currentSong;
|
||||||
|
|
||||||
SongItem nextSong;
|
SongItem nextSong;
|
||||||
if(enqeueCurrent) {
|
if(enqeueCurrent)
|
||||||
|
{
|
||||||
nextSong = lastSong;
|
nextSong = lastSong;
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
int nextIndex = songModel->findNextIndex(songModel->findSongIndexById(lastSong.uniqueId), shuffleMode);
|
int nextIndex = songModel->findNextIndex(songModel->findSongIndexById(lastSong.uniqueId), shuffleMode);
|
||||||
nextSong = songModel->getSong(nextIndex);
|
nextSong = songModel->getSong(nextIndex);
|
||||||
}
|
}
|
||||||
|
|
@ -513,7 +551,8 @@ void MainWindow::on_actionSavePlaylist()
|
||||||
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/playlist.json",
|
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/playlist.json",
|
||||||
"JSON Files (*.json);;All Files (*)");
|
"JSON Files (*.json);;All Files (*)");
|
||||||
|
|
||||||
if (!filePath.isEmpty()) {
|
if (!filePath.isEmpty())
|
||||||
|
{
|
||||||
savePlaylist(filePath);
|
savePlaylist(filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -523,7 +562,8 @@ void MainWindow::on_actionLoadPlaylist()
|
||||||
QString filePath = QFileDialog::getOpenFileName(this, "Load Playlist",
|
QString filePath = QFileDialog::getOpenFileName(this, "Load Playlist",
|
||||||
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation),
|
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation),
|
||||||
"JSON Files (*.json);;All Files (*)");
|
"JSON Files (*.json);;All Files (*)");
|
||||||
if (!filePath.isEmpty()) {
|
if (!filePath.isEmpty())
|
||||||
|
{
|
||||||
songModel->clear();
|
songModel->clear();
|
||||||
flushGenerationQueue();
|
flushGenerationQueue();
|
||||||
loadPlaylist(filePath);
|
loadPlaylist(filePath);
|
||||||
|
|
@ -535,7 +575,8 @@ void MainWindow::on_actionAppendPlaylist()
|
||||||
QString filePath = QFileDialog::getOpenFileName(this, "Load Playlist",
|
QString filePath = QFileDialog::getOpenFileName(this, "Load Playlist",
|
||||||
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation),
|
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation),
|
||||||
"JSON Files (*.json);;All Files (*)");
|
"JSON Files (*.json);;All Files (*)");
|
||||||
if (!filePath.isEmpty()) {
|
if (!filePath.isEmpty())
|
||||||
|
{
|
||||||
loadPlaylist(filePath);
|
loadPlaylist(filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -545,7 +586,8 @@ void MainWindow::on_actionSaveSong()
|
||||||
QString filePath = QFileDialog::getSaveFileName(this, "Save Playlist",
|
QString filePath = QFileDialog::getSaveFileName(this, "Save Playlist",
|
||||||
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/song.json",
|
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/song.json",
|
||||||
"JSON Files (*.json);;All Files (*)");
|
"JSON Files (*.json);;All Files (*)");
|
||||||
if (!filePath.isEmpty()) {
|
if (!filePath.isEmpty())
|
||||||
|
{
|
||||||
QJsonArray songsArray;
|
QJsonArray songsArray;
|
||||||
QJsonParseError parseError;
|
QJsonParseError parseError;
|
||||||
QJsonDocument songDoc = QJsonDocument::fromJson(currentSong.json.toUtf8(), &parseError);
|
QJsonDocument songDoc = QJsonDocument::fromJson(currentSong.json.toUtf8(), &parseError);
|
||||||
|
|
@ -573,7 +615,8 @@ void MainWindow::savePlaylist(const QString &filePath)
|
||||||
{
|
{
|
||||||
// Get current songs from the model
|
// Get current songs from the model
|
||||||
QList<SongItem> songs;
|
QList<SongItem> songs;
|
||||||
for (int i = 0; i < songModel->rowCount(); ++i) {
|
for (int i = 0; i < songModel->rowCount(); ++i)
|
||||||
|
{
|
||||||
songs.append(songModel->getSong(i));
|
songs.append(songModel->getSong(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -583,9 +626,11 @@ void MainWindow::savePlaylist(const QString &filePath)
|
||||||
void MainWindow::loadPlaylist(const QString& filePath)
|
void MainWindow::loadPlaylist(const QString& filePath)
|
||||||
{
|
{
|
||||||
QList<SongItem> songs;
|
QList<SongItem> songs;
|
||||||
if (loadPlaylistFromJson(filePath, songs)) {
|
if (loadPlaylistFromJson(filePath, songs))
|
||||||
|
{
|
||||||
// Add loaded songs
|
// Add loaded songs
|
||||||
for (const SongItem &song : songs) {
|
for (const SongItem &song : songs)
|
||||||
|
{
|
||||||
songModel->addSong(song);
|
songModel->addSong(song);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -603,7 +648,8 @@ void MainWindow::autoSavePlaylist()
|
||||||
|
|
||||||
// Get current songs from the model
|
// Get current songs from the model
|
||||||
QList<SongItem> songs;
|
QList<SongItem> songs;
|
||||||
for (int i = 0; i < songModel->rowCount(); ++i) {
|
for (int i = 0; i < songModel->rowCount(); ++i)
|
||||||
|
{
|
||||||
songs.append(songModel->getSong(i));
|
songs.append(songModel->getSong(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -617,9 +663,11 @@ void MainWindow::autoLoadPlaylist()
|
||||||
QString filePath = appConfigPath + "/playlist.json";
|
QString filePath = appConfigPath + "/playlist.json";
|
||||||
|
|
||||||
// Check if the auto-save file exists
|
// Check if the auto-save file exists
|
||||||
if (QFile::exists(filePath)) {
|
if (QFile::exists(filePath))
|
||||||
|
{
|
||||||
QList<SongItem> songs;
|
QList<SongItem> songs;
|
||||||
if (loadPlaylistFromJson(filePath, songs)) {
|
if (loadPlaylistFromJson(filePath, songs))
|
||||||
|
{
|
||||||
songModel->clear();
|
songModel->clear();
|
||||||
for (const SongItem &song : songs)
|
for (const SongItem &song : songs)
|
||||||
songModel->addSong(song);
|
songModel->addSong(song);
|
||||||
|
|
@ -631,7 +679,8 @@ bool MainWindow::savePlaylistToJson(const QString &filePath, const QList<SongIte
|
||||||
{
|
{
|
||||||
QJsonArray songsArray;
|
QJsonArray songsArray;
|
||||||
|
|
||||||
for (const SongItem &song : songs) {
|
for (const SongItem &song : songs)
|
||||||
|
{
|
||||||
QJsonObject songObj;
|
QJsonObject songObj;
|
||||||
songObj["caption"] = song.caption;
|
songObj["caption"] = song.caption;
|
||||||
songObj["lyrics"] = song.lyrics;
|
songObj["lyrics"] = song.lyrics;
|
||||||
|
|
@ -648,7 +697,8 @@ bool MainWindow::savePlaylistToJson(const QString &filePath, const QList<SongIte
|
||||||
QByteArray jsonData = doc.toJson();
|
QByteArray jsonData = doc.toJson();
|
||||||
|
|
||||||
QFile file(filePath);
|
QFile file(filePath);
|
||||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||||
|
{
|
||||||
qWarning() << "Could not open file for writing:" << filePath;
|
qWarning() << "Could not open file for writing:" << filePath;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -662,7 +712,8 @@ bool MainWindow::savePlaylistToJson(const QString &filePath, const QList<SongIte
|
||||||
bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList<SongItem> &songs)
|
bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList<SongItem> &songs)
|
||||||
{
|
{
|
||||||
QFile file(filePath);
|
QFile file(filePath);
|
||||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||||
|
{
|
||||||
qWarning() << "Could not open file for reading:" << filePath;
|
qWarning() << "Could not open file for reading:" << filePath;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -675,12 +726,14 @@ bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList<SongItem> &
|
||||||
QJsonParseError parseError;
|
QJsonParseError parseError;
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &parseError);
|
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &parseError);
|
||||||
|
|
||||||
if (parseError.error != QJsonParseError::NoError) {
|
if (parseError.error != QJsonParseError::NoError)
|
||||||
|
{
|
||||||
qWarning() << "JSON parse error:" << parseError.errorString();
|
qWarning() << "JSON parse error:" << parseError.errorString();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!doc.isObject()) {
|
if (!doc.isObject())
|
||||||
|
{
|
||||||
qWarning() << "JSON root is not an object";
|
qWarning() << "JSON root is not an object";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -688,12 +741,14 @@ bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList<SongItem> &
|
||||||
QJsonObject rootObj = doc.object();
|
QJsonObject rootObj = doc.object();
|
||||||
|
|
||||||
// Check for version compatibility
|
// Check for version compatibility
|
||||||
if (rootObj.contains("version") && rootObj["version"].toString() != "1.0") {
|
if (rootObj.contains("version") && rootObj["version"].toString() != "1.0")
|
||||||
|
{
|
||||||
qWarning() << "Unsupported playlist version:" << rootObj["version"].toString();
|
qWarning() << "Unsupported playlist version:" << rootObj["version"].toString();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!rootObj.contains("songs") || !rootObj["songs"].isArray()) {
|
if (!rootObj.contains("songs") || !rootObj["songs"].isArray())
|
||||||
|
{
|
||||||
qWarning() << "Invalid playlist format: missing songs array";
|
qWarning() << "Invalid playlist format: missing songs array";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -702,29 +757,37 @@ bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList<SongItem> &
|
||||||
|
|
||||||
qDebug()<<"Loading"<<songsArray.size()<<"songs";
|
qDebug()<<"Loading"<<songsArray.size()<<"songs";
|
||||||
|
|
||||||
for (const QJsonValue &value : songsArray) {
|
for (const QJsonValue &value : songsArray)
|
||||||
if (!value.isObject()) continue;
|
{
|
||||||
|
if (!value.isObject())
|
||||||
|
continue;
|
||||||
|
|
||||||
QJsonObject songObj = value.toObject();
|
QJsonObject songObj = value.toObject();
|
||||||
SongItem song;
|
SongItem song;
|
||||||
|
|
||||||
if (songObj.contains("caption")) {
|
if (songObj.contains("caption"))
|
||||||
|
{
|
||||||
song.caption = songObj["caption"].toString();
|
song.caption = songObj["caption"].toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (songObj.contains("lyrics")) {
|
if (songObj.contains("lyrics"))
|
||||||
|
{
|
||||||
song.lyrics = songObj["lyrics"].toString();
|
song.lyrics = songObj["lyrics"].toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load vocalLanguage if present
|
// Load vocalLanguage if present
|
||||||
if (songObj.contains("vocalLanguage")) {
|
if (songObj.contains("vocalLanguage"))
|
||||||
|
{
|
||||||
song.vocalLanguage = songObj["vocalLanguage"].toString();
|
song.vocalLanguage = songObj["vocalLanguage"].toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load uniqueId if present (for backward compatibility)
|
// Load uniqueId if present (for backward compatibility)
|
||||||
if (songObj.contains("uniqueId")) {
|
if (songObj.contains("uniqueId"))
|
||||||
|
{
|
||||||
song.uniqueId = static_cast<uint64_t>(songObj["uniqueId"].toInteger());
|
song.uniqueId = static_cast<uint64_t>(songObj["uniqueId"].toInteger());
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
// Generate new ID for old playlists without uniqueId
|
// Generate new ID for old playlists without uniqueId
|
||||||
song.uniqueId = QRandomGenerator::global()->generate64();
|
song.uniqueId = QRandomGenerator::global()->generate64();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,10 @@
|
||||||
#include "AceStepWorker.h"
|
#include "AceStepWorker.h"
|
||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
QT_BEGIN_NAMESPACE
|
||||||
namespace Ui { class MainWindow; }
|
namespace Ui
|
||||||
|
{
|
||||||
|
class MainWindow;
|
||||||
|
}
|
||||||
QT_END_NAMESPACE
|
QT_END_NAMESPACE
|
||||||
|
|
||||||
class MainWindow : public QMainWindow
|
class MainWindow : public QMainWindow
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<ui version="4.0">
|
<ui version="4.0">
|
||||||
<class>MainWindow</class>
|
<class>MainWindow</class>
|
||||||
<widget class="QMainWindow" name="MainWindow">
|
<widget class="QMainWindow" name="MainWindow">
|
||||||
<property name="geometry">
|
<property name="geometry">
|
||||||
|
|
@ -356,4 +356,4 @@
|
||||||
<include location="../res/resources.qrc"/>
|
<include location="../res/resources.qrc"/>
|
||||||
</resources>
|
</resources>
|
||||||
<connections/>
|
<connections/>
|
||||||
</ui>
|
</ui>
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@ SongDialog::SongDialog(QWidget *parent, const QString &caption, const QString &l
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
|
|
||||||
// Set initial values if provided
|
// Set initial values if provided
|
||||||
if (!caption.isEmpty()) {
|
if (!caption.isEmpty())
|
||||||
|
{
|
||||||
ui->captionEdit->setPlainText(caption);
|
ui->captionEdit->setPlainText(caption);
|
||||||
}
|
}
|
||||||
if (!lyrics.isEmpty()) {
|
if (!lyrics.isEmpty())
|
||||||
|
{
|
||||||
ui->lyricsEdit->setPlainText(lyrics);
|
ui->lyricsEdit->setPlainText(lyrics);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,12 +31,16 @@ SongDialog::SongDialog(QWidget *parent, const QString &caption, const QString &l
|
||||||
ui->vocalLanguageCombo->addItem("Russian (ru)", "ru");
|
ui->vocalLanguageCombo->addItem("Russian (ru)", "ru");
|
||||||
|
|
||||||
// Set current language if provided
|
// Set current language if provided
|
||||||
if (!vocalLanguage.isEmpty()) {
|
if (!vocalLanguage.isEmpty())
|
||||||
|
{
|
||||||
int index = ui->vocalLanguageCombo->findData(vocalLanguage);
|
int index = ui->vocalLanguageCombo->findData(vocalLanguage);
|
||||||
if (index >= 0) {
|
if (index >= 0)
|
||||||
|
{
|
||||||
ui->vocalLanguageCombo->setCurrentIndex(index);
|
ui->vocalLanguageCombo->setCurrentIndex(index);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
ui->vocalLanguageCombo->setCurrentIndex(0); // Default to unset
|
ui->vocalLanguageCombo->setCurrentIndex(0); // Default to unset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -63,7 +69,8 @@ void SongDialog::on_okButton_clicked()
|
||||||
{
|
{
|
||||||
// Validate that caption is not empty
|
// Validate that caption is not empty
|
||||||
QString caption = getCaption();
|
QString caption = getCaption();
|
||||||
if (caption.trimmed().isEmpty()) {
|
if (caption.trimmed().isEmpty())
|
||||||
|
{
|
||||||
QMessageBox::warning(this, "Invalid Input", "Caption cannot be empty.");
|
QMessageBox::warning(this, "Invalid Input", "Caption cannot be empty.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui
|
||||||
|
{
|
||||||
class SongDialog;
|
class SongDialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -13,7 +14,8 @@ class SongDialog : public QDialog
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit SongDialog(QWidget *parent = nullptr, const QString &caption = "", const QString &lyrics = "", const QString &vocalLanguage = "");
|
explicit SongDialog(QWidget *parent = nullptr, const QString &caption = "", const QString &lyrics = "",
|
||||||
|
const QString &vocalLanguage = "");
|
||||||
~SongDialog();
|
~SongDialog();
|
||||||
|
|
||||||
QString getCaption() const;
|
QString getCaption() const;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<ui version="4.0">
|
<ui version="4.0">
|
||||||
<class>SongDialog</class>
|
<class>SongDialog</class>
|
||||||
<widget class="QDialog" name="SongDialog">
|
<widget class="QDialog" name="SongDialog">
|
||||||
<property name="geometry">
|
<property name="geometry">
|
||||||
|
|
@ -106,4 +106,4 @@
|
||||||
</widget>
|
</widget>
|
||||||
<resources/>
|
<resources/>
|
||||||
<connections/>
|
<connections/>
|
||||||
</ui>
|
</ui>
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
#include <QRandomGenerator>
|
#include <QRandomGenerator>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
class SongItem {
|
class SongItem
|
||||||
|
{
|
||||||
public:
|
public:
|
||||||
QString caption;
|
QString caption;
|
||||||
QString lyrics;
|
QString lyrics;
|
||||||
|
|
@ -13,7 +14,8 @@ public:
|
||||||
QString json;
|
QString json;
|
||||||
|
|
||||||
inline SongItem(const QString &caption = "", const QString &lyrics = "")
|
inline SongItem(const QString &caption = "", const QString &lyrics = "")
|
||||||
: caption(caption), lyrics(lyrics) {
|
: caption(caption), lyrics(lyrics)
|
||||||
|
{
|
||||||
// Generate a unique ID using cryptographically secure random number
|
// Generate a unique ID using cryptographically secure random number
|
||||||
uniqueId = QRandomGenerator::global()->generate64();
|
uniqueId = QRandomGenerator::global()->generate64();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,24 +32,29 @@ QVariant SongListModel::data(const QModelIndex &index, int role) const
|
||||||
|
|
||||||
const SongItem &song = songList[index.row()];
|
const SongItem &song = songList[index.row()];
|
||||||
|
|
||||||
switch (role) {
|
switch (role)
|
||||||
|
{
|
||||||
case Qt::DisplayRole:
|
case Qt::DisplayRole:
|
||||||
// Column 0: Play indicator column
|
// Column 0: Play indicator column
|
||||||
if (index.column() == 0) {
|
if (index.column() == 0)
|
||||||
|
{
|
||||||
return index.row() == m_playingIndex ? "▶" : "";
|
return index.row() == m_playingIndex ? "▶" : "";
|
||||||
}
|
}
|
||||||
// Column 1: Song name
|
// Column 1: Song name
|
||||||
else if (index.column() == 1) {
|
else if (index.column() == 1)
|
||||||
|
{
|
||||||
return song.caption;
|
return song.caption;
|
||||||
}
|
}
|
||||||
// Column 2: Vocal language
|
// Column 2: Vocal language
|
||||||
else if (index.column() == 2) {
|
else if (index.column() == 2)
|
||||||
|
{
|
||||||
return !song.vocalLanguage.isEmpty() ? song.vocalLanguage : "--";
|
return !song.vocalLanguage.isEmpty() ? song.vocalLanguage : "--";
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Qt::FontRole:
|
case Qt::FontRole:
|
||||||
// Make play indicator bold and larger
|
// Make play indicator bold and larger
|
||||||
if (index.column() == 0 && index.row() == m_playingIndex) {
|
if (index.column() == 0 && index.row() == m_playingIndex)
|
||||||
|
{
|
||||||
QFont font = QApplication::font();
|
QFont font = QApplication::font();
|
||||||
font.setBold(true);
|
font.setBold(true);
|
||||||
return font;
|
return font;
|
||||||
|
|
@ -57,7 +62,8 @@ QVariant SongListModel::data(const QModelIndex &index, int role) const
|
||||||
break;
|
break;
|
||||||
case Qt::TextAlignmentRole:
|
case Qt::TextAlignmentRole:
|
||||||
// Center align the play indicator
|
// Center align the play indicator
|
||||||
if (index.column() == 0) {
|
if (index.column() == 0)
|
||||||
|
{
|
||||||
return Qt::AlignCenter;
|
return Qt::AlignCenter;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -83,7 +89,8 @@ bool SongListModel::setData(const QModelIndex &index, const QVariant &value, int
|
||||||
|
|
||||||
SongItem &song = songList[index.row()];
|
SongItem &song = songList[index.row()];
|
||||||
|
|
||||||
switch (role) {
|
switch (role)
|
||||||
|
{
|
||||||
case CaptionRole:
|
case CaptionRole:
|
||||||
song.caption = value.toString();
|
song.caption = value.toString();
|
||||||
break;
|
break;
|
||||||
|
|
@ -119,7 +126,8 @@ void SongListModel::addSong(const SongItem &song)
|
||||||
|
|
||||||
void SongListModel::removeSong(int index)
|
void SongListModel::removeSong(int index)
|
||||||
{
|
{
|
||||||
if (index >= 0 && index < songList.size()) {
|
if (index >= 0 && index < songList.size())
|
||||||
|
{
|
||||||
beginRemoveRows(QModelIndex(), index, index);
|
beginRemoveRows(QModelIndex(), index, index);
|
||||||
songList.removeAt(index);
|
songList.removeAt(index);
|
||||||
endRemoveRows();
|
endRemoveRows();
|
||||||
|
|
@ -140,7 +148,8 @@ bool SongListModel::empty()
|
||||||
|
|
||||||
SongItem SongListModel::getSong(int index) const
|
SongItem SongListModel::getSong(int index) const
|
||||||
{
|
{
|
||||||
if (index >= 0 && index < songList.size()) {
|
if (index >= 0 && index < songList.size())
|
||||||
|
{
|
||||||
return songList[index];
|
return songList[index];
|
||||||
}
|
}
|
||||||
return SongItem();
|
return SongItem();
|
||||||
|
|
@ -148,7 +157,8 @@ SongItem SongListModel::getSong(int index) const
|
||||||
|
|
||||||
QVariant SongListModel::headerData(int section, Qt::Orientation orientation, int role) const
|
QVariant SongListModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||||
{
|
{
|
||||||
if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
|
if (role == Qt::DisplayRole && orientation == Qt::Horizontal)
|
||||||
|
{
|
||||||
// Hide headers since we don't need column titles
|
// Hide headers since we don't need column titles
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
|
|
@ -161,11 +171,13 @@ void SongListModel::setPlayingIndex(int index)
|
||||||
m_playingIndex = index;
|
m_playingIndex = index;
|
||||||
|
|
||||||
// Update both the old and new playing indices to trigger UI updates
|
// Update both the old and new playing indices to trigger UI updates
|
||||||
if (oldPlayingIndex >= 0 && oldPlayingIndex < songList.size()) {
|
if (oldPlayingIndex >= 0 && oldPlayingIndex < songList.size())
|
||||||
|
{
|
||||||
emit dataChanged(this->index(oldPlayingIndex, 0), this->index(oldPlayingIndex, 0));
|
emit dataChanged(this->index(oldPlayingIndex, 0), this->index(oldPlayingIndex, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index >= 0 && index < songList.size()) {
|
if (index >= 0 && index < songList.size())
|
||||||
|
{
|
||||||
emit dataChanged(this->index(index, 0), this->index(index, 0));
|
emit dataChanged(this->index(index, 0), this->index(index, 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -180,7 +192,8 @@ int SongListModel::findNextIndex(int currentIndex, bool shuffle) const
|
||||||
if (songList.isEmpty())
|
if (songList.isEmpty())
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
if (shuffle) {
|
if (shuffle)
|
||||||
|
{
|
||||||
// Simple random selection for shuffle mode
|
// Simple random selection for shuffle mode
|
||||||
QRandomGenerator generator;
|
QRandomGenerator generator;
|
||||||
return generator.bounded(songList.size());
|
return generator.bounded(songList.size());
|
||||||
|
|
@ -188,7 +201,8 @@ int SongListModel::findNextIndex(int currentIndex, bool shuffle) const
|
||||||
|
|
||||||
// Sequential playback
|
// Sequential playback
|
||||||
int nextIndex = currentIndex + 1;
|
int nextIndex = currentIndex + 1;
|
||||||
if (nextIndex >= songList.size()) {
|
if (nextIndex >= songList.size())
|
||||||
|
{
|
||||||
nextIndex = 0; // Loop back to beginning
|
nextIndex = 0; // Loop back to beginning
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,8 +211,10 @@ int SongListModel::findNextIndex(int currentIndex, bool shuffle) const
|
||||||
|
|
||||||
int SongListModel::findSongIndexById(uint64_t uniqueId) const
|
int SongListModel::findSongIndexById(uint64_t uniqueId) const
|
||||||
{
|
{
|
||||||
for (int i = 0; i < songList.size(); ++i) {
|
for (int i = 0; i < songList.size(); ++i)
|
||||||
if (songList[i].uniqueId == uniqueId) {
|
{
|
||||||
|
if (songList[i].uniqueId == uniqueId)
|
||||||
|
{
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ class SongListModel : public QAbstractTableModel
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum Roles {
|
enum Roles
|
||||||
|
{
|
||||||
CaptionRole = Qt::UserRole + 1,
|
CaptionRole = Qt::UserRole + 1,
|
||||||
LyricsRole = Qt::UserRole + 2,
|
LyricsRole = Qt::UserRole + 2,
|
||||||
VocalLanguageRole = Qt::UserRole + 3,
|
VocalLanguageRole = Qt::UserRole + 3,
|
||||||
|
|
@ -42,7 +43,10 @@ public:
|
||||||
|
|
||||||
// Playing indicator
|
// Playing indicator
|
||||||
void setPlayingIndex(int index);
|
void setPlayingIndex(int index);
|
||||||
int playingIndex() const { return m_playingIndex; }
|
int playingIndex() const
|
||||||
|
{
|
||||||
|
return m_playingIndex;
|
||||||
|
}
|
||||||
|
|
||||||
// Find song by unique ID
|
// Find song by unique ID
|
||||||
int findSongIndexById(uint64_t uniqueId) const;
|
int findSongIndexById(uint64_t uniqueId) const;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ ClickableSlider::ClickableSlider(Qt::Orientation orientation, QWidget *parent)
|
||||||
|
|
||||||
void ClickableSlider::mousePressEvent(QMouseEvent *event)
|
void ClickableSlider::mousePressEvent(QMouseEvent *event)
|
||||||
{
|
{
|
||||||
if (event->button() == Qt::LeftButton) {
|
if (event->button() == Qt::LeftButton)
|
||||||
|
{
|
||||||
int val = pixelPosToRangeValue(event->pos());
|
int val = pixelPosToRangeValue(event->pos());
|
||||||
|
|
||||||
// Block signals temporarily to avoid infinite recursion
|
// Block signals temporarily to avoid infinite recursion
|
||||||
|
|
@ -24,7 +25,9 @@ void ClickableSlider::mousePressEvent(QMouseEvent *event)
|
||||||
// Emit both valueChanged and sliderMoved signals for compatibility
|
// Emit both valueChanged and sliderMoved signals for compatibility
|
||||||
emit valueChanged(val);
|
emit valueChanged(val);
|
||||||
emit sliderMoved(val);
|
emit sliderMoved(val);
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
// Call base class implementation for other buttons
|
// Call base class implementation for other buttons
|
||||||
QSlider::mousePressEvent(event);
|
QSlider::mousePressEvent(event);
|
||||||
}
|
}
|
||||||
|
|
@ -42,11 +45,14 @@ int ClickableSlider::pixelPosToRangeValue(const QPoint &pos)
|
||||||
int sliderMin;
|
int sliderMin;
|
||||||
int sliderMax;
|
int sliderMax;
|
||||||
|
|
||||||
if (orientation() == Qt::Horizontal) {
|
if (orientation() == Qt::Horizontal)
|
||||||
|
{
|
||||||
sliderLength = sr.width();
|
sliderLength = sr.width();
|
||||||
sliderMin = gr.x();
|
sliderMin = gr.x();
|
||||||
sliderMax = gr.right() - sliderLength + 1;
|
sliderMax = gr.right() - sliderLength + 1;
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
sliderLength = sr.height();
|
sliderLength = sr.height();
|
||||||
sliderMin = gr.y();
|
sliderMin = gr.y();
|
||||||
sliderMax = gr.bottom() - sliderLength + 1;
|
sliderMax = gr.bottom() - sliderLength + 1;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue