Sidebar header gains the controls flyout (auto-follow, font size on the 280px-adapted scale, dimming, active color, uppercase, copy lyrics, reset). Every setting persists per user via the new lyrics_prefs store (loaded on all session activation paths, applied into LyricsState, Tauri defaults). Settings gains the lyrics cache row with honest per-user stats and clear (the F1 fix shipped in qbz-lyrics). Parity and @tr sweep over the sidebar surfaces per the review inventory. Closes the S1-S5 implementation run of the Lyrics epic: Qobuz-first word-synced lyrics (real per-word timestamps - richer than Tauri's line-level estimation), LRCLIB/lyrics.ovh fallback, shared per-user cache with additive wsync column, 30Hz sync engine with word-anchored karaoke fill, auto-center scroll, and QConnect peer-following. cc
217 lines
7.5 KiB
Plaintext
217 lines
7.5 KiB
Plaintext
// Lyrics display-settings flyout panel — 1:1 to Tauri's
|
|
// `LyricsControlsPopover.svelte` (268px popover below-right of the sliders
|
|
// trigger), ADAPTED to the Slint flyout idiom (alignment standard "Modals &
|
|
// flyouts": padding 14px / spacing 12px, toggle rows = label
|
|
// `horizontal-stretch: 1` + control trailing, footer actions right-aligned
|
|
// `alignment: end` with the shared auto-width SecondaryButton; selects are
|
|
// `QbzSelect` with explicit menu-width — never pill option buttons, ADR-008,
|
|
// so Tauri's segmented S/M/L/XL and Off/Soft/Strong rows become selects).
|
|
//
|
|
// Row order is Tauri's exactly: Auto-follow / Font / Size / Active color /
|
|
// Uppercase / Dimming / footer (Copy lyrics + Reset).
|
|
//
|
|
// Deviations (spec §6): D6 — active color is a fixed swatch palette (Slint
|
|
// has no native color-picker input); the "Theme" reset link shows only while
|
|
// a custom color is set (exact Tauri chip semantics). Q6 RESOLVED SHIPPED —
|
|
// the font row is included: the four non-system families are bundled TTFs
|
|
// embedded at compile time (see LyricsLinesView.slint).
|
|
//
|
|
// This component is the popup CONTENT; the PopupWindow shell (explicit
|
|
// height + close-policy, the documented footgun) lives at the anchor site in
|
|
// LyricsSidebar. Every mutation writes the LyricsState in-out pref directly
|
|
// (live preview) and fires prefs-changed() so Rust persists.
|
|
|
|
import { Theme } from "../foundation/semantic-colors.slint";
|
|
import { Radius } from "../foundation/radius.slint";
|
|
import { QbzToggle } from "../primitives/QbzToggle.slint";
|
|
import { QbzSelect } from "../primitives/QbzSelect.slint";
|
|
import { QbzTextLink } from "../primitives/QbzTextLink.slint";
|
|
import { SecondaryButton } from "../primitives/SecondaryButton.slint";
|
|
import { LyricsState } from "../state.slint";
|
|
|
|
// One selectable color swatch (D6 palette). Ring-highlighted while it is the
|
|
// active custom color.
|
|
component ColorSwatch inherits Rectangle {
|
|
in property <color> swatch;
|
|
property <bool> selected:
|
|
LyricsState.use-custom-color && LyricsState.custom-color == root.swatch;
|
|
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 9px;
|
|
background: root.swatch;
|
|
border-width: root.selected ? 2px : (ta.has-hover ? 1px : 0px);
|
|
border-color: root.selected ? Theme.text-primary : Theme.border-muted;
|
|
|
|
ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
LyricsState.use-custom-color = true;
|
|
LyricsState.custom-color = root.swatch;
|
|
LyricsState.prefs-changed();
|
|
}
|
|
}
|
|
}
|
|
|
|
component RowLabel inherits Text {
|
|
color: Theme.text-secondary;
|
|
font-size: 12px;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 1;
|
|
}
|
|
|
|
export component LyricsControlsPanel inherits Rectangle {
|
|
// Copy gate — Tauri canCopy: loaded && !error && lines > 0
|
|
// (LyricsSidebar.svelte:35).
|
|
property <bool> can-copy:
|
|
LyricsState.status == 2 && LyricsState.lines.length > 0;
|
|
|
|
background: Theme.surface-main;
|
|
border-radius: Radius.sm;
|
|
border-width: 1px;
|
|
border-color: Theme.border-muted;
|
|
drop-shadow-blur: 24px;
|
|
drop-shadow-color: Theme.card-shadow;
|
|
drop-shadow-offset-y: 6px;
|
|
|
|
VerticalLayout {
|
|
padding: 14px;
|
|
spacing: 12px;
|
|
|
|
// Auto-follow.
|
|
HorizontalLayout {
|
|
height: 22px;
|
|
spacing: 8px;
|
|
RowLabel { text: @tr("Auto-follow"); }
|
|
QbzToggle {
|
|
checked: LyricsState.auto-follow;
|
|
toggled(v) => {
|
|
LyricsState.auto-follow = v;
|
|
LyricsState.prefs-changed();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Font (Q6 shipped — embedded families).
|
|
HorizontalLayout {
|
|
height: 30px;
|
|
spacing: 8px;
|
|
RowLabel { text: @tr("Font"); }
|
|
QbzSelect {
|
|
sm: true;
|
|
menu-width: 178px;
|
|
options: [
|
|
@tr("System"),
|
|
@tr("LINE Seed JP"),
|
|
@tr("Montserrat"),
|
|
@tr("Noto Sans"),
|
|
@tr("Source Sans 3"),
|
|
];
|
|
current-index: LyricsState.font-index;
|
|
selected(i) => {
|
|
LyricsState.font-index = i;
|
|
LyricsState.prefs-changed();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Size (Tauri segmented S/M/L/XL -> select, flyout idiom).
|
|
HorizontalLayout {
|
|
height: 30px;
|
|
spacing: 8px;
|
|
RowLabel { text: @tr("Size"); }
|
|
QbzSelect {
|
|
sm: true;
|
|
menu-width: 120px;
|
|
options: [@tr("S"), @tr("M"), @tr("L"), @tr("XL")];
|
|
current-index: LyricsState.size-index;
|
|
selected(i) => {
|
|
LyricsState.size-index = i;
|
|
LyricsState.prefs-changed();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Active color — label (+ "Theme" reset while custom), swatch palette
|
|
// underneath, right-aligned on the control grid (D6).
|
|
VerticalLayout {
|
|
height: 44px;
|
|
spacing: 8px;
|
|
HorizontalLayout {
|
|
spacing: 8px;
|
|
RowLabel { text: @tr("Active color"); }
|
|
if LyricsState.use-custom-color: QbzTextLink {
|
|
label: @tr("Theme");
|
|
clicked => {
|
|
LyricsState.use-custom-color = false;
|
|
LyricsState.prefs-changed();
|
|
}
|
|
}
|
|
}
|
|
HorizontalLayout {
|
|
alignment: end;
|
|
spacing: 6px;
|
|
ColorSwatch { swatch: #8b5cf6; }
|
|
ColorSwatch { swatch: #ec4899; }
|
|
ColorSwatch { swatch: #ef4444; }
|
|
ColorSwatch { swatch: #f97316; }
|
|
ColorSwatch { swatch: #eab308; }
|
|
ColorSwatch { swatch: #22c55e; }
|
|
ColorSwatch { swatch: #06b6d4; }
|
|
ColorSwatch { swatch: #3b82f6; }
|
|
}
|
|
}
|
|
|
|
// Uppercase.
|
|
HorizontalLayout {
|
|
height: 22px;
|
|
spacing: 8px;
|
|
RowLabel { text: @tr("Uppercase"); }
|
|
QbzToggle {
|
|
checked: LyricsState.uppercase;
|
|
toggled(v) => {
|
|
LyricsState.uppercase = v;
|
|
LyricsState.prefs-changed();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Dimming (Tauri segmented Off/Soft/Strong -> select).
|
|
HorizontalLayout {
|
|
height: 30px;
|
|
spacing: 8px;
|
|
RowLabel { text: @tr("Dimming"); }
|
|
QbzSelect {
|
|
sm: true;
|
|
menu-width: 130px;
|
|
options: [@tr("Off"), @tr("Soft"), @tr("Strong")];
|
|
current-index: LyricsState.dimming-mode;
|
|
selected(i) => {
|
|
LyricsState.dimming-mode = i;
|
|
LyricsState.prefs-changed();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Footer: Copy lyrics (gated) + Reset, right-aligned (standard).
|
|
HorizontalLayout {
|
|
height: 34px;
|
|
alignment: end;
|
|
spacing: 8px;
|
|
SecondaryButton {
|
|
label: @tr("Copy lyrics");
|
|
enabled: root.can-copy;
|
|
clicked => {
|
|
LyricsState.copy-lyrics();
|
|
}
|
|
}
|
|
SecondaryButton {
|
|
label: @tr("Reset");
|
|
clicked => {
|
|
LyricsState.reset-prefs();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|