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:
@@ -41,6 +41,11 @@ enum QobuzEvent {
|
|||||||
EV_GENRES_OK = 27,
|
EV_GENRES_OK = 27,
|
||||||
EV_FEATURED_ALBUMS_OK = 28,
|
EV_FEATURED_ALBUMS_OK = 28,
|
||||||
EV_DYNAMIC_SUGGEST_OK = 29,
|
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
|
// Callback signature
|
||||||
@@ -96,13 +101,18 @@ void qobuz_backend_get_albums_tracks(QobuzBackendOpaque *backend, const char *al
|
|||||||
|
|
||||||
// Browse
|
// Browse
|
||||||
void qobuz_backend_get_genres(QobuzBackendOpaque *backend);
|
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
|
// Playlist management
|
||||||
void qobuz_backend_create_playlist(QobuzBackendOpaque *backend, const char *name);
|
void qobuz_backend_create_playlist(QobuzBackendOpaque *backend, const char *name);
|
||||||
void qobuz_backend_delete_playlist(QobuzBackendOpaque *backend, int64_t playlist_id);
|
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_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_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
|
// Favorites modification
|
||||||
void qobuz_backend_add_fav_track(QobuzBackendOpaque *backend, int64_t track_id);
|
void qobuz_backend_add_fav_track(QobuzBackendOpaque *backend, int64_t track_id);
|
||||||
|
|||||||
@@ -416,7 +416,7 @@ impl QobuzClient {
|
|||||||
|
|
||||||
pub async fn get_featured_albums(
|
pub async fn get_featured_albums(
|
||||||
&self,
|
&self,
|
||||||
genre_id: i64,
|
genre_ids: &str,
|
||||||
kind: &str,
|
kind: &str,
|
||||||
limit: u32,
|
limit: u32,
|
||||||
offset: u32,
|
offset: u32,
|
||||||
@@ -425,7 +425,64 @@ impl QobuzClient {
|
|||||||
.get_request("album/getFeatured")
|
.get_request("album/getFeatured")
|
||||||
.query(&[
|
.query(&[
|
||||||
("type", kind.to_string()),
|
("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<Value> {
|
||||||
|
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<Value> {
|
||||||
|
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<Value> {
|
||||||
|
let resp = self
|
||||||
|
.get_request("playlist/search")
|
||||||
|
.query(&[
|
||||||
|
("query", query.to_string()),
|
||||||
("limit", limit.to_string()),
|
("limit", limit.to_string()),
|
||||||
("offset", offset.to_string()),
|
("offset", offset.to_string()),
|
||||||
])
|
])
|
||||||
@@ -779,6 +836,26 @@ impl QobuzClient {
|
|||||||
Ok(())
|
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<()> {
|
pub async fn add_fav_track(&self, track_id: i64) -> Result<()> {
|
||||||
let resp = self
|
let resp = self
|
||||||
.get_request("favorite/create")
|
.get_request("favorite/create")
|
||||||
|
|||||||
176
rust/src/lib.rs
176
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_GENRES_OK: c_int = 27;
|
||||||
pub const EV_FEATURED_ALBUMS_OK: c_int = 28;
|
pub const EV_FEATURED_ALBUMS_OK: c_int = 28;
|
||||||
pub const EV_DYNAMIC_SUGGEST_OK: c_int = 29;
|
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 ----------
|
// ---------- Callback ----------
|
||||||
|
|
||||||
@@ -481,7 +486,7 @@ pub unsafe extern "C" fn qobuz_backend_get_genres(ptr: *mut Backend) {
|
|||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn qobuz_backend_get_featured_albums(
|
pub unsafe extern "C" fn qobuz_backend_get_featured_albums(
|
||||||
ptr: *mut Backend,
|
ptr: *mut Backend,
|
||||||
genre_id: i64,
|
genre_ids: *const c_char,
|
||||||
kind: *const c_char,
|
kind: *const c_char,
|
||||||
limit: u32,
|
limit: u32,
|
||||||
offset: u32,
|
offset: u32,
|
||||||
@@ -490,13 +495,14 @@ pub unsafe extern "C" fn qobuz_backend_get_featured_albums(
|
|||||||
let client = inner.client.clone();
|
let client = inner.client.clone();
|
||||||
let cb = inner.cb;
|
let cb = inner.cb;
|
||||||
let ud = inner.ud;
|
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();
|
let kind_str = CStr::from_ptr(kind).to_string_lossy().into_owned();
|
||||||
|
|
||||||
spawn(inner, async move {
|
spawn(inner, async move {
|
||||||
let result = client
|
let result = client
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.get_featured_albums(genre_id, &kind_str, limit, offset)
|
.get_featured_albums(&genre_ids_str, &kind_str, limit, offset)
|
||||||
.await;
|
.await;
|
||||||
match result {
|
match result {
|
||||||
Ok(r) => {
|
Ok(r) => {
|
||||||
@@ -506,7 +512,7 @@ pub unsafe extern "C" fn qobuz_backend_get_featured_albums(
|
|||||||
"items": items,
|
"items": items,
|
||||||
"total": total,
|
"total": total,
|
||||||
"type": kind_str,
|
"type": kind_str,
|
||||||
"genre_id": genre_id,
|
"genre_ids": genre_ids_str,
|
||||||
});
|
});
|
||||||
call_cb(
|
call_cb(
|
||||||
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 ----------
|
// ---------- Playlist ----------
|
||||||
|
|
||||||
#[no_mangle]
|
#[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())),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -100,9 +100,29 @@ void QobuzBackend::getGenres()
|
|||||||
qobuz_backend_get_genres(m_backend);
|
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 ----
|
// ---- favorites ----
|
||||||
@@ -166,6 +186,16 @@ void QobuzBackend::deleteTrackFromPlaylist(qint64 playlistId, qint64 playlistTra
|
|||||||
qobuz_backend_delete_track_from_playlist(m_backend, playlistId, playlistTrackId);
|
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 ----
|
// ---- fav modification ----
|
||||||
|
|
||||||
void QobuzBackend::addFavTrack(qint64 trackId)
|
void QobuzBackend::addFavTrack(qint64 trackId)
|
||||||
@@ -302,6 +332,15 @@ void QobuzBackend::onEvent(int eventType, const QString &json)
|
|||||||
case 28: // EV_FEATURED_ALBUMS_OK
|
case 28: // EV_FEATURED_ALBUMS_OK
|
||||||
emit featuredAlbumsLoaded(obj);
|
emit featuredAlbumsLoaded(obj);
|
||||||
break;
|
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:
|
case EV_ARTIST_ERR:
|
||||||
emit error(obj["error"].toString());
|
emit error(obj["error"].toString());
|
||||||
break;
|
break;
|
||||||
@@ -338,6 +377,12 @@ void QobuzBackend::onEvent(int eventType, const QString &json)
|
|||||||
case 22: // EV_PLAYLIST_TRACK_ADDED
|
case 22: // EV_PLAYLIST_TRACK_ADDED
|
||||||
emit playlistTrackAdded(static_cast<qint64>(obj["playlist_id"].toDouble()));
|
emit playlistTrackAdded(static_cast<qint64>(obj["playlist_id"].toDouble()));
|
||||||
break;
|
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:
|
case EV_USER_OK:
|
||||||
emit userLoaded(obj);
|
emit userLoaded(obj);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ public:
|
|||||||
void getAlbumsTracks(const QStringList &albumIds);
|
void getAlbumsTracks(const QStringList &albumIds);
|
||||||
void getPlaylist(qint64 playlistId, quint32 offset = 0, quint32 limit = 500);
|
void getPlaylist(qint64 playlistId, quint32 offset = 0, quint32 limit = 500);
|
||||||
void getGenres();
|
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 ---
|
// --- favorites ---
|
||||||
void getFavTracks(quint32 offset = 0, quint32 limit = 500);
|
void getFavTracks(quint32 offset = 0, quint32 limit = 500);
|
||||||
@@ -54,6 +57,8 @@ public:
|
|||||||
void deletePlaylist(qint64 playlistId);
|
void deletePlaylist(qint64 playlistId);
|
||||||
void addTrackToPlaylist(qint64 playlistId, qint64 trackId);
|
void addTrackToPlaylist(qint64 playlistId, qint64 trackId);
|
||||||
void deleteTrackFromPlaylist(qint64 playlistId, qint64 playlistTrackId);
|
void deleteTrackFromPlaylist(qint64 playlistId, qint64 playlistTrackId);
|
||||||
|
void subscribePlaylist(qint64 playlistId);
|
||||||
|
void unsubscribePlaylist(qint64 playlistId);
|
||||||
|
|
||||||
// --- fav modification ---
|
// --- fav modification ---
|
||||||
void addFavTrack(qint64 trackId);
|
void addFavTrack(qint64 trackId);
|
||||||
@@ -93,10 +98,15 @@ signals:
|
|||||||
void dynamicSuggestionsLoaded(const QJsonObject &result);
|
void dynamicSuggestionsLoaded(const QJsonObject &result);
|
||||||
void genresLoaded(const QJsonObject &result);
|
void genresLoaded(const QJsonObject &result);
|
||||||
void featuredAlbumsLoaded(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 playlistLoaded(const QJsonObject &playlist);
|
||||||
void playlistCreated(const QJsonObject &playlist);
|
void playlistCreated(const QJsonObject &playlist);
|
||||||
void playlistDeleted(const QJsonObject &result);
|
void playlistDeleted(const QJsonObject &result);
|
||||||
void playlistTrackAdded(qint64 playlistId);
|
void playlistTrackAdded(qint64 playlistId);
|
||||||
|
void playlistSubscribed(qint64 playlistId);
|
||||||
|
void playlistUnsubscribed(qint64 playlistId);
|
||||||
|
|
||||||
// favorites
|
// favorites
|
||||||
void favTracksLoaded(const QJsonObject &result);
|
void favTracksLoaded(const QJsonObject &result);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ enum NodeType {
|
|||||||
NodeFavArtists,
|
NodeFavArtists,
|
||||||
NodePlaylist,
|
NodePlaylist,
|
||||||
NodeBrowseGenres,
|
NodeBrowseGenres,
|
||||||
|
NodeBrowsePlaylists,
|
||||||
};
|
};
|
||||||
|
|
||||||
Library::Library(QobuzBackend *backend, QWidget *parent)
|
Library::Library(QobuzBackend *backend, QWidget *parent)
|
||||||
@@ -79,6 +80,9 @@ void Library::buildStaticNodes()
|
|||||||
|
|
||||||
auto *genresItem = new QTreeWidgetItem(m_browseNode, QStringList{tr("Genres")});
|
auto *genresItem = new QTreeWidgetItem(m_browseNode, QStringList{tr("Genres")});
|
||||||
genresItem->setData(0, TypeRole, NodeBrowseGenres);
|
genresItem->setData(0, TypeRole, NodeBrowseGenres);
|
||||||
|
|
||||||
|
auto *playlistsItem = new QTreeWidgetItem(m_browseNode, QStringList{tr("Playlists")});
|
||||||
|
playlistsItem->setData(0, TypeRole, NodeBrowsePlaylists);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Library::refresh()
|
void Library::refresh()
|
||||||
@@ -176,6 +180,7 @@ void Library::onItemClicked(QTreeWidgetItem *item, int)
|
|||||||
case NodeFavAlbums: emit favAlbumsRequested(); break;
|
case NodeFavAlbums: emit favAlbumsRequested(); break;
|
||||||
case NodeFavArtists: emit favArtistsRequested(); break;
|
case NodeFavArtists: emit favArtistsRequested(); break;
|
||||||
case NodeBrowseGenres: emit browseGenresRequested(); break;
|
case NodeBrowseGenres: emit browseGenresRequested(); break;
|
||||||
|
case NodeBrowsePlaylists: emit browsePlaylistsRequested(); break;
|
||||||
case NodePlaylist: {
|
case NodePlaylist: {
|
||||||
const qint64 id = item->data(0, IdRole).toLongLong();
|
const qint64 id = item->data(0, IdRole).toLongLong();
|
||||||
const QString name = item->data(0, NameRole).toString();
|
const QString name = item->data(0, NameRole).toString();
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ namespace List
|
|||||||
void favAlbumsRequested();
|
void favAlbumsRequested();
|
||||||
void favArtistsRequested();
|
void favArtistsRequested();
|
||||||
void browseGenresRequested();
|
void browseGenresRequested();
|
||||||
|
void browsePlaylistsRequested();
|
||||||
void playlistRequested(qint64 playlistId, const QString &name);
|
void playlistRequested(qint64 playlistId, const QString &name);
|
||||||
/// Emitted after playlists are loaded so others can cache the list.
|
/// Emitted after playlists are loaded so others can cache the list.
|
||||||
void userPlaylistsChanged(const QVector<QPair<qint64, QString>> &playlists);
|
void userPlaylistsChanged(const QVector<QPair<qint64, QString>> &playlists);
|
||||||
|
|||||||
@@ -127,6 +127,18 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
|
|||||||
m_backend->getPlaylist(playlistId);
|
m_backend->getPlaylist(playlistId);
|
||||||
statusBar()->showMessage(tr("Track added to playlist"), 3000);
|
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::trackChanged, this, &MainWindow::onTrackChanged);
|
||||||
connect(m_backend, &QobuzBackend::error, this, [this](const QString &msg) {
|
connect(m_backend, &QobuzBackend::error, this, [this](const QString &msg) {
|
||||||
statusBar()->showMessage(tr("Error: %1").arg(msg), 6000);
|
statusBar()->showMessage(tr("Error: %1").arg(msg), 6000);
|
||||||
@@ -210,6 +222,10 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
|
|||||||
m_content->showGenreBrowser();
|
m_content->showGenreBrowser();
|
||||||
statusBar()->showMessage(tr("Browse Genres"));
|
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 ----
|
// ---- Track list → playback / playlist management ----
|
||||||
connect(m_content->tracksList(), &List::Tracks::playTrackRequested,
|
connect(m_content->tracksList(), &List::Tracks::playTrackRequested,
|
||||||
@@ -243,6 +259,18 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
|
|||||||
this, &MainWindow::onSearchAlbumSelected);
|
this, &MainWindow::onSearchAlbumSelected);
|
||||||
connect(m_content, &MainContent::artistRequested,
|
connect(m_content, &MainContent::artistRequested,
|
||||||
this, &MainWindow::onSearchArtistSelected);
|
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,
|
connect(m_content, &MainContent::playTrackRequested,
|
||||||
this, &MainWindow::onPlayTrackRequested);
|
this, &MainWindow::onPlayTrackRequested);
|
||||||
|
|
||||||
@@ -462,7 +490,20 @@ void MainWindow::onArtistLoaded(const QJsonObject &artist)
|
|||||||
|
|
||||||
void MainWindow::onPlaylistLoaded(const QJsonObject &playlist)
|
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(
|
statusBar()->showMessage(
|
||||||
tr("Playlist: %1").arg(playlist["name"].toString()), 4000);
|
tr("Playlist: %1").arg(playlist["name"].toString()), 4000);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
#include "genrebrowser.hpp"
|
#include "genrebrowser.hpp"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QHeaderView>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QListWidget>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QSignalBlocker>
|
||||||
#include <QTreeWidgetItem>
|
#include <QTreeWidgetItem>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
GenreBrowserView::GenreBrowserView(QobuzBackend *backend, QWidget *parent)
|
GenreBrowserView::GenreBrowserView(QobuzBackend *backend, QWidget *parent)
|
||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
, m_backend(backend)
|
, m_backend(backend)
|
||||||
@@ -19,41 +27,127 @@ GenreBrowserView::GenreBrowserView(QobuzBackend *backend, QWidget *parent)
|
|||||||
auto *topLayout = new QHBoxLayout(topBar);
|
auto *topLayout = new QHBoxLayout(topBar);
|
||||||
topLayout->setContentsMargins(8, 6, 8, 6);
|
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 = new QComboBox(this);
|
||||||
m_genreCombo->setMinimumWidth(160);
|
m_genreCombo->setMinimumWidth(160);
|
||||||
topLayout->addWidget(m_genreCombo);
|
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 = 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);
|
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();
|
topLayout->addStretch();
|
||||||
layout->addWidget(topBar);
|
layout->addWidget(topBar);
|
||||||
|
|
||||||
|
m_resultsStack = new QStackedWidget(this);
|
||||||
|
|
||||||
m_albumList = new AlbumListView(this);
|
m_albumList = new AlbumListView(this);
|
||||||
m_albumList->setContextMenuPolicy(Qt::CustomContextMenu);
|
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,
|
connect(m_backend, &QobuzBackend::genresLoaded,
|
||||||
this, &GenreBrowserView::onGenresLoaded);
|
this, &GenreBrowserView::onGenresLoaded);
|
||||||
connect(m_backend, &QobuzBackend::featuredAlbumsLoaded,
|
connect(m_backend, &QobuzBackend::featuredAlbumsLoaded,
|
||||||
this, &GenreBrowserView::onFeaturedAlbumsLoaded);
|
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),
|
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),
|
connect(m_typeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||||
this, &GenreBrowserView::onSelectionChanged);
|
this, &GenreBrowserView::onSelectionChanged);
|
||||||
|
connect(m_playlistSearchBox, &QLineEdit::returnPressed,
|
||||||
|
this, &GenreBrowserView::onSelectionChanged);
|
||||||
|
connect(m_playlistSearchBtn, &QPushButton::clicked,
|
||||||
|
this, &GenreBrowserView::onSelectionChanged);
|
||||||
connect(m_albumList, &AlbumListView::albumSelected,
|
connect(m_albumList, &AlbumListView::albumSelected,
|
||||||
this, &GenreBrowserView::albumSelected);
|
this, &GenreBrowserView::albumSelected);
|
||||||
connect(m_albumList, &QTreeWidget::customContextMenuRequested,
|
connect(m_albumList, &QTreeWidget::customContextMenuRequested,
|
||||||
this, &GenreBrowserView::onAlbumContextMenu);
|
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()
|
void GenreBrowserView::ensureGenresLoaded()
|
||||||
@@ -62,11 +156,152 @@ void GenreBrowserView::ensureGenresLoaded()
|
|||||||
m_backend->getGenres();
|
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)
|
void GenreBrowserView::onGenresLoaded(const QJsonObject &result)
|
||||||
{
|
{
|
||||||
m_genresLoaded = true;
|
m_genresLoaded = true;
|
||||||
m_genreCombo->blockSignals(true);
|
m_genreCombo->blockSignals(true);
|
||||||
m_genreCombo->clear();
|
m_genreCombo->clear();
|
||||||
|
m_genreCombo->addItem(tr("All genres"), QStringLiteral("__all__"));
|
||||||
|
|
||||||
const QJsonArray items = result["items"].toArray();
|
const QJsonArray items = result["items"].toArray();
|
||||||
for (const auto &value : items) {
|
for (const auto &value : items) {
|
||||||
@@ -76,23 +311,81 @@ void GenreBrowserView::onGenresLoaded(const QJsonObject &result)
|
|||||||
static_cast<qint64>(genre["id"].toDouble()));
|
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);
|
m_genreCombo->blockSignals(false);
|
||||||
onSelectionChanged();
|
onSelectionChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GenreBrowserView::onFeaturedAlbumsLoaded(const QJsonObject &result)
|
void GenreBrowserView::onFeaturedAlbumsLoaded(const QJsonObject &result)
|
||||||
{
|
{
|
||||||
|
m_resultsStack->setCurrentIndex(0);
|
||||||
m_albumList->setAlbums(result["items"].toArray());
|
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()
|
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)
|
if (m_genreCombo->count() == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const qint64 genreId = m_genreCombo->currentData().toLongLong();
|
const QString genreIds = currentGenreIds();
|
||||||
const QString type = m_typeCombo->currentData().toString();
|
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)
|
void GenreBrowserView::onAlbumContextMenu(const QPoint &pos)
|
||||||
@@ -120,3 +413,55 @@ void GenreBrowserView::onAlbumContextMenu(const QPoint &pos)
|
|||||||
|
|
||||||
menu.exec(m_albumList->viewport()->mapToGlobal(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 "albumlistview.hpp"
|
||||||
|
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLineEdit>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QSet>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QStackedWidget>
|
||||||
|
#include <QTreeWidget>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
class GenreBrowserView : public QWidget
|
class GenreBrowserView : public QWidget
|
||||||
@@ -13,24 +19,57 @@ class GenreBrowserView : public QWidget
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
enum class BrowseMode {
|
||||||
|
Genres,
|
||||||
|
PlaylistSearch,
|
||||||
|
};
|
||||||
|
|
||||||
explicit GenreBrowserView(QobuzBackend *backend, QWidget *parent = nullptr);
|
explicit GenreBrowserView(QobuzBackend *backend, QWidget *parent = nullptr);
|
||||||
|
|
||||||
void ensureGenresLoaded();
|
void ensureGenresLoaded();
|
||||||
|
void setBrowseMode(BrowseMode mode);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void albumSelected(const QString &albumId);
|
void albumSelected(const QString &albumId);
|
||||||
void artistSelected(qint64 artistId);
|
void artistSelected(qint64 artistId);
|
||||||
|
void playlistSelected(qint64 playlistId);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onGenresLoaded(const QJsonObject &result);
|
void onGenresLoaded(const QJsonObject &result);
|
||||||
void onFeaturedAlbumsLoaded(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 onSelectionChanged();
|
||||||
void onAlbumContextMenu(const QPoint &pos);
|
void onAlbumContextMenu(const QPoint &pos);
|
||||||
|
void onPlaylistActivated(QTreeWidgetItem *item, int column);
|
||||||
|
void onPlaylistContextMenu(const QPoint &pos);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QobuzBackend *m_backend = nullptr;
|
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_genreCombo = nullptr;
|
||||||
QComboBox *m_typeCombo = nullptr;
|
QComboBox *m_typeCombo = nullptr;
|
||||||
|
QLineEdit *m_playlistSearchBox = nullptr;
|
||||||
|
QPushButton *m_playlistSearchBtn = nullptr;
|
||||||
|
QStackedWidget *m_resultsStack = nullptr;
|
||||||
AlbumListView *m_albumList = nullptr;
|
AlbumListView *m_albumList = nullptr;
|
||||||
|
QTreeWidget *m_playlistList = nullptr;
|
||||||
|
BrowseMode m_mode = BrowseMode::Genres;
|
||||||
bool m_genresLoaded = false;
|
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();
|
const qint64 id = m_header->artistId();
|
||||||
if (id > 0) emit artistRequested(id);
|
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_albumList = new AlbumListView(this);
|
||||||
m_artistList = new ArtistListView(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_artistView, &ArtistView::playTrackRequested, this, &MainContent::playTrackRequested);
|
||||||
connect(m_genreBrowser, &GenreBrowserView::albumSelected, this, &MainContent::albumRequested);
|
connect(m_genreBrowser, &GenreBrowserView::albumSelected, this, &MainContent::albumRequested);
|
||||||
connect(m_genreBrowser, &GenreBrowserView::artistSelected, this, &MainContent::artistRequested);
|
connect(m_genreBrowser, &GenreBrowserView::artistSelected, this, &MainContent::artistRequested);
|
||||||
|
connect(m_genreBrowser, &GenreBrowserView::playlistSelected, this, &MainContent::playlistRequested);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainContent::showWelcome() { m_stack->setCurrentIndex(0); }
|
void MainContent::showWelcome() { m_stack->setCurrentIndex(0); }
|
||||||
@@ -73,9 +81,9 @@ void MainContent::showAlbum(const QJsonObject &album)
|
|||||||
m_stack->setCurrentIndex(1);
|
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_tracks->loadPlaylist(playlist);
|
||||||
m_stack->setCurrentIndex(1);
|
m_stack->setCurrentIndex(1);
|
||||||
}
|
}
|
||||||
@@ -130,5 +138,18 @@ void MainContent::onDeepShuffleTracks(const QJsonArray &tracks)
|
|||||||
void MainContent::showGenreBrowser()
|
void MainContent::showGenreBrowser()
|
||||||
{
|
{
|
||||||
m_genreBrowser->ensureGenresLoaded();
|
m_genreBrowser->ensureGenresLoaded();
|
||||||
|
m_genreBrowser->setBrowseMode(GenreBrowserView::BrowseMode::Genres);
|
||||||
m_stack->setCurrentIndex(5);
|
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 showWelcome();
|
||||||
void showAlbum(const QJsonObject &album);
|
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 showFavTracks(const QJsonObject &result);
|
||||||
void showSearchTracks(const QJsonArray &tracks);
|
void showSearchTracks(const QJsonArray &tracks);
|
||||||
void showFavAlbums(const QJsonObject &result);
|
void showFavAlbums(const QJsonObject &result);
|
||||||
@@ -36,12 +36,16 @@ public:
|
|||||||
void setFavArtistIds(const QSet<qint64> &ids);
|
void setFavArtistIds(const QSet<qint64> &ids);
|
||||||
void onDeepShuffleTracks(const QJsonArray &tracks);
|
void onDeepShuffleTracks(const QJsonArray &tracks);
|
||||||
void showGenreBrowser();
|
void showGenreBrowser();
|
||||||
|
void showPlaylistBrowser();
|
||||||
|
void setCurrentPlaylistFollowed(bool followed);
|
||||||
|
|
||||||
ArtistView *artistView() const { return m_artistView; }
|
ArtistView *artistView() const { return m_artistView; }
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void albumRequested(const QString &albumId);
|
void albumRequested(const QString &albumId);
|
||||||
void artistRequested(qint64 artistId);
|
void artistRequested(qint64 artistId);
|
||||||
|
void playlistRequested(qint64 playlistId);
|
||||||
|
void playlistFollowToggled(qint64 playlistId, bool follow);
|
||||||
void playTrackRequested(qint64 trackId);
|
void playTrackRequested(qint64 trackId);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -93,6 +93,13 @@ public:
|
|||||||
"QPushButton:pressed { background: #333; }"));
|
"QPushButton:pressed { background: #333; }"));
|
||||||
btnRow->addWidget(m_shuffleBtn);
|
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();
|
btnRow->addStretch();
|
||||||
vlay->addLayout(btnRow);
|
vlay->addLayout(btnRow);
|
||||||
vlay->addStretch(1);
|
vlay->addStretch(1);
|
||||||
@@ -113,9 +120,13 @@ public:
|
|||||||
|
|
||||||
QPushButton *playButton() { return m_playBtn; }
|
QPushButton *playButton() { return m_playBtn; }
|
||||||
QPushButton *shuffleButton() { return m_shuffleBtn; }
|
QPushButton *shuffleButton() { return m_shuffleBtn; }
|
||||||
|
QPushButton *followButton() { return m_followBtn; }
|
||||||
|
|
||||||
QPushButton *subtitleButton() { return m_subtitle; }
|
QPushButton *subtitleButton() { return m_subtitle; }
|
||||||
qint64 artistId() const { return m_artistId; }
|
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)
|
void setAlbum(const QJsonObject &album)
|
||||||
{
|
{
|
||||||
@@ -127,14 +138,21 @@ public:
|
|||||||
m_subtitle->setEnabled(m_artistId > 0);
|
m_subtitle->setEnabled(m_artistId > 0);
|
||||||
m_subtitle->setCursor(m_artistId > 0 ? Qt::PointingHandCursor : Qt::ArrowCursor);
|
m_subtitle->setCursor(m_artistId > 0 ? Qt::PointingHandCursor : Qt::ArrowCursor);
|
||||||
m_meta->setText(buildAlbumMeta(album));
|
m_meta->setText(buildAlbumMeta(album));
|
||||||
|
m_followBtn->hide();
|
||||||
|
m_playlistId = 0;
|
||||||
|
m_playlistFollowed = false;
|
||||||
|
m_playlistOwned = false;
|
||||||
fetchArt(album["image"].toObject());
|
fetchArt(album["image"].toObject());
|
||||||
show();
|
show();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPlaylist(const QJsonObject &playlist)
|
void setPlaylist(const QJsonObject &playlist, bool isFollowed, bool isOwned)
|
||||||
{
|
{
|
||||||
m_title->setText(playlist["name"].toString());
|
m_title->setText(playlist["name"].toString());
|
||||||
m_artistId = 0;
|
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 desc = playlist["description"].toString();
|
||||||
const QString owner = playlist["owner"].toObject()["name"].toString();
|
const QString owner = playlist["owner"].toObject()["name"].toString();
|
||||||
m_subtitle->setText(desc.isEmpty() ? owner : desc);
|
m_subtitle->setText(desc.isEmpty() ? owner : desc);
|
||||||
@@ -142,6 +160,16 @@ public:
|
|||||||
m_subtitle->setCursor(Qt::ArrowCursor);
|
m_subtitle->setCursor(Qt::ArrowCursor);
|
||||||
m_meta->setText(buildPlaylistMeta(playlist));
|
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)
|
// Try images300 → images150 → images (API returns mosaic arrays, not image_rectangle)
|
||||||
const QJsonArray imgs300 = playlist["images300"].toArray();
|
const QJsonArray imgs300 = playlist["images300"].toArray();
|
||||||
const QJsonArray imgs150 = playlist["images150"].toArray();
|
const QJsonArray imgs150 = playlist["images150"].toArray();
|
||||||
@@ -156,6 +184,13 @@ public:
|
|||||||
show();
|
show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setPlaylistFollowed(bool followed)
|
||||||
|
{
|
||||||
|
m_playlistFollowed = followed;
|
||||||
|
if (!m_playlistOwned)
|
||||||
|
m_followBtn->setText(m_playlistFollowed ? tr("Unfollow") : tr("Follow"));
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void fetchArt(const QJsonObject &img)
|
void fetchArt(const QJsonObject &img)
|
||||||
{
|
{
|
||||||
@@ -221,7 +256,11 @@ private:
|
|||||||
QLabel *m_meta = nullptr;
|
QLabel *m_meta = nullptr;
|
||||||
QPushButton *m_playBtn = nullptr;
|
QPushButton *m_playBtn = nullptr;
|
||||||
QPushButton *m_shuffleBtn = nullptr;
|
QPushButton *m_shuffleBtn = nullptr;
|
||||||
|
QPushButton *m_followBtn = nullptr;
|
||||||
QNetworkAccessManager *m_nam = nullptr;
|
QNetworkAccessManager *m_nam = nullptr;
|
||||||
QString m_currentArtUrl;
|
QString m_currentArtUrl;
|
||||||
qint64 m_artistId = 0;
|
qint64 m_artistId = 0;
|
||||||
|
qint64 m_playlistId = 0;
|
||||||
|
bool m_playlistFollowed = false;
|
||||||
|
bool m_playlistOwned = false;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user