feat(slint): genre filter advanced view + child-genre filtering
Adds the advanced view to the Discover genre filter and recovers the
child-genre filtering the discovery-v2 home redesign dropped.
Child-genre recovery:
- The old (pre-redesign) home expanded a parent selection to its
descendant genre NAMES and filtered content client-side; the
discovery-v2 redesign passed only the explicitly-selected ids to
the discover index, so selecting a parent stopped covering its
children. Here filter_ids() expands the selection to all loaded
descendant ids and feeds that to get_discover_index; selecting a
genre eager-loads its descendant subtree so the expansion is
complete.
Advanced view:
- Search box + a scrollable 3-level genre tree (parent / child /
grandchild), flattened in Rust into visible GenreTreeRow rows
honoring per-node expansion. Each row: indent by level, expand
arrow (lazy-loads children on first expand), checkbox, name, and
loaded child count. Search returns a flat match list across all
loaded genres.
- Opening the advanced view eager-loads every parent's children so
counts show up front; grandchildren stay lazy until a child is
expanded.
State/wiring:
- GenreTreeRow struct; GenreFilterState gains tree + search-query;
GenreFilterActions gains toggle-expand + search.
- genre_filter STATE moves to LazyLock (HashMap/HashSet aren't
const) and gains children map, expanded set, and search query.
- main.rs wires toggle-expand (lazy child load), search, and the
advanced-open eager child load; toggle now eager-loads
descendants before the re-fetch.
cc
This commit is contained in:
4 files changed
+466
-32
No files matched your search
@@ -7,10 +7,99 @@
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { Radius } from "../foundation/radius.slint";
|
||||
import { GenreFilterState, GenreFilterActions, GenreChip } from "../state.slint";
|
||||
import { GenreFilterState, GenreFilterActions, GenreChip, GenreTreeRow } from "../state.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
import { QbzToggle } from "../primitives/QbzToggle.slint";
|
||||
|
||||
// One row of the advanced-view tree: indent + expand arrow + checkbox
|
||||
// + name + child count.
|
||||
component GenreTreeRowItem inherits Rectangle {
|
||||
in property <GenreTreeRow> node;
|
||||
callback toggle(string);
|
||||
callback toggle-expand(string);
|
||||
|
||||
height: 34px;
|
||||
border-radius: 6px;
|
||||
background: ta.has-hover ? Theme.surface-hover : transparent;
|
||||
|
||||
HorizontalLayout {
|
||||
padding-left: 6px + root.node.level * 18px;
|
||||
padding-right: 10px;
|
||||
spacing: 6px;
|
||||
|
||||
// Expand arrow (parents / children with kids).
|
||||
Rectangle {
|
||||
width: 20px;
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
if root.node.has-children: QbzIcon {
|
||||
source: root.node.expanded
|
||||
? @image-url("../assets/icons/chevron-down.svg")
|
||||
: @image-url("../assets/icons/chevron-right.svg");
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
tint: Theme.text-muted;
|
||||
}
|
||||
}
|
||||
arrow-ta := TouchArea {
|
||||
mouse-cursor: root.node.has-children ? pointer : default;
|
||||
clicked => {
|
||||
if (root.node.has-children) {
|
||||
root.toggle-expand(root.node.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Checkbox.
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
Rectangle {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
border-width: 1.5px;
|
||||
border-color: root.node.selected ? Theme.accent : Theme.border-muted;
|
||||
background: root.node.selected ? Theme.accent : transparent;
|
||||
QbzIcon {
|
||||
visible: root.node.selected;
|
||||
source: @image-url("../assets/icons/check.svg");
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
tint: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.node.name;
|
||||
color: root.node.selected ? Theme.text-primary : Theme.text-secondary;
|
||||
font-size: 13px;
|
||||
font-weight: root.node.level == 0 ? Typography.medium : Typography.regular;
|
||||
vertical-alignment: center;
|
||||
horizontal-stretch: 1;
|
||||
overflow: elide;
|
||||
}
|
||||
if root.node.count > 0: Text {
|
||||
text: root.node.count;
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.legal;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
// Row click (outside the arrow) toggles selection.
|
||||
ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
// Sits below the arrow's TouchArea, so arrow clicks win.
|
||||
clicked => {
|
||||
root.toggle(root.node.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component GenreCard inherits Rectangle {
|
||||
in property <GenreChip> chip;
|
||||
callback clicked(string);
|
||||
@@ -171,15 +260,72 @@ export component GenreFilterPopup inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Advanced view stub (tree/search/counts: next increment) -
|
||||
if GenreFilterState.advanced: Rectangle {
|
||||
height: 80px;
|
||||
Text {
|
||||
text: @tr("Advanced view coming soon.");
|
||||
color: Theme.text-muted;
|
||||
font-size: 13px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
// --- Advanced view: search + 3-level genre tree -------------
|
||||
if GenreFilterState.advanced: VerticalLayout {
|
||||
spacing: 10px;
|
||||
|
||||
// Search box.
|
||||
Rectangle {
|
||||
height: 34px;
|
||||
border-radius: 6px;
|
||||
background: Theme.surface-elevated;
|
||||
border-width: 1px;
|
||||
border-color: search-input.has-focus ? Theme.border-subtle : Theme.surface-elevated;
|
||||
HorizontalLayout {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
spacing: 8px;
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
QbzIcon {
|
||||
source: @image-url("../assets/icons/search.svg");
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
tint: Theme.text-muted;
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
horizontal-stretch: 1;
|
||||
search-input := TextInput {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: Theme.text-primary;
|
||||
font-size: 13px;
|
||||
vertical-alignment: center;
|
||||
single-line: true;
|
||||
text <=> GenreFilterState.search-query;
|
||||
edited => {
|
||||
GenreFilterActions.search(self.text);
|
||||
}
|
||||
}
|
||||
Text {
|
||||
visible: search-input.text == "";
|
||||
text: @tr("Search genres…");
|
||||
color: Theme.text-muted;
|
||||
font-size: 13px;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tree (scrollable).
|
||||
Flickable {
|
||||
height: 320px;
|
||||
viewport-width: self.width;
|
||||
viewport-height: tree-col.preferred-height;
|
||||
tree-col := VerticalLayout {
|
||||
spacing: 1px;
|
||||
for entry in GenreFilterState.tree: GenreTreeRowItem {
|
||||
node: entry;
|
||||
toggle(id) => {
|
||||
GenreFilterActions.toggle(id);
|
||||
}
|
||||
toggle-expand(id) => {
|
||||
GenreFilterActions.toggle-expand(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user