- Local Library Tracks: build the play queue from the already-loaded rows (instant) instead of re-querying the DB + filling covers on every click. - Mouse back/forward buttons now drive history nav over track rows and album cards (a TouchArea swallows them, so each interactive surface forwards them; the AppShell catcher only saw the gaps). - ExpandableSearch open width trimmed 20px (no longer rides over left controls on small screens). - QbzSelect toolbar (Group/Sort/filter) trimmed 40px total. cc
157 lines
5.8 KiB
Plaintext
157 lines
5.8 KiB
Plaintext
// Reusable expandable page-search — the exact pattern from the JUMP TO
|
|
// bar (ArtistView): a magnifier that, on click, opens a right-anchored
|
|
// input that grows to the LEFT (over whatever sits to its left), with
|
|
// a leading magnifier glyph + a trailing X that clears and closes.
|
|
//
|
|
// Self-contained: occupies a small fixed footprint when closed; the
|
|
// open overlay overflows to the left, so nothing to its right moves.
|
|
|
|
import { Theme } from "../foundation/semantic-colors.slint";
|
|
import { Typography } from "../foundation/typography.slint";
|
|
import { QbzIcon } from "QbzIcon.slint";
|
|
|
|
export component ExpandableSearch inherits Rectangle {
|
|
in property <string> placeholder: "Search…";
|
|
// Trimmed 20px so the open field doesn't ride over the controls to its
|
|
// left on small screens.
|
|
in property <length> max-open-width: 196px;
|
|
in-out property <bool> open: false;
|
|
in-out property <string> query;
|
|
// Bootstrap-style small variant. Default is the standard size.
|
|
in property <bool> sm: false;
|
|
callback edited(string);
|
|
|
|
// Defer the focus: focusing the input synchronously inside `changed open`
|
|
// races the open animation/layout (and can panic with "Recursion
|
|
// detected"). A one-shot timer grabs focus a tick later, reliably.
|
|
focus-timer := Timer {
|
|
interval: 30ms;
|
|
running: false;
|
|
triggered => {
|
|
input.focus();
|
|
self.running = false;
|
|
}
|
|
}
|
|
changed open => {
|
|
if (root.open) {
|
|
focus-timer.running = true;
|
|
focus-timer.restart();
|
|
}
|
|
}
|
|
|
|
// Closed footprint — just the magnifier slot.
|
|
width: root.sm ? 30px : 34px;
|
|
height: root.sm ? 30px : 34px;
|
|
|
|
// Closed: the magnifier toggle. Fades (not toggled) so open/close is
|
|
// smooth in both directions.
|
|
Rectangle {
|
|
opacity: root.open ? 0.0 : 1.0;
|
|
animate opacity { duration: 120ms; easing: ease-in-out; }
|
|
width: root.sm ? 30px : 34px;
|
|
height: root.sm ? 30px : 34px;
|
|
border-radius: 6px;
|
|
background: open-ta.has-hover ? Theme.surface-hover : transparent;
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/search.svg");
|
|
width: root.sm ? 14px : 16px;
|
|
height: root.sm ? 14px : 16px;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
tint: open-ta.has-hover ? Theme.text-primary : Theme.text-muted;
|
|
}
|
|
open-ta := TouchArea {
|
|
enabled: !root.open;
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.open = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Open: right-anchored overlay that grows left. Always rendered; its width
|
|
// animates to 0 when closed (clip hides it), so the shrink is also smooth.
|
|
overlay := Rectangle {
|
|
x: parent.width - self.width;
|
|
width: root.open ? root.max-open-width : 0px;
|
|
height: root.sm ? 30px : 34px;
|
|
background: Theme.surface-card;
|
|
border-radius: 6px;
|
|
border-width: root.open ? 1px : 0px;
|
|
border-color: input.has-focus ? Theme.accent : Theme.border-subtle;
|
|
clip: true;
|
|
animate width { duration: 200ms; easing: ease-in-out; }
|
|
|
|
HorizontalLayout {
|
|
padding-left: 11px;
|
|
padding-right: 5px;
|
|
spacing: 8px;
|
|
// Leading magnifier glyph (decorative).
|
|
VerticalLayout {
|
|
horizontal-stretch: 0;
|
|
alignment: center;
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/search.svg");
|
|
width: 14px;
|
|
height: 14px;
|
|
tint: Theme.text-muted;
|
|
}
|
|
}
|
|
// Input + placeholder.
|
|
Rectangle {
|
|
horizontal-stretch: 1;
|
|
clip: true;
|
|
input := TextInput {
|
|
width: 100%;
|
|
height: 100%;
|
|
color: Theme.text-primary;
|
|
font-size: Typography.body;
|
|
vertical-alignment: center;
|
|
single-line: true;
|
|
text <=> root.query;
|
|
edited => {
|
|
root.edited(self.text);
|
|
}
|
|
}
|
|
Text {
|
|
visible: input.text == "";
|
|
width: 100%;
|
|
height: 100%;
|
|
text: root.placeholder;
|
|
color: Theme.text-muted;
|
|
font-size: Typography.body;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
}
|
|
// Trailing X — clears + closes (back to the magnifier).
|
|
VerticalLayout {
|
|
horizontal-stretch: 0;
|
|
alignment: center;
|
|
Rectangle {
|
|
width: root.sm ? 22px : 24px;
|
|
height: root.sm ? 22px : 24px;
|
|
border-radius: 4px;
|
|
background: clear-ta.has-hover ? Theme.surface-elevated : transparent;
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/x.svg");
|
|
width: 12px;
|
|
height: 12px;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
tint: clear-ta.has-hover ? Theme.text-primary : Theme.text-muted;
|
|
}
|
|
clear-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.query = "";
|
|
root.edited("");
|
|
root.open = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|