feat: add seamless lazy loading for genre and playlist views
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
Introduce paged loading with early prefetch for genre albums/playlists and playlist tracks, while preserving full-data behavior for deep shuffle and playlist play-all actions.
This commit is contained in:
@@ -67,6 +67,7 @@ void qobuz_backend_get_dynamic_suggestions(QobuzBackendOpaque *backend, const ch
|
|||||||
void qobuz_backend_get_album(QobuzBackendOpaque *backend, const char *album_id);
|
void qobuz_backend_get_album(QobuzBackendOpaque *backend, const char *album_id);
|
||||||
void qobuz_backend_get_artist(QobuzBackendOpaque *backend, int64_t artist_id);
|
void qobuz_backend_get_artist(QobuzBackendOpaque *backend, int64_t artist_id);
|
||||||
void qobuz_backend_get_playlist(QobuzBackendOpaque *backend, int64_t playlist_id, uint32_t offset, uint32_t limit);
|
void qobuz_backend_get_playlist(QobuzBackendOpaque *backend, int64_t playlist_id, uint32_t offset, uint32_t limit);
|
||||||
|
void qobuz_backend_get_playlist_all(QobuzBackendOpaque *backend, int64_t playlist_id);
|
||||||
|
|
||||||
// Favorites
|
// Favorites
|
||||||
void qobuz_backend_get_fav_tracks(QobuzBackendOpaque *backend, uint32_t offset, uint32_t limit);
|
void qobuz_backend_get_fav_tracks(QobuzBackendOpaque *backend, uint32_t offset, uint32_t limit);
|
||||||
|
|||||||
@@ -639,6 +639,51 @@ impl QobuzClient {
|
|||||||
Ok(serde_json::from_value(body)?)
|
Ok(serde_json::from_value(body)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_playlist_all(&self, playlist_id: i64) -> Result<PlaylistDto> {
|
||||||
|
const PAGE_LIMIT: u32 = 500;
|
||||||
|
|
||||||
|
let mut playlist = self.get_playlist(playlist_id, 0, PAGE_LIMIT).await?;
|
||||||
|
|
||||||
|
let mut all_items = playlist
|
||||||
|
.tracks
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|t| t.items.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let mut total = playlist
|
||||||
|
.tracks
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|t| t.total)
|
||||||
|
.unwrap_or(all_items.len() as i32);
|
||||||
|
if total < all_items.len() as i32 {
|
||||||
|
total = all_items.len() as i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut offset = all_items.len() as u32;
|
||||||
|
while (offset as i32) < total {
|
||||||
|
let page = self.get_playlist(playlist_id, offset, PAGE_LIMIT).await?;
|
||||||
|
let mut page_items = page
|
||||||
|
.tracks
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|t| t.items.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
if page_items.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
all_items.append(&mut page_items);
|
||||||
|
offset = all_items.len() as u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(tracks) = playlist.tracks.as_mut() {
|
||||||
|
tracks.items = Some(all_items);
|
||||||
|
tracks.total = Some(total);
|
||||||
|
tracks.offset = Some(0);
|
||||||
|
tracks.limit = Some(PAGE_LIMIT as i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(playlist)
|
||||||
|
}
|
||||||
|
|
||||||
/// Fetch all favorite IDs (tracks, albums, artists) in one call.
|
/// Fetch all favorite IDs (tracks, albums, artists) in one call.
|
||||||
async fn get_fav_ids(&self) -> Result<FavIdsDto> {
|
async fn get_fav_ids(&self) -> Result<FavIdsDto> {
|
||||||
let resp = self
|
let resp = self
|
||||||
|
|||||||
@@ -514,6 +514,8 @@ pub unsafe extern "C" fn qobuz_backend_get_featured_albums(
|
|||||||
"total": total,
|
"total": total,
|
||||||
"type": kind_str,
|
"type": kind_str,
|
||||||
"genre_ids": genre_ids_str,
|
"genre_ids": genre_ids_str,
|
||||||
|
"offset": offset,
|
||||||
|
"limit": limit,
|
||||||
});
|
});
|
||||||
call_cb(
|
call_cb(
|
||||||
cb,
|
cb,
|
||||||
@@ -557,6 +559,8 @@ pub unsafe extern "C" fn qobuz_backend_get_featured_playlists(
|
|||||||
"total": total,
|
"total": total,
|
||||||
"type": kind_str,
|
"type": kind_str,
|
||||||
"genre_ids": genre_ids_str,
|
"genre_ids": genre_ids_str,
|
||||||
|
"offset": offset,
|
||||||
|
"limit": limit,
|
||||||
});
|
});
|
||||||
call_cb(
|
call_cb(
|
||||||
cb,
|
cb,
|
||||||
@@ -600,6 +604,8 @@ pub unsafe extern "C" fn qobuz_backend_discover_playlists(
|
|||||||
"total": total,
|
"total": total,
|
||||||
"genre_ids": genre_ids_str,
|
"genre_ids": genre_ids_str,
|
||||||
"tags": tags_str,
|
"tags": tags_str,
|
||||||
|
"offset": offset,
|
||||||
|
"limit": limit,
|
||||||
});
|
});
|
||||||
call_cb(
|
call_cb(
|
||||||
cb,
|
cb,
|
||||||
@@ -640,6 +646,8 @@ pub unsafe extern "C" fn qobuz_backend_search_playlists(
|
|||||||
"items": items,
|
"items": items,
|
||||||
"total": total,
|
"total": total,
|
||||||
"query": query_str,
|
"query": query_str,
|
||||||
|
"offset": offset,
|
||||||
|
"limit": limit,
|
||||||
});
|
});
|
||||||
call_cb(
|
call_cb(
|
||||||
cb,
|
cb,
|
||||||
@@ -684,6 +692,28 @@ pub unsafe extern "C" fn qobuz_backend_get_playlist(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn qobuz_backend_get_playlist_all(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 {
|
||||||
|
let result = client.lock().await.get_playlist_all(playlist_id).await;
|
||||||
|
let (ev, json) = match result {
|
||||||
|
Ok(r) => {
|
||||||
|
let mut v = serde_json::to_value(&r).unwrap_or_default();
|
||||||
|
if let serde_json::Value::Object(ref mut obj) = v {
|
||||||
|
obj.insert("full_load".to_string(), serde_json::Value::Bool(true));
|
||||||
|
}
|
||||||
|
(EV_PLAYLIST_OK, serde_json::to_string(&v).unwrap_or_default())
|
||||||
|
}
|
||||||
|
Err(e) => (EV_PLAYLIST_ERR, err_json(&e.to_string())),
|
||||||
|
};
|
||||||
|
call_cb(cb, ud, ev, &json);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Favorites ----------
|
// ---------- Favorites ----------
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
|||||||
@@ -95,6 +95,11 @@ void QobuzBackend::getPlaylist(qint64 playlistId, quint32 offset, quint32 limit)
|
|||||||
qobuz_backend_get_playlist(m_backend, playlistId, offset, limit);
|
qobuz_backend_get_playlist(m_backend, playlistId, offset, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void QobuzBackend::getPlaylistAll(qint64 playlistId)
|
||||||
|
{
|
||||||
|
qobuz_backend_get_playlist_all(m_backend, playlistId);
|
||||||
|
}
|
||||||
|
|
||||||
void QobuzBackend::getGenres()
|
void QobuzBackend::getGenres()
|
||||||
{
|
{
|
||||||
qobuz_backend_get_genres(m_backend);
|
qobuz_backend_get_genres(m_backend);
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public:
|
|||||||
void getArtistReleases(qint64 artistId, const QString &releaseType, quint32 limit = 50, quint32 offset = 0);
|
void getArtistReleases(qint64 artistId, const QString &releaseType, quint32 limit = 50, quint32 offset = 0);
|
||||||
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 getPlaylistAll(qint64 playlistId);
|
||||||
void getGenres();
|
void getGenres();
|
||||||
void getFeaturedAlbums(const QString &genreIds, 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 getFeaturedPlaylists(const QString &genreIds, const QString &kind, quint32 limit = 25, quint32 offset = 0);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <QHeaderView>
|
#include <QHeaderView>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
|
#include <QScrollBar>
|
||||||
|
|
||||||
namespace List
|
namespace List
|
||||||
{
|
{
|
||||||
@@ -41,12 +42,45 @@ Tracks::Tracks(QobuzBackend *backend, PlayQueue *queue, QWidget *parent)
|
|||||||
setFirstColumnSpanned(row, {}, true);
|
setFirstColumnSpanned(row, {}, true);
|
||||||
setSortingEnabled(!m_model->hasMultipleDiscs());
|
setSortingEnabled(!m_model->hasMultipleDiscs());
|
||||||
});
|
});
|
||||||
|
connect(verticalScrollBar(), &QScrollBar::valueChanged,
|
||||||
|
this, [this](int) { maybeLoadMorePlaylistTracks(); });
|
||||||
|
|
||||||
|
connect(m_backend, &QobuzBackend::playlistLoaded, this,
|
||||||
|
[this](const QJsonObject &playlist) {
|
||||||
|
if (!m_pendingPlayAll)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const qint64 id = static_cast<qint64>(playlist["id"].toDouble());
|
||||||
|
if (id != m_playlistId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!playlist["full_load"].toBool())
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_pendingPlayAll = false;
|
||||||
|
const bool shuffle = m_pendingPlayAllShuffle;
|
||||||
|
m_pendingPlayAllShuffle = false;
|
||||||
|
|
||||||
|
const QJsonArray items = playlist["tracks"].toObject()["items"].toArray();
|
||||||
|
if (items.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_queue->setContext(items, 0);
|
||||||
|
if (shuffle && !m_queue->shuffleEnabled())
|
||||||
|
m_queue->shuffleNow();
|
||||||
|
|
||||||
|
const qint64 firstId = static_cast<qint64>(m_queue->current()["id"].toDouble());
|
||||||
|
if (firstId > 0)
|
||||||
|
emit playTrackRequested(firstId);
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Tracks::loadTracks(const QJsonArray &tracks)
|
void Tracks::loadTracks(const QJsonArray &tracks)
|
||||||
{
|
{
|
||||||
setPlaylistContext(0);
|
setPlaylistContext(0);
|
||||||
|
m_pendingPlayAll = false;
|
||||||
|
m_pendingPlayAllShuffle = false;
|
||||||
setColumnHidden(TrackListModel::ColAlbum, false);
|
setColumnHidden(TrackListModel::ColAlbum, false);
|
||||||
m_model->setTracks(tracks, false, /*useSequential=*/true);
|
m_model->setTracks(tracks, false, /*useSequential=*/true);
|
||||||
}
|
}
|
||||||
@@ -54,6 +88,8 @@ void Tracks::loadTracks(const QJsonArray &tracks)
|
|||||||
void Tracks::loadAlbum(const QJsonObject &album)
|
void Tracks::loadAlbum(const QJsonObject &album)
|
||||||
{
|
{
|
||||||
setPlaylistContext(0);
|
setPlaylistContext(0);
|
||||||
|
m_pendingPlayAll = false;
|
||||||
|
m_pendingPlayAllShuffle = false;
|
||||||
setColumnHidden(TrackListModel::ColAlbum, true);
|
setColumnHidden(TrackListModel::ColAlbum, true);
|
||||||
const QJsonArray items = album["tracks"].toObject()["items"].toArray();
|
const QJsonArray items = album["tracks"].toObject()["items"].toArray();
|
||||||
m_model->setTracks(items); // album: use track_number
|
m_model->setTracks(items); // album: use track_number
|
||||||
@@ -67,21 +103,72 @@ void Tracks::loadPlaylist(const QJsonObject &playlist)
|
|||||||
const qint64 myId = AppSettings::instance().userId();
|
const qint64 myId = AppSettings::instance().userId();
|
||||||
const bool isOwned = (myId > 0 && ownId == myId);
|
const bool isOwned = (myId > 0 && ownId == myId);
|
||||||
setPlaylistContext(id, isOwned);
|
setPlaylistContext(id, isOwned);
|
||||||
const QJsonArray items = playlist["tracks"].toObject()["items"].toArray();
|
const QJsonObject tracksObj = playlist["tracks"].toObject();
|
||||||
|
const QJsonArray items = tracksObj["items"].toArray();
|
||||||
|
const int offset = tracksObj["offset"].toInt(0);
|
||||||
|
m_playlistTrackTotal = tracksObj["total"].toInt(items.size());
|
||||||
|
m_playlistLoadedCount = offset + items.size();
|
||||||
|
m_playlistLoadingMore = false;
|
||||||
m_model->setTracks(items, /*usePosition=*/true);
|
m_model->setTracks(items, /*usePosition=*/true);
|
||||||
|
maybeLoadMorePlaylistTracks();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tracks::appendPlaylistPage(const QJsonObject &playlist)
|
||||||
|
{
|
||||||
|
if (m_playlistId <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const qint64 id = static_cast<qint64>(playlist["id"].toDouble());
|
||||||
|
if (id != m_playlistId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QJsonObject tracksObj = playlist["tracks"].toObject();
|
||||||
|
const QJsonArray items = tracksObj["items"].toArray();
|
||||||
|
const int offset = tracksObj["offset"].toInt(m_playlistLoadedCount);
|
||||||
|
const int total = tracksObj["total"].toInt(m_playlistTrackTotal);
|
||||||
|
|
||||||
|
if (total > 0)
|
||||||
|
m_playlistTrackTotal = total;
|
||||||
|
|
||||||
|
// Ignore stale/duplicate pages.
|
||||||
|
if (offset < m_playlistLoadedCount) {
|
||||||
|
m_playlistLoadingMore = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!items.isEmpty()) {
|
||||||
|
m_model->appendTracks(items, /*usePosition=*/true);
|
||||||
|
m_playlistLoadedCount = offset + items.size();
|
||||||
|
} else {
|
||||||
|
m_playlistLoadedCount = qMax(m_playlistLoadedCount, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_playlistLoadingMore = false;
|
||||||
|
maybeLoadMorePlaylistTracks();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Tracks::loadSearchTracks(const QJsonArray &tracks)
|
void Tracks::loadSearchTracks(const QJsonArray &tracks)
|
||||||
{
|
{
|
||||||
setPlaylistContext(0);
|
setPlaylistContext(0);
|
||||||
|
m_pendingPlayAll = false;
|
||||||
|
m_pendingPlayAllShuffle = false;
|
||||||
setColumnHidden(TrackListModel::ColAlbum, false);
|
setColumnHidden(TrackListModel::ColAlbum, false);
|
||||||
m_model->setTracks(tracks, false, /*useSequential=*/true);
|
m_model->setTracks(tracks, false, /*useSequential=*/true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Tracks::setPlaylistContext(qint64 playlistId, bool isOwned)
|
void Tracks::setPlaylistContext(qint64 playlistId, bool isOwned)
|
||||||
{
|
{
|
||||||
|
if (m_playlistId != playlistId) {
|
||||||
|
m_pendingPlayAll = false;
|
||||||
|
m_pendingPlayAllShuffle = false;
|
||||||
|
m_playlistLoadingMore = false;
|
||||||
|
}
|
||||||
m_playlistId = playlistId;
|
m_playlistId = playlistId;
|
||||||
m_playlistIsOwned = isOwned;
|
m_playlistIsOwned = isOwned;
|
||||||
|
if (playlistId <= 0) {
|
||||||
|
m_playlistTrackTotal = 0;
|
||||||
|
m_playlistLoadedCount = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Tracks::setUserPlaylists(const QVector<QPair<qint64, QString>> &playlists)
|
void Tracks::setUserPlaylists(const QVector<QPair<qint64, QString>> &playlists)
|
||||||
@@ -113,6 +200,14 @@ void Tracks::playAll(bool shuffle)
|
|||||||
{
|
{
|
||||||
const QJsonArray tracks = m_model->currentTracksJson();
|
const QJsonArray tracks = m_model->currentTracksJson();
|
||||||
if (tracks.isEmpty()) return;
|
if (tracks.isEmpty()) return;
|
||||||
|
|
||||||
|
if (m_playlistId > 0 && m_playlistTrackTotal > tracks.size()) {
|
||||||
|
m_pendingPlayAll = true;
|
||||||
|
m_pendingPlayAllShuffle = shuffle;
|
||||||
|
m_backend->getPlaylistAll(m_playlistId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
m_queue->setContext(tracks, 0);
|
m_queue->setContext(tracks, 0);
|
||||||
// Shuffle once without touching the global shuffle flag — so a subsequent
|
// Shuffle once without touching the global shuffle flag — so a subsequent
|
||||||
// double-click on a track plays in normal order (unless global shuffle is on).
|
// double-click on a track plays in normal order (unless global shuffle is on).
|
||||||
@@ -123,6 +218,30 @@ void Tracks::playAll(bool shuffle)
|
|||||||
emit playTrackRequested(firstId);
|
emit playTrackRequested(firstId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Tracks::maybeLoadMorePlaylistTracks()
|
||||||
|
{
|
||||||
|
if (m_playlistId <= 0)
|
||||||
|
return;
|
||||||
|
if (m_pendingPlayAll)
|
||||||
|
return;
|
||||||
|
if (m_playlistLoadingMore)
|
||||||
|
return;
|
||||||
|
if (m_playlistTrackTotal > 0 && m_playlistLoadedCount >= m_playlistTrackTotal)
|
||||||
|
return;
|
||||||
|
|
||||||
|
QScrollBar *bar = verticalScrollBar();
|
||||||
|
if (!bar)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Start prefetching before the absolute bottom so paging feels seamless.
|
||||||
|
constexpr int kPrefetchPx = 180;
|
||||||
|
if (bar->maximum() > 0 && bar->value() < (bar->maximum() - kPrefetchPx))
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_playlistLoadingMore = true;
|
||||||
|
m_backend->getPlaylist(m_playlistId, static_cast<quint32>(m_playlistLoadedCount), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void Tracks::onDoubleClicked(const QModelIndex &index)
|
void Tracks::onDoubleClicked(const QModelIndex &index)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ namespace List
|
|||||||
void loadTracks(const QJsonArray &tracks);
|
void loadTracks(const QJsonArray &tracks);
|
||||||
void loadAlbum(const QJsonObject &album);
|
void loadAlbum(const QJsonObject &album);
|
||||||
void loadPlaylist(const QJsonObject &playlist);
|
void loadPlaylist(const QJsonObject &playlist);
|
||||||
|
void appendPlaylistPage(const QJsonObject &playlist);
|
||||||
void loadSearchTracks(const QJsonArray &tracks);
|
void loadSearchTracks(const QJsonArray &tracks);
|
||||||
|
|
||||||
/// Called when the backend fires EV_TRACK_CHANGED so the playing row is highlighted.
|
/// Called when the backend fires EV_TRACK_CHANGED so the playing row is highlighted.
|
||||||
@@ -55,9 +56,16 @@ namespace List
|
|||||||
QobuzBackend *m_backend = nullptr;
|
QobuzBackend *m_backend = nullptr;
|
||||||
PlayQueue *m_queue = nullptr;
|
PlayQueue *m_queue = nullptr;
|
||||||
qint64 m_playlistId = 0;
|
qint64 m_playlistId = 0;
|
||||||
|
int m_playlistTrackTotal = 0;
|
||||||
|
int m_playlistLoadedCount = 0;
|
||||||
bool m_playlistIsOwned = false;
|
bool m_playlistIsOwned = false;
|
||||||
|
bool m_playlistLoadingMore = false;
|
||||||
|
bool m_pendingPlayAll = false;
|
||||||
|
bool m_pendingPlayAllShuffle = false;
|
||||||
QVector<QPair<qint64, QString>> m_userPlaylists;
|
QVector<QPair<qint64, QString>> m_userPlaylists;
|
||||||
|
|
||||||
|
void maybeLoadMorePlaylistTracks();
|
||||||
|
|
||||||
void onDoubleClicked(const QModelIndex &index);
|
void onDoubleClicked(const QModelIndex &index);
|
||||||
void onContextMenu(const QPoint &pos);
|
void onContextMenu(const QPoint &pos);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -150,6 +150,9 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
|
|||||||
connect(m_library, &List::Library::userPlaylistIdsChanged,
|
connect(m_library, &List::Library::userPlaylistIdsChanged,
|
||||||
this, [this](const QSet<qint64> &playlistIds) {
|
this, [this](const QSet<qint64> &playlistIds) {
|
||||||
m_userPlaylistIds = playlistIds;
|
m_userPlaylistIds = playlistIds;
|
||||||
|
const qint64 currentPlaylistId = m_content->tracksList()->playlistId();
|
||||||
|
if (currentPlaylistId > 0)
|
||||||
|
m_content->setCurrentPlaylistFollowed(m_userPlaylistIds.contains(currentPlaylistId));
|
||||||
});
|
});
|
||||||
connect(m_library, &List::Library::userPlaylistsChanged,
|
connect(m_library, &List::Library::userPlaylistsChanged,
|
||||||
this, &MainWindow::onUserPlaylistsChanged);
|
this, &MainWindow::onUserPlaylistsChanged);
|
||||||
@@ -537,6 +540,13 @@ void MainWindow::onArtistLoaded(const QJsonObject &artist)
|
|||||||
|
|
||||||
void MainWindow::onPlaylistLoaded(const QJsonObject &playlist)
|
void MainWindow::onPlaylistLoaded(const QJsonObject &playlist)
|
||||||
{
|
{
|
||||||
|
const bool fullLoad = playlist["full_load"].toBool(false);
|
||||||
|
const int trackOffset = playlist["tracks"].toObject()["offset"].toInt(0);
|
||||||
|
if (!fullLoad && trackOffset > 0) {
|
||||||
|
m_content->tracksList()->appendPlaylistPage(playlist);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const qint64 id = static_cast<qint64>(playlist["id"].toDouble());
|
const qint64 id = static_cast<qint64>(playlist["id"].toDouble());
|
||||||
const qint64 ownerId = static_cast<qint64>(playlist["owner"].toObject()["id"].toDouble());
|
const qint64 ownerId = static_cast<qint64>(playlist["owner"].toObject()["id"].toDouble());
|
||||||
const qint64 myId = AppSettings::instance().userId();
|
const qint64 myId = AppSettings::instance().userId();
|
||||||
|
|||||||
@@ -109,6 +109,92 @@ void TrackListModel::setTracks(const QJsonArray &tracks,
|
|||||||
emit sortApplied();
|
emit sortApplied();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TrackListModel::appendTracks(const QJsonArray &tracks,
|
||||||
|
bool usePosition,
|
||||||
|
bool useSequential)
|
||||||
|
{
|
||||||
|
if (tracks.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Keep append path simple and stable: disc-header mode is handled by reset path.
|
||||||
|
if (m_hasMultipleDiscs && !usePosition && !useSequential) {
|
||||||
|
QJsonArray all = currentTracksJson();
|
||||||
|
for (const QJsonValue &v : tracks)
|
||||||
|
all.append(v);
|
||||||
|
setTracks(all, usePosition, useSequential);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int seq = 1;
|
||||||
|
if (useSequential || usePosition) {
|
||||||
|
for (const TrackItem &t : m_tracks)
|
||||||
|
if (!t.isDiscHeader)
|
||||||
|
++seq;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<TrackItem> parsed;
|
||||||
|
parsed.reserve(tracks.size());
|
||||||
|
for (const QJsonValue &v : tracks) {
|
||||||
|
const QJsonObject t = v.toObject();
|
||||||
|
TrackItem item;
|
||||||
|
item.id = static_cast<qint64>(t["id"].toDouble());
|
||||||
|
item.playlistTrackId = static_cast<qint64>(t["playlist_track_id"].toDouble());
|
||||||
|
item.discNumber = t["media_number"].toInt(1);
|
||||||
|
item.duration = static_cast<qint64>(t["duration"].toDouble());
|
||||||
|
item.streamable = t["streamable"].toBool(true);
|
||||||
|
item.hiRes = t["hires_streamable"].toBool();
|
||||||
|
item.raw = t;
|
||||||
|
|
||||||
|
const QString base = t["title"].toString();
|
||||||
|
const QString version = t["version"].toString().trimmed();
|
||||||
|
item.title = version.isEmpty() ? base
|
||||||
|
: base + QStringLiteral(" (") + version + QLatin1Char(')');
|
||||||
|
|
||||||
|
if (useSequential) {
|
||||||
|
item.number = seq++;
|
||||||
|
} else if (usePosition) {
|
||||||
|
const int pos = t["position"].toInt();
|
||||||
|
item.number = pos > 0 ? pos : seq;
|
||||||
|
++seq;
|
||||||
|
} else {
|
||||||
|
item.number = t["track_number"].toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonObject performer = t["performer"].toObject();
|
||||||
|
item.artist = performer["name"].toString();
|
||||||
|
if (item.artist.isEmpty()) {
|
||||||
|
const QJsonValue n = t["album"].toObject()["artist"].toObject()["name"];
|
||||||
|
item.artist = n.isObject() ? n.toObject()["display"].toString() : n.toString();
|
||||||
|
}
|
||||||
|
if (item.artist.isEmpty()) {
|
||||||
|
const QJsonValue n = t["artist"].toObject()["name"];
|
||||||
|
item.artist = n.isObject() ? n.toObject()["display"].toString() : n.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonObject album = t["album"].toObject();
|
||||||
|
item.album = album["title"].toString();
|
||||||
|
item.albumId = album["id"].toString();
|
||||||
|
|
||||||
|
parsed.append(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const int first = m_tracks.size();
|
||||||
|
const int last = first + parsed.size() - 1;
|
||||||
|
beginInsertRows({}, first, last);
|
||||||
|
m_tracks += parsed;
|
||||||
|
endInsertRows();
|
||||||
|
|
||||||
|
if (!m_hasMultipleDiscs && m_sortColumn >= 0) {
|
||||||
|
emit layoutAboutToBeChanged();
|
||||||
|
sortData(m_sortColumn, m_sortOrder);
|
||||||
|
emit layoutChanged();
|
||||||
|
emit sortApplied();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void TrackListModel::clear()
|
void TrackListModel::clear()
|
||||||
{
|
{
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ public:
|
|||||||
void setTracks(const QJsonArray &tracks,
|
void setTracks(const QJsonArray &tracks,
|
||||||
bool usePosition = false,
|
bool usePosition = false,
|
||||||
bool useSequential = false);
|
bool useSequential = false);
|
||||||
|
void appendTracks(const QJsonArray &tracks,
|
||||||
|
bool usePosition = false,
|
||||||
|
bool useSequential = false);
|
||||||
void clear();
|
void clear();
|
||||||
void setPlayingId(qint64 id);
|
void setPlayingId(qint64 id);
|
||||||
qint64 playingId() const { return m_playingId; }
|
qint64 playingId() const { return m_playingId; }
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ GenreBrowserView::GenreBrowserView(QobuzBackend *backend, PlayQueue *queue, QWid
|
|||||||
this, &GenreBrowserView::onPlaylistActivated);
|
this, &GenreBrowserView::onPlaylistActivated);
|
||||||
connect(m_playlistList, &QTreeWidget::customContextMenuRequested,
|
connect(m_playlistList, &QTreeWidget::customContextMenuRequested,
|
||||||
this, &GenreBrowserView::onPlaylistContextMenu);
|
this, &GenreBrowserView::onPlaylistContextMenu);
|
||||||
|
connect(m_albumList->verticalScrollBar(), &QScrollBar::valueChanged,
|
||||||
|
this, &GenreBrowserView::onAlbumScroll);
|
||||||
|
connect(m_playlistList->verticalScrollBar(), &QScrollBar::valueChanged,
|
||||||
|
this, &GenreBrowserView::onPlaylistScroll);
|
||||||
|
|
||||||
m_kindCombo->setCurrentIndex(0);
|
m_kindCombo->setCurrentIndex(0);
|
||||||
refreshModeUi();
|
refreshModeUi();
|
||||||
@@ -358,40 +362,125 @@ void GenreBrowserView::onGenresLoaded(const QJsonObject &result)
|
|||||||
void GenreBrowserView::onFeaturedAlbumsLoaded(const QJsonObject &result)
|
void GenreBrowserView::onFeaturedAlbumsLoaded(const QJsonObject &result)
|
||||||
{
|
{
|
||||||
m_resultsStack->setCurrentIndex(0);
|
m_resultsStack->setCurrentIndex(0);
|
||||||
m_albumList->setAlbums(result["items"].toArray());
|
const QString genreIds = result["genre_ids"].toString();
|
||||||
|
const QString type = result["type"].toString();
|
||||||
|
const int offset = result["offset"].toInt();
|
||||||
|
if (genreIds != m_lastAlbumGenreIds || type != m_lastAlbumType)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QJsonArray items = result["items"].toArray();
|
||||||
|
if (offset <= 0)
|
||||||
|
m_albumList->setAlbums(items);
|
||||||
|
else
|
||||||
|
m_albumList->addAlbums(items);
|
||||||
|
|
||||||
|
m_albumTotal = result["total"].toInt();
|
||||||
|
m_albumOffset = offset + items.size();
|
||||||
|
if (items.isEmpty())
|
||||||
|
m_albumTotal = m_albumOffset;
|
||||||
|
m_loadingAlbums = false;
|
||||||
|
|
||||||
|
if (m_collectAlbumsForDeepShuffle) {
|
||||||
|
if (m_albumOffset < m_albumTotal) {
|
||||||
|
requestAlbumsPage(m_lastAlbumGenreIds, m_lastAlbumType, m_albumOffset, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_collectAlbumsForDeepShuffle = false;
|
||||||
|
startDeepShuffleFromLoadedAlbums();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the viewport is not scrollable yet, eagerly fetch more pages.
|
||||||
|
QScrollBar *bar = m_albumList->verticalScrollBar();
|
||||||
|
if (bar && bar->maximum() == 0 && m_albumOffset < m_albumTotal)
|
||||||
|
requestAlbumsPage(m_lastAlbumGenreIds, m_lastAlbumType, m_albumOffset, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GenreBrowserView::onFeaturedPlaylistsLoaded(const QJsonObject &result)
|
void GenreBrowserView::onFeaturedPlaylistsLoaded(const QJsonObject &result)
|
||||||
{
|
{
|
||||||
m_resultsStack->setCurrentIndex(1);
|
m_resultsStack->setCurrentIndex(1);
|
||||||
setPlaylistItems(result["items"].toArray());
|
const QString genreIds = result["genre_ids"].toString();
|
||||||
|
const QString type = result["type"].toString();
|
||||||
|
const int offset = result["offset"].toInt();
|
||||||
|
if (genreIds != m_lastPlaylistGenreIds || type != m_lastPlaylistType)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QJsonArray items = result["items"].toArray();
|
||||||
|
setPlaylistItems(items, offset > 0);
|
||||||
|
m_playlistTotal = result["total"].toInt();
|
||||||
|
m_playlistOffset = offset + items.size();
|
||||||
|
if (items.isEmpty())
|
||||||
|
m_playlistTotal = m_playlistOffset;
|
||||||
|
m_loadingPlaylists = false;
|
||||||
|
|
||||||
|
QScrollBar *bar = m_playlistList->verticalScrollBar();
|
||||||
|
if (bar && bar->maximum() == 0 && m_playlistOffset < m_playlistTotal)
|
||||||
|
requestPlaylistsPage(m_lastPlaylistGenreIds, m_lastPlaylistType, m_lastPlaylistTags, m_lastPlaylistQuery, m_playlistOffset, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GenreBrowserView::onDiscoverPlaylistsLoaded(const QJsonObject &result)
|
void GenreBrowserView::onDiscoverPlaylistsLoaded(const QJsonObject &result)
|
||||||
{
|
{
|
||||||
m_resultsStack->setCurrentIndex(1);
|
m_resultsStack->setCurrentIndex(1);
|
||||||
setPlaylistItems(result["items"].toArray());
|
const QString genreIds = result["genre_ids"].toString();
|
||||||
|
const QString tags = result["tags"].toString();
|
||||||
|
const int offset = result["offset"].toInt();
|
||||||
|
if (genreIds != m_lastPlaylistGenreIds || tags != m_lastPlaylistTags)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QJsonArray items = result["items"].toArray();
|
||||||
|
setPlaylistItems(items, offset > 0);
|
||||||
|
m_playlistTotal = result["total"].toInt();
|
||||||
|
m_playlistOffset = offset + items.size();
|
||||||
|
if (items.isEmpty())
|
||||||
|
m_playlistTotal = m_playlistOffset;
|
||||||
|
m_loadingPlaylists = false;
|
||||||
|
|
||||||
|
QScrollBar *bar = m_playlistList->verticalScrollBar();
|
||||||
|
if (bar && bar->maximum() == 0 && m_playlistOffset < m_playlistTotal)
|
||||||
|
requestPlaylistsPage(m_lastPlaylistGenreIds, m_lastPlaylistType, m_lastPlaylistTags, m_lastPlaylistQuery, m_playlistOffset, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GenreBrowserView::onPlaylistSearchLoaded(const QJsonObject &result)
|
void GenreBrowserView::onPlaylistSearchLoaded(const QJsonObject &result)
|
||||||
{
|
{
|
||||||
m_resultsStack->setCurrentIndex(1);
|
m_resultsStack->setCurrentIndex(1);
|
||||||
setPlaylistItems(result["items"].toArray());
|
const QString query = result["query"].toString();
|
||||||
|
const int offset = result["offset"].toInt();
|
||||||
|
if (query != m_lastPlaylistQuery)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QJsonArray items = result["items"].toArray();
|
||||||
|
setPlaylistItems(items, offset > 0);
|
||||||
|
m_playlistTotal = result["total"].toInt();
|
||||||
|
m_playlistOffset = offset + items.size();
|
||||||
|
if (items.isEmpty())
|
||||||
|
m_playlistTotal = m_playlistOffset;
|
||||||
|
m_loadingPlaylists = false;
|
||||||
|
|
||||||
|
QScrollBar *bar = m_playlistList->verticalScrollBar();
|
||||||
|
if (bar && bar->maximum() == 0 && m_playlistOffset < m_playlistTotal)
|
||||||
|
requestPlaylistsPage(m_lastPlaylistGenreIds, m_lastPlaylistType, m_lastPlaylistTags, m_lastPlaylistQuery, m_playlistOffset, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GenreBrowserView::onSelectionChanged()
|
void GenreBrowserView::onSelectionChanged()
|
||||||
{
|
{
|
||||||
|
m_collectAlbumsForDeepShuffle = false;
|
||||||
|
|
||||||
if (m_mode == BrowseMode::PlaylistSearch) {
|
if (m_mode == BrowseMode::PlaylistSearch) {
|
||||||
m_resultsStack->setCurrentIndex(1);
|
m_resultsStack->setCurrentIndex(1);
|
||||||
m_playlistSearchLabel->setVisible(true);
|
m_playlistSearchLabel->setVisible(true);
|
||||||
m_playlistSearchBox->setVisible(true);
|
m_playlistSearchBox->setVisible(true);
|
||||||
m_playlistSearchBtn->setVisible(true);
|
m_playlistSearchBtn->setVisible(true);
|
||||||
m_deepShuffleBtn->setVisible(false);
|
m_deepShuffleBtn->setVisible(false);
|
||||||
|
m_deepShuffleBtn->setEnabled(true);
|
||||||
|
m_deepShuffleBtn->setText(tr("⇄ Deep Shuffle"));
|
||||||
const QString query = m_playlistSearchBox->text().trimmed();
|
const QString query = m_playlistSearchBox->text().trimmed();
|
||||||
if (query.size() < 2) {
|
if (query.size() < 2) {
|
||||||
m_playlistList->clear();
|
m_playlistList->clear();
|
||||||
|
m_playlistOffset = 0;
|
||||||
|
m_playlistTotal = 0;
|
||||||
|
m_loadingPlaylists = false;
|
||||||
} else {
|
} else {
|
||||||
m_backend->searchPlaylists(query, 8, 0);
|
requestPlaylistsPage(QString(), QStringLiteral("search"), QString(), query, 0, false);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -408,20 +497,25 @@ void GenreBrowserView::onSelectionChanged()
|
|||||||
|
|
||||||
if (kind == QStringLiteral("playlists")) {
|
if (kind == QStringLiteral("playlists")) {
|
||||||
m_resultsStack->setCurrentIndex(1);
|
m_resultsStack->setCurrentIndex(1);
|
||||||
|
m_deepShuffleBtn->setVisible(false);
|
||||||
|
m_deepShuffleBtn->setEnabled(true);
|
||||||
|
m_deepShuffleBtn->setText(tr("⇄ Deep Shuffle"));
|
||||||
if (type == QStringLiteral("discover-new"))
|
if (type == QStringLiteral("discover-new"))
|
||||||
m_backend->discoverPlaylists(genreIds, QStringLiteral("new"), 25, 0);
|
requestPlaylistsPage(genreIds, type, QStringLiteral("new"), QString(), 0, false);
|
||||||
else if (type == QStringLiteral("discover-hires"))
|
else if (type == QStringLiteral("discover-hires"))
|
||||||
m_backend->discoverPlaylists(genreIds, QStringLiteral("hi-res"), 25, 0);
|
requestPlaylistsPage(genreIds, type, QStringLiteral("hi-res"), QString(), 0, false);
|
||||||
else if (type == QStringLiteral("discover-focus"))
|
else if (type == QStringLiteral("discover-focus"))
|
||||||
m_backend->discoverPlaylists(genreIds, QStringLiteral("focus"), 25, 0);
|
requestPlaylistsPage(genreIds, type, QStringLiteral("focus"), QString(), 0, false);
|
||||||
else if (type == QStringLiteral("discover-qobuzdigs"))
|
else if (type == QStringLiteral("discover-qobuzdigs"))
|
||||||
m_backend->discoverPlaylists(genreIds, QStringLiteral("qobuzdigs"), 25, 0);
|
requestPlaylistsPage(genreIds, type, QStringLiteral("qobuzdigs"), QString(), 0, false);
|
||||||
else
|
else
|
||||||
m_backend->getFeaturedPlaylists(genreIds, type, 25, 0);
|
requestPlaylistsPage(genreIds, type, QString(), QString(), 0, false);
|
||||||
} else {
|
} else {
|
||||||
m_resultsStack->setCurrentIndex(0);
|
m_resultsStack->setCurrentIndex(0);
|
||||||
m_deepShuffleBtn->setVisible(m_mode == BrowseMode::Genres);
|
m_deepShuffleBtn->setVisible(m_mode == BrowseMode::Genres);
|
||||||
m_backend->getFeaturedAlbums(genreIds, type, 50, 0);
|
m_deepShuffleBtn->setEnabled(true);
|
||||||
|
m_deepShuffleBtn->setText(tr("⇄ Deep Shuffle"));
|
||||||
|
requestAlbumsPage(genreIds, type, 0, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,11 +530,14 @@ QStringList GenreBrowserView::currentAlbumIds() const
|
|||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
void GenreBrowserView::onDeepShuffleClicked()
|
void GenreBrowserView::startDeepShuffleFromLoadedAlbums()
|
||||||
{
|
{
|
||||||
const QStringList albumIds = currentAlbumIds();
|
const QStringList albumIds = currentAlbumIds();
|
||||||
if (albumIds.isEmpty())
|
if (albumIds.isEmpty()) {
|
||||||
|
m_deepShuffleBtn->setEnabled(true);
|
||||||
|
m_deepShuffleBtn->setText(tr("⇄ Deep Shuffle"));
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
m_waitingDeepShuffle = true;
|
m_waitingDeepShuffle = true;
|
||||||
m_deepShuffleBtn->setEnabled(false);
|
m_deepShuffleBtn->setEnabled(false);
|
||||||
@@ -448,12 +545,113 @@ void GenreBrowserView::onDeepShuffleClicked()
|
|||||||
m_backend->getAlbumsTracks(albumIds);
|
m_backend->getAlbumsTracks(albumIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GenreBrowserView::requestAlbumsPage(const QString &genreIds, const QString &type, int offset, bool append)
|
||||||
|
{
|
||||||
|
if (append && m_loadingAlbums)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!append) {
|
||||||
|
m_loadingAlbums = false;
|
||||||
|
m_albumOffset = 0;
|
||||||
|
m_albumTotal = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_lastAlbumGenreIds = genreIds;
|
||||||
|
m_lastAlbumType = type;
|
||||||
|
m_loadingAlbums = true;
|
||||||
|
m_backend->getFeaturedAlbums(genreIds, type, 50, static_cast<quint32>(offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GenreBrowserView::requestPlaylistsPage(const QString &genreIds, const QString &type, const QString &tags, const QString &query, int offset, bool append)
|
||||||
|
{
|
||||||
|
if (append && m_loadingPlaylists)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!append) {
|
||||||
|
m_loadingPlaylists = false;
|
||||||
|
m_playlistOffset = 0;
|
||||||
|
m_playlistTotal = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_lastPlaylistGenreIds = genreIds;
|
||||||
|
m_lastPlaylistType = type;
|
||||||
|
m_lastPlaylistTags = tags;
|
||||||
|
m_lastPlaylistQuery = query;
|
||||||
|
m_loadingPlaylists = true;
|
||||||
|
|
||||||
|
if (type == QStringLiteral("search")) {
|
||||||
|
m_backend->searchPlaylists(query, 8, static_cast<quint32>(offset));
|
||||||
|
} else if (type.startsWith(QStringLiteral("discover-"))) {
|
||||||
|
m_backend->discoverPlaylists(genreIds, tags, 25, static_cast<quint32>(offset));
|
||||||
|
} else {
|
||||||
|
m_backend->getFeaturedPlaylists(genreIds, type, 25, static_cast<quint32>(offset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GenreBrowserView::onAlbumScroll(int value)
|
||||||
|
{
|
||||||
|
if (m_mode != BrowseMode::Genres)
|
||||||
|
return;
|
||||||
|
if (m_kindCombo->currentData().toString() != QStringLiteral("albums"))
|
||||||
|
return;
|
||||||
|
if (m_loadingAlbums)
|
||||||
|
return;
|
||||||
|
if (m_albumOffset >= m_albumTotal)
|
||||||
|
return;
|
||||||
|
|
||||||
|
QScrollBar *bar = m_albumList->verticalScrollBar();
|
||||||
|
if (!bar || value < (bar->maximum() - 12))
|
||||||
|
return;
|
||||||
|
|
||||||
|
requestAlbumsPage(m_lastAlbumGenreIds, m_lastAlbumType, m_albumOffset, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GenreBrowserView::onPlaylistScroll(int value)
|
||||||
|
{
|
||||||
|
if (m_loadingPlaylists)
|
||||||
|
return;
|
||||||
|
if (m_playlistOffset >= m_playlistTotal)
|
||||||
|
return;
|
||||||
|
|
||||||
|
QScrollBar *bar = m_playlistList->verticalScrollBar();
|
||||||
|
if (!bar || value < (bar->maximum() - 12))
|
||||||
|
return;
|
||||||
|
|
||||||
|
requestPlaylistsPage(
|
||||||
|
m_lastPlaylistGenreIds,
|
||||||
|
m_lastPlaylistType,
|
||||||
|
m_lastPlaylistTags,
|
||||||
|
m_lastPlaylistQuery,
|
||||||
|
m_playlistOffset,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GenreBrowserView::onDeepShuffleClicked()
|
||||||
|
{
|
||||||
|
m_deepShuffleBtn->setEnabled(false);
|
||||||
|
m_deepShuffleBtn->setText(tr("Loading…"));
|
||||||
|
|
||||||
|
if (m_loadingAlbums) {
|
||||||
|
m_collectAlbumsForDeepShuffle = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_albumOffset < m_albumTotal) {
|
||||||
|
m_collectAlbumsForDeepShuffle = true;
|
||||||
|
requestAlbumsPage(m_lastAlbumGenreIds, m_lastAlbumType, m_albumOffset, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
startDeepShuffleFromLoadedAlbums();
|
||||||
|
}
|
||||||
|
|
||||||
bool GenreBrowserView::tryHandleDeepShuffleTracks(const QJsonArray &tracks)
|
bool GenreBrowserView::tryHandleDeepShuffleTracks(const QJsonArray &tracks)
|
||||||
{
|
{
|
||||||
if (!m_waitingDeepShuffle)
|
if (!m_waitingDeepShuffle)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
m_waitingDeepShuffle = false;
|
m_waitingDeepShuffle = false;
|
||||||
|
m_collectAlbumsForDeepShuffle = false;
|
||||||
m_deepShuffleBtn->setEnabled(true);
|
m_deepShuffleBtn->setEnabled(true);
|
||||||
m_deepShuffleBtn->setText(tr("⇄ Deep Shuffle"));
|
m_deepShuffleBtn->setText(tr("⇄ Deep Shuffle"));
|
||||||
|
|
||||||
@@ -523,8 +721,9 @@ void GenreBrowserView::onPlaylistContextMenu(const QPoint &pos)
|
|||||||
menu.exec(m_playlistList->viewport()->mapToGlobal(pos));
|
menu.exec(m_playlistList->viewport()->mapToGlobal(pos));
|
||||||
}
|
}
|
||||||
|
|
||||||
void GenreBrowserView::setPlaylistItems(const QJsonArray &items)
|
void GenreBrowserView::setPlaylistItems(const QJsonArray &items, bool append)
|
||||||
{
|
{
|
||||||
|
if (!append)
|
||||||
m_playlistList->clear();
|
m_playlistList->clear();
|
||||||
|
|
||||||
QFont tagFont;
|
QFont tagFont;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include <QStackedWidget>
|
#include <QStackedWidget>
|
||||||
#include <QTreeWidget>
|
#include <QTreeWidget>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
#include <QScrollBar>
|
||||||
|
|
||||||
class GenreBrowserView : public QWidget
|
class GenreBrowserView : public QWidget
|
||||||
{
|
{
|
||||||
@@ -48,6 +49,8 @@ private slots:
|
|||||||
void onPlaylistActivated(QTreeWidgetItem *item, int column);
|
void onPlaylistActivated(QTreeWidgetItem *item, int column);
|
||||||
void onPlaylistContextMenu(const QPoint &pos);
|
void onPlaylistContextMenu(const QPoint &pos);
|
||||||
void onDeepShuffleClicked();
|
void onDeepShuffleClicked();
|
||||||
|
void onAlbumScroll(int value);
|
||||||
|
void onPlaylistScroll(int value);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QobuzBackend *m_backend = nullptr;
|
QobuzBackend *m_backend = nullptr;
|
||||||
@@ -72,12 +75,28 @@ private:
|
|||||||
int m_lastGenreComboIndex = 0;
|
int m_lastGenreComboIndex = 0;
|
||||||
QSet<qint64> m_multiGenreIds;
|
QSet<qint64> m_multiGenreIds;
|
||||||
bool m_waitingDeepShuffle = false;
|
bool m_waitingDeepShuffle = false;
|
||||||
|
bool m_collectAlbumsForDeepShuffle = false;
|
||||||
|
bool m_loadingAlbums = false;
|
||||||
|
bool m_loadingPlaylists = false;
|
||||||
|
int m_albumOffset = 0;
|
||||||
|
int m_albumTotal = 0;
|
||||||
|
int m_playlistOffset = 0;
|
||||||
|
int m_playlistTotal = 0;
|
||||||
|
QString m_lastAlbumGenreIds;
|
||||||
|
QString m_lastAlbumType;
|
||||||
|
QString m_lastPlaylistGenreIds;
|
||||||
|
QString m_lastPlaylistType;
|
||||||
|
QString m_lastPlaylistTags;
|
||||||
|
QString m_lastPlaylistQuery;
|
||||||
|
|
||||||
void refreshModeUi();
|
void refreshModeUi();
|
||||||
void refreshGenreTypeChoices();
|
void refreshGenreTypeChoices();
|
||||||
QString currentGenreIds() const;
|
QString currentGenreIds() const;
|
||||||
QStringList currentAlbumIds() const;
|
QStringList currentAlbumIds() const;
|
||||||
|
void startDeepShuffleFromLoadedAlbums();
|
||||||
|
void requestAlbumsPage(const QString &genreIds, const QString &type, int offset, bool append);
|
||||||
|
void requestPlaylistsPage(const QString &genreIds, const QString &type, const QString &tags, const QString &query, int offset, bool append);
|
||||||
bool chooseMultiGenres();
|
bool chooseMultiGenres();
|
||||||
void updateMultiGenreLabel();
|
void updateMultiGenreLabel();
|
||||||
void setPlaylistItems(const QJsonArray &items);
|
void setPlaylistItems(const QJsonArray &items, bool append = false);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user