feat(ui): carry lyrics translation state and render bilingual lines

LyricsLineItem gains translation-text (empty when the doc has none) and
LyricsState the translation props (translation-available, show-translation,
toggle-translation callback) per spec section C. LyricsLinesView renders
the translation under its original line while show-translation is on —
smaller (~72%) and dimmer, static text that follows the line; the karaoke
fill stays keyed on the original timings/text. Secondary surfaces share
the model, so the field flows through untouched.

km
This commit is contained in:
vicrodh committed 2026-07-22 19:58:35 -06:00
1 parent 619f6d5abc
commit 499f652b71
2 files changed
+45

No files matched your search

@@ -70,6 +70,9 @@ import "../../../../static/fonts/SourceSans3-VariableFont_wght.ttf";
component LyricsLineRow inherits VerticalLayout {
in property <int> index;
in property <string> line-text;
// Translation of this line (Qobuz v10); empty when the doc has none.
// Rendered under the original line while LyricsState.show-translation.
in property <string> translation-text;
in property <length> size-inactive;
in property <length> size-active;
// 0 = off, 1 = soft, 2 = strong (Tauri default) — pref-driven (S5).
@@ -86,6 +89,10 @@ component LyricsLineRow inherits VerticalLayout {
// eager init already saw it active.
in property <int> recenter-epoch;
// Gap between the original line and its translation (no effect while
// the translation is hidden — a single child has no spacing).
spacing: 3px;
// Tauri renders uppercase via CSS text-transform; Slint transforms the
// string (display only — the engine still matches on the raw text).
property <string> display-text:
@@ -255,6 +262,26 @@ component LyricsLineRow inherits VerticalLayout {
}
}
}
// Translation (Qobuz v10): static text UNDER the original line — smaller
// (~72%, mobile reference) and dimmer. The karaoke fill above stays keyed
// on the ORIGINAL timings/text; this follows its line statically and
// joins the same dimming ladder (never uppercased — it's a translation,
// not the lyric). No-op while the toggle is off or the doc has none.
if LyricsState.show-translation && root.translation-text != "": Text {
text: root.translation-text;
wrap: word-wrap;
opacity: root.line-opacity();
color: root.is-past ? Theme.text-muted : Theme.text-secondary.with-alpha(0.7);
font-family: root.font-name;
font-size: (root.is-active ? root.size-active : root.size-inactive) * 0.72;
font-weight: Typography.regular;
letter-spacing: 0.2px;
animate opacity {
duration: 220ms;
easing: ease-out;
}
}
}
export component LyricsLinesView inherits Rectangle {
@@ -386,6 +413,7 @@ export component LyricsLinesView inherits Rectangle {
for line[idx] in LyricsState.lines: LyricsLineRow {
index: idx;
line-text: line.text;
translation-text: line.translation-text;
size-inactive: root.size-inactive;
size-active: root.size-active;
dimming-mode: root.dimming-mode;
+17
View File
@@ -4454,6 +4454,11 @@ export struct LyricsLineItem {
time-ms: int,
end-ms: int,
has-words: bool,
// Translation of this line (Qobuz v10), aligned 1:1 with `text` by the
// Rust glue; empty when the doc has no translation (or it wasn't
// requested). Rendered under the original line only while
// `LyricsState.show-translation` is on.
translation-text: string,
}
// One VISUAL line of the ACTIVE logical line, for the per-visual-line karaoke
@@ -4488,6 +4493,18 @@ export global LyricsState {
// Playing-track meta for the header ("{title} - {artist}").
in property <string> track-title;
in property <string> track-artist;
// ---- Translation (Qobuz API v10) ---------------------------------------
// The current doc advertises at least one translation language
// (`translation_langs` non-empty) — gates the sidebar's floating toggle.
// False on every other state, so the sidebar renders exactly as before.
in property <bool> translation-available: false;
// User intent: render the translation under each original line. Driven by
// Rust (`toggle-translation`): ON refetches with the target language and
// flips this only when the translation actually arrived; OFF is pure UI.
in-out property <bool> show-translation: false;
// Sidebar floating-button action (wired in main.rs).
callback toggle-translation();
// Surfaced backend error text (status == 4) — dynamic content, not chrome.
in property <string> error;
// Active line index; -1 = none. The S4 sync engine drives it.