feat: more work on M2, this time language selection
This commit is contained in:
@@ -27,6 +27,17 @@ impl UiLocale {
|
||||
pub fn all() -> &'static [UiLocale] {
|
||||
&[Self::En, Self::De, Self::Fr, Self::It, Self::Es]
|
||||
}
|
||||
|
||||
/// Unicode regional-indicator flag emoji for this locale.
|
||||
pub fn flag_emoji(self) -> &'static str {
|
||||
match self {
|
||||
Self::En => "\u{1F1EC}\u{1F1E7}", // 🇬🇧
|
||||
Self::De => "\u{1F1E9}\u{1F1EA}", // 🇩🇪
|
||||
Self::Fr => "\u{1F1EB}\u{1F1F7}", // 🇫🇷
|
||||
Self::It => "\u{1F1EE}\u{1F1F9}", // 🇮🇹
|
||||
Self::Es => "\u{1F1EA}\u{1F1F8}", // 🇪🇸
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for UiLocale {
|
||||
|
||||
5
crates/bds-ui/assets/flags/de.svg
Normal file
5
crates/bds-ui/assets/flags/de.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 40">
|
||||
<rect width="60" height="14" fill="#000000"/>
|
||||
<rect y="14" width="60" height="13" fill="#DD0000"/>
|
||||
<rect y="27" width="60" height="13" fill="#FFCC00"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 226 B |
5
crates/bds-ui/assets/flags/es.svg
Normal file
5
crates/bds-ui/assets/flags/es.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 40">
|
||||
<rect width="60" height="10" fill="#AA151B"/>
|
||||
<rect y="10" width="60" height="20" fill="#F1BF00"/>
|
||||
<rect y="30" width="60" height="10" fill="#AA151B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 226 B |
5
crates/bds-ui/assets/flags/fr.svg
Normal file
5
crates/bds-ui/assets/flags/fr.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 40">
|
||||
<rect width="20" height="40" fill="#002395"/>
|
||||
<rect x="20" width="20" height="40" fill="#FFFFFF"/>
|
||||
<rect x="40" width="20" height="40" fill="#ED2939"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 226 B |
7
crates/bds-ui/assets/flags/gb.svg
Normal file
7
crates/bds-ui/assets/flags/gb.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 40">
|
||||
<rect width="60" height="40" fill="#012169"/>
|
||||
<path d="M0 0 L60 40 M60 0 L0 40" stroke="#FFFFFF" stroke-width="8"/>
|
||||
<path d="M0 0 L60 40 M60 0 L0 40" stroke="#C8102E" stroke-width="4"/>
|
||||
<path d="M30 0 V40 M0 20 H60" stroke="#FFFFFF" stroke-width="12"/>
|
||||
<path d="M30 0 V40 M0 20 H60" stroke="#C8102E" stroke-width="6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 397 B |
5
crates/bds-ui/assets/flags/it.svg
Normal file
5
crates/bds-ui/assets/flags/it.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 40">
|
||||
<rect width="20" height="40" fill="#009246"/>
|
||||
<rect x="20" width="20" height="40" fill="#FFFFFF"/>
|
||||
<rect x="40" width="20" height="40" fill="#CE2B37"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 226 B |
@@ -64,6 +64,7 @@ pub enum Message {
|
||||
// Settings
|
||||
SetOfflineMode(bool),
|
||||
SetUiLocale(UiLocale),
|
||||
ToggleLocaleDropdown,
|
||||
|
||||
// Blog actions (dispatched to engine)
|
||||
RebuildDatabase,
|
||||
@@ -113,6 +114,7 @@ pub struct BdsApp {
|
||||
|
||||
// Flags
|
||||
offline_mode: bool,
|
||||
locale_dropdown_open: bool,
|
||||
|
||||
// macOS lifecycle receiver
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -213,6 +215,7 @@ impl BdsApp {
|
||||
menu_registry: registry,
|
||||
ui_locale: locale,
|
||||
offline_mode: false,
|
||||
locale_dropdown_open: false,
|
||||
#[cfg(target_os = "macos")]
|
||||
_lifecycle_rx,
|
||||
},
|
||||
@@ -422,9 +425,14 @@ impl BdsApp {
|
||||
}
|
||||
Message::SetUiLocale(locale) => {
|
||||
self.ui_locale = locale;
|
||||
self.locale_dropdown_open = false;
|
||||
menu::update_menu_labels(&self.menu_registry, locale);
|
||||
Task::none()
|
||||
}
|
||||
Message::ToggleLocaleDropdown => {
|
||||
self.locale_dropdown_open = !self.locale_dropdown_open;
|
||||
Task::none()
|
||||
}
|
||||
|
||||
// ── Blog engine actions ──
|
||||
Message::RebuildDatabase => {
|
||||
@@ -474,6 +482,7 @@ impl BdsApp {
|
||||
0, // post_count — populated in later milestones
|
||||
0, // media_count — populated in later milestones
|
||||
self.offline_mode,
|
||||
self.locale_dropdown_open,
|
||||
self.ui_locale,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,45 +1,13 @@
|
||||
use iced::widget::{button, container, pick_list, row, text, Space};
|
||||
use iced::widget::{button, column, container, row, text, Space};
|
||||
use iced::widget::text::Shaping;
|
||||
use iced::{Alignment, Background, Border, Color, Element, Length, Theme};
|
||||
|
||||
use bds_core::i18n::UiLocale;
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::i18n::{t, tw};
|
||||
use crate::i18n::tw;
|
||||
use crate::state::navigation::TaskSnapshot;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Locale option wrapper for pick_list (needs ToString + PartialEq + Clone)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Wraps a UiLocale with a display label for the pick_list dropdown.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct LocaleOption {
|
||||
locale: UiLocale,
|
||||
label: String,
|
||||
}
|
||||
|
||||
impl ToString for LocaleOption {
|
||||
fn to_string(&self) -> String {
|
||||
self.label.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl LocaleOption {
|
||||
fn new(locale: UiLocale, display_locale: UiLocale) -> Self {
|
||||
let key = match locale {
|
||||
UiLocale::En => "language.en",
|
||||
UiLocale::De => "language.de",
|
||||
UiLocale::Fr => "language.fr",
|
||||
UiLocale::It => "language.it",
|
||||
UiLocale::Es => "language.es",
|
||||
};
|
||||
Self {
|
||||
locale,
|
||||
label: t(display_locale, key),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Styles
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -88,22 +56,52 @@ fn airplane_active(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick list style matching status bar.
|
||||
fn locale_pick_list_style(_theme: &Theme, status: pick_list::Status) -> pick_list::Style {
|
||||
/// Dropdown trigger button style.
|
||||
fn dropdown_trigger(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
let bg = match status {
|
||||
pick_list::Status::Hovered => Color::from_rgb(0.22, 0.22, 0.27),
|
||||
button::Status::Hovered => Color::from_rgb(0.22, 0.22, 0.27),
|
||||
_ => Color::TRANSPARENT,
|
||||
};
|
||||
pick_list::Style {
|
||||
text_color: Color::from_rgb(0.70, 0.70, 0.75),
|
||||
placeholder_color: Color::from_rgb(0.50, 0.50, 0.55),
|
||||
handle_color: Color::from_rgb(0.50, 0.50, 0.55),
|
||||
background: Background::Color(bg),
|
||||
button::Style {
|
||||
background: Some(Background::Color(bg)),
|
||||
text_color: Color::WHITE,
|
||||
border: Border {
|
||||
color: Color::from_rgb(0.25, 0.25, 0.30),
|
||||
width: 1.0,
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 3.0.into(),
|
||||
},
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Dropdown item style.
|
||||
fn dropdown_item(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
let bg = match status {
|
||||
button::Status::Hovered => Color::from_rgb(0.25, 0.25, 0.30),
|
||||
_ => Color::from_rgb(0.18, 0.18, 0.22),
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(bg)),
|
||||
text_color: Color::WHITE,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Dropdown container background.
|
||||
fn dropdown_bg(_theme: &Theme) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(Color::from_rgb(0.18, 0.18, 0.22))),
|
||||
border: Border {
|
||||
color: Color::from_rgb(0.30, 0.30, 0.35),
|
||||
width: 1.0,
|
||||
radius: 4.0.into(),
|
||||
},
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +115,7 @@ pub fn view(
|
||||
media_count: usize,
|
||||
locale: UiLocale,
|
||||
offline_mode: bool,
|
||||
locale_dropdown_open: bool,
|
||||
task_snapshots: &[TaskSnapshot],
|
||||
) -> Element<'static, Message> {
|
||||
let label_color = Color::from_rgb(0.60, 0.60, 0.65);
|
||||
@@ -148,41 +147,35 @@ pub fn view(
|
||||
let posts_label = tw(locale, "statusBar.posts", &[("count", &post_count.to_string())]);
|
||||
let media_label = tw(locale, "statusBar.media", &[("count", &media_count.to_string())]);
|
||||
|
||||
// Airplane mode toggle — just the ✈ icon, like bDS
|
||||
let airplane_btn = button(text("\u{2708}").size(13))
|
||||
// Airplane mode toggle — ✈ icon
|
||||
let airplane_btn = button(
|
||||
text("\u{2708}").size(13).shaping(Shaping::Advanced),
|
||||
)
|
||||
.on_press(Message::SetOfflineMode(!offline_mode))
|
||||
.padding([2, 4])
|
||||
.style(if offline_mode { airplane_active } else { airplane_inactive });
|
||||
|
||||
// Language selector — "UI" label + pick_list dropdown, like bDS
|
||||
let options: Vec<LocaleOption> = UiLocale::all()
|
||||
.iter()
|
||||
.map(|l| LocaleOption::new(*l, locale))
|
||||
.collect();
|
||||
let selected = Some(LocaleOption::new(locale, locale));
|
||||
// Language selector — current flag as trigger
|
||||
let trigger_flag = text(locale.flag_emoji())
|
||||
.size(14)
|
||||
.shaping(Shaping::Advanced);
|
||||
|
||||
let locale_picker = pick_list(
|
||||
options,
|
||||
selected,
|
||||
|opt: LocaleOption| Message::SetUiLocale(opt.locale),
|
||||
)
|
||||
.text_size(11)
|
||||
.padding([2, 4])
|
||||
.width(Length::Shrink)
|
||||
.style(locale_pick_list_style);
|
||||
let locale_trigger = button(trigger_flag)
|
||||
.on_press(Message::ToggleLocaleDropdown)
|
||||
.padding([1, 4])
|
||||
.style(dropdown_trigger);
|
||||
|
||||
let right = row![
|
||||
text(posts_label).size(11).color(label_color),
|
||||
text(media_label).size(11).color(label_color),
|
||||
airplane_btn,
|
||||
text(t(locale, "statusBar.ui")).size(11).color(label_color),
|
||||
locale_picker,
|
||||
locale_trigger,
|
||||
text("bDS").size(11).color(Color::from_rgb(0.45, 0.45, 0.50)),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
container(
|
||||
let status_row = container(
|
||||
row![
|
||||
left,
|
||||
Space::with_width(Length::Fill),
|
||||
@@ -193,6 +186,43 @@ pub fn view(
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fixed(24.0))
|
||||
.style(bar_style)
|
||||
.into()
|
||||
.style(bar_style);
|
||||
|
||||
// If dropdown is open, stack it above the status bar
|
||||
if locale_dropdown_open {
|
||||
let items: Vec<Element<'static, Message>> = UiLocale::all()
|
||||
.iter()
|
||||
.map(|&l| {
|
||||
let flag_text = text(l.flag_emoji())
|
||||
.size(16)
|
||||
.shaping(Shaping::Advanced);
|
||||
|
||||
button(flag_text)
|
||||
.on_press(Message::SetUiLocale(l))
|
||||
.padding([4, 8])
|
||||
.width(Length::Fill)
|
||||
.style(dropdown_item)
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let dropdown_menu = container(
|
||||
iced::widget::Column::with_children(items).spacing(2).padding(4),
|
||||
)
|
||||
.style(dropdown_bg);
|
||||
|
||||
// Align dropdown to the right, above the status bar
|
||||
let dropdown_row = row![
|
||||
Space::with_width(Length::Fill),
|
||||
dropdown_menu,
|
||||
// offset from right edge to roughly align with the flag trigger
|
||||
Space::with_width(Length::Fixed(40.0)),
|
||||
];
|
||||
|
||||
column![dropdown_row, status_row]
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
} else {
|
||||
status_row.into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ pub fn view(
|
||||
post_count: usize,
|
||||
media_count: usize,
|
||||
offline_mode: bool,
|
||||
locale_dropdown_open: bool,
|
||||
// i18n
|
||||
locale: UiLocale,
|
||||
) -> Element<'static, Message> {
|
||||
@@ -97,6 +98,7 @@ pub fn view(
|
||||
media_count,
|
||||
locale,
|
||||
offline_mode,
|
||||
locale_dropdown_open,
|
||||
task_snapshots,
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user