feat(slint): you-may-also-like discovery section with sticky dismiss
Closes the network sidebar with the fifth section — tag-based "You may
also like" discovery, with a per-tag dismiss store so a thumbs-down
sticks across artist pages that share the same primary genre tag.
Backend (`qbz-integrations` + `qbz-core`):
- DiscoveryArtist and DiscoveryResponse DTOs added to
qbz-integrations::musicbrainz::models.
- QbzCore::musicbrainz_discover_artists ports
v2_get_discovery_artists end to end (sans the RecoState local
"play count > 2" filter, which has no equivalent in the Slint
shell yet): primary-tag search, exclude seed + similar +
dismissed, deterministic shuffle keyed on seed_mbid, validate
each candidate on Qobuz, fall back to the secondary tag if we
have fewer than 5 matches.
- normalize_artist_name re-exported at the qbz-core crate root so
the dismiss handler in qbz-slint can use the same key shape that
the discovery pipeline filters on.
- Deterministic shuffle implemented with a simple xorshift PRNG
seeded from a stable hash of seed_mbid — keeps qbz-core free of
the rand dep.
Slint controller (`crates/qbz-slint/src/`):
- discovery_dismiss.rs — small JSON store at
`<data-dir>/qbz/discovery_dismiss.json` keyed by lowercased tag
-> list of normalized artist names. dismissed_for_tag is passed
into the discovery call as the filter source; dismiss() is
invoked by the thumbs-down handler.
- artist::load_mb_discovery / apply_mb_discovery /
remove_discovery_artist plumb the pipeline result through the
standard load-off-thread / apply-on-UI split. Discovery state
includes primary_tag so the dismiss handler can persist under
the right key.
- main.rs chains discovery after relationships in the same MB
enrichment task. NetworkSidebarActions.discovery-dismissed (now
carrying mbid + name) normalizes the name, persists the
dismiss, and removes the row from the visible list.
Slint UI (`ui/state.slint`, `ui/artist/ArtistPageView.slint`):
- discovery-tag property added to NetworkSidebarState.
- YOU MAY ALSO LIKE section after Relationships — one row per
candidate (SidebarLink + a 24x24 thumbs-down button on the
right). Clicking the link navigates via the artist-clicked
callback when a Qobuz id is present.
- thumbs-down.svg icon added to ui/assets/icons (Lucide
ThumbsDown).
cc
This commit is contained in:
9 files changed
+529
-16
No files matched your search
@@ -981,6 +981,88 @@ export component ArtistPageView inherits Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Discovery: "You may also like" --------------
|
||||
if NetworkSidebarState.mb-available
|
||||
&& (NetworkSidebarState.discovery-loading
|
||||
|| NetworkSidebarState.discovery-artists.length > 0):
|
||||
VerticalLayout {
|
||||
padding-left: 14px;
|
||||
padding-right: 14px;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
spacing: 4px;
|
||||
|
||||
Text {
|
||||
text: @tr("YOU MAY ALSO LIKE");
|
||||
color: Theme.text-muted;
|
||||
font-size: 11px;
|
||||
font-weight: Typography.semibold;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
if NetworkSidebarState.discovery-loading
|
||||
&& NetworkSidebarState.discovery-artists.length == 0: Text {
|
||||
text: @tr("Loading…");
|
||||
color: Theme.text-muted;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
// Each row: SidebarLink filling the row plus a
|
||||
// small thumbs-down button anchored to the
|
||||
// right. The link click navigates to the
|
||||
// Qobuz artist; the thumbs-down emits the
|
||||
// dismiss callback so the row drops out and
|
||||
// never reappears for this tag.
|
||||
for disc in NetworkSidebarState.discovery-artists:
|
||||
HorizontalLayout {
|
||||
spacing: 2px;
|
||||
|
||||
SidebarLink {
|
||||
horizontal-stretch: 1;
|
||||
label: disc.name;
|
||||
icon: @image-url("../assets/icons/user.svg");
|
||||
tooltip: disc.name;
|
||||
clicked => {
|
||||
if (disc.qobuz-id != "") {
|
||||
NetworkSidebarActions.artist-clicked(
|
||||
disc.qobuz-id);
|
||||
}
|
||||
}
|
||||
}
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
horizontal-stretch: 0;
|
||||
Rectangle {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
background: dismiss-ta.has-hover
|
||||
? Theme.surface-elevated
|
||||
: transparent;
|
||||
QbzIcon {
|
||||
source: @image-url(
|
||||
"../assets/icons/thumbs-down.svg");
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
tint: dismiss-ta.has-hover
|
||||
? Theme.text-primary
|
||||
: Theme.text-muted;
|
||||
}
|
||||
dismiss-ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
NetworkSidebarActions
|
||||
.discovery-dismissed(
|
||||
disc.mbid, disc.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 14V2" />
|
||||
<path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 375 B |
@@ -322,9 +322,14 @@ export global NetworkSidebarState {
|
||||
in property <bool> relationships-loading: false;
|
||||
in property <MbRelationshipsData> relationships;
|
||||
|
||||
// Discovery (MB tag + LB similar) section — populated in phase 8.
|
||||
// Discovery (MB tag-based) section — populated in phase 8.
|
||||
in property <bool> discovery-loading: false;
|
||||
in property <[DiscoveryArtist]> discovery-artists: [];
|
||||
// Primary tag the current discovery batch was seeded with —
|
||||
// delivered as the first argument of the discovery-dismissed
|
||||
// callback so the Rust side can persist the dismiss under the
|
||||
// right key.
|
||||
in property <string> discovery-tag;
|
||||
}
|
||||
|
||||
// Callbacks the sidebar emits. Each delivers the minimum payload the
|
||||
@@ -342,7 +347,9 @@ export global NetworkSidebarActions {
|
||||
// Relationship row clicked — opens MusicianPage with the role.
|
||||
callback musician-clicked(string /* name */, string /* role */);
|
||||
// Discovery dismiss — removes the row and remembers the rejection.
|
||||
callback discovery-dismissed(string /* mbid */);
|
||||
// Carries both the MBID (model key for removal) and the name
|
||||
// (persisted into the per-tag dismiss store after normalization).
|
||||
callback discovery-dismissed(string /* mbid */, string /* name */);
|
||||
// Sidebar toggled — Rust persists the new open state.
|
||||
callback toggle();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user