feat: add playlist browse/search discovery and follow controls
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
This commit is contained in:
@@ -100,9 +100,29 @@ void QobuzBackend::getGenres()
|
||||
qobuz_backend_get_genres(m_backend);
|
||||
}
|
||||
|
||||
void QobuzBackend::getFeaturedAlbums(qint64 genreId, const QString &kind, quint32 limit, quint32 offset)
|
||||
void QobuzBackend::getFeaturedAlbums(const QString &genreIds, const QString &kind, quint32 limit, quint32 offset)
|
||||
{
|
||||
qobuz_backend_get_featured_albums(m_backend, genreId, kind.toUtf8().constData(), limit, offset);
|
||||
qobuz_backend_get_featured_albums(m_backend, genreIds.toUtf8().constData(), kind.toUtf8().constData(), limit, offset);
|
||||
}
|
||||
|
||||
void QobuzBackend::getFeaturedPlaylists(const QString &genreIds, const QString &kind, quint32 limit, quint32 offset)
|
||||
{
|
||||
qobuz_backend_get_featured_playlists(m_backend, genreIds.toUtf8().constData(), kind.toUtf8().constData(), limit, offset);
|
||||
}
|
||||
|
||||
void QobuzBackend::discoverPlaylists(const QString &genreIds, const QString &tags, quint32 limit, quint32 offset)
|
||||
{
|
||||
qobuz_backend_discover_playlists(
|
||||
m_backend,
|
||||
genreIds.toUtf8().constData(),
|
||||
tags.toUtf8().constData(),
|
||||
limit,
|
||||
offset);
|
||||
}
|
||||
|
||||
void QobuzBackend::searchPlaylists(const QString &query, quint32 limit, quint32 offset)
|
||||
{
|
||||
qobuz_backend_search_playlists(m_backend, query.toUtf8().constData(), limit, offset);
|
||||
}
|
||||
|
||||
// ---- favorites ----
|
||||
@@ -166,6 +186,16 @@ void QobuzBackend::deleteTrackFromPlaylist(qint64 playlistId, qint64 playlistTra
|
||||
qobuz_backend_delete_track_from_playlist(m_backend, playlistId, playlistTrackId);
|
||||
}
|
||||
|
||||
void QobuzBackend::subscribePlaylist(qint64 playlistId)
|
||||
{
|
||||
qobuz_backend_subscribe_playlist(m_backend, playlistId);
|
||||
}
|
||||
|
||||
void QobuzBackend::unsubscribePlaylist(qint64 playlistId)
|
||||
{
|
||||
qobuz_backend_unsubscribe_playlist(m_backend, playlistId);
|
||||
}
|
||||
|
||||
// ---- fav modification ----
|
||||
|
||||
void QobuzBackend::addFavTrack(qint64 trackId)
|
||||
@@ -302,6 +332,15 @@ void QobuzBackend::onEvent(int eventType, const QString &json)
|
||||
case 28: // EV_FEATURED_ALBUMS_OK
|
||||
emit featuredAlbumsLoaded(obj);
|
||||
break;
|
||||
case 30: // EV_FEATURED_PLAYLISTS_OK
|
||||
emit featuredPlaylistsLoaded(obj);
|
||||
break;
|
||||
case 31: // EV_DISCOVER_PLAYLISTS_OK
|
||||
emit discoverPlaylistsLoaded(obj);
|
||||
break;
|
||||
case 32: // EV_PLAYLIST_SEARCH_OK
|
||||
emit playlistSearchLoaded(obj);
|
||||
break;
|
||||
case EV_ARTIST_ERR:
|
||||
emit error(obj["error"].toString());
|
||||
break;
|
||||
@@ -338,6 +377,12 @@ void QobuzBackend::onEvent(int eventType, const QString &json)
|
||||
case 22: // EV_PLAYLIST_TRACK_ADDED
|
||||
emit playlistTrackAdded(static_cast<qint64>(obj["playlist_id"].toDouble()));
|
||||
break;
|
||||
case 33: // EV_PLAYLIST_SUBSCRIBED
|
||||
emit playlistSubscribed(static_cast<qint64>(obj["playlist_id"].toDouble()));
|
||||
break;
|
||||
case 34: // EV_PLAYLIST_UNSUBSCRIBED
|
||||
emit playlistUnsubscribed(static_cast<qint64>(obj["playlist_id"].toDouble()));
|
||||
break;
|
||||
case EV_USER_OK:
|
||||
emit userLoaded(obj);
|
||||
break;
|
||||
|
||||
@@ -36,7 +36,10 @@ public:
|
||||
void getAlbumsTracks(const QStringList &albumIds);
|
||||
void getPlaylist(qint64 playlistId, quint32 offset = 0, quint32 limit = 500);
|
||||
void getGenres();
|
||||
void getFeaturedAlbums(qint64 genreId, const QString &kind, quint32 limit = 50, quint32 offset = 0);
|
||||
void getFeaturedAlbums(const QString &genreIds, const QString &kind, quint32 limit = 50, quint32 offset = 0);
|
||||
void getFeaturedPlaylists(const QString &genreIds, const QString &kind, quint32 limit = 25, quint32 offset = 0);
|
||||
void discoverPlaylists(const QString &genreIds, const QString &tags = QString(), quint32 limit = 25, quint32 offset = 0);
|
||||
void searchPlaylists(const QString &query, quint32 limit = 8, quint32 offset = 0);
|
||||
|
||||
// --- favorites ---
|
||||
void getFavTracks(quint32 offset = 0, quint32 limit = 500);
|
||||
@@ -54,6 +57,8 @@ public:
|
||||
void deletePlaylist(qint64 playlistId);
|
||||
void addTrackToPlaylist(qint64 playlistId, qint64 trackId);
|
||||
void deleteTrackFromPlaylist(qint64 playlistId, qint64 playlistTrackId);
|
||||
void subscribePlaylist(qint64 playlistId);
|
||||
void unsubscribePlaylist(qint64 playlistId);
|
||||
|
||||
// --- fav modification ---
|
||||
void addFavTrack(qint64 trackId);
|
||||
@@ -93,10 +98,15 @@ signals:
|
||||
void dynamicSuggestionsLoaded(const QJsonObject &result);
|
||||
void genresLoaded(const QJsonObject &result);
|
||||
void featuredAlbumsLoaded(const QJsonObject &result);
|
||||
void featuredPlaylistsLoaded(const QJsonObject &result);
|
||||
void discoverPlaylistsLoaded(const QJsonObject &result);
|
||||
void playlistSearchLoaded(const QJsonObject &result);
|
||||
void playlistLoaded(const QJsonObject &playlist);
|
||||
void playlistCreated(const QJsonObject &playlist);
|
||||
void playlistDeleted(const QJsonObject &result);
|
||||
void playlistTrackAdded(qint64 playlistId);
|
||||
void playlistSubscribed(qint64 playlistId);
|
||||
void playlistUnsubscribed(qint64 playlistId);
|
||||
|
||||
// favorites
|
||||
void favTracksLoaded(const QJsonObject &result);
|
||||
|
||||
@@ -21,6 +21,7 @@ enum NodeType {
|
||||
NodeFavArtists,
|
||||
NodePlaylist,
|
||||
NodeBrowseGenres,
|
||||
NodeBrowsePlaylists,
|
||||
};
|
||||
|
||||
Library::Library(QobuzBackend *backend, QWidget *parent)
|
||||
@@ -79,6 +80,9 @@ void Library::buildStaticNodes()
|
||||
|
||||
auto *genresItem = new QTreeWidgetItem(m_browseNode, QStringList{tr("Genres")});
|
||||
genresItem->setData(0, TypeRole, NodeBrowseGenres);
|
||||
|
||||
auto *playlistsItem = new QTreeWidgetItem(m_browseNode, QStringList{tr("Playlists")});
|
||||
playlistsItem->setData(0, TypeRole, NodeBrowsePlaylists);
|
||||
}
|
||||
|
||||
void Library::refresh()
|
||||
@@ -176,6 +180,7 @@ void Library::onItemClicked(QTreeWidgetItem *item, int)
|
||||
case NodeFavAlbums: emit favAlbumsRequested(); break;
|
||||
case NodeFavArtists: emit favArtistsRequested(); break;
|
||||
case NodeBrowseGenres: emit browseGenresRequested(); break;
|
||||
case NodeBrowsePlaylists: emit browsePlaylistsRequested(); break;
|
||||
case NodePlaylist: {
|
||||
const qint64 id = item->data(0, IdRole).toLongLong();
|
||||
const QString name = item->data(0, NameRole).toString();
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace List
|
||||
void favAlbumsRequested();
|
||||
void favArtistsRequested();
|
||||
void browseGenresRequested();
|
||||
void browsePlaylistsRequested();
|
||||
void playlistRequested(qint64 playlistId, const QString &name);
|
||||
/// Emitted after playlists are loaded so others can cache the list.
|
||||
void userPlaylistsChanged(const QVector<QPair<qint64, QString>> &playlists);
|
||||
|
||||
@@ -127,6 +127,18 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
|
||||
m_backend->getPlaylist(playlistId);
|
||||
statusBar()->showMessage(tr("Track added to playlist"), 3000);
|
||||
});
|
||||
connect(m_backend, &QobuzBackend::playlistSubscribed, this, [this](qint64 playlistId) {
|
||||
m_library->refresh();
|
||||
if (m_content->tracksList()->playlistId() == playlistId)
|
||||
m_content->setCurrentPlaylistFollowed(true);
|
||||
statusBar()->showMessage(tr("Playlist followed"), 3000);
|
||||
});
|
||||
connect(m_backend, &QobuzBackend::playlistUnsubscribed, this, [this](qint64 playlistId) {
|
||||
m_library->refresh();
|
||||
if (m_content->tracksList()->playlistId() == playlistId)
|
||||
m_content->setCurrentPlaylistFollowed(false);
|
||||
statusBar()->showMessage(tr("Playlist unfollowed"), 3000);
|
||||
});
|
||||
connect(m_backend, &QobuzBackend::trackChanged, this, &MainWindow::onTrackChanged);
|
||||
connect(m_backend, &QobuzBackend::error, this, [this](const QString &msg) {
|
||||
statusBar()->showMessage(tr("Error: %1").arg(msg), 6000);
|
||||
@@ -210,6 +222,10 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
|
||||
m_content->showGenreBrowser();
|
||||
statusBar()->showMessage(tr("Browse Genres"));
|
||||
});
|
||||
connect(m_library, &List::Library::browsePlaylistsRequested, this, [this] {
|
||||
m_content->showPlaylistBrowser();
|
||||
statusBar()->showMessage(tr("Browse Playlists"));
|
||||
});
|
||||
|
||||
// ---- Track list → playback / playlist management ----
|
||||
connect(m_content->tracksList(), &List::Tracks::playTrackRequested,
|
||||
@@ -243,6 +259,18 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
|
||||
this, &MainWindow::onSearchAlbumSelected);
|
||||
connect(m_content, &MainContent::artistRequested,
|
||||
this, &MainWindow::onSearchArtistSelected);
|
||||
connect(m_content, &MainContent::playlistRequested,
|
||||
this, [this](qint64 playlistId) {
|
||||
m_backend->getPlaylist(playlistId);
|
||||
statusBar()->showMessage(tr("Loading playlist…"));
|
||||
});
|
||||
connect(m_content, &MainContent::playlistFollowToggled,
|
||||
this, [this](qint64 playlistId, bool follow) {
|
||||
if (follow)
|
||||
m_backend->subscribePlaylist(playlistId);
|
||||
else
|
||||
m_backend->unsubscribePlaylist(playlistId);
|
||||
});
|
||||
connect(m_content, &MainContent::playTrackRequested,
|
||||
this, &MainWindow::onPlayTrackRequested);
|
||||
|
||||
@@ -462,7 +490,20 @@ void MainWindow::onArtistLoaded(const QJsonObject &artist)
|
||||
|
||||
void MainWindow::onPlaylistLoaded(const QJsonObject &playlist)
|
||||
{
|
||||
m_content->showPlaylist(playlist);
|
||||
const qint64 id = static_cast<qint64>(playlist["id"].toDouble());
|
||||
const qint64 ownerId = static_cast<qint64>(playlist["owner"].toObject()["id"].toDouble());
|
||||
const qint64 myId = AppSettings::instance().userId();
|
||||
const bool isOwned = (myId > 0 && ownerId == myId);
|
||||
|
||||
bool isFollowed = false;
|
||||
for (const auto &pl : m_userPlaylists) {
|
||||
if (pl.first == id) {
|
||||
isFollowed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
m_content->showPlaylist(playlist, isFollowed, isOwned);
|
||||
statusBar()->showMessage(
|
||||
tr("Playlist: %1").arg(playlist["name"].toString()), 4000);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
#include "genrebrowser.hpp"
|
||||
|
||||
#include <QAction>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QHeaderView>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QMenu>
|
||||
#include <QPushButton>
|
||||
#include <QSignalBlocker>
|
||||
#include <QTreeWidgetItem>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
GenreBrowserView::GenreBrowserView(QobuzBackend *backend, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_backend(backend)
|
||||
@@ -19,41 +27,127 @@ GenreBrowserView::GenreBrowserView(QobuzBackend *backend, QWidget *parent)
|
||||
auto *topLayout = new QHBoxLayout(topBar);
|
||||
topLayout->setContentsMargins(8, 6, 8, 6);
|
||||
|
||||
topLayout->addWidget(new QLabel(tr("Genre:"), this));
|
||||
m_browseLabel = new QLabel(tr("Show:"), this);
|
||||
topLayout->addWidget(m_browseLabel);
|
||||
m_kindCombo = new QComboBox(this);
|
||||
m_kindCombo->addItem(tr("Albums"), QStringLiteral("albums"));
|
||||
m_kindCombo->addItem(tr("Playlists"), QStringLiteral("playlists"));
|
||||
topLayout->addWidget(m_kindCombo);
|
||||
|
||||
m_gapAfterKind = new QWidget(this);
|
||||
m_gapAfterKind->setFixedWidth(12);
|
||||
topLayout->addWidget(m_gapAfterKind);
|
||||
|
||||
m_genreLabel = new QLabel(tr("Genre:"), this);
|
||||
topLayout->addWidget(m_genreLabel);
|
||||
m_genreCombo = new QComboBox(this);
|
||||
m_genreCombo->setMinimumWidth(160);
|
||||
topLayout->addWidget(m_genreCombo);
|
||||
|
||||
topLayout->addSpacing(16);
|
||||
m_gapAfterGenre = new QWidget(this);
|
||||
m_gapAfterGenre->setFixedWidth(16);
|
||||
topLayout->addWidget(m_gapAfterGenre);
|
||||
|
||||
topLayout->addWidget(new QLabel(tr("Type:"), this));
|
||||
m_typeLabel = new QLabel(tr("Type:"), this);
|
||||
topLayout->addWidget(m_typeLabel);
|
||||
m_typeCombo = new QComboBox(this);
|
||||
m_typeCombo->addItem(tr("New Releases"), QStringLiteral("new-releases"));
|
||||
m_typeCombo->addItem(tr("Best Sellers"), QStringLiteral("best-sellers"));
|
||||
m_typeCombo->addItem(tr("Most Streamed"), QStringLiteral("most-streamed"));
|
||||
m_typeCombo->addItem(tr("Editor Picks"), QStringLiteral("editor-picks"));
|
||||
m_typeCombo->addItem(tr("Press Awards"), QStringLiteral("press-awards"));
|
||||
topLayout->addWidget(m_typeCombo);
|
||||
|
||||
m_playlistSearchLabel = new QLabel(tr("Search:"), this);
|
||||
m_playlistSearchLabel->setVisible(false);
|
||||
topLayout->addWidget(m_playlistSearchLabel);
|
||||
|
||||
m_playlistSearchBox = new QLineEdit(this);
|
||||
m_playlistSearchBox->setPlaceholderText(tr("Search playlists..."));
|
||||
m_playlistSearchBox->setClearButtonEnabled(true);
|
||||
m_playlistSearchBox->setVisible(false);
|
||||
m_playlistSearchBox->setMinimumWidth(180);
|
||||
m_playlistSearchBox->setMaximumWidth(320);
|
||||
topLayout->addWidget(m_playlistSearchBox);
|
||||
|
||||
m_playlistSearchBtn = new QPushButton(tr("Search"), this);
|
||||
m_playlistSearchBtn->setVisible(false);
|
||||
topLayout->addWidget(m_playlistSearchBtn);
|
||||
|
||||
topLayout->addStretch();
|
||||
layout->addWidget(topBar);
|
||||
|
||||
m_resultsStack = new QStackedWidget(this);
|
||||
|
||||
m_albumList = new AlbumListView(this);
|
||||
m_albumList->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
layout->addWidget(m_albumList, 1);
|
||||
|
||||
m_playlistList = new QTreeWidget(this);
|
||||
m_playlistList->setColumnCount(4);
|
||||
m_playlistList->setHeaderLabels({tr(""), tr("Playlist"), tr("Owner"), tr("Tracks")});
|
||||
m_playlistList->setRootIsDecorated(false);
|
||||
m_playlistList->setAlternatingRowColors(true);
|
||||
m_playlistList->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
m_playlistList->setSortingEnabled(true);
|
||||
m_playlistList->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
m_playlistList->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
||||
m_playlistList->header()->setSectionResizeMode(1, QHeaderView::Stretch);
|
||||
m_playlistList->header()->setSectionResizeMode(2, QHeaderView::Stretch);
|
||||
m_playlistList->header()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
|
||||
m_playlistList->header()->setStretchLastSection(false);
|
||||
|
||||
m_resultsStack->addWidget(m_albumList);
|
||||
m_resultsStack->addWidget(m_playlistList);
|
||||
layout->addWidget(m_resultsStack, 1);
|
||||
|
||||
connect(m_backend, &QobuzBackend::genresLoaded,
|
||||
this, &GenreBrowserView::onGenresLoaded);
|
||||
connect(m_backend, &QobuzBackend::featuredAlbumsLoaded,
|
||||
this, &GenreBrowserView::onFeaturedAlbumsLoaded);
|
||||
connect(m_backend, &QobuzBackend::featuredPlaylistsLoaded,
|
||||
this, &GenreBrowserView::onFeaturedPlaylistsLoaded);
|
||||
connect(m_backend, &QobuzBackend::discoverPlaylistsLoaded,
|
||||
this, &GenreBrowserView::onDiscoverPlaylistsLoaded);
|
||||
connect(m_backend, &QobuzBackend::playlistSearchLoaded,
|
||||
this, &GenreBrowserView::onPlaylistSearchLoaded);
|
||||
|
||||
connect(m_genreCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &GenreBrowserView::onSelectionChanged);
|
||||
this, [this](int index) {
|
||||
const QString data = m_genreCombo->itemData(index).toString();
|
||||
if (data == QStringLiteral("__multi__")) {
|
||||
if (!chooseMultiGenres()) {
|
||||
const QSignalBlocker blocker(m_genreCombo);
|
||||
m_genreCombo->setCurrentIndex(m_lastGenreComboIndex);
|
||||
return;
|
||||
}
|
||||
m_lastGenreComboIndex = index;
|
||||
updateMultiGenreLabel();
|
||||
} else {
|
||||
m_lastGenreComboIndex = index;
|
||||
if (data == QStringLiteral("__all__")) {
|
||||
m_multiGenreIds.clear();
|
||||
updateMultiGenreLabel();
|
||||
}
|
||||
}
|
||||
onSelectionChanged();
|
||||
});
|
||||
connect(m_kindCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, [this](int) {
|
||||
refreshGenreTypeChoices();
|
||||
onSelectionChanged();
|
||||
});
|
||||
connect(m_typeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &GenreBrowserView::onSelectionChanged);
|
||||
connect(m_playlistSearchBox, &QLineEdit::returnPressed,
|
||||
this, &GenreBrowserView::onSelectionChanged);
|
||||
connect(m_playlistSearchBtn, &QPushButton::clicked,
|
||||
this, &GenreBrowserView::onSelectionChanged);
|
||||
connect(m_albumList, &AlbumListView::albumSelected,
|
||||
this, &GenreBrowserView::albumSelected);
|
||||
connect(m_albumList, &QTreeWidget::customContextMenuRequested,
|
||||
this, &GenreBrowserView::onAlbumContextMenu);
|
||||
connect(m_playlistList, &QTreeWidget::itemDoubleClicked,
|
||||
this, &GenreBrowserView::onPlaylistActivated);
|
||||
connect(m_playlistList, &QTreeWidget::customContextMenuRequested,
|
||||
this, &GenreBrowserView::onPlaylistContextMenu);
|
||||
|
||||
m_kindCombo->setCurrentIndex(0);
|
||||
refreshModeUi();
|
||||
}
|
||||
|
||||
void GenreBrowserView::ensureGenresLoaded()
|
||||
@@ -62,11 +156,152 @@ void GenreBrowserView::ensureGenresLoaded()
|
||||
m_backend->getGenres();
|
||||
}
|
||||
|
||||
void GenreBrowserView::setBrowseMode(BrowseMode mode)
|
||||
{
|
||||
if (m_mode == mode)
|
||||
return;
|
||||
|
||||
m_mode = mode;
|
||||
refreshModeUi();
|
||||
onSelectionChanged();
|
||||
}
|
||||
|
||||
void GenreBrowserView::refreshModeUi()
|
||||
{
|
||||
const bool genreMode = (m_mode == BrowseMode::Genres);
|
||||
|
||||
m_browseLabel->setVisible(genreMode);
|
||||
m_kindCombo->setVisible(genreMode);
|
||||
m_gapAfterKind->setVisible(genreMode);
|
||||
m_genreLabel->setVisible(genreMode);
|
||||
m_genreCombo->setVisible(genreMode);
|
||||
m_gapAfterGenre->setVisible(genreMode);
|
||||
m_typeLabel->setVisible(genreMode);
|
||||
m_typeCombo->setVisible(genreMode);
|
||||
|
||||
if (genreMode) {
|
||||
m_playlistSearchBox->setVisible(false);
|
||||
m_playlistSearchLabel->setVisible(false);
|
||||
m_playlistSearchBtn->setVisible(false);
|
||||
refreshGenreTypeChoices();
|
||||
return;
|
||||
}
|
||||
|
||||
m_typeCombo->blockSignals(true);
|
||||
m_typeCombo->clear();
|
||||
m_typeCombo->addItem(tr("Search"), QStringLiteral("search"));
|
||||
m_typeCombo->blockSignals(false);
|
||||
m_playlistSearchLabel->setVisible(true);
|
||||
m_playlistSearchBox->setVisible(true);
|
||||
m_playlistSearchBtn->setVisible(true);
|
||||
m_resultsStack->setCurrentIndex(1);
|
||||
}
|
||||
|
||||
void GenreBrowserView::refreshGenreTypeChoices()
|
||||
{
|
||||
m_typeCombo->blockSignals(true);
|
||||
m_typeCombo->clear();
|
||||
|
||||
const QString kind = m_kindCombo->currentData().toString();
|
||||
if (kind == QStringLiteral("playlists")) {
|
||||
m_typeCombo->addItem(tr("Featured: Last Created"), QStringLiteral("last-created"));
|
||||
m_typeCombo->addItem(tr("Discover: New"), QStringLiteral("discover-new"));
|
||||
m_typeCombo->addItem(tr("Discover: Hi-Res"), QStringLiteral("discover-hires"));
|
||||
m_typeCombo->addItem(tr("Discover: Focus"), QStringLiteral("discover-focus"));
|
||||
m_typeCombo->addItem(tr("Discover: Qobuz Digs"), QStringLiteral("discover-qobuzdigs"));
|
||||
m_resultsStack->setCurrentIndex(1);
|
||||
} else {
|
||||
m_typeCombo->addItem(tr("New Releases"), QStringLiteral("new-releases"));
|
||||
m_typeCombo->addItem(tr("Best Sellers"), QStringLiteral("best-sellers"));
|
||||
m_typeCombo->addItem(tr("Most Streamed"), QStringLiteral("most-streamed"));
|
||||
m_typeCombo->addItem(tr("Editor Picks"), QStringLiteral("editor-picks"));
|
||||
m_typeCombo->addItem(tr("Press Awards"), QStringLiteral("press-awards"));
|
||||
m_resultsStack->setCurrentIndex(0);
|
||||
}
|
||||
|
||||
m_typeCombo->blockSignals(false);
|
||||
}
|
||||
|
||||
QString GenreBrowserView::currentGenreIds() const
|
||||
{
|
||||
const QString data = m_genreCombo->currentData().toString();
|
||||
if (data == QStringLiteral("__all__"))
|
||||
return QString();
|
||||
|
||||
if (data == QStringLiteral("__multi__")) {
|
||||
if (m_multiGenreIds.isEmpty())
|
||||
return QString();
|
||||
|
||||
QList<qint64> ids = m_multiGenreIds.values();
|
||||
std::sort(ids.begin(), ids.end());
|
||||
QStringList out;
|
||||
out.reserve(ids.size());
|
||||
for (qint64 id : ids)
|
||||
out.push_back(QString::number(id));
|
||||
return out.join(QLatin1Char(','));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
bool GenreBrowserView::chooseMultiGenres()
|
||||
{
|
||||
QDialog dlg(this);
|
||||
dlg.setWindowTitle(tr("Select genres"));
|
||||
dlg.resize(320, 420);
|
||||
|
||||
auto *layout = new QVBoxLayout(&dlg);
|
||||
auto *list = new QListWidget(&dlg);
|
||||
list->setAlternatingRowColors(true);
|
||||
layout->addWidget(list, 1);
|
||||
|
||||
for (int i = 0; i < m_genreCombo->count(); ++i) {
|
||||
const QString data = m_genreCombo->itemData(i).toString();
|
||||
if (data == QStringLiteral("__all__") || data == QStringLiteral("__multi__"))
|
||||
continue;
|
||||
|
||||
auto *item = new QListWidgetItem(m_genreCombo->itemText(i), list);
|
||||
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
||||
const qint64 id = data.toLongLong();
|
||||
item->setData(Qt::UserRole, id);
|
||||
item->setCheckState(m_multiGenreIds.contains(id) ? Qt::Checked : Qt::Unchecked);
|
||||
}
|
||||
|
||||
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg);
|
||||
layout->addWidget(buttons);
|
||||
connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
|
||||
|
||||
if (dlg.exec() != QDialog::Accepted)
|
||||
return false;
|
||||
|
||||
m_multiGenreIds.clear();
|
||||
for (int i = 0; i < list->count(); ++i) {
|
||||
QListWidgetItem *item = list->item(i);
|
||||
if (item->checkState() == Qt::Checked)
|
||||
m_multiGenreIds.insert(item->data(Qt::UserRole).toLongLong());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void GenreBrowserView::updateMultiGenreLabel()
|
||||
{
|
||||
const int multiIndex = m_genreCombo->count() - 1;
|
||||
if (multiIndex < 0)
|
||||
return;
|
||||
|
||||
const QString text = m_multiGenreIds.isEmpty()
|
||||
? tr("Multiple...")
|
||||
: tr("Multiple (%1)").arg(m_multiGenreIds.size());
|
||||
m_genreCombo->setItemText(multiIndex, text);
|
||||
}
|
||||
|
||||
void GenreBrowserView::onGenresLoaded(const QJsonObject &result)
|
||||
{
|
||||
m_genresLoaded = true;
|
||||
m_genreCombo->blockSignals(true);
|
||||
m_genreCombo->clear();
|
||||
m_genreCombo->addItem(tr("All genres"), QStringLiteral("__all__"));
|
||||
|
||||
const QJsonArray items = result["items"].toArray();
|
||||
for (const auto &value : items) {
|
||||
@@ -76,23 +311,81 @@ void GenreBrowserView::onGenresLoaded(const QJsonObject &result)
|
||||
static_cast<qint64>(genre["id"].toDouble()));
|
||||
}
|
||||
|
||||
m_genreCombo->addItem(tr("Multiple..."), QStringLiteral("__multi__"));
|
||||
updateMultiGenreLabel();
|
||||
m_lastGenreComboIndex = 0;
|
||||
m_genreCombo->setCurrentIndex(0);
|
||||
|
||||
m_genreCombo->blockSignals(false);
|
||||
onSelectionChanged();
|
||||
}
|
||||
|
||||
void GenreBrowserView::onFeaturedAlbumsLoaded(const QJsonObject &result)
|
||||
{
|
||||
m_resultsStack->setCurrentIndex(0);
|
||||
m_albumList->setAlbums(result["items"].toArray());
|
||||
}
|
||||
|
||||
void GenreBrowserView::onFeaturedPlaylistsLoaded(const QJsonObject &result)
|
||||
{
|
||||
m_resultsStack->setCurrentIndex(1);
|
||||
setPlaylistItems(result["items"].toArray());
|
||||
}
|
||||
|
||||
void GenreBrowserView::onDiscoverPlaylistsLoaded(const QJsonObject &result)
|
||||
{
|
||||
m_resultsStack->setCurrentIndex(1);
|
||||
setPlaylistItems(result["items"].toArray());
|
||||
}
|
||||
|
||||
void GenreBrowserView::onPlaylistSearchLoaded(const QJsonObject &result)
|
||||
{
|
||||
m_resultsStack->setCurrentIndex(1);
|
||||
setPlaylistItems(result["items"].toArray());
|
||||
}
|
||||
|
||||
void GenreBrowserView::onSelectionChanged()
|
||||
{
|
||||
if (m_mode == BrowseMode::PlaylistSearch) {
|
||||
m_resultsStack->setCurrentIndex(1);
|
||||
m_playlistSearchLabel->setVisible(true);
|
||||
m_playlistSearchBox->setVisible(true);
|
||||
m_playlistSearchBtn->setVisible(true);
|
||||
const QString query = m_playlistSearchBox->text().trimmed();
|
||||
if (query.size() < 2) {
|
||||
m_playlistList->clear();
|
||||
} else {
|
||||
m_backend->searchPlaylists(query, 8, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_genreCombo->count() == 0)
|
||||
return;
|
||||
|
||||
const qint64 genreId = m_genreCombo->currentData().toLongLong();
|
||||
const QString genreIds = currentGenreIds();
|
||||
const QString type = m_typeCombo->currentData().toString();
|
||||
m_backend->getFeaturedAlbums(genreId, type, 50, 0);
|
||||
const QString kind = m_kindCombo->currentData().toString();
|
||||
m_playlistSearchLabel->setVisible(false);
|
||||
m_playlistSearchBox->setVisible(false);
|
||||
m_playlistSearchBtn->setVisible(false);
|
||||
|
||||
if (kind == QStringLiteral("playlists")) {
|
||||
m_resultsStack->setCurrentIndex(1);
|
||||
if (type == QStringLiteral("discover-new"))
|
||||
m_backend->discoverPlaylists(genreIds, QStringLiteral("new"), 25, 0);
|
||||
else if (type == QStringLiteral("discover-hires"))
|
||||
m_backend->discoverPlaylists(genreIds, QStringLiteral("hi-res"), 25, 0);
|
||||
else if (type == QStringLiteral("discover-focus"))
|
||||
m_backend->discoverPlaylists(genreIds, QStringLiteral("focus"), 25, 0);
|
||||
else if (type == QStringLiteral("discover-qobuzdigs"))
|
||||
m_backend->discoverPlaylists(genreIds, QStringLiteral("qobuzdigs"), 25, 0);
|
||||
else
|
||||
m_backend->getFeaturedPlaylists(genreIds, type, 25, 0);
|
||||
} else {
|
||||
m_resultsStack->setCurrentIndex(0);
|
||||
m_backend->getFeaturedAlbums(genreIds, type, 50, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void GenreBrowserView::onAlbumContextMenu(const QPoint &pos)
|
||||
@@ -120,3 +413,55 @@ void GenreBrowserView::onAlbumContextMenu(const QPoint &pos)
|
||||
|
||||
menu.exec(m_albumList->viewport()->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
void GenreBrowserView::onPlaylistActivated(QTreeWidgetItem *item, int)
|
||||
{
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
const qint64 playlistId = item->data(0, Qt::UserRole).toLongLong();
|
||||
if (playlistId > 0)
|
||||
emit playlistSelected(playlistId);
|
||||
}
|
||||
|
||||
void GenreBrowserView::onPlaylistContextMenu(const QPoint &pos)
|
||||
{
|
||||
QTreeWidgetItem *item = m_playlistList->itemAt(pos);
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
const qint64 playlistId = item->data(0, Qt::UserRole).toLongLong();
|
||||
if (playlistId <= 0)
|
||||
return;
|
||||
|
||||
QMenu menu(this);
|
||||
auto *openPlaylist = menu.addAction(tr("Open Playlist"));
|
||||
connect(openPlaylist, &QAction::triggered, this, [this, playlistId] {
|
||||
emit playlistSelected(playlistId);
|
||||
});
|
||||
menu.exec(m_playlistList->viewport()->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
void GenreBrowserView::setPlaylistItems(const QJsonArray &items)
|
||||
{
|
||||
m_playlistList->clear();
|
||||
|
||||
QFont tagFont;
|
||||
tagFont.setBold(true);
|
||||
tagFont.setPointSizeF(tagFont.pointSizeF() * 0.85);
|
||||
|
||||
for (const auto &value : items) {
|
||||
const QJsonObject playlist = value.toObject();
|
||||
const qint64 playlistId = static_cast<qint64>(playlist["id"].toDouble());
|
||||
const QString name = playlist["name"].toString();
|
||||
const QString owner = playlist["owner"].toObject()["name"].toString();
|
||||
const int tracksCount = playlist["tracks_count"].toInt();
|
||||
|
||||
auto *item = new QTreeWidgetItem(m_playlistList,
|
||||
QStringList{QStringLiteral("P"), name, owner, tracksCount > 0 ? QString::number(tracksCount) : QString()});
|
||||
item->setData(0, Qt::UserRole, playlistId);
|
||||
item->setForeground(0, QColor(QStringLiteral("#2B7CD3")));
|
||||
item->setFont(0, tagFont);
|
||||
item->setTextAlignment(0, Qt::AlignCenter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,14 @@
|
||||
#include "albumlistview.hpp"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QSet>
|
||||
#include <QPushButton>
|
||||
#include <QStackedWidget>
|
||||
#include <QTreeWidget>
|
||||
#include <QWidget>
|
||||
|
||||
class GenreBrowserView : public QWidget
|
||||
@@ -13,24 +19,57 @@ class GenreBrowserView : public QWidget
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum class BrowseMode {
|
||||
Genres,
|
||||
PlaylistSearch,
|
||||
};
|
||||
|
||||
explicit GenreBrowserView(QobuzBackend *backend, QWidget *parent = nullptr);
|
||||
|
||||
void ensureGenresLoaded();
|
||||
void setBrowseMode(BrowseMode mode);
|
||||
|
||||
signals:
|
||||
void albumSelected(const QString &albumId);
|
||||
void artistSelected(qint64 artistId);
|
||||
void playlistSelected(qint64 playlistId);
|
||||
|
||||
private slots:
|
||||
void onGenresLoaded(const QJsonObject &result);
|
||||
void onFeaturedAlbumsLoaded(const QJsonObject &result);
|
||||
void onFeaturedPlaylistsLoaded(const QJsonObject &result);
|
||||
void onDiscoverPlaylistsLoaded(const QJsonObject &result);
|
||||
void onPlaylistSearchLoaded(const QJsonObject &result);
|
||||
void onSelectionChanged();
|
||||
void onAlbumContextMenu(const QPoint &pos);
|
||||
void onPlaylistActivated(QTreeWidgetItem *item, int column);
|
||||
void onPlaylistContextMenu(const QPoint &pos);
|
||||
|
||||
private:
|
||||
QobuzBackend *m_backend = nullptr;
|
||||
QLabel *m_browseLabel = nullptr;
|
||||
QLabel *m_genreLabel = nullptr;
|
||||
QLabel *m_typeLabel = nullptr;
|
||||
QLabel *m_playlistSearchLabel = nullptr;
|
||||
QWidget *m_gapAfterKind = nullptr;
|
||||
QWidget *m_gapAfterGenre = nullptr;
|
||||
QComboBox *m_kindCombo = nullptr;
|
||||
QComboBox *m_genreCombo = nullptr;
|
||||
QComboBox *m_typeCombo = nullptr;
|
||||
QLineEdit *m_playlistSearchBox = nullptr;
|
||||
QPushButton *m_playlistSearchBtn = nullptr;
|
||||
QStackedWidget *m_resultsStack = nullptr;
|
||||
AlbumListView *m_albumList = nullptr;
|
||||
QTreeWidget *m_playlistList = nullptr;
|
||||
BrowseMode m_mode = BrowseMode::Genres;
|
||||
bool m_genresLoaded = false;
|
||||
int m_lastGenreComboIndex = 0;
|
||||
QSet<qint64> m_multiGenreIds;
|
||||
|
||||
void refreshModeUi();
|
||||
void refreshGenreTypeChoices();
|
||||
QString currentGenreIds() const;
|
||||
bool chooseMultiGenres();
|
||||
void updateMultiGenreLabel();
|
||||
void setPlaylistItems(const QJsonArray &items);
|
||||
};
|
||||
|
||||
@@ -41,6 +41,13 @@ MainContent::MainContent(QobuzBackend *backend, PlayQueue *queue, QWidget *paren
|
||||
const qint64 id = m_header->artistId();
|
||||
if (id > 0) emit artistRequested(id);
|
||||
});
|
||||
QObject::connect(m_header->followButton(), &QPushButton::clicked,
|
||||
[this] {
|
||||
const qint64 id = m_header->playlistId();
|
||||
if (id <= 0 || m_header->playlistOwned())
|
||||
return;
|
||||
emit playlistFollowToggled(id, !m_header->playlistFollowed());
|
||||
});
|
||||
|
||||
m_albumList = new AlbumListView(this);
|
||||
m_artistList = new ArtistListView(this);
|
||||
@@ -62,6 +69,7 @@ MainContent::MainContent(QobuzBackend *backend, PlayQueue *queue, QWidget *paren
|
||||
connect(m_artistView, &ArtistView::playTrackRequested, this, &MainContent::playTrackRequested);
|
||||
connect(m_genreBrowser, &GenreBrowserView::albumSelected, this, &MainContent::albumRequested);
|
||||
connect(m_genreBrowser, &GenreBrowserView::artistSelected, this, &MainContent::artistRequested);
|
||||
connect(m_genreBrowser, &GenreBrowserView::playlistSelected, this, &MainContent::playlistRequested);
|
||||
}
|
||||
|
||||
void MainContent::showWelcome() { m_stack->setCurrentIndex(0); }
|
||||
@@ -73,9 +81,9 @@ void MainContent::showAlbum(const QJsonObject &album)
|
||||
m_stack->setCurrentIndex(1);
|
||||
}
|
||||
|
||||
void MainContent::showPlaylist(const QJsonObject &playlist)
|
||||
void MainContent::showPlaylist(const QJsonObject &playlist, bool isFollowed, bool isOwned)
|
||||
{
|
||||
m_header->setPlaylist(playlist);
|
||||
m_header->setPlaylist(playlist, isFollowed, isOwned);
|
||||
m_tracks->loadPlaylist(playlist);
|
||||
m_stack->setCurrentIndex(1);
|
||||
}
|
||||
@@ -130,5 +138,18 @@ void MainContent::onDeepShuffleTracks(const QJsonArray &tracks)
|
||||
void MainContent::showGenreBrowser()
|
||||
{
|
||||
m_genreBrowser->ensureGenresLoaded();
|
||||
m_genreBrowser->setBrowseMode(GenreBrowserView::BrowseMode::Genres);
|
||||
m_stack->setCurrentIndex(5);
|
||||
}
|
||||
|
||||
void MainContent::showPlaylistBrowser()
|
||||
{
|
||||
m_genreBrowser->ensureGenresLoaded();
|
||||
m_genreBrowser->setBrowseMode(GenreBrowserView::BrowseMode::PlaylistSearch);
|
||||
m_stack->setCurrentIndex(5);
|
||||
}
|
||||
|
||||
void MainContent::setCurrentPlaylistFollowed(bool followed)
|
||||
{
|
||||
m_header->setPlaylistFollowed(followed);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ public:
|
||||
|
||||
void showWelcome();
|
||||
void showAlbum(const QJsonObject &album);
|
||||
void showPlaylist(const QJsonObject &playlist);
|
||||
void showPlaylist(const QJsonObject &playlist, bool isFollowed, bool isOwned);
|
||||
void showFavTracks(const QJsonObject &result);
|
||||
void showSearchTracks(const QJsonArray &tracks);
|
||||
void showFavAlbums(const QJsonObject &result);
|
||||
@@ -36,12 +36,16 @@ public:
|
||||
void setFavArtistIds(const QSet<qint64> &ids);
|
||||
void onDeepShuffleTracks(const QJsonArray &tracks);
|
||||
void showGenreBrowser();
|
||||
void showPlaylistBrowser();
|
||||
void setCurrentPlaylistFollowed(bool followed);
|
||||
|
||||
ArtistView *artistView() const { return m_artistView; }
|
||||
|
||||
signals:
|
||||
void albumRequested(const QString &albumId);
|
||||
void artistRequested(qint64 artistId);
|
||||
void playlistRequested(qint64 playlistId);
|
||||
void playlistFollowToggled(qint64 playlistId, bool follow);
|
||||
void playTrackRequested(qint64 trackId);
|
||||
|
||||
private:
|
||||
|
||||
@@ -93,6 +93,13 @@ public:
|
||||
"QPushButton:pressed { background: #333; }"));
|
||||
btnRow->addWidget(m_shuffleBtn);
|
||||
|
||||
m_followBtn = new QPushButton(tr("Follow"), info);
|
||||
m_followBtn->setStyleSheet(btnBase +
|
||||
QStringLiteral("QPushButton { background: #2a2a2a; color: #ddd; border: 1px solid #666; }"
|
||||
"QPushButton:pressed { background: #333; }"));
|
||||
m_followBtn->hide();
|
||||
btnRow->addWidget(m_followBtn);
|
||||
|
||||
btnRow->addStretch();
|
||||
vlay->addLayout(btnRow);
|
||||
vlay->addStretch(1);
|
||||
@@ -113,9 +120,13 @@ public:
|
||||
|
||||
QPushButton *playButton() { return m_playBtn; }
|
||||
QPushButton *shuffleButton() { return m_shuffleBtn; }
|
||||
QPushButton *followButton() { return m_followBtn; }
|
||||
|
||||
QPushButton *subtitleButton() { return m_subtitle; }
|
||||
qint64 artistId() const { return m_artistId; }
|
||||
qint64 playlistId() const { return m_playlistId; }
|
||||
bool playlistFollowed() const { return m_playlistFollowed; }
|
||||
bool playlistOwned() const { return m_playlistOwned; }
|
||||
|
||||
void setAlbum(const QJsonObject &album)
|
||||
{
|
||||
@@ -127,14 +138,21 @@ public:
|
||||
m_subtitle->setEnabled(m_artistId > 0);
|
||||
m_subtitle->setCursor(m_artistId > 0 ? Qt::PointingHandCursor : Qt::ArrowCursor);
|
||||
m_meta->setText(buildAlbumMeta(album));
|
||||
m_followBtn->hide();
|
||||
m_playlistId = 0;
|
||||
m_playlistFollowed = false;
|
||||
m_playlistOwned = false;
|
||||
fetchArt(album["image"].toObject());
|
||||
show();
|
||||
}
|
||||
|
||||
void setPlaylist(const QJsonObject &playlist)
|
||||
void setPlaylist(const QJsonObject &playlist, bool isFollowed, bool isOwned)
|
||||
{
|
||||
m_title->setText(playlist["name"].toString());
|
||||
m_artistId = 0;
|
||||
m_playlistId = static_cast<qint64>(playlist["id"].toDouble());
|
||||
m_playlistFollowed = isFollowed;
|
||||
m_playlistOwned = isOwned;
|
||||
const QString desc = playlist["description"].toString();
|
||||
const QString owner = playlist["owner"].toObject()["name"].toString();
|
||||
m_subtitle->setText(desc.isEmpty() ? owner : desc);
|
||||
@@ -142,6 +160,16 @@ public:
|
||||
m_subtitle->setCursor(Qt::ArrowCursor);
|
||||
m_meta->setText(buildPlaylistMeta(playlist));
|
||||
|
||||
if (m_playlistOwned) {
|
||||
m_followBtn->setText(tr("Owned"));
|
||||
m_followBtn->setEnabled(false);
|
||||
m_followBtn->show();
|
||||
} else {
|
||||
m_followBtn->setText(m_playlistFollowed ? tr("Unfollow") : tr("Follow"));
|
||||
m_followBtn->setEnabled(m_playlistId > 0);
|
||||
m_followBtn->show();
|
||||
}
|
||||
|
||||
// Try images300 → images150 → images (API returns mosaic arrays, not image_rectangle)
|
||||
const QJsonArray imgs300 = playlist["images300"].toArray();
|
||||
const QJsonArray imgs150 = playlist["images150"].toArray();
|
||||
@@ -156,6 +184,13 @@ public:
|
||||
show();
|
||||
}
|
||||
|
||||
void setPlaylistFollowed(bool followed)
|
||||
{
|
||||
m_playlistFollowed = followed;
|
||||
if (!m_playlistOwned)
|
||||
m_followBtn->setText(m_playlistFollowed ? tr("Unfollow") : tr("Follow"));
|
||||
}
|
||||
|
||||
private:
|
||||
void fetchArt(const QJsonObject &img)
|
||||
{
|
||||
@@ -221,7 +256,11 @@ private:
|
||||
QLabel *m_meta = nullptr;
|
||||
QPushButton *m_playBtn = nullptr;
|
||||
QPushButton *m_shuffleBtn = nullptr;
|
||||
QPushButton *m_followBtn = nullptr;
|
||||
QNetworkAccessManager *m_nam = nullptr;
|
||||
QString m_currentArtUrl;
|
||||
qint64 m_artistId = 0;
|
||||
qint64 m_playlistId = 0;
|
||||
bool m_playlistFollowed = false;
|
||||
bool m_playlistOwned = false;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user