feat(ui): translation language setting in the lyrics flyout
New QbzSelect row (Auto (account language) + en es fr de it pt nl ja ru,
the owner-approved list) after the Size row; language names are native
literals, the same convention as the Appearance dropdown. The options
array lives on LyricsState (reseeded from Rust on language change) and
the choice persists per-user in lyrics_prefs.json as translation_language
("auto" or an ISO 639-1 code), sanitized on load (unknown -> auto) and
round-tripped through apply_to_ui/persist_from_ui like the other prefs.
Flyout popup height grows by the new row (and the comment now counts the
lite-fill row it had missed).
km
This commit is contained in:
4 files changed
+84
-4
No files matched your search
@@ -8,7 +8,9 @@
|
||||
// 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).
|
||||
// Uppercase / Dimming / footer (Copy lyrics + Reset) — plus two Slint-side
|
||||
// additions in order: Translation language (v10, after Size) and Lite fill
|
||||
// (perf, after Uppercase).
|
||||
//
|
||||
// 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
|
||||
@@ -132,6 +134,25 @@ export component LyricsControlsPanel inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Translation language (Qobuz v10) — Auto (account language) + the
|
||||
// 9 approved ISO 639-1 targets; options reseeded from Rust on
|
||||
// language change (the array carries one @tr'd label).
|
||||
HorizontalLayout {
|
||||
height: 30px;
|
||||
spacing: 8px;
|
||||
RowLabel { text: @tr("Translation language"); }
|
||||
QbzSelect {
|
||||
sm: true;
|
||||
menu-width: 178px;
|
||||
options: LyricsState.translation-language-options;
|
||||
current-index: LyricsState.translation-language-index;
|
||||
selected(i) => {
|
||||
LyricsState.translation-language-index = i;
|
||||
LyricsState.prefs-changed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Active color — label (+ "Theme" reset while custom), swatch palette
|
||||
// underneath, right-aligned on the control grid (D6).
|
||||
VerticalLayout {
|
||||
|
||||
@@ -176,9 +176,10 @@ export component LyricsSidebar inherits Rectangle {
|
||||
x: 32px + 8px + 28px - 260px;
|
||||
y: 32px + 8px;
|
||||
width: 260px;
|
||||
// padding 28 + rows (22+30+30+44+22+30+34 = 212) +
|
||||
// spacing 12 x 6 = 72.
|
||||
height: 312px;
|
||||
// padding 28 + rows (22+30+30+30+44+22+22+30+34 =
|
||||
// 264, incl. lite-fill + translation language) +
|
||||
// spacing 12 x 8 = 96.
|
||||
height: 388px;
|
||||
close-policy: PopupClosePolicy.close-on-click-outside;
|
||||
LyricsControlsPanel { }
|
||||
}
|
||||
|
||||
@@ -4555,6 +4555,25 @@ export global LyricsState {
|
||||
// between line changes). Default false (smooth karaoke). Kiosk / weak-GPU
|
||||
// tiers force it on via ShellState.reduce-motion regardless of this pref.
|
||||
in-out property <bool> lite-fill: false;
|
||||
// Translation language (Qobuz v10, persisted like the other prefs):
|
||||
// 0 Auto (account language) · 1..9 = en es fr de it pt nl ja ru. Option
|
||||
// labels: the Auto entry is @tr'd, the language names are native
|
||||
// literals — the same convention as AppearanceState.languages. The @tr
|
||||
// DEFAULT is not reactive, so `reseed_i18n_labels` re-pushes the array
|
||||
// on language change.
|
||||
in-out property <int> translation-language-index: 0;
|
||||
in-out property <[string]> translation-language-options: [
|
||||
@tr("Auto (account language)"),
|
||||
"English",
|
||||
"Español",
|
||||
"Français",
|
||||
"Deutsch",
|
||||
"Italiano",
|
||||
"Português",
|
||||
"Nederlands",
|
||||
"日本語",
|
||||
"Русский",
|
||||
];
|
||||
// Active (karaoke fill) color. `use-custom-color: false` = follow the theme
|
||||
// accent (Tauri activeColor: ''). The palette swatches set both (deviation
|
||||
// D6: swatches instead of a native color input — Slint has none).
|
||||
|
||||
@@ -56,6 +56,10 @@ pub struct LyricsPrefs {
|
||||
/// karaoke sweep, so the lyrics surface stops driving continuous repaints.
|
||||
#[serde(default)]
|
||||
pub lite_fill: bool,
|
||||
/// Translation target (Qobuz v10): "auto" (account language, UI-locale
|
||||
/// fallback) or an explicit ISO 639-1 code from [`TRANSLATION_LANGS`].
|
||||
#[serde(default = "d_translation_lang")]
|
||||
pub translation_language: String,
|
||||
}
|
||||
|
||||
fn d_true() -> bool {
|
||||
@@ -70,6 +74,9 @@ fn d_size() -> String {
|
||||
fn d_dimming() -> String {
|
||||
"strong".to_string()
|
||||
}
|
||||
fn d_translation_lang() -> String {
|
||||
"auto".to_string()
|
||||
}
|
||||
|
||||
impl Default for LyricsPrefs {
|
||||
/// Tauri defaults (`lyricsDisplayStore.ts:37-44`).
|
||||
@@ -82,6 +89,7 @@ impl Default for LyricsPrefs {
|
||||
active_color: String::new(),
|
||||
uppercase: false,
|
||||
lite_fill: false,
|
||||
translation_language: d_translation_lang(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,6 +97,11 @@ impl Default for LyricsPrefs {
|
||||
const FONTS: [&str; 5] = ["system", "line-seed-jp", "montserrat", "noto-sans", "source-sans-3"];
|
||||
const SIZES: [&str; 4] = ["small", "medium", "large", "xl"];
|
||||
const DIMMINGS: [&str; 3] = ["off", "soft", "strong"];
|
||||
/// Translation targets (owner-approved list, spec §Decisions.2): "auto" +
|
||||
/// the observed `translation_langs` ∪ app UI locales. Order matches the
|
||||
/// flyout's `translation-language-options` (Auto, en es fr de it pt nl ja ru).
|
||||
const TRANSLATION_LANGS: [&str; 10] =
|
||||
["auto", "en", "es", "fr", "de", "it", "pt", "nl", "ja", "ru"];
|
||||
|
||||
/// `#RRGGBB` (or empty = theme) — the Tauri validator
|
||||
/// (`lyricsDisplayStore.ts:46-52`).
|
||||
@@ -115,6 +128,9 @@ impl LyricsPrefs {
|
||||
if !valid_color(&self.active_color) {
|
||||
self.active_color.clear();
|
||||
}
|
||||
if !TRANSLATION_LANGS.contains(&self.translation_language.as_str()) {
|
||||
self.translation_language = d_translation_lang();
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -188,6 +204,13 @@ fn dimming_index(dimming: &str) -> i32 {
|
||||
DIMMINGS.iter().position(|d| *d == dimming).unwrap_or(2) as i32
|
||||
}
|
||||
|
||||
fn translation_lang_index(lang: &str) -> i32 {
|
||||
TRANSLATION_LANGS
|
||||
.iter()
|
||||
.position(|l| *l == lang)
|
||||
.unwrap_or(0) as i32
|
||||
}
|
||||
|
||||
fn parse_color(hex: &str) -> Option<slint::Color> {
|
||||
if hex.len() != 7 || !hex.starts_with('#') {
|
||||
return None;
|
||||
@@ -218,6 +241,7 @@ pub fn apply_to_ui(window: &AppWindow, prefs: &LyricsPrefs) {
|
||||
None => state.set_use_custom_color(false),
|
||||
}
|
||||
state.set_lite_fill(prefs.lite_fill);
|
||||
state.set_translation_language_index(translation_lang_index(&prefs.translation_language));
|
||||
}
|
||||
|
||||
/// Read the in-out props back into a pref set and persist — the
|
||||
@@ -248,6 +272,11 @@ pub fn persist_from_ui(window: &AppWindow) {
|
||||
},
|
||||
uppercase: state.get_uppercase(),
|
||||
lite_fill: state.get_lite_fill(),
|
||||
translation_language: TRANSLATION_LANGS
|
||||
.get(state.get_translation_language_index().max(0) as usize)
|
||||
.copied()
|
||||
.unwrap_or("auto")
|
||||
.to_string(),
|
||||
};
|
||||
save(&prefs);
|
||||
}
|
||||
@@ -273,6 +302,7 @@ mod tests {
|
||||
assert_eq!(p.dimming, "strong");
|
||||
assert_eq!(p.active_color, "");
|
||||
assert!(!p.uppercase);
|
||||
assert_eq!(p.translation_language, "auto");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -291,6 +321,7 @@ mod tests {
|
||||
active_color: "purple".into(),
|
||||
uppercase: true,
|
||||
lite_fill: false,
|
||||
translation_language: "klingon".into(),
|
||||
}
|
||||
.sanitized();
|
||||
assert!(!p.auto_follow); // bools pass through
|
||||
@@ -299,6 +330,8 @@ mod tests {
|
||||
assert_eq!(p.font_size, "medium");
|
||||
assert_eq!(p.dimming, "strong");
|
||||
assert_eq!(p.active_color, "");
|
||||
// Unknown translation language falls back to Auto.
|
||||
assert_eq!(p.translation_language, "auto");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -311,12 +344,14 @@ mod tests {
|
||||
active_color: "#8b5cf6".into(),
|
||||
uppercase: false,
|
||||
lite_fill: true,
|
||||
translation_language: "it".into(),
|
||||
}
|
||||
.sanitized();
|
||||
assert_eq!(p.font, "line-seed-jp");
|
||||
assert_eq!(p.font_size, "xl");
|
||||
assert_eq!(p.dimming, "off");
|
||||
assert_eq!(p.active_color, "#8b5cf6");
|
||||
assert_eq!(p.translation_language, "it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -340,10 +375,14 @@ mod tests {
|
||||
for (i, d) in DIMMINGS.iter().enumerate() {
|
||||
assert_eq!(dimming_index(d), i as i32);
|
||||
}
|
||||
for (i, l) in TRANSLATION_LANGS.iter().enumerate() {
|
||||
assert_eq!(translation_lang_index(l), i as i32);
|
||||
}
|
||||
// Unknowns fall back to the defaults' indices.
|
||||
assert_eq!(font_index("nope"), 0);
|
||||
assert_eq!(size_index("nope"), 1);
|
||||
assert_eq!(dimming_index("nope"), 2);
|
||||
assert_eq!(translation_lang_index("nope"), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user