feat: artist/page endpoint, multi-disc fix, playlist ownership, UX improvements

- Switch artist view to artist/page API (proper sections: Albums, Singles & EPs,
  Live, Compilations; version in titles like "Deluxe")
- Fix artist sections categorization using releases[].type from artist/page
- Add getUser() backend call; fetch on session restore when userId=0 to fix
  playlist ownership (Remove from playlist / Delete playlist were missing)
- Fix multi-disc double-click / Play Now queue start index (disc headers were
  counted in row index but excluded from currentTracksJson)
- Hide redundant Album column when viewing an album
- Make artist name in context header clickable (navigates to artist page)
- Fix gap between title and artist name in context header (addStretch)
- Fix queue panel track titles to include version field
- Fix album list to show version in title column

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
joren
2026-03-24 23:09:04 +01:00
parent 69fb818c38
commit 872fdecdce
16 changed files with 272 additions and 58 deletions

View File

@@ -17,17 +17,27 @@ void TrackListModel::setTracks(const QJsonArray &tracks,
m_tracks.clear();
m_tracks.reserve(tracks.size());
// Parse into a temporary list first so we can detect multi-disc
QVector<TrackItem> parsed;
parsed.reserve(tracks.size());
int seq = 1;
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.title = t["title"].toString();
item.discNumber = t["media_number"].toInt(1);
item.duration = static_cast<qint64>(t["duration"].toDouble());
item.hiRes = t["hires_streamable"].toBool();
item.streamable = t["streamable"].toBool(true);
item.raw = t;
item.streamable = t["streamable"].toBool(true);
item.hiRes = t["hires_streamable"].toBool();
item.raw = t;
// Combine title + version ("Melody" + "Vocal Remix" → "Melody (Vocal Remix)")
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++;
@@ -48,17 +58,46 @@ void TrackListModel::setTracks(const QJsonArray &tracks,
item.album = album["title"].toString();
item.albumId = album["id"].toString();
m_tracks.append(item);
parsed.append(item);
}
// Re-apply sort silently inside the reset (no layout signals needed here)
if (m_sortColumn >= 0)
sortData(m_sortColumn, m_sortOrder);
// Multi-disc only makes sense for album context (not playlists / fav / search)
int maxDisc = 1;
if (!usePosition && !useSequential) {
for (const TrackItem &t : parsed)
maxDisc = qMax(maxDisc, t.discNumber);
}
m_hasMultipleDiscs = (maxDisc > 1);
if (m_hasMultipleDiscs) {
// Sort by disc then track number
std::stable_sort(parsed.begin(), parsed.end(), [](const TrackItem &a, const TrackItem &b) {
return a.discNumber != b.discNumber ? a.discNumber < b.discNumber
: a.number < b.number;
});
// Interleave disc header items
int currentDisc = -1;
for (const TrackItem &t : parsed) {
if (t.discNumber != currentDisc) {
TrackItem header;
header.isDiscHeader = true;
header.discNumber = t.discNumber;
header.title = tr("Disc %1").arg(t.discNumber);
m_tracks.append(header);
currentDisc = t.discNumber;
}
m_tracks.append(t);
}
} else {
m_tracks = parsed;
// Re-apply sort silently inside the reset
if (m_sortColumn >= 0)
sortData(m_sortColumn, m_sortOrder);
}
endResetModel();
// Tell external listeners the sorted order is ready (e.g. PlayQueue sync)
if (m_sortColumn >= 0)
if (!m_hasMultipleDiscs && m_sortColumn >= 0)
emit sortApplied();
}
@@ -115,6 +154,23 @@ void TrackListModel::setPlayingId(qint64 id)
{Qt::FontRole, Qt::DecorationRole});
}
Qt::ItemFlags TrackListModel::flags(const QModelIndex &index) const
{
if (!index.isValid() || index.row() >= m_tracks.size())
return Qt::NoItemFlags;
if (m_tracks.at(index.row()).isDiscHeader)
return Qt::ItemIsEnabled;
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
QVector<int> TrackListModel::discHeaderRows() const
{
QVector<int> rows;
for (int i = 0; i < m_tracks.size(); ++i)
if (m_tracks[i].isDiscHeader) rows.append(i);
return rows;
}
int TrackListModel::rowCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : m_tracks.size();
@@ -131,6 +187,19 @@ QVariant TrackListModel::data(const QModelIndex &index, int role) const
return {};
const TrackItem &t = m_tracks.at(index.row());
// Disc header rows: styled separator spanning all columns via setFirstColumnSpanned
if (t.isDiscHeader) {
if (role == Qt::DisplayRole && index.column() == ColNumber)
return t.title;
if (role == Qt::FontRole) {
QFont f; f.setBold(true); return f;
}
if (role == Qt::ForegroundRole)
return QColor(0xFF, 0xB2, 0x32);
return {};
}
const bool isPlaying = (t.id == m_playingId && m_playingId != 0);
if (role == Qt::DisplayRole) {
@@ -213,7 +282,8 @@ void TrackListModel::sort(int column, Qt::SortOrder order)
m_sortColumn = column;
m_sortOrder = order;
if (m_tracks.isEmpty()) return;
// Multi-disc albums keep their disc-ordered layout; don't re-sort
if (m_hasMultipleDiscs || m_tracks.isEmpty()) return;
emit layoutAboutToBeChanged();
sortData(column, order);