feat: add seamless lazy loading for genre and playlist views
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:
joren
2026-03-31 10:43:36 +02:00
parent 4ebd5ed3f0
commit e453f8acf3
12 changed files with 543 additions and 17 deletions

View File

@@ -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_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_all(QobuzBackendOpaque *backend, int64_t playlist_id);
// Favorites
void qobuz_backend_get_fav_tracks(QobuzBackendOpaque *backend, uint32_t offset, uint32_t limit);

View File

@@ -639,6 +639,51 @@ impl QobuzClient {
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.
async fn get_fav_ids(&self) -> Result<FavIdsDto> {
let resp = self

View File

@@ -514,6 +514,8 @@ pub unsafe extern "C" fn qobuz_backend_get_featured_albums(
"total": total,
"type": kind_str,
"genre_ids": genre_ids_str,
"offset": offset,
"limit": limit,
});
call_cb(
cb,
@@ -557,6 +559,8 @@ pub unsafe extern "C" fn qobuz_backend_get_featured_playlists(
"total": total,
"type": kind_str,
"genre_ids": genre_ids_str,
"offset": offset,
"limit": limit,
});
call_cb(
cb,
@@ -600,6 +604,8 @@ pub unsafe extern "C" fn qobuz_backend_discover_playlists(
"total": total,
"genre_ids": genre_ids_str,
"tags": tags_str,
"offset": offset,
"limit": limit,
});
call_cb(
cb,
@@ -640,6 +646,8 @@ pub unsafe extern "C" fn qobuz_backend_search_playlists(
"items": items,
"total": total,
"query": query_str,
"offset": offset,
"limit": limit,
});
call_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 ----------
#[no_mangle]

View File

@@ -95,6 +95,11 @@ void QobuzBackend::getPlaylist(qint64 playlistId, quint32 offset, quint32 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()
{
qobuz_backend_get_genres(m_backend);

View File

@@ -35,6 +35,7 @@ public:
void getArtistReleases(qint64 artistId, const QString &releaseType, quint32 limit = 50, quint32 offset = 0);
void getAlbumsTracks(const QStringList &albumIds);
void getPlaylist(qint64 playlistId, quint32 offset = 0, quint32 limit = 500);
void getPlaylistAll(qint64 playlistId);
void getGenres();
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);

View File

@@ -5,6 +5,7 @@
#include <QHeaderView>
#include <QMenu>
#include <QAction>
#include <QScrollBar>
namespace List
{
@@ -41,12 +42,45 @@ Tracks::Tracks(QobuzBackend *backend, PlayQueue *queue, QWidget *parent)
setFirstColumnSpanned(row, {}, true);
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)
{
setPlaylistContext(0);
m_pendingPlayAll = false;
m_pendingPlayAllShuffle = false;
setColumnHidden(TrackListModel::ColAlbum, false);
m_model->setTracks(tracks, false, /*useSequential=*/true);
}
@@ -54,6 +88,8 @@ void Tracks::loadTracks(const QJsonArray &tracks)
void Tracks::loadAlbum(const QJsonObject &album)
{
setPlaylistContext(0);
m_pendingPlayAll = false;
m_pendingPlayAllShuffle = false;
setColumnHidden(TrackListModel::ColAlbum, true);
const QJsonArray items = album["tracks"].toObject()["items"].toArray();
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 bool isOwned = (myId > 0 && ownId == myId);
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);
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)
{
setPlaylistContext(0);
m_pendingPlayAll = false;
m_pendingPlayAllShuffle = false;
setColumnHidden(TrackListModel::ColAlbum, false);
m_model->setTracks(tracks, false, /*useSequential=*/true);
}
void Tracks::setPlaylistContext(qint64 playlistId, bool isOwned)
{
if (m_playlistId != playlistId) {
m_pendingPlayAll = false;
m_pendingPlayAllShuffle = false;
m_playlistLoadingMore = false;
}
m_playlistId = playlistId;
m_playlistIsOwned = isOwned;
if (playlistId <= 0) {
m_playlistTrackTotal = 0;
m_playlistLoadedCount = 0;
}
}
void Tracks::setUserPlaylists(const QVector<QPair<qint64, QString>> &playlists)
@@ -113,6 +200,14 @@ void Tracks::playAll(bool shuffle)
{
const QJsonArray tracks = m_model->currentTracksJson();
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);
// 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).
@@ -123,6 +218,30 @@ void Tracks::playAll(bool shuffle)
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)
{

View File

@@ -24,6 +24,7 @@ namespace List
void loadTracks(const QJsonArray &tracks);
void loadAlbum(const QJsonObject &album);
void loadPlaylist(const QJsonObject &playlist);
void appendPlaylistPage(const QJsonObject &playlist);
void loadSearchTracks(const QJsonArray &tracks);
/// Called when the backend fires EV_TRACK_CHANGED so the playing row is highlighted.
@@ -55,9 +56,16 @@ namespace List
QobuzBackend *m_backend = nullptr;
PlayQueue *m_queue = nullptr;
qint64 m_playlistId = 0;
int m_playlistTrackTotal = 0;
int m_playlistLoadedCount = 0;
bool m_playlistIsOwned = false;
bool m_playlistLoadingMore = false;
bool m_pendingPlayAll = false;
bool m_pendingPlayAllShuffle = false;
QVector<QPair<qint64, QString>> m_userPlaylists;
void maybeLoadMorePlaylistTracks();
void onDoubleClicked(const QModelIndex &index);
void onContextMenu(const QPoint &pos);
};

View File

