playlist handling
This commit is contained in:
parent
c4d2fa3ffa
commit
134e827053
5 changed files with 418 additions and 67 deletions
289
MainWindow.cpp
289
MainWindow.cpp
|
|
@ -10,6 +10,8 @@
|
||||||
#include <QTextEdit>
|
#include <QTextEdit>
|
||||||
#include <QDialogButtonBox>
|
#include <QDialogButtonBox>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QDir>
|
||||||
|
|
||||||
MainWindow::MainWindow(QWidget *parent)
|
MainWindow::MainWindow(QWidget *parent)
|
||||||
: QMainWindow(parent),
|
: QMainWindow(parent),
|
||||||
|
|
@ -32,16 +34,22 @@ MainWindow::MainWindow(QWidget *parent)
|
||||||
// Load settings
|
// Load settings
|
||||||
loadSettings();
|
loadSettings();
|
||||||
|
|
||||||
|
// Auto-load playlist from config location on startup
|
||||||
|
autoLoadPlaylist();
|
||||||
|
|
||||||
// Connect signals and slots
|
// Connect signals and slots
|
||||||
connect(ui->actionAdvancedSettings, &QAction::triggered, this, &MainWindow::on_advancedSettingsButton_clicked);
|
connect(ui->actionAdvancedSettings, &QAction::triggered, this, &MainWindow::on_advancedSettingsButton_clicked);
|
||||||
|
connect(ui->actionSavePlaylist, &QAction::triggered, this, &MainWindow::on_actionSavePlaylist);
|
||||||
|
connect(ui->actionLoadPlaylist, &QAction::triggered, this, &MainWindow::on_actionLoadPlaylist);
|
||||||
|
connect(ui->actionQuit, &QAction::triggered, this, [this](){close();});
|
||||||
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(aceStepWorker, &AceStepWorker::songGenerated, this, &MainWindow::songGenerated);
|
connect(aceStepWorker, &AceStepWorker::songGenerated, this, &MainWindow::songGenerated);
|
||||||
connect(aceStepWorker, &AceStepWorker::generationError, this, &MainWindow::generationError);
|
connect(aceStepWorker, &AceStepWorker::generationError, this, &MainWindow::generationError);
|
||||||
connect(aceStepWorker, &AceStepWorker::progressUpdate, ui->progressBar, &QProgressBar::setValue);
|
connect(aceStepWorker, &AceStepWorker::progressUpdate, ui->progressBar, &QProgressBar::setValue);
|
||||||
|
|
||||||
// Connect double-click on song list for editing
|
// Connect double-click on song list for editing (works with QTableView too)
|
||||||
connect(ui->songListView, &QListView::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) {
|
||||||
|
|
@ -51,6 +59,9 @@ MainWindow::MainWindow(QWidget *parent)
|
||||||
|
|
||||||
MainWindow::~MainWindow()
|
MainWindow::~MainWindow()
|
||||||
{
|
{
|
||||||
|
// Auto-save playlist before closing
|
||||||
|
autoSavePlaylist();
|
||||||
|
|
||||||
saveSettings();
|
saveSettings();
|
||||||
delete ui;
|
delete ui;
|
||||||
}
|
}
|
||||||
|
|
@ -60,9 +71,20 @@ void MainWindow::setupUI()
|
||||||
// Setup song list view
|
// Setup song list view
|
||||||
ui->songListView->setModel(songModel);
|
ui->songListView->setModel(songModel);
|
||||||
|
|
||||||
// Make sure the list view is read-only (no inline editing)
|
// Make sure the table view is read-only (no inline editing)
|
||||||
ui->songListView->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
ui->songListView->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||||
|
|
||||||
|
// Hide headers for cleaner appearance
|
||||||
|
ui->songListView->horizontalHeader()->hide();
|
||||||
|
ui->songListView->verticalHeader()->hide();
|
||||||
|
|
||||||
|
// Configure column sizes
|
||||||
|
ui->songListView->setColumnWidth(0, 40); // Fixed width for play indicator column
|
||||||
|
ui->songListView->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); // Expand caption column
|
||||||
|
|
||||||
|
// Enable row selection and disable column selection
|
||||||
|
ui->songListView->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||||
|
|
||||||
// Add some default songs
|
// Add some default songs
|
||||||
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", "");
|
||||||
|
|
@ -84,8 +106,7 @@ 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",
|
jsonTemplate = settings.value("jsonTemplate", "{\n\t\"inference_steps\": 8,\n\t\"shift\": 3.0,\n\t\"vocal_language\": \"en\"\n}").toString();
|
||||||
"{\"inference_steps\": 8, \"shift\": 3.0, \"vocal_language\": \"en\"}").toString();
|
|
||||||
|
|
||||||
// Load shuffle mode
|
// Load shuffle mode
|
||||||
shuffleMode = settings.value("shuffleMode", false).toBool();
|
shuffleMode = settings.value("shuffleMode", false).toBool();
|
||||||
|
|
@ -217,7 +238,6 @@ void MainWindow::on_stopButton_clicked()
|
||||||
audioPlayer->stop();
|
audioPlayer->stop();
|
||||||
isPlaying = false;
|
isPlaying = false;
|
||||||
isPaused = false;
|
isPaused = false;
|
||||||
ui->statusLabel->setText("Stopped");
|
|
||||||
updateControls();
|
updateControls();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -251,24 +271,38 @@ void MainWindow::on_songListView_doubleClicked(const QModelIndex &index)
|
||||||
|
|
||||||
// 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
|
||||||
disconnect(ui->songListView, &QListView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked);
|
disconnect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked);
|
||||||
|
|
||||||
int row = index.row();
|
int row = index.row();
|
||||||
SongItem song = songModel->getSong(row);
|
|
||||||
|
|
||||||
SongDialog dialog(this, song.caption, song.lyrics);
|
// Different behavior based on which column was clicked
|
||||||
|
if (index.column() == 0) {
|
||||||
|
// Column 0 (play indicator): Stop current playback and play this song
|
||||||
|
if (isPlaying) {
|
||||||
|
audioPlayer->stop();
|
||||||
|
}
|
||||||
|
|
||||||
if (dialog.exec() == QDialog::Accepted) {
|
// Set this as the current song and generate/play it
|
||||||
QString caption = dialog.getCaption();
|
currentSongIndex = row;
|
||||||
QString lyrics = dialog.getLyrics();
|
generateAndPlayNext();
|
||||||
|
} else if (index.column() == 1) {
|
||||||
|
// Column 1 (caption): Edit the song
|
||||||
|
SongItem song = songModel->getSong(row);
|
||||||
|
|
||||||
// Update the model
|
SongDialog dialog(this, song.caption, song.lyrics);
|
||||||
songModel->setData(songModel->index(row, 0), caption, SongListModel::CaptionRole);
|
|
||||||
songModel->setData(songModel->index(row, 0), lyrics, SongListModel::LyricsRole);
|
if (dialog.exec() == QDialog::Accepted) {
|
||||||
|
QString caption = dialog.getCaption();
|
||||||
|
QString lyrics = dialog.getLyrics();
|
||||||
|
|
||||||
|
// Update the model - use column 1 for the song name
|
||||||
|
songModel->setData(songModel->index(row, 1), caption, SongListModel::CaptionRole);
|
||||||
|
songModel->setData(songModel->index(row, 1), lyrics, SongListModel::LyricsRole);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reconnect the signal after dialog is closed
|
// Reconnect the signal after dialog is closed
|
||||||
connect(ui->songListView, &QListView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked);
|
connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::on_removeSongButton_clicked()
|
void MainWindow::on_removeSongButton_clicked()
|
||||||
|
|
@ -276,21 +310,16 @@ 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)
|
||||||
int row = currentIndex.row();
|
int row = currentIndex.row();
|
||||||
|
|
||||||
QMessageBox::StandardButton reply;
|
songModel->removeSong(row);
|
||||||
reply = QMessageBox::question(this, "Remove Song", "Are you sure you want to remove this song?",
|
|
||||||
QMessageBox::Yes | QMessageBox::No);
|
|
||||||
|
|
||||||
if (reply == QMessageBox::Yes) {
|
// Select next item or previous if at end
|
||||||
songModel->removeSong(row);
|
int newRow = qMin(row, songModel->rowCount() - 1);
|
||||||
|
if (newRow >= 0) {
|
||||||
// Select next item or previous if at end
|
QModelIndex newIndex = songModel->index(newRow, 0);
|
||||||
int newRow = qMin(row, songModel->rowCount() - 1);
|
ui->songListView->setCurrentIndex(newIndex);
|
||||||
if (newRow >= 0) {
|
|
||||||
QModelIndex newIndex = songModel->index(newRow, 0);
|
|
||||||
ui->songListView->setCurrentIndex(newIndex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -375,6 +404,14 @@ void MainWindow::playbackStarted()
|
||||||
startNextSongGeneration();
|
startNextSongGeneration();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainWindow::highlightCurrentSong()
|
||||||
|
{
|
||||||
|
if (currentSongIndex >= 0 && currentSongIndex < songModel->rowCount()) {
|
||||||
|
// Update the model to show play icon for current song
|
||||||
|
songModel->setPlayingIndex(currentSongIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::songGenerated(const QString &filePath)
|
void MainWindow::songGenerated(const QString &filePath)
|
||||||
{
|
{
|
||||||
if (!QFile::exists(filePath)) {
|
if (!QFile::exists(filePath)) {
|
||||||
|
|
@ -389,11 +426,14 @@ void MainWindow::songGenerated(const QString &filePath)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ui->statusLabel->setText("Playing: " + QFileInfo(filePath).baseName());
|
ui->statusLabel->setText("");
|
||||||
|
|
||||||
// Play the generated song
|
// Play the generated song
|
||||||
audioPlayer->play(filePath);
|
audioPlayer->play(filePath);
|
||||||
|
|
||||||
|
// Highlight the current song in the list
|
||||||
|
highlightCurrentSong();
|
||||||
|
|
||||||
// Connect position and duration updates for the slider
|
// Connect position and duration updates for the slider
|
||||||
connect(audioPlayer, &AudioPlayer::positionChanged, this, &MainWindow::updatePosition);
|
connect(audioPlayer, &AudioPlayer::positionChanged, this, &MainWindow::updatePosition);
|
||||||
connect(audioPlayer, &AudioPlayer::durationChanged, this, &MainWindow::updateDuration);
|
connect(audioPlayer, &AudioPlayer::durationChanged, this, &MainWindow::updateDuration);
|
||||||
|
|
@ -405,7 +445,6 @@ void MainWindow::playNextSong()
|
||||||
|
|
||||||
// Check if we have a pre-generated next song
|
// Check if we have a pre-generated next song
|
||||||
if (!nextSongFilePath.isEmpty()) {
|
if (!nextSongFilePath.isEmpty()) {
|
||||||
ui->statusLabel->setText("Playing: " + QFileInfo(nextSongFilePath).baseName());
|
|
||||||
audioPlayer->play(nextSongFilePath);
|
audioPlayer->play(nextSongFilePath);
|
||||||
nextSongFilePath.clear();
|
nextSongFilePath.clear();
|
||||||
|
|
||||||
|
|
@ -415,7 +454,8 @@ void MainWindow::playNextSong()
|
||||||
currentSongIndex = nextIndex;
|
currentSongIndex = nextIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start generating the song after this one
|
// Highlight the current song and start generating the next one
|
||||||
|
highlightCurrentSong();
|
||||||
startNextSongGeneration();
|
startNextSongGeneration();
|
||||||
} else {
|
} else {
|
||||||
// Find next song index and generate it
|
// Find next song index and generate it
|
||||||
|
|
@ -428,7 +468,6 @@ void MainWindow::playNextSong()
|
||||||
// No more songs
|
// No more songs
|
||||||
isPlaying = false;
|
isPlaying = false;
|
||||||
isPaused = false;
|
isPaused = false;
|
||||||
ui->statusLabel->setText("Finished playback");
|
|
||||||
updateControls();
|
updateControls();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -488,3 +527,189 @@ void MainWindow::on_positionSlider_sliderMoved(int position)
|
||||||
audioPlayer->setPosition(position);
|
audioPlayer->setPosition(position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Playlist save/load methods
|
||||||
|
void MainWindow::on_actionSavePlaylist()
|
||||||
|
{
|
||||||
|
QString filePath = QFileDialog::getSaveFileName(this, "Save Playlist",
|
||||||
|
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/playlist.json",
|
||||||
|
"JSON Files (*.json);;All Files (*)");
|
||||||
|
|
||||||
|
if (!filePath.isEmpty()) {
|
||||||
|
savePlaylist(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_actionLoadPlaylist()
|
||||||
|
{
|
||||||
|
QString filePath = QFileDialog::getOpenFileName(this, "Load Playlist",
|
||||||
|
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation),
|
||||||
|
"JSON Files (*.json);;All Files (*)");
|
||||||
|
|
||||||
|
if (!filePath.isEmpty()) {
|
||||||
|
loadPlaylist();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::savePlaylist(const QString &filePath)
|
||||||
|
{
|
||||||
|
// Get current songs from the model
|
||||||
|
QList<SongItem> songs;
|
||||||
|
for (int i = 0; i < songModel->rowCount(); ++i) {
|
||||||
|
songs.append(songModel->getSong(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
savePlaylistToJson(filePath, songs);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::loadPlaylist()
|
||||||
|
{
|
||||||
|
QString filePath = QFileDialog::getOpenFileName(this, "Load Playlist",
|
||||||
|
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation),
|
||||||
|
"JSON Files (*.json);;All Files (*)");
|
||||||
|
|
||||||
|
if (!filePath.isEmpty()) {
|
||||||
|
QList<SongItem> songs;
|
||||||
|
if (loadPlaylistFromJson(filePath, songs)) {
|
||||||
|
// Clear current playlist
|
||||||
|
while (songModel->rowCount() > 0) {
|
||||||
|
songModel->removeSong(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add loaded songs
|
||||||
|
for (const SongItem &song : songs) {
|
||||||
|
songModel->addSong(song);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::autoSavePlaylist()
|
||||||
|
{
|
||||||
|
QString configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation);
|
||||||
|
QString appConfigPath = configPath + "/MusicGenerator/AceStepGUI";
|
||||||
|
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
QDir().mkpath(appConfigPath);
|
||||||
|
|
||||||
|
QString filePath = appConfigPath + "/playlist.json";
|
||||||
|
|
||||||
|
// Get current songs from the model
|
||||||
|
QList<SongItem> songs;
|
||||||
|
for (int i = 0; i < songModel->rowCount(); ++i) {
|
||||||
|
songs.append(songModel->getSong(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
savePlaylistToJson(filePath, songs);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::autoLoadPlaylist()
|
||||||
|
{
|
||||||
|
QString configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation);
|
||||||
|
QString appConfigPath = configPath + "/MusicGenerator/AceStepGUI";
|
||||||
|
QString filePath = appConfigPath + "/playlist.json";
|
||||||
|
|
||||||
|
// Check if the auto-save file exists
|
||||||
|
if (QFile::exists(filePath)) {
|
||||||
|
QList<SongItem> songs;
|
||||||
|
if (loadPlaylistFromJson(filePath, songs)) {
|
||||||
|
// Clear default songs and add loaded ones
|
||||||
|
while (songModel->rowCount() > 0) {
|
||||||
|
songModel->removeSong(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const SongItem &song : songs) {
|
||||||
|
songModel->addSong(song);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MainWindow::savePlaylistToJson(const QString &filePath, const QList<SongItem> &songs)
|
||||||
|
{
|
||||||
|
QJsonArray songsArray;
|
||||||
|
|
||||||
|
for (const SongItem &song : songs) {
|
||||||
|
QJsonObject songObj;
|
||||||
|
songObj["caption"] = song.caption;
|
||||||
|
songObj["lyrics"] = song.lyrics;
|
||||||
|
songsArray.append(songObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject rootObj;
|
||||||
|
rootObj["songs"] = songsArray;
|
||||||
|
rootObj["version"] = "1.0";
|
||||||
|
|
||||||
|
QJsonDocument doc(rootObj);
|
||||||
|
QByteArray jsonData = doc.toJson();
|
||||||
|
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||||
|
qWarning() << "Could not open file for writing:" << filePath;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.write(jsonData);
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList<SongItem> &songs)
|
||||||
|
{
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
qWarning() << "Could not open file for reading:" << filePath;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray jsonData = file.readAll();
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
QJsonParseError parseError;
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &parseError);
|
||||||
|
|
||||||
|
if (parseError.error != QJsonParseError::NoError) {
|
||||||
|
qWarning() << "JSON parse error:" << parseError.errorString();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc.isObject()) {
|
||||||
|
qWarning() << "JSON root is not an object";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject rootObj = doc.object();
|
||||||
|
|
||||||
|
// Check for version compatibility
|
||||||
|
if (rootObj.contains("version") && rootObj["version"].toString() != "1.0") {
|
||||||
|
qWarning() << "Unsupported playlist version:" << rootObj["version"].toString();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rootObj.contains("songs") || !rootObj["songs"].isArray()) {
|
||||||
|
qWarning() << "Invalid playlist format: missing songs array";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonArray songsArray = rootObj["songs"].toArray();
|
||||||
|
|
||||||
|
for (const QJsonValue &value : songsArray) {
|
||||||
|
if (!value.isObject()) continue;
|
||||||
|
|
||||||
|
QJsonObject songObj = value.toObject();
|
||||||
|
SongItem song;
|
||||||
|
|
||||||
|
if (songObj.contains("caption")) {
|
||||||
|
song.caption = songObj["caption"].toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (songObj.contains("lyrics")) {
|
||||||
|
song.lyrics = songObj["lyrics"].toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
songs.append(song);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
|
||||||
18
MainWindow.h
18
MainWindow.h
|
|
@ -5,6 +5,10 @@
|
||||||
#include <QListWidgetItem>
|
#include <QListWidgetItem>
|
||||||
#include <QStandardItemModel>
|
#include <QStandardItemModel>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonDocument>
|
||||||
#include "SongListModel.h"
|
#include "SongListModel.h"
|
||||||
#include "AudioPlayer.h"
|
#include "AudioPlayer.h"
|
||||||
#include "AceStepWorker.h"
|
#include "AceStepWorker.h"
|
||||||
|
|
@ -43,6 +47,9 @@ private slots:
|
||||||
void generationFinished();
|
void generationFinished();
|
||||||
void generationError(const QString &error);
|
void generationError(const QString &error);
|
||||||
|
|
||||||
|
void on_actionSavePlaylist();
|
||||||
|
void on_actionLoadPlaylist();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void startNextSongGeneration();
|
void startNextSongGeneration();
|
||||||
|
|
||||||
|
|
@ -72,8 +79,19 @@ private:
|
||||||
// Pre-generated song file path
|
// Pre-generated song file path
|
||||||
QString nextSongFilePath;
|
QString nextSongFilePath;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void highlightCurrentSong();
|
||||||
|
|
||||||
void loadSettings();
|
void loadSettings();
|
||||||
void saveSettings();
|
void saveSettings();
|
||||||
|
void loadPlaylist();
|
||||||
|
void savePlaylist(const QString &filePath);
|
||||||
|
void autoSavePlaylist();
|
||||||
|
void autoLoadPlaylist();
|
||||||
|
|
||||||
|
bool savePlaylistToJson(const QString &filePath, const QList<SongItem> &songs);
|
||||||
|
bool loadPlaylistFromJson(const QString &filePath, QList<SongItem> &songs);
|
||||||
|
|
||||||
void setupUI();
|
void setupUI();
|
||||||
void updateControls();
|
void updateControls();
|
||||||
void generateAndPlayNext();
|
void generateAndPlayNext();
|
||||||
|
|
|
||||||
100
MainWindow.ui
100
MainWindow.ui
|
|
@ -11,7 +11,7 @@
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
<string>Music Generator GUI</string>
|
<string>Aceradio</string>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QWidget" name="centralwidget">
|
<widget class="QWidget" name="centralwidget">
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
|
@ -21,13 +21,12 @@
|
||||||
<string>Music Generator</string>
|
<string>Music Generator</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="alignment">
|
<property name="alignment">
|
||||||
<set>Qt::AlignCenter</set>
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
</property>
|
</property>
|
||||||
|
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QListView" name="songListView">
|
<widget class="QTableView" name="songListView">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>0</width>
|
<width>0</width>
|
||||||
|
|
@ -35,7 +34,10 @@
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="editTriggers">
|
<property name="editTriggers">
|
||||||
<set>QAbstractItemView::NoEditTriggers</set>
|
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
|
||||||
|
</property>
|
||||||
|
<property name="selectionBehavior">
|
||||||
|
<enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|
@ -58,7 +60,7 @@
|
||||||
<item>
|
<item>
|
||||||
<spacer name="horizontalSpacer">
|
<spacer name="horizontalSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Orientation::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizeHint" stdset="0">
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
|
|
@ -81,8 +83,14 @@
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QSlider" name="positionSlider">
|
<widget class="QSlider" name="positionSlider">
|
||||||
|
<property name="mouseTracking">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="tabletTracking">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Orientation::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|
@ -98,10 +106,10 @@
|
||||||
<item>
|
<item>
|
||||||
<widget class="QFrame" name="controlsFrame">
|
<widget class="QFrame" name="controlsFrame">
|
||||||
<property name="frameShape">
|
<property name="frameShape">
|
||||||
<enum>QFrame::StyledPanel</enum>
|
<enum>QFrame::Shape::StyledPanel</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="frameShadow">
|
<property name="frameShadow">
|
||||||
<enum>QFrame::Raised</enum>
|
<enum>QFrame::Shadow::Raised</enum>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QHBoxLayout" name="controlsLayout">
|
<layout class="QHBoxLayout" name="controlsLayout">
|
||||||
<item>
|
<item>
|
||||||
|
|
@ -110,9 +118,7 @@
|
||||||
<string>Play</string>
|
<string>Play</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset theme="media-playback-start">
|
<iconset theme="media-playback-start"/>
|
||||||
<normaloff>.</normaloff>.
|
|
||||||
</iconset>
|
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|
@ -122,9 +128,7 @@
|
||||||
<string>Pause</string>
|
<string>Pause</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset theme="media-playback-pause">
|
<iconset theme="media-playback-pause"/>
|
||||||
<normaloff>.</normaloff>.
|
|
||||||
</iconset>
|
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|
@ -134,9 +138,7 @@
|
||||||
<string>Skip</string>
|
<string>Skip</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset theme="media-skip-forward">
|
<iconset theme="media-skip-forward"/>
|
||||||
<normaloff>.</normaloff>.
|
|
||||||
</iconset>
|
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|
@ -146,9 +148,7 @@
|
||||||
<string>Stop</string>
|
<string>Stop</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset theme="media-playback-stop">
|
<iconset theme="media-playback-stop"/>
|
||||||
<normaloff>.</normaloff>.
|
|
||||||
</iconset>
|
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|
@ -157,20 +157,18 @@
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Shuffle</string>
|
<string>Shuffle</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="media-playlist-shuffle"/>
|
||||||
|
</property>
|
||||||
<property name="checkable">
|
<property name="checkable">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
|
||||||
<iconset theme="media-playlist-shuffle">
|
|
||||||
<normaloff>.</normaloff>.
|
|
||||||
</iconset>
|
|
||||||
</property>
|
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<spacer name="horizontalSpacer_2">
|
<spacer name="horizontalSpacer_2">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Orientation::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizeHint" stdset="0">
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
|
|
@ -195,7 +193,7 @@
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="statusLabel">
|
<widget class="QLabel" name="statusLabel">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Ready</string>
|
<string/>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|
@ -209,21 +207,63 @@
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>800</width>
|
<width>800</width>
|
||||||
<height>22</height>
|
<height>32</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
|
<widget class="QMenu" name="menuFile">
|
||||||
|
<property name="title">
|
||||||
|
<string>File</string>
|
||||||
|
</property>
|
||||||
|
<addaction name="actionSavePlaylist"/>
|
||||||
|
<addaction name="actionLoadPlaylist"/>
|
||||||
|
<addaction name="actionQuit"/>
|
||||||
|
</widget>
|
||||||
<widget class="QMenu" name="menuSettings">
|
<widget class="QMenu" name="menuSettings">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>Settings</string>
|
<string>Settings</string>
|
||||||
</property>
|
</property>
|
||||||
<addaction name="actionAdvancedSettings"/>
|
<addaction name="actionAdvancedSettings"/>
|
||||||
</widget>
|
</widget>
|
||||||
|
<addaction name="menuFile"/>
|
||||||
<addaction name="menuSettings"/>
|
<addaction name="menuSettings"/>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QStatusBar" name="statusbar"/>
|
<widget class="QStatusBar" name="statusbar"/>
|
||||||
|
<action name="actionSavePlaylist">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="QIcon::ThemeIcon::DocumentSaveAs"/>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Save Playlist</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl+S</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionLoadPlaylist">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="QIcon::ThemeIcon::DocumentOpen"/>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Load Playlist...</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl+O</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
<action name="actionAdvancedSettings">
|
<action name="actionAdvancedSettings">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Advanced Settings...</string>
|
<string>Ace Step</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionQuit">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="QIcon::ThemeIcon::ApplicationExit"/>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Quit</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl+Q</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
#include "SongListModel.h"
|
#include "SongListModel.h"
|
||||||
|
#include <QApplication>
|
||||||
#include <QTime>
|
#include <QTime>
|
||||||
#include <QRandomGenerator>
|
#include <QRandomGenerator>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QFont>
|
||||||
|
|
||||||
SongListModel::SongListModel(QObject *parent)
|
SongListModel::SongListModel(QObject *parent)
|
||||||
: QAbstractListModel(parent)
|
: QAbstractTableModel(parent),
|
||||||
|
m_playingIndex(-1)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -15,6 +18,12 @@ int SongListModel::rowCount(const QModelIndex &parent) const
|
||||||
return songList.size();
|
return songList.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int SongListModel::columnCount(const QModelIndex &parent) const
|
||||||
|
{
|
||||||
|
// We have 2 columns: play indicator and song name
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
QVariant SongListModel::data(const QModelIndex &index, int role) const
|
QVariant SongListModel::data(const QModelIndex &index, int role) const
|
||||||
{
|
{
|
||||||
if (!index.isValid() || index.row() >= songList.size())
|
if (!index.isValid() || index.row() >= songList.size())
|
||||||
|
|
@ -24,13 +33,40 @@ QVariant SongListModel::data(const QModelIndex &index, int role) const
|
||||||
|
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case Qt::DisplayRole:
|
case Qt::DisplayRole:
|
||||||
|
// Column 0: Play indicator column
|
||||||
|
if (index.column() == 0) {
|
||||||
|
return index.row() == m_playingIndex ? "▶" : "";
|
||||||
|
}
|
||||||
|
// Column 1: Song name
|
||||||
|
else if (index.column() == 1) {
|
||||||
|
return song.caption;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Qt::FontRole:
|
||||||
|
// Make play indicator bold and larger
|
||||||
|
if (index.column() == 0 && index.row() == m_playingIndex) {
|
||||||
|
QFont font = QApplication::font();
|
||||||
|
font.setBold(true);
|
||||||
|
return font;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Qt::TextAlignmentRole:
|
||||||
|
// Center align the play indicator
|
||||||
|
if (index.column() == 0) {
|
||||||
|
return Qt::AlignCenter;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case CaptionRole:
|
case CaptionRole:
|
||||||
return song.caption;
|
return song.caption;
|
||||||
case LyricsRole:
|
case LyricsRole:
|
||||||
return song.lyrics;
|
return song.lyrics;
|
||||||
|
case IsPlayingRole:
|
||||||
|
return index.row() == m_playingIndex;
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return QVariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SongListModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
bool SongListModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
||||||
|
|
@ -88,6 +124,30 @@ SongItem SongListModel::getSong(int index) const
|
||||||
return SongItem();
|
return SongItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QVariant SongListModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||||
|
{
|
||||||
|
if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
|
||||||
|
// Hide headers since we don't need column titles
|
||||||
|
return QVariant();
|
||||||
|
}
|
||||||
|
return QAbstractTableModel::headerData(section, orientation, role);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SongListModel::setPlayingIndex(int index)
|
||||||
|
{
|
||||||
|
int oldPlayingIndex = m_playingIndex;
|
||||||
|
m_playingIndex = index;
|
||||||
|
|
||||||
|
// Update both the old and new playing indices to trigger UI updates
|
||||||
|
if (oldPlayingIndex >= 0 && oldPlayingIndex < songList.size()) {
|
||||||
|
emit dataChanged(this->index(oldPlayingIndex, 0), this->index(oldPlayingIndex, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index >= 0 && index < songList.size()) {
|
||||||
|
emit dataChanged(this->index(index, 0), this->index(index, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int SongListModel::findNextIndex(int currentIndex, bool shuffle) const
|
int SongListModel::findNextIndex(int currentIndex, bool shuffle) const
|
||||||
{
|
{
|
||||||
if (songList.isEmpty())
|
if (songList.isEmpty())
|
||||||
|
|
|
||||||
|
|
@ -14,21 +14,24 @@ public:
|
||||||
: caption(caption), lyrics(lyrics) {}
|
: caption(caption), lyrics(lyrics) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
class SongListModel : public QAbstractListModel
|
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,
|
||||||
|
IsPlayingRole = Qt::UserRole + 3
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit SongListModel(QObject *parent = nullptr);
|
explicit SongListModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
// Basic functionality:
|
// Basic functionality:
|
||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
|
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
|
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
// Editable:
|
// Editable:
|
||||||
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
|
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
|
||||||
|
|
@ -40,8 +43,13 @@ public:
|
||||||
SongItem getSong(int index) const;
|
SongItem getSong(int index) const;
|
||||||
int findNextIndex(int currentIndex, bool shuffle = false) const;
|
int findNextIndex(int currentIndex, bool shuffle = false) const;
|
||||||
|
|
||||||
|
// Playing indicator
|
||||||
|
void setPlayingIndex(int index);
|
||||||
|
int playingIndex() const { return m_playingIndex; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QList<SongItem> songList;
|
QList<SongItem> songList;
|
||||||
|
int m_playingIndex;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // SONGLISTMODEL_H
|
#endif // SONGLISTMODEL_H
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue