From 499f652b71bfee06e5873410adda701f564f0d60 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Wed, 22 Jul 2026 19:58:35 -0600 Subject: [PATCH] feat(ui): carry lyrics translation state and render bilingual lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/qbz-ui/ui/shell/LyricsLinesView.slint | 28 ++++++++++++++++++++ crates/qbz-ui/ui/state.slint | 17 ++++++++++++ 2 files changed, 45 insertions(+) diff --git a/crates/qbz-ui/ui/shell/LyricsLinesView.slint b/crates/qbz-ui/ui/shell/LyricsLinesView.slint index 2f763f049..b39e232e6 100644 --- a/crates/qbz-ui/ui/shell/LyricsLinesView.slint +++ b/crates/qbz-ui/ui/shell/LyricsLinesView.slint @@ -70,6 +70,9 @@ import "../../../../static/fonts/SourceSans3-VariableFont_wght.ttf"; component LyricsLineRow inherits VerticalLayout { in property index; in property 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 translation-text; in property size-inactive; in property 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 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 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; diff --git a/crates/qbz-ui/ui/state.slint b/crates/qbz-ui/ui/state.slint index 888bb7a96..269c6640b 100644 --- a/crates/qbz-ui/ui/state.slint +++ b/crates/qbz-ui/ui/state.slint @@ -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 track-title; in property 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 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 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 error; // Active line index; -1 = none. The S4 sync engine drives it.