feat: initial qobuz-qt source
Lightweight Qt6 desktop client for Qobuz with a Rust audio backend (Symphonia/CPAL via staticlib FFI). Mirrors the spotify-qt layout: toolbar with playback controls, library/context docks on the left, tabbed search side panel on the right, queue panel, now-playing dock. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,45 +2,63 @@
|
||||
|
||||
#include <QJsonValue>
|
||||
#include <QColor>
|
||||
#include <QIcon>
|
||||
#include <algorithm>
|
||||
|
||||
TrackListModel::TrackListModel(QObject *parent)
|
||||
: QAbstractTableModel(parent)
|
||||
{}
|
||||
|
||||
void TrackListModel::setTracks(const QJsonArray &tracks)
|
||||
void TrackListModel::setTracks(const QJsonArray &tracks,
|
||||
bool usePosition,
|
||||
bool useSequential)
|
||||
{
|
||||
beginResetModel();
|
||||
m_tracks.clear();
|
||||
m_tracks.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.number = t["track_number"].toInt();
|
||||
item.title = t["title"].toString();
|
||||
item.duration = static_cast<qint64>(t["duration"].toDouble());
|
||||
item.hiRes = t["hires_streamable"].toBool();
|
||||
item.streamable = t["streamable"].toBool();
|
||||
item.streamable = t["streamable"].toBool(true);
|
||||
item.raw = t;
|
||||
|
||||
// Performer / artist
|
||||
const QJsonObject performer = t["performer"].toObject();
|
||||
item.artist = performer["name"].toString();
|
||||
if (item.artist.isEmpty()) {
|
||||
const QJsonObject album = t["album"].toObject();
|
||||
const QJsonObject artist = album["artist"].toObject();
|
||||
item.artist = artist["name"].toString();
|
||||
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();
|
||||
}
|
||||
|
||||
// Album
|
||||
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.album = album["title"].toString();
|
||||
item.albumId = album["id"].toString();
|
||||
|
||||
m_tracks.append(item);
|
||||
}
|
||||
|
||||
// Re-apply sort silently inside the reset (no layout signals needed here)
|
||||
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)
|
||||
emit sortApplied();
|
||||
}
|
||||
|
||||
void TrackListModel::clear()
|
||||
@@ -50,16 +68,22 @@ void TrackListModel::clear()
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
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});
|
||||
}
|
||||
|
||||
int TrackListModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid()) return 0;
|
||||
return m_tracks.size();
|
||||
return parent.isValid() ? 0 : m_tracks.size();
|
||||
}
|
||||
|
||||
int TrackListModel::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid()) return 0;
|
||||
return ColCount;
|
||||
return parent.isValid() ? 0 : ColCount;
|
||||
}
|
||||
|
||||
QVariant TrackListModel::data(const QModelIndex &index, int role) const
|
||||
@@ -68,6 +92,7 @@ QVariant TrackListModel::data(const QModelIndex &index, int role) const
|
||||
return {};
|
||||
|
||||
const TrackItem &t = m_tracks.at(index.row());
|
||||
const bool isPlaying = (t.id == m_playingId && m_playingId != 0);
|
||||
|
||||
if (role == Qt::DisplayRole) {
|
||||
switch (index.column()) {
|
||||
@@ -79,8 +104,19 @@ QVariant TrackListModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
}
|
||||
|
||||
if (role == Qt::ForegroundRole && !t.streamable) {
|
||||
return QColor(Qt::gray);
|
||||
if (role == Qt::FontRole && isPlaying) {
|
||||
QFont f;
|
||||
f.setBold(true);
|
||||
return f;
|
||||
}
|
||||
|
||||
if (role == Qt::ForegroundRole) {
|
||||
if (!t.streamable) return QColor(Qt::gray);
|
||||
if (isPlaying) return QColor(0x1d, 0xb9, 0x54); // Qobuz green
|
||||
}
|
||||
|
||||
if (role == Qt::DecorationRole && index.column() == ColNumber && isPlaying) {
|
||||
return QIcon(QStringLiteral(":/res/icons/media-track-show-active.svg"));
|
||||
}
|
||||
|
||||
if (role == TrackIdRole) return t.id;
|
||||
@@ -92,19 +128,56 @@ QVariant TrackListModel::data(const QModelIndex &index, int role) const
|
||||
|
||||
QVariant TrackListModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
if (orientation != Qt::Horizontal || role != Qt::DisplayRole)
|
||||
return {};
|
||||
if (orientation != Qt::Horizontal) return {};
|
||||
|
||||
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::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;
|
||||
|
||||
if (m_tracks.isEmpty()) return;
|
||||
|
||||
emit layoutAboutToBeChanged();
|
||||
sortData(column, order);
|
||||
emit layoutChanged();
|
||||
|
||||
emit sortApplied();
|
||||
}
|
||||
|
||||
QString TrackListModel::formatDuration(qint64 secs)
|
||||
{
|
||||
const int m = static_cast<int>(secs / 60);
|
||||
|
||||
Reference in New Issue
Block a user