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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user