feat(qbz-qt): phase 15 — search 1:1 (cortinilla + full page with filters)

Intelligent search engine reused from qbz_app (SWR cache, interaction
recording, rank_within, top_for_query; app-menu kill switch on the
shared pref). Header field fully live: 220ms debounce, keyboard
semantics (arrows/Enter/Esc), clear, idle-close. Cortinilla as a plain
overlay (pointer stays on the field — Slint's exact reason): top
result, spec-ordered sections with View more, skeleton, row activation
dispatches (album/artist views, track play). Search page: 5 searchType
filter radios (Main Artist/Performer/Composer/Label/Release Name),
5 scope tabs, Most-popular hero, carousels, per-type grids/lists with
paged Load more. Perf: cortinilla 245ms cached / 1.1s cold. Fixed an
artwork attach bug the screenshots exposed (url/path slot mixup).
18/18 tests.

km
This commit is contained in:
vicrodh committed 2026-07-28 00:49:53 -06:00
1 parent 9f86a40f52
commit 9eb46d5e25
10 files changed
+2936 -5

No files matched your search

+109
View File
@@ -174,6 +174,21 @@ pub mod qbz_bridge {
#[qproperty(f32, ambient_surface_alpha)]
#[qproperty(f32, ambient_bar_alpha)]
// --- Search (phase 15) ---------------------------------------------
// Cortinilla (live dropdown): open/loading flags, ONE JSON payload
// (search_qt.rs CortinillaData: query/top/sections with controller
// flat indices), keyboard selection + its content-space scroll y.
#[qproperty(bool, cortinilla_open)]
#[qproperty(bool, cortinilla_loading)]
#[qproperty(QString, cortinilla_json)]
#[qproperty(i32, selected_index)]
#[qproperty(f32, cortinilla_scroll_y)]
// Results page: ONE JSON document (search_qt.rs SearchPageDoc).
#[qproperty(QString, search_json)]
// The intelligent-search kill switch (ui_prefs pref state — the
// app-menu check; live-flippable, no restart).
#[qproperty(bool, intelligent_search)]
type QbzBridge = super::QbzBridgeRust;
/// Called once from Main.qml's Component.onCompleted: registers the
@@ -414,6 +429,42 @@ pub mod qbz_bridge {
/// unlike the titlebar).
#[qinvokable]
fn toggle_ambient_background(self: Pin<&mut QbzBridge>);
// --- Search (phase 15) ---------------------------------------------
/// Header field keystrokes (QML-debounced 220ms, >= 2 chars): drive
/// the cortinilla live query.
#[qinvokable]
fn search_live(self: Pin<&mut QbzBridge>, query: QString);
/// Enter with the cortinilla closed: full results page (All tab).
#[qinvokable]
fn search_submit(self: Pin<&mut QbzBridge>, query: QString);
/// Cortinilla: Esc / click-outside / idle-close / page change.
#[qinvokable]
fn cortinilla_dismiss(self: Pin<&mut QbzBridge>);
/// Arrow keys: delta -1 (up) / +1 (down) through the flat list.
#[qinvokable]
fn cortinilla_move_selection(self: Pin<&mut QbzBridge>, delta: i32);
/// Row click or Enter on the keyboard-selected row.
#[qinvokable]
fn cortinilla_row_clicked(self: Pin<&mut QbzBridge>, index: i32);
/// Section "View more" (kind: album | track | artist | playlist).
#[qinvokable]
fn cortinilla_view_more(self: Pin<&mut QbzBridge>, kind: QString);
/// The Enter affordance with no keyboard selection: Search > All.
#[qinvokable]
fn cortinilla_search_all(self: Pin<&mut QbzBridge>);
/// Results page: the five-tab strip.
#[qinvokable]
fn search_tab_changed(self: Pin<&mut QbzBridge>, tab: i32);
/// Per-type tab "Load more".
#[qinvokable]
fn search_load_more(self: Pin<&mut QbzBridge>, tab: i32);
/// searchType filter radios (0 = none, 1..5 = the chips).
#[qinvokable]
fn search_filter_changed(self: Pin<&mut QbzBridge>, index: i32);
/// App-menu intelligent-search toggle (the 2.0.0 opt-out module).
#[qinvokable]
fn toggle_intelligent_search(self: Pin<&mut QbzBridge>);
}
impl cxx_qt::Threading for QbzBridge {}
@@ -496,6 +547,13 @@ pub struct QbzBridgeRust {
ambient_dim: f32,
ambient_surface_alpha: f32,
ambient_bar_alpha: f32,
cortinilla_open: bool,
cortinilla_loading: bool,
cortinilla_json: QString,
selected_index: i32,
cortinilla_scroll_y: f32,
search_json: QString,
intelligent_search: bool,
}
impl Default for QbzBridgeRust {
@@ -569,6 +627,13 @@ impl Default for QbzBridgeRust {
ambient_dim: crate::settings_qt::ambient_dim(),
ambient_surface_alpha: crate::settings_qt::ambient_surface_alpha(),
ambient_bar_alpha: crate::settings_qt::ambient_bar_alpha(),
cortinilla_open: false,
cortinilla_loading: false,
cortinilla_json: QString::from("{}"),
selected_index: -1,
cortinilla_scroll_y: 0.0,
search_json: QString::from("{}"),
intelligent_search: crate::search_qt::intelligent_search_pref(),
}
}
}
@@ -851,4 +916,48 @@ impl qbz_bridge::QbzBridge {
pub fn toggle_ambient_background(self: Pin<&mut Self>) {
crate::toggle_ambient_background();
}
pub fn search_live(self: Pin<&mut Self>, query: QString) {
crate::search_live(query.to_string());
}
pub fn search_submit(self: Pin<&mut Self>, query: QString) {
crate::search_submit(query.to_string());
}
pub fn cortinilla_dismiss(self: Pin<&mut Self>) {
crate::search_qt::dismiss();
}
pub fn cortinilla_move_selection(self: Pin<&mut Self>, delta: i32) {
crate::search_qt::move_selection(delta);
}
pub fn cortinilla_row_clicked(self: Pin<&mut Self>, index: i32) {
crate::search_qt::row_clicked(index);
}
pub fn cortinilla_view_more(self: Pin<&mut Self>, kind: QString) {
crate::cortinilla_view_more(kind.to_string());
}
pub fn cortinilla_search_all(self: Pin<&mut Self>) {
crate::cortinilla_search_all();
}
pub fn search_tab_changed(self: Pin<&mut Self>, tab: i32) {
crate::search_qt::tab_changed(tab);
}
pub fn search_load_more(self: Pin<&mut Self>, tab: i32) {
crate::search_load_more(tab);
}
pub fn search_filter_changed(self: Pin<&mut Self>, index: i32) {
crate::search_filter_changed(index);
}
pub fn toggle_intelligent_search(self: Pin<&mut Self>) {
crate::toggle_intelligent_search();
}
}