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