diff --git a/rust/include/qobuz_backend.h b/rust/include/qobuz_backend.h index 97ea035..51ef63d 100644 --- a/rust/include/qobuz_backend.h +++ b/rust/include/qobuz_backend.h @@ -41,6 +41,11 @@ enum QobuzEvent { EV_GENRES_OK = 27, EV_FEATURED_ALBUMS_OK = 28, EV_DYNAMIC_SUGGEST_OK = 29, + EV_FEATURED_PLAYLISTS_OK = 30, + EV_DISCOVER_PLAYLISTS_OK = 31, + EV_PLAYLIST_SEARCH_OK = 32, + EV_PLAYLIST_SUBSCRIBED = 33, + EV_PLAYLIST_UNSUBSCRIBED = 34, }; // Callback signature @@ -96,13 +101,18 @@ void qobuz_backend_get_albums_tracks(QobuzBackendOpaque *backend, const char *al // Browse void qobuz_backend_get_genres(QobuzBackendOpaque *backend); -void qobuz_backend_get_featured_albums(QobuzBackendOpaque *backend, int64_t genre_id, const char *kind, uint32_t limit, uint32_t offset); +void qobuz_backend_get_featured_albums(QobuzBackendOpaque *backend, const char *genre_ids, const char *kind, uint32_t limit, uint32_t offset); +void qobuz_backend_get_featured_playlists(QobuzBackendOpaque *backend, const char *genre_ids, const char *kind, uint32_t limit, uint32_t offset); +void qobuz_backend_discover_playlists(QobuzBackendOpaque *backend, const char *genre_ids, const char *tags, uint32_t limit, uint32_t offset); +void qobuz_backend_search_playlists(QobuzBackendOpaque *backend, const char *query, uint32_t limit, uint32_t offset); // Playlist management void qobuz_backend_create_playlist(QobuzBackendOpaque *backend, const char *name); void qobuz_backend_delete_playlist(QobuzBackendOpaque *backend, int64_t playlist_id); void qobuz_backend_add_track_to_playlist(QobuzBackendOpaque *backend, int64_t playlist_id, int64_t track_id); void qobuz_backend_delete_track_from_playlist(QobuzBackendOpaque *backend, int64_t playlist_id, int64_t playlist_track_id); +void qobuz_backend_subscribe_playlist(QobuzBackendOpaque *backend, int64_t playlist_id); +void qobuz_backend_unsubscribe_playlist(QobuzBackendOpaque *backend, int64_t playlist_id); // Favorites modification void qobuz_backend_add_fav_track(QobuzBackendOpaque *backend, int64_t track_id); diff --git a/rust/src/api/client.rs b/rust/src/api/client.rs index 1556e0f..03a56c4 100644 --- a/rust/src/api/client.rs +++ b/rust/src/api/client.rs @@ -416,7 +416,7 @@ impl QobuzClient { pub async fn get_featured_albums( &self, - genre_id: i64, + genre_ids: &str, kind: &str, limit: u32, offset: u32, @@ -425,7 +425,64 @@ impl QobuzClient { .get_request("album/getFeatured") .query(&[ ("type", kind.to_string()), - ("genre_id", genre_id.to_string()), + ("genre_id", genre_ids.to_string()), + ("limit", limit.to_string()), + ("offset", offset.to_string()), + ]) + .send() + .await?; + Self::check_response(resp).await + } + + pub async fn get_featured_playlists( + &self, + genre_ids: &str, + kind: &str, + limit: u32, + offset: u32, + ) -> Result { + let resp = self + .get_request("playlist/getFeatured") + .query(&[ + ("type", kind.to_string()), + ("genre_ids", genre_ids.to_string()), + ("limit", limit.to_string()), + ("offset", offset.to_string()), + ]) + .send() + .await?; + Self::check_response(resp).await + } + + pub async fn discover_playlists( + &self, + genre_ids: &str, + tags: &str, + limit: u32, + offset: u32, + ) -> Result { + let mut query = vec![ + ("genre_ids", genre_ids.to_string()), + ("limit", limit.to_string()), + ("offset", offset.to_string()), + ]; + if !tags.is_empty() { + query.push(("tags", tags.to_string())); + } + + let resp = self + .get_request("discover/playlists") + .query(&query) + .send() + .await?; + Self::check_response(resp).await + } + + pub async fn search_playlists(&self, query: &str, limit: u32, offset: u32) -> Result { + let resp = self + .get_request("playlist/search") + .query(&[ + ("query", query.to_string()), ("limit", limit.to_string()), ("offset", offset.to_string()), ]) @@ -779,6 +836,26 @@ impl QobuzClient { Ok(()) } + pub async fn subscribe_playlist(&self, playlist_id: i64) -> Result<()> { + let resp = self + .get_request("playlist/subscribe") + .query(&[("playlist_id", playlist_id.to_string())]) + .send() + .await?; + Self::check_response(resp).await?; + Ok(()) + } + + pub async fn unsubscribe_playlist(&self, playlist_id: i64) -> Result<()> { + let resp = self + .get_request("playlist/unsubscribe") + .query(&[("playlist_id", playlist_id.to_string())]) + .send() + .await?; + Self::check_response(resp).await?; + Ok(()) + } + pub async fn add_fav_track(&self, track_id: i64) -> Result<()> { let resp = self .get_request("favorite/create") diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 842c54e..67d97d4 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -49,6 +49,11 @@ pub const EV_MOST_POPULAR_OK: c_int = 26; pub const EV_GENRES_OK: c_int = 27; pub const EV_FEATURED_ALBUMS_OK: c_int = 28; pub const EV_DYNAMIC_SUGGEST_OK: c_int = 29; +pub const EV_FEATURED_PLAYLISTS_OK: c_int = 30; +pub const EV_DISCOVER_PLAYLISTS_OK: c_int = 31; +pub const EV_PLAYLIST_SEARCH_OK: c_int = 32; +pub const EV_PLAYLIST_SUBSCRIBED: c_int = 33; +pub const EV_PLAYLIST_UNSUBSCRIBED: c_int = 34; // ---------- Callback ---------- @@ -481,7 +486,7 @@ pub unsafe extern "C" fn qobuz_backend_get_genres(ptr: *mut Backend) { #[no_mangle] pub unsafe extern "C" fn qobuz_backend_get_featured_albums( ptr: *mut Backend, - genre_id: i64, + genre_ids: *const c_char, kind: *const c_char, limit: u32, offset: u32, @@ -490,13 +495,14 @@ pub unsafe extern "C" fn qobuz_backend_get_featured_albums( let client = inner.client.clone(); let cb = inner.cb; let ud = inner.ud; + let genre_ids_str = CStr::from_ptr(genre_ids).to_string_lossy().into_owned(); let kind_str = CStr::from_ptr(kind).to_string_lossy().into_owned(); spawn(inner, async move { let result = client .lock() .await - .get_featured_albums(genre_id, &kind_str, limit, offset) + .get_featured_albums(&genre_ids_str, &kind_str, limit, offset) .await; match result { Ok(r) => { @@ -506,7 +512,7 @@ pub unsafe extern "C" fn qobuz_backend_get_featured_albums( "items": items, "total": total, "type": kind_str, - "genre_id": genre_id, + "genre_ids": genre_ids_str, }); call_cb( cb, @@ -520,6 +526,132 @@ pub unsafe extern "C" fn qobuz_backend_get_featured_albums( }); } +#[no_mangle] +pub unsafe extern "C" fn qobuz_backend_get_featured_playlists( + ptr: *mut Backend, + genre_ids: *const c_char, + kind: *const c_char, + limit: u32, + offset: u32, +) { + let inner = &(*ptr).0; + let client = inner.client.clone(); + let cb = inner.cb; + let ud = inner.ud; + let kind_str = CStr::from_ptr(kind).to_string_lossy().into_owned(); + let genre_ids_str = CStr::from_ptr(genre_ids).to_string_lossy().into_owned(); + + spawn(inner, async move { + let result = client + .lock() + .await + .get_featured_playlists(&genre_ids_str, &kind_str, limit, offset) + .await; + match result { + Ok(r) => { + let items = r["playlists"]["items"].clone(); + let total = r["playlists"]["total"].as_i64().unwrap_or(0); + let out = serde_json::json!({ + "items": items, + "total": total, + "type": kind_str, + "genre_ids": genre_ids_str, + }); + call_cb( + cb, + ud, + EV_FEATURED_PLAYLISTS_OK, + &serde_json::to_string(&out).unwrap_or_default(), + ); + } + Err(e) => call_cb(cb, ud, EV_GENERIC_ERR, &err_json(&e.to_string())), + } + }); +} + +#[no_mangle] +pub unsafe extern "C" fn qobuz_backend_discover_playlists( + ptr: *mut Backend, + genre_ids: *const c_char, + tags: *const c_char, + limit: u32, + offset: u32, +) { + let inner = &(*ptr).0; + let client = inner.client.clone(); + let cb = inner.cb; + let ud = inner.ud; + let genre_ids_str = CStr::from_ptr(genre_ids).to_string_lossy().into_owned(); + let tags_str = CStr::from_ptr(tags).to_string_lossy().into_owned(); + + spawn(inner, async move { + let result = client + .lock() + .await + .discover_playlists(&genre_ids_str, &tags_str, limit, offset) + .await; + match result { + Ok(r) => { + let items = r["items"].clone(); + let total = r["total"].as_i64().unwrap_or(0); + let out = serde_json::json!({ + "items": items, + "total": total, + "genre_ids": genre_ids_str, + "tags": tags_str, + }); + call_cb( + cb, + ud, + EV_DISCOVER_PLAYLISTS_OK, + &serde_json::to_string(&out).unwrap_or_default(), + ); + } + Err(e) => call_cb(cb, ud, EV_GENERIC_ERR, &err_json(&e.to_string())), + } + }); +} + +#[no_mangle] +pub unsafe extern "C" fn qobuz_backend_search_playlists( + ptr: *mut Backend, + query: *const c_char, + limit: u32, + offset: u32, +) { + let inner = &(*ptr).0; + let client = inner.client.clone(); + let cb = inner.cb; + let ud = inner.ud; + let query_str = CStr::from_ptr(query).to_string_lossy().into_owned(); + + spawn(inner, async move { + let result = client + .lock() + .await + .search_playlists(&query_str, limit, offset) + .await; + match result { + Ok(r) => { + let items = r["playlists"]["items"].clone(); + let total = r["playlists"]["total"].as_i64().unwrap_or(0); + let out = serde_json::json!({ + "items": items, + "total": total, + "query": query_str, + }); + call_cb( + cb, + ud, + EV_PLAYLIST_SEARCH_OK, + &serde_json::to_string(&out).unwrap_or_default(), + ); + } + Err(e) => call_cb(cb, ud, EV_GENERIC_ERR, &err_json(&e.to_string())), + } + }); +} + // ---------- Playlist ---------- #[no_mangle] @@ -1129,3 +1261,41 @@ pub unsafe extern "C" fn qobuz_backend_delete_track_from_playlist( } }); } + +#[no_mangle] +pub unsafe extern "C" fn qobuz_backend_subscribe_playlist(ptr: *mut Backend, playlist_id: i64) { + let inner = &(*ptr).0; + let client = inner.client.clone(); + let cb = inner.cb; + let ud = inner.ud; + spawn(inner, async move { + match client.lock().await.subscribe_playlist(playlist_id).await { + Ok(()) => call_cb( + cb, + ud, + EV_PLAYLIST_SUBSCRIBED, + &serde_json::json!({"playlist_id": playlist_id}).to_string(), + ), + Err(e) => call_cb(cb, ud, EV_GENERIC_ERR, &err_json(&e.to_string())), + } + }); +} + +#[no_mangle] +pub unsafe extern "C" fn qobuz_backend_unsubscribe_playlist(ptr: *mut Backend, playlist_id: i64) { + let inner = &(*ptr).0; + let client = inner.client.clone(); + let cb = inner.cb; + let ud = inner.ud; + spawn(inner, async move { + match client.lock().await.unsubscribe_playlist(playlist_id).await { + Ok(()) => call_cb( + cb, + ud, + EV_PLAYLIST_UNSUBSCRIBED, + &serde_json::json!({"playlist_id": playlist_id}).to_string(), + ), + Err(e) => call_cb(cb, ud, EV_GENERIC_ERR, &err_json(&e.to_string())), + } + }); +} diff --git a/src/backend/qobuzbackend.cpp b/src/backend/qobuzbackend.cpp index d4bbd3a..8c1497b 100644 --- a/src/backend/qobuzbackend.cpp +++ b/src/backend/qobuzbackend.cpp @@ -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(obj["playlist_id"].toDouble())); break; + case 33: // EV_PLAYLIST_SUBSCRIBED + emit playlistSubscribed(static_cast(obj["playlist_id"].toDouble())); + break; + case 34: // EV_PLAYLIST_UNSUBSCRIBED + emit playlistUnsubscribed(static_cast(obj["playlist_id"].toDouble())); + break; case EV_USER_OK: emit userLoaded(obj); break; diff --git a/src/backend/qobuzbackend.hpp b/src/backend/qobuzbackend.hpp index aada7f5..557df1f 100644 --- a/src/backend/qobuzbackend.hpp +++ b/src/backend/qobuzbackend.hpp @@ -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); diff --git a/src/list/library.cpp b/src/list/library.cpp index d00158b..672d05a 100644 --- a/src/list/library.cpp +++ b/src/list/library.cpp @@ -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(); diff --git a/src/list/library.hpp b/src/list/library.hpp index 06d46ee..43d828a 100644 --- a/src/list/library.hpp +++ b/src/list/library.hpp @@ -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> &playlists); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index eff6e2e..93d0d17 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -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(playlist["id"].toDouble()); + const qint64 ownerId = static_cast(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); } diff --git a/src/view/genrebrowser.cpp b/src/view/genrebrowser.cpp index 80e9e91..3f0a0d7 100644 --- a/src/view/genrebrowser.cpp +++ b/src/view/genrebrowser.cpp @@ -1,12 +1,20 @@ #include "genrebrowser.hpp" #include +#include +#include +#include #include #include +#include #include +#include +#include #include #include +#include + 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::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::of(&QComboBox::currentIndexChanged), + this, [this](int) { + refreshGenreTypeChoices(); + onSelectionChanged(); + }); connect(m_typeCombo, QOverload::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 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(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(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); + } +} diff --git a/src/view/genrebrowser.hpp b/src/view/genrebrowser.hpp index d90efd2..84f0919 100644 --- a/src/view/genrebrowser.hpp +++ b/src/view/genrebrowser.hpp @@ -4,8 +4,14 @@ #include "albumlistview.hpp" #include +#include +#include #include #include +#include +#include +#include +#include #include 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 m_multiGenreIds; + + void refreshModeUi(); + void refreshGenreTypeChoices(); + QString currentGenreIds() const; + bool chooseMultiGenres(); + void updateMultiGenreLabel(); + void setPlaylistItems(const QJsonArray &items); }; diff --git a/src/view/maincontent.cpp b/src/view/maincontent.cpp index 2d78ee9..006936e 100644 --- a/src/view/maincontent.cpp +++ b/src/view/maincontent.cpp @@ -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); +} diff --git a/src/view/maincontent.hpp b/src/view/maincontent.hpp index 106301f..f1d8dab 100644 --- a/src/view/maincontent.hpp +++ b/src/view/maincontent.hpp @@ -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 &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: diff --git a/src/view/trackcontextheader.hpp b/src/view/trackcontextheader.hpp index daf51e0..2894e39 100644 --- a/src/view/trackcontextheader.hpp +++ b/src/view/trackcontextheader.hpp @@ -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(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; };