#include "tracklistmodel.hpp" #include #include #include #include TrackListModel::TrackListModel(QObject *parent) : QAbstractTableModel(parent) {} void TrackListModel::setTracks(const QJsonArray &tracks, bool usePosition, bool useSequential) { beginResetModel(); m_tracks.clear(); m_tracks.reserve(tracks.size()); // Parse into a temporary list first so we can detect multi-disc QVector parsed; parsed.reserve(tracks.size()); int seq = 1; for (const QJsonValue &v : tracks) { const QJsonObject t = v.toObject(); TrackItem item; item.id = static_cast(t["id"].toDouble()); item.playlistTrackId = static_cast(t["playlist_track_id"].toDouble()); item.discNumber = t["media_number"].toInt(1); item.duration = static_cast(t["duration"].toDouble()); 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++; } 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()) item.artist = t["album"].toObject()["artist"].toObject()["name"].toString(); const QJsonObject album = t["album"].toObject(); item.album = album["title"].toString(); item.albumId = album["id"].toString(); parsed.append(item); } // 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(); if (!m_hasMultipleDiscs && m_sortColumn >= 0) emit sortApplied(); } void TrackListModel::clear() { beginResetModel(); m_tracks.clear(); endResetModel(); } void TrackListModel::removeTrack(int row) { if (row < 0 || row >= m_tracks.size()) return; beginRemoveRows({}, row, row); m_tracks.removeAt(row); endRemoveRows(); } void TrackListModel::setFavIds(const QSet &ids) { m_favIds = ids; if (!m_tracks.isEmpty()) emit dataChanged(index(0, ColTitle), index(rowCount() - 1, ColTitle), {Qt::DecorationRole}); } void TrackListModel::addFavId(qint64 id) { m_favIds.insert(id); for (int r = 0; r < m_tracks.size(); ++r) { if (m_tracks[r].id == id) { const auto idx = index(r, ColTitle); emit dataChanged(idx, idx, {Qt::DecorationRole}); } } } void TrackListModel::removeFavId(qint64 id) { m_favIds.remove(id); for (int r = 0; r < m_tracks.size(); ++r) { if (m_tracks[r].id == id) { const auto idx = index(r, ColTitle); emit dataChanged(idx, idx, {Qt::DecorationRole}); } } } void TrackListModel::setPlayingId(qint64 id) { m_playingId = id; if (!m_tracks.isEmpty()) emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1), {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 TrackListModel::discHeaderRows() const { QVector 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(); } int TrackListModel::columnCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : ColCount; } QVariant TrackListModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() >= m_tracks.size()) 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) { switch (index.column()) { case ColNumber: return t.number > 0 ? QString::number(t.number) : QString(); case ColTitle: return t.title; case ColArtist: return t.artist; case ColAlbum: return t.album; case ColDuration: return formatDuration(t.duration); } } if (role == Qt::FontRole && isPlaying) { QFont f; f.setBold(true); return f; } if (role == Qt::ForegroundRole) { if (!t.streamable) return QColor(0x55, 0x55, 0x55); if (isPlaying) return QColor(0xFF, 0xB2, 0x32); // Qobuz orange } if (role == Qt::DecorationRole && index.column() == ColNumber && isPlaying) { return QIcon(QStringLiteral(":/res/icons/media-track-show-active.svg")); } if (role == Qt::DecorationRole && index.column() == ColTitle && m_favIds.contains(t.id)) { return QIcon(QStringLiteral(":/res/icons/starred-symbolic.svg")); } if (role == TrackIdRole) return t.id; if (role == TrackJsonRole) return t.raw; if (role == HiResRole) return t.hiRes; if (role == PlaylistTrackIdRole) return t.playlistTrackId; return {}; } QVariant TrackListModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation != Qt::Horizontal) return {}; if (role == Qt::DisplayRole) { switch (section) { case ColNumber: return tr("#"); case ColTitle: return tr("Title"); case ColArtist: return tr("Artist"); case ColAlbum: return tr("Album"); case ColDuration: return tr("Duration"); } } if (role == Qt::DecorationRole && section == m_sortColumn) { return QIcon(QStringLiteral(":/res/icons/view-sort-ascending.svg")); } return {}; } void TrackListModel::sortData(int column, Qt::SortOrder order) { auto cmp = [&](const TrackItem &a, const TrackItem &b) -> bool { bool less = false; switch (column) { case ColNumber: less = a.number < b.number; break; case ColTitle: less = a.title < b.title; break; case ColArtist: less = a.artist < b.artist; break; case ColAlbum: less = a.album < b.album; break; case ColDuration: less = a.duration < b.duration; break; default: less = false; } return order == Qt::AscendingOrder ? less : !less; }; std::stable_sort(m_tracks.begin(), m_tracks.end(), cmp); } void TrackListModel::sort(int column, Qt::SortOrder order) { m_sortColumn = column; m_sortOrder = order; // Multi-disc albums keep their disc-ordered layout; don't re-sort if (m_hasMultipleDiscs || m_tracks.isEmpty()) return; emit layoutAboutToBeChanged(); sortData(column, order); emit layoutChanged(); emit sortApplied(); } QString TrackListModel::formatDuration(qint64 secs) { if (secs < 0) secs = 0; const qint64 m = secs / 60; const qint64 s = secs % 60; return QStringLiteral("%1:%2").arg(m).arg(s, 2, 10, QLatin1Char('0')); }