@@ -150,6 +150,9 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
connect(m_library, &List::Library::userPlaylistIdsChanged,
this, [this](const QSet<qint64> &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,
this, &MainWindow::onUserPlaylistsChanged);
@@ -537,6 +540,13 @@ void MainWindow::onArtistLoaded(const QJsonObject &artist)
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 ownerId = static_cast<qint64>(playlist["owner"].toObject()["id"].toDouble());
const qint64 myId = AppSettings::instance().userId();

View File

@@ -109,6 +109,92 @@ void TrackListModel::setTracks(const QJsonArray &tracks,
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()
{
beginResetModel();

View File

@@ -51,6 +51,9 @@ public:
void setTracks(const QJsonArray &tracks,
bool usePosition = false,
bool useSequential = false);
void appendTracks(const QJsonArray &tracks,
bool usePosition = false,
bool useSequential = false);
void clear();
void setPlayingId(qint64 id);
qint64 playingId() const { return m_playingId; }

View File

@@ -176,6 +176,10 @@ GenreBrowserView::GenreBrowserView(QobuzBackend *backend, PlayQueue *queue, QWid
this, &GenreBrowserView::onPlaylistActivated);
connect(m_playlistList, &QTreeWidget::customContextMenuRequested,
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);
refreshModeUi();
@@ -358,40 +362,125 @@ void GenreBrowserView::onGenresLoaded(const QJsonObject &result)
void GenreBrowserView::onFeaturedAlbumsLoaded(const QJsonObject &result)
{
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)
{
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)
{
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)
{
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()
{
m_collectAlbumsForDeepShuffle = false;
if (m_mode == BrowseMode::PlaylistSearch) {
m_resultsStack->setCurrentIndex(1);
m_playlistSearchLabel->setVisible(true);
m_playlistSearchBox->setVisible(true);
m_playlistSearchBtn->setVisible(true);
m_deepShuffleBtn->setVisible(false);
m_deepShuffleBtn->setEnabled(true);
m_deepShuffleBtn->setText(tr("⇄ Deep Shuffle"));
const QString query = m_playlistSearchBox->text().trimmed();
if (query.size() < 2) {
m_playlistList->clear();
m_playlistOffset = 0;
m_playlistTotal = 0;
m_loadingPlaylists = false;
} else {
m_backend->searchPlaylists(query, 8, 0);
requestPlaylistsPage(QString(), QStringLiteral("search"), QString(), query, 0, false);
}
return;
}
@@ -408,20 +497,25 @@ void GenreBrowserView::onSelectionChanged()
if (kind == QStringLiteral("playlists")) {
m_resultsStack->setCurrentIndex(1);
m_deepShuffleBtn->setVisible(false);
m_deepShuffleBtn->setEnabled(true);
m_deepShuffleBtn->setText(tr("⇄ Deep Shuffle"));
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"))
m_backend->discoverPlaylists(genreIds, QStringLiteral("hi-res"), 25, 0);
requestPlaylistsPage(genreIds, type, QStringLiteral("hi-res"), QString(), 0, false);
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"))
m_backend->discoverPlaylists(genreIds, QStringLiteral("qobuzdigs"), 25, 0);
requestPlaylistsPage(genreIds, type, QStringLiteral("qobuzdigs"), QString(), 0, false);
else
m_backend->getFeaturedPlaylists(genreIds, type, 25, 0);
requestPlaylistsPage(genreIds, type, QString(), QString(), 0, false);
} else {
m_resultsStack->setCurrentIndex(0);
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;
}
void GenreBrowserView::onDeepShuffleClicked()
void GenreBrowserView::startDeepShuffleFromLoadedAlbums()
{
const QStringList albumIds = currentAlbumIds();
if (albumIds.isEmpty())
if (albumIds.isEmpty()) {
m_deepShuffleBtn->setEnabled(true);
m_deepShuffleBtn->setText(tr("⇄ Deep Shuffle"));
return;
}
m_waitingDeepShuffle = true;
m_deepShuffleBtn->setEnabled(false);
@@ -448,12 +545,113 @@ void GenreBrowserView::onDeepShuffleClicked()
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)
{
if (!m_waitingDeepShuffle)
return false;
m_waitingDeepShuffle = false;
m_collectAlbumsForDeepShuffle = false;
m_deepShuffleBtn->setEnabled(true);
m_deepShuffleBtn->setText(tr("⇄ Deep Shuffle"));
@@ -523,9 +721,10 @@ void GenreBrowserView::onPlaylistContextMenu(const QPoint &pos)
menu.exec(m_playlistList->viewport()->mapToGlobal(pos));
}
void GenreBrowserView::setPlaylistItems(const QJsonArray &items)
void GenreBrowserView::setPlaylistItems(const QJsonArray &items, bool append)
{
m_playlistList->clear();
if (!append)
m_playlistList->clear();
QFont tagFont;
tagFont.setBold(true);

View File

@@ -14,6 +14,7 @@
#include <QStackedWidget>
#include <QTreeWidget>
#include <QWidget>
#include <QScrollBar>
class GenreBrowserView : public QWidget
{
@@ -48,6 +49,8 @@ private slots:
void onPlaylistActivated(QTreeWidgetItem *item, int column);
void onPlaylistContextMenu(const QPoint &pos);
void onDeepShuffleClicked();
void onAlbumScroll(int value);
void onPlaylistScroll(int value);
private:
QobuzBackend *m_backend = nullptr;
@@ -72,12 +75,28 @@ private:
int m_lastGenreComboIndex = 0;
QSet<qint64> m_multiGenreIds;
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 refreshGenreTypeChoices();
QString currentGenreIds() 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();
void updateMultiGenreLabel();
void setPlaylistItems(const QJsonArray &items);
void setPlaylistItems(const QJsonArray &items, bool append = false);
};