feat(qt): cortinilla local row click routing
Local rows route through the local seams, never the Qobuz ones. Their ids
live in a different space entirely: an album id is a group key, a track id is
a library row id, and an artist row has NO id at all. Handing any of them to
the catalog routes would 404 or, worse, open a stranger's album that happens
to share the number.
- album -> the local album view, by group key.
- artist -> the LocalLibrary Artists tab BY NAME, because there is no id to
route by.
- track -> played from the per-query snapshot.
Playing from the snapshot rather than re-resolving the id is deliberate: the
snapshot is the exact vector the payload was built from, so the queue matches
what the user is looking at - including the Plex rows that were prepended,
whose order a fresh lookup would not reproduce. The queue is the WHOLE
fetched vector starting at the clicked row, which is the reference's
behaviour: the dropdown shows three tracks, but activating one gives a real
queue instead of an orphan.
The position lookup is STRICT. If the clicked row is not in the snapshot it
logs and does nothing; falling back to index 0 would silently play a
different track than the one under the cursor, which is worse than not
responding.
Local rows stay out of the learned ranking, which the existing guard already
handled - it was dead code until now because no local rows existed. The
reason is the same id-space problem: a bucket keyed on a local id could never
be matched again, and a local artist has no id to key on.
Playlist rows are no longer inert. The note calling them impossible said
there was no playlist view in the port; open_playlist has existed and
PlaylistView.qml ships, so the row now opens the playlist like every other
surface does. PARITY-DEBT row 28 loses one of its sites.
Contract: qbz-nix-docs/qt-frontend/2026-08-03-cortinilla-parity, commit C8
(sections 3.1-3.3, 3.6, R7-2).
Gate: build clean, 258/258 tests, both QML audits OK, offscreen smoke
0 complaints + home published.
cc
This commit is contained in:
2 files changed
+59
-4
No files matched your search
@@ -375,7 +375,12 @@ pub async fn play_current_if_local(runtime: &Runtime, track_id: u64) -> bool {
|
||||
|
||||
/// Set the queue to `tracks` and start `start` (playback.rs
|
||||
/// `play_local_tracks_now`: `set_queue` then the AUDIBLE step).
|
||||
async fn play_rows(runtime: &Runtime, tracks: Vec<LocalTrack>, start: usize, shuffle: bool) {
|
||||
pub(crate) async fn play_rows(
|
||||
runtime: &Runtime,
|
||||
tracks: Vec<LocalTrack>,
|
||||
start: usize,
|
||||
shuffle: bool,
|
||||
) {
|
||||
if tracks.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -952,8 +952,13 @@ fn flat_index_content_y(data: &CortinillaData, flat_index: i32) -> f64 {
|
||||
}
|
||||
|
||||
/// Click / Enter on a row: record the interaction (Capa B) and dispatch by
|
||||
/// kind (album -> album view, artist -> artist view, track -> play,
|
||||
/// playlist -> inert POC-NOTE). The QML clears the input + closes itself.
|
||||
/// kind. Qobuz rows go to the catalog views; LOCAL rows go to the local seams
|
||||
/// (album by group key, artist BY NAME, track from the snapshot). The QML
|
||||
/// clears the input and closes itself.
|
||||
///
|
||||
/// Local rows are deliberately NOT recorded into the learned ranking: their
|
||||
/// ids are in a different space from the catalog's, and a local artist row
|
||||
/// has no id at all, so a bucket keyed on them could never be matched again.
|
||||
pub fn row_clicked(flat_index: i32) {
|
||||
let snap = LAST_CORT.lock().unwrap_or_else(|e| e.into_inner()).clone();
|
||||
let Some(data) = snap else { return };
|
||||
@@ -973,6 +978,22 @@ pub fn row_clicked(flat_index: i32) {
|
||||
};
|
||||
record(&data.query, &row.kind, &row.id, action);
|
||||
}
|
||||
// LOCAL rows route through the local seams, never the Qobuz ones: their
|
||||
// ids live in a different space entirely (an album id is a group key, an
|
||||
// artist row has NO id, a track id is a library row id). Handing any of
|
||||
// them to the catalog routes would 404 or, worse, open someone else's
|
||||
// album that happens to share the number.
|
||||
if row.source == "local" {
|
||||
match row.kind.as_str() {
|
||||
// The group key opens the local album view directly.
|
||||
"album" => crate::local_bridge::open_album_by_id(row.id),
|
||||
// BY NAME — local artists have no id (search_local.rs).
|
||||
"artist" => crate::local_album_actions::open_artist_by_name(row.title),
|
||||
"track" => play_local_row(&row.id),
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
match row.kind.as_str() {
|
||||
"album" => crate::open_album(row.id),
|
||||
"artist" => crate::open_artist(row.id),
|
||||
@@ -981,11 +1002,40 @@ pub fn row_clicked(flat_index: i32) {
|
||||
crate::play_track(id);
|
||||
}
|
||||
}
|
||||
// playlist: no playlist view in the POC (POC-NOTE — inert).
|
||||
"playlist" => crate::open_playlist(row.id),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Play a local cortinilla track row from the per-query snapshot.
|
||||
///
|
||||
/// The row id is NOT re-resolved against the database: the snapshot is the
|
||||
/// exact vector the payload was built from, so playing from it guarantees the
|
||||
/// queue matches what the user is looking at — including the Plex rows that
|
||||
/// were prepended, which a fresh lookup by id would not reproduce in order.
|
||||
///
|
||||
/// The queue is the WHOLE fetched vector (76 rows normally, 136 expanded)
|
||||
/// starting at the clicked row, which is the reference's behaviour: the
|
||||
/// dropdown shows three of them, but activating one gives you a real queue
|
||||
/// rather than a single orphan track.
|
||||
fn play_local_row(row_id: &str) {
|
||||
let rows = LAST_CORT_LOCAL
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone();
|
||||
// STRICT position: if the clicked row is not in the snapshot, do nothing.
|
||||
// Falling back to 0 would silently play a DIFFERENT track than the one
|
||||
// under the cursor, which is worse than not responding.
|
||||
let Some(start) = rows.iter().position(|t| t.id.to_string() == row_id) else {
|
||||
log::warn!("[qbz-qt] cortinilla: local row {row_id} not in the snapshot — ignored");
|
||||
return;
|
||||
};
|
||||
let runtime = crate::app();
|
||||
crate::spawn(async move {
|
||||
crate::local_playback::play_rows(&runtime, rows, start, false).await;
|
||||
});
|
||||
}
|
||||
|
||||
/// "View more" on a section: full search page on the matching tab
|
||||
/// (album -> 1, track -> 2, artist -> 3, playlist -> 4).
|
||||
pub async fn view_more(runtime: &Arc<AppRuntime<LoggingAdapter>>, kind: &str) {
|
||||
|
||||
Reference in new issue
Block a user