feat: show top tracks on artist profile with play/shuffle
- Adds extra=topTracks to artist/page API request - Embeds a List::Tracks widget at the top of ArtistView showing the artist's most popular tracks, with Play and Shuffle buttons - Bubbles playTrackRequested through MainContent up to MainWindow - Also adds the viz PCM ring buffer FFI infrastructure (for future spectrum widget) to the Rust backend Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -79,6 +79,11 @@ void qobuz_backend_set_replaygain(QobuzBackendOpaque *backend, bool enabled);
|
|||||||
void qobuz_backend_set_gapless(QobuzBackendOpaque *backend, bool enabled);
|
void qobuz_backend_set_gapless(QobuzBackendOpaque *backend, bool enabled);
|
||||||
void qobuz_backend_prefetch_track(QobuzBackendOpaque *backend, int64_t track_id, int32_t format_id);
|
void qobuz_backend_prefetch_track(QobuzBackendOpaque *backend, int64_t track_id, int32_t format_id);
|
||||||
|
|
||||||
|
// Visualizer PCM access
|
||||||
|
uint32_t qobuz_backend_viz_read(QobuzBackendOpaque *backend, float *buf, uint32_t max_samples);
|
||||||
|
uint32_t qobuz_backend_viz_sample_rate(const QobuzBackendOpaque *backend);
|
||||||
|
uint32_t qobuz_backend_viz_channels(const QobuzBackendOpaque *backend);
|
||||||
|
|
||||||
// Playlist management
|
// Playlist management
|
||||||
void qobuz_backend_create_playlist(QobuzBackendOpaque *backend, const char *name);
|
void qobuz_backend_create_playlist(QobuzBackendOpaque *backend, const char *name);
|
||||||
void qobuz_backend_delete_playlist(QobuzBackendOpaque *backend, int64_t playlist_id);
|
void qobuz_backend_delete_playlist(QobuzBackendOpaque *backend, int64_t playlist_id);
|
||||||
|
|||||||
@@ -258,7 +258,10 @@ impl QobuzClient {
|
|||||||
pub async fn get_artist_page(&self, artist_id: i64) -> Result<Value> {
|
pub async fn get_artist_page(&self, artist_id: i64) -> Result<Value> {
|
||||||
let resp = self
|
let resp = self
|
||||||
.get_request("artist/page")
|
.get_request("artist/page")
|
||||||
.query(&[("artist_id", artist_id.to_string())])
|
.query(&[
|
||||||
|
("artist_id", artist_id.to_string()),
|
||||||
|
("extra", "topTracks".to_string()),
|
||||||
|
])
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
Self::check_response(resp).await
|
Self::check_response(resp).await
|
||||||
|
|||||||
@@ -590,6 +590,34 @@ pub unsafe extern "C" fn qobuz_backend_get_user(ptr: *mut Backend) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- Visualizer PCM access ----------
|
||||||
|
|
||||||
|
/// Read up to `max_samples` f32 PCM values into `buf`.
|
||||||
|
/// Returns the number of samples actually read.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn qobuz_backend_viz_read(
|
||||||
|
ptr: *mut Backend,
|
||||||
|
buf: *mut f32,
|
||||||
|
max_samples: u32,
|
||||||
|
) -> u32 {
|
||||||
|
let consumer = &(*ptr).0.player.status.viz_consumer;
|
||||||
|
let Ok(mut lock) = consumer.try_lock() else { return 0 };
|
||||||
|
let slice = std::slice::from_raw_parts_mut(buf, max_samples as usize);
|
||||||
|
rb::RbConsumer::read(&mut *lock, slice).unwrap_or(0) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns current sample rate of the audio stream (0 if idle).
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn qobuz_backend_viz_sample_rate(ptr: *const Backend) -> u32 {
|
||||||
|
(*ptr).0.player.status.viz_sample_rate.load(std::sync::atomic::Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns current channel count (0 if idle).
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn qobuz_backend_viz_channels(ptr: *const Backend) -> u32 {
|
||||||
|
(*ptr).0.player.status.viz_channels.load(std::sync::atomic::Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Playlist management ----------
|
// ---------- Playlist management ----------
|
||||||
|
|
||||||
pub const EV_PLAYLIST_CREATED: c_int = 20;
|
pub const EV_PLAYLIST_CREATED: c_int = 20;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use rb::RB;
|
||||||
use std::io::{self, Read, Seek, SeekFrom};
|
use std::io::{self, Read, Seek, SeekFrom};
|
||||||
use std::sync::{
|
use std::sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
@@ -176,8 +177,12 @@ pub fn play_track_inline(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if audio_output.is_none() {
|
if audio_output.is_none() {
|
||||||
*audio_output = Some(AudioOutput::try_open(sample_rate, channels)?);
|
let mut ao = AudioOutput::try_open(sample_rate, channels)?;
|
||||||
|
ao.set_viz_producer(status.viz_ring.producer());
|
||||||
|
*audio_output = Some(ao);
|
||||||
}
|
}
|
||||||
|
status.viz_sample_rate.store(sample_rate, Ordering::Relaxed);
|
||||||
|
status.viz_channels.store(channels as u32, Ordering::Relaxed);
|
||||||
let ao = audio_output.as_mut().unwrap();
|
let ao = audio_output.as_mut().unwrap();
|
||||||
|
|
||||||
let mut stopped = false;
|
let mut stopped = false;
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
mod decoder;
|
mod decoder;
|
||||||
pub mod output;
|
pub mod output;
|
||||||
|
|
||||||
|
use rb::{SpscRb, RB};
|
||||||
use std::sync::{
|
use std::sync::{
|
||||||
atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering},
|
atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicU8, Ordering},
|
||||||
Arc,
|
Arc,
|
||||||
};
|
};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::api::TrackDto;
|
use crate::api::TrackDto;
|
||||||
|
|
||||||
|
/// Size of the visualizer ring buffer in f32 samples (~180ms at 44.1kHz stereo).
|
||||||
|
const VIZ_RING_SIZE: usize = 16 * 1024;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum PlayerCommand {
|
pub enum PlayerCommand {
|
||||||
Play(TrackInfo),
|
Play(TrackInfo),
|
||||||
@@ -50,10 +54,17 @@ pub struct PlayerStatus {
|
|||||||
pub replaygain_gain: Arc<std::sync::Mutex<f32>>,
|
pub replaygain_gain: Arc<std::sync::Mutex<f32>>,
|
||||||
/// When false the audio output is torn down after each track, producing a gap.
|
/// When false the audio output is torn down after each track, producing a gap.
|
||||||
pub gapless: Arc<AtomicBool>,
|
pub gapless: Arc<AtomicBool>,
|
||||||
|
/// Visualizer ring buffer (consumer side, read by FFI).
|
||||||
|
pub viz_ring: Arc<SpscRb<f32>>,
|
||||||
|
pub viz_consumer: Arc<std::sync::Mutex<rb::Consumer<f32>>>,
|
||||||
|
pub viz_sample_rate: Arc<AtomicU32>,
|
||||||
|
pub viz_channels: Arc<AtomicU32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlayerStatus {
|
impl PlayerStatus {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
let viz_ring = Arc::new(SpscRb::new(VIZ_RING_SIZE));
|
||||||
|
let viz_consumer = Arc::new(std::sync::Mutex::new(viz_ring.consumer()));
|
||||||
Self {
|
Self {
|
||||||
state: Arc::new(std::sync::Mutex::new(PlayerState::Idle)),
|
state: Arc::new(std::sync::Mutex::new(PlayerState::Idle)),
|
||||||
position_secs: Arc::new(AtomicU64::new(0)),
|
position_secs: Arc::new(AtomicU64::new(0)),
|
||||||
@@ -65,6 +76,10 @@ impl PlayerStatus {
|
|||||||
seek_target_secs: Arc::new(AtomicU64::new(0)),
|
seek_target_secs: Arc::new(AtomicU64::new(0)),
|
||||||
replaygain_gain: Arc::new(std::sync::Mutex::new(1.0)),
|
replaygain_gain: Arc::new(std::sync::Mutex::new(1.0)),
|
||||||
gapless: Arc::new(AtomicBool::new(false)),
|
gapless: Arc::new(AtomicBool::new(false)),
|
||||||
|
viz_ring,
|
||||||
|
viz_consumer,
|
||||||
|
viz_sample_rate: Arc::new(AtomicU32::new(0)),
|
||||||
|
viz_channels: Arc::new(AtomicU32::new(0)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const RING_BUFFER_SIZE: usize = 32 * 1024;
|
|||||||
|
|
||||||
pub struct AudioOutput {
|
pub struct AudioOutput {
|
||||||
ring_buf_producer: rb::Producer<f32>,
|
ring_buf_producer: rb::Producer<f32>,
|
||||||
|
viz_producer: Option<rb::Producer<f32>>,
|
||||||
_stream: cpal::Stream,
|
_stream: cpal::Stream,
|
||||||
pub sample_rate: u32,
|
pub sample_rate: u32,
|
||||||
pub channels: usize,
|
pub channels: usize,
|
||||||
@@ -51,12 +52,17 @@ impl AudioOutput {
|
|||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
ring_buf_producer: producer,
|
ring_buf_producer: producer,
|
||||||
|
viz_producer: None,
|
||||||
_stream: stream,
|
_stream: stream,
|
||||||
sample_rate,
|
sample_rate,
|
||||||
channels,
|
channels,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_viz_producer(&mut self, producer: rb::Producer<f32>) {
|
||||||
|
self.viz_producer = Some(producer);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn write(
|
pub fn write(
|
||||||
&mut self,
|
&mut self,
|
||||||
decoded: AudioBufferRef<'_>,
|
decoded: AudioBufferRef<'_>,
|
||||||
@@ -70,6 +76,11 @@ impl AudioOutput {
|
|||||||
sample_buf.copy_interleaved_ref(decoded);
|
sample_buf.copy_interleaved_ref(decoded);
|
||||||
let samples: Vec<f32> = sample_buf.samples().iter().map(|s| s * volume).collect();
|
let samples: Vec<f32> = sample_buf.samples().iter().map(|s| s * volume).collect();
|
||||||
|
|
||||||
|
// Best-effort copy for visualizer (non-blocking, ok to drop samples)
|
||||||
|
if let Some(ref mut viz) = self.viz_producer {
|
||||||
|
let _ = viz.write(&samples);
|
||||||
|
}
|
||||||
|
|
||||||
let mut remaining = &samples[..];
|
let mut remaining = &samples[..];
|
||||||
while !remaining.is_empty() {
|
while !remaining.is_empty() {
|
||||||
if stop.load(Ordering::SeqCst) {
|
if stop.load(Ordering::SeqCst) {
|
||||||
|
|||||||
@@ -186,6 +186,21 @@ quint64 QobuzBackend::duration() const { return qobuz_backend_get_duration(m_bac
|
|||||||
int QobuzBackend::volume() const { return qobuz_backend_get_volume(m_backend); }
|
int QobuzBackend::volume() const { return qobuz_backend_get_volume(m_backend); }
|
||||||
int QobuzBackend::state() const { return qobuz_backend_get_state(m_backend); }
|
int QobuzBackend::state() const { return qobuz_backend_get_state(m_backend); }
|
||||||
|
|
||||||
|
quint32 QobuzBackend::vizRead(float *buf, quint32 maxSamples)
|
||||||
|
{
|
||||||
|
return qobuz_backend_viz_read(m_backend, buf, maxSamples);
|
||||||
|
}
|
||||||
|
|
||||||
|
quint32 QobuzBackend::vizSampleRate() const
|
||||||
|
{
|
||||||
|
return qobuz_backend_viz_sample_rate(m_backend);
|
||||||
|
}
|
||||||
|
|
||||||
|
quint32 QobuzBackend::vizChannels() const
|
||||||
|
{
|
||||||
|
return qobuz_backend_viz_channels(m_backend);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- private slots ----
|
// ---- private slots ----
|
||||||
|
|
||||||
void QobuzBackend::onPositionTick()
|
void QobuzBackend::onPositionTick()
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ public:
|
|||||||
/// 1 = playing, 2 = paused, 0 = idle
|
/// 1 = playing, 2 = paused, 0 = idle
|
||||||
int state() const;
|
int state() const;
|
||||||
|
|
||||||
|
// --- visualizer PCM ---
|
||||||
|
quint32 vizRead(float *buf, quint32 maxSamples);
|
||||||
|
quint32 vizSampleRate() const;
|
||||||
|
quint32 vizChannels() const;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
// auth
|
// auth
|
||||||
void loginSuccess(const QString &token, const QJsonObject &user);
|
void loginSuccess(const QString &token, const QJsonObject &user);
|
||||||
|
|||||||
@@ -155,6 +155,8 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
|
|||||||
this, &MainWindow::onSearchAlbumSelected);
|
this, &MainWindow::onSearchAlbumSelected);
|
||||||
connect(m_content, &MainContent::artistRequested,
|
connect(m_content, &MainContent::artistRequested,
|
||||||
this, &MainWindow::onSearchArtistSelected);
|
this, &MainWindow::onSearchArtistSelected);
|
||||||
|
connect(m_content, &MainContent::playTrackRequested,
|
||||||
|
this, &MainWindow::onPlayTrackRequested);
|
||||||
|
|
||||||
// ---- Queue panel ----
|
// ---- Queue panel ----
|
||||||
connect(m_queuePanel, &QueuePanel::skipToTrackRequested,
|
connect(m_queuePanel, &QueuePanel::skipToTrackRequested,
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ void ArtistSection::updateToggleText(int count)
|
|||||||
// ArtistView
|
// ArtistView
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
ArtistView::ArtistView(QWidget *parent)
|
ArtistView::ArtistView(QobuzBackend *backend, PlayQueue *queue, QWidget *parent)
|
||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
{
|
{
|
||||||
auto *outerLayout = new QVBoxLayout(this);
|
auto *outerLayout = new QVBoxLayout(this);
|
||||||
@@ -105,6 +105,43 @@ ArtistView::ArtistView(QWidget *parent)
|
|||||||
sectLayout->setContentsMargins(0, 0, 0, 0);
|
sectLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
sectLayout->setSpacing(8);
|
sectLayout->setSpacing(8);
|
||||||
|
|
||||||
|
// --- Top Tracks section ---
|
||||||
|
m_topTracksSection = new QWidget(content);
|
||||||
|
auto *ttLayout = new QVBoxLayout(m_topTracksSection);
|
||||||
|
ttLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
ttLayout->setSpacing(0);
|
||||||
|
|
||||||
|
// Header row: label + play + shuffle
|
||||||
|
auto *ttHeader = new QWidget(m_topTracksSection);
|
||||||
|
auto *ttHeaderLayout = new QHBoxLayout(ttHeader);
|
||||||
|
ttHeaderLayout->setContentsMargins(6, 4, 6, 4);
|
||||||
|
ttHeaderLayout->setSpacing(6);
|
||||||
|
|
||||||
|
auto *ttLabel = new QLabel(tr("Popular Tracks"), ttHeader);
|
||||||
|
QFont ttFont = ttLabel->font();
|
||||||
|
ttFont.setBold(true);
|
||||||
|
ttLabel->setFont(ttFont);
|
||||||
|
ttHeaderLayout->addWidget(ttLabel, 1);
|
||||||
|
|
||||||
|
auto *playBtn = new QPushButton(tr("▶ Play"), ttHeader);
|
||||||
|
auto *shuffleBtn = new QPushButton(tr("⇀ Shuffle"), ttHeader);
|
||||||
|
ttHeaderLayout->addWidget(playBtn);
|
||||||
|
ttHeaderLayout->addWidget(shuffleBtn);
|
||||||
|
|
||||||
|
ttLayout->addWidget(ttHeader);
|
||||||
|
|
||||||
|
m_topTracks = new List::Tracks(backend, queue, m_topTracksSection);
|
||||||
|
// Limit visible height so it doesn't swamp the page
|
||||||
|
m_topTracks->setMaximumHeight(320);
|
||||||
|
ttLayout->addWidget(m_topTracks);
|
||||||
|
|
||||||
|
connect(playBtn, &QPushButton::clicked, m_topTracks, [this] { m_topTracks->playAll(false); });
|
||||||
|
connect(shuffleBtn, &QPushButton::clicked, m_topTracks, [this] { m_topTracks->playAll(true); });
|
||||||
|
connect(m_topTracks, &List::Tracks::playTrackRequested, this, &ArtistView::playTrackRequested);
|
||||||
|
|
||||||
|
sectLayout->addWidget(m_topTracksSection);
|
||||||
|
|
||||||
|
// --- Album sections ---
|
||||||
m_secAlbums = new ArtistSection(tr("Albums"), content);
|
m_secAlbums = new ArtistSection(tr("Albums"), content);
|
||||||
m_secEps = new ArtistSection(tr("Singles & EPs"), content);
|
m_secEps = new ArtistSection(tr("Singles & EPs"), content);
|
||||||
m_secLive = new ArtistSection(tr("Live"), content);
|
m_secLive = new ArtistSection(tr("Live"), content);
|
||||||
@@ -153,8 +190,12 @@ void ArtistView::setArtist(const QJsonObject &artist)
|
|||||||
m_bioLabel->setVisible(false);
|
m_bioLabel->setVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Top tracks: artist/page?extra=topTracks returns {"topTracks": {"items": [...]}}
|
||||||
|
const QJsonArray topTracks = artist["topTracks"].toObject()["items"].toArray();
|
||||||
|
m_topTracks->loadTracks(topTracks);
|
||||||
|
m_topTracksSection->setVisible(!topTracks.isEmpty());
|
||||||
|
|
||||||
// releases is an array of {type, has_more, items[]}
|
// releases is an array of {type, has_more, items[]}
|
||||||
// types we care about: "album", "epSingle", "live"
|
|
||||||
const QJsonArray releases = artist["releases"].toArray();
|
const QJsonArray releases = artist["releases"].toArray();
|
||||||
QJsonArray albums, eps, live, compilations;
|
QJsonArray albums, eps, live, compilations;
|
||||||
for (const QJsonValue &rv : releases) {
|
for (const QJsonValue &rv : releases) {
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "albumlistview.hpp"
|
#include "albumlistview.hpp"
|
||||||
|
#include "../list/tracks.hpp"
|
||||||
|
#include "../backend/qobuzbackend.hpp"
|
||||||
|
#include "../playqueue.hpp"
|
||||||
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QToolButton>
|
#include <QToolButton>
|
||||||
|
#include <QPushButton>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
|
|
||||||
@@ -31,23 +35,29 @@ private:
|
|||||||
void updateToggleText(int count);
|
void updateToggleText(int count);
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Artist detail page: name, biography, and albums split into collapsible sections
|
/// Artist detail page: name, biography, top tracks, and albums split into
|
||||||
/// (Albums / EPs & Singles / Other) keyed on the release_type field.
|
/// collapsible sections (Albums / EPs & Singles / Live / Compilations).
|
||||||
class ArtistView : public QWidget
|
class ArtistView : public QWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ArtistView(QWidget *parent = nullptr);
|
explicit ArtistView(QobuzBackend *backend, PlayQueue *queue, QWidget *parent = nullptr);
|
||||||
|
|
||||||
void setArtist(const QJsonObject &artist);
|
void setArtist(const QJsonObject &artist);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void albumSelected(const QString &albumId);
|
void albumSelected(const QString &albumId);
|
||||||
|
void playTrackRequested(qint64 trackId);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QLabel *m_nameLabel = nullptr;
|
QLabel *m_nameLabel = nullptr;
|
||||||
QLabel *m_bioLabel = nullptr;
|
QLabel *m_bioLabel = nullptr;
|
||||||
|
|
||||||
|
// Top tracks section
|
||||||
|
QWidget *m_topTracksSection = nullptr;
|
||||||
|
List::Tracks *m_topTracks = nullptr;
|
||||||
|
|
||||||
ArtistSection *m_secAlbums = nullptr;
|
ArtistSection *m_secAlbums = nullptr;
|
||||||
ArtistSection *m_secEps = nullptr;
|
ArtistSection *m_secEps = nullptr;
|
||||||
ArtistSection *m_secLive = nullptr;
|
ArtistSection *m_secLive = nullptr;
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ MainContent::MainContent(QobuzBackend *backend, PlayQueue *queue, QWidget *paren
|
|||||||
|
|
||||||
m_albumList = new AlbumListView(this);
|
m_albumList = new AlbumListView(this);
|
||||||
m_artistList = new ArtistListView(this);
|
m_artistList = new ArtistListView(this);
|
||||||
m_artistView = new ArtistView(this);
|
m_artistView = new ArtistView(backend, queue, this);
|
||||||
|
|
||||||
m_stack->addWidget(m_welcome); // 0
|
m_stack->addWidget(m_welcome); // 0
|
||||||
m_stack->addWidget(tracksPage); // 1
|
m_stack->addWidget(tracksPage); // 1
|
||||||
@@ -57,6 +57,7 @@ MainContent::MainContent(QobuzBackend *backend, PlayQueue *queue, QWidget *paren
|
|||||||
connect(m_albumList, &AlbumListView::albumSelected, this, &MainContent::albumRequested);
|
connect(m_albumList, &AlbumListView::albumSelected, this, &MainContent::albumRequested);
|
||||||
connect(m_artistList, &ArtistListView::artistSelected, this, &MainContent::artistRequested);
|
connect(m_artistList, &ArtistListView::artistSelected, this, &MainContent::artistRequested);
|
||||||
connect(m_artistView, &ArtistView::albumSelected, this, &MainContent::albumRequested);
|
connect(m_artistView, &ArtistView::albumSelected, this, &MainContent::albumRequested);
|
||||||
|
connect(m_artistView, &ArtistView::playTrackRequested, this, &MainContent::playTrackRequested);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainContent::showWelcome() { m_stack->setCurrentIndex(0); }
|
void MainContent::showWelcome() { m_stack->setCurrentIndex(0); }
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public:
|
|||||||
signals:
|
signals:
|
||||||
void albumRequested(const QString &albumId);
|
void albumRequested(const QString &albumId);
|
||||||
void artistRequested(qint64 artistId);
|
void artistRequested(qint64 artistId);
|
||||||
|
void playTrackRequested(qint64 trackId);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QobuzBackend *m_backend = nullptr;
|
QobuzBackend *m_backend = nullptr;
|
||||||
|
|||||||
Reference in New Issue
Block a user