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] {
|
pub fn all() -> &'static [UiLocale] {
|
||||||
&[Self::En, Self::De, Self::Fr, Self::It, Self::Es]
|
&[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 {
|
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
|
// Settings
|
||||||
SetOfflineMode(bool),
|
SetOfflineMode(bool),
|
||||||
SetUiLocale(UiLocale),
|
SetUiLocale(UiLocale),
|
||||||
|
ToggleLocaleDropdown,
|
||||||
|
|
||||||
// Blog actions (dispatched to engine)
|
// Blog actions (dispatched to engine)
|
||||||
RebuildDatabase,
|
RebuildDatabase,
|
||||||
@@ -113,6 +114,7 @@ pub struct BdsApp {
|
|||||||
|
|
||||||
// Flags
|
// Flags
|
||||||
offline_mode: bool,
|
offline_mode: bool,
|
||||||
|
locale_dropdown_open: bool,
|
||||||
|
|
||||||
// macOS lifecycle receiver
|
// macOS lifecycle receiver
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
@@ -213,6 +215,7 @@ impl BdsApp {
|
|||||||
menu_registry: registry,
|
menu_registry: registry,
|
||||||
ui_locale: locale,
|
ui_locale: locale,
|
||||||
offline_mode: false,
|
offline_mode: false,
|
||||||
|
locale_dropdown_open: false,
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
_lifecycle_rx,
|
_lifecycle_rx,
|
||||||
},
|
},
|
||||||
@@ -422,9 +425,14 @@ impl BdsApp {
|
|||||||
}
|
}
|
||||||
Message::SetUiLocale(locale) => {
|
Message::SetUiLocale(locale) => {
|
||||||
self.ui_locale = locale;
|
self.ui_locale = locale;
|
||||||
|
self.locale_dropdown_open = false;
|
||||||
menu::update_menu_labels(&self.menu_registry, locale);
|
menu::update_menu_labels(&self.menu_registry, locale);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
Message::ToggleLocaleDropdown => {
|
||||||
|
self.locale_dropdown_open = !self.locale_dropdown_open;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
// ── Blog engine actions ──
|
// ── Blog engine actions ──
|
||||||
Message::RebuildDatabase => {
|
Message::RebuildDatabase => {
|
||||||
@@ -474,6 +482,7 @@ impl BdsApp {
|
|||||||
0, // post_count — populated in later milestones
|
0, // post_count — populated in later milestones
|
||||||
0, // media_count — populated in later milestones
|
0, // media_count — populated in later milestones
|
||||||
self.offline_mode,
|
self.offline_mode,
|
||||||
|
self.locale_dropdown_open,
|
||||||
self.ui_locale,
|
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 iced::{Alignment, Background, Border, Color, Element, Length, Theme};
|
||||||
|
|
||||||
use bds_core::i18n::UiLocale;
|
use bds_core::i18n::UiLocale;
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::i18n::{t, tw};
|
use crate::i18n::tw;
|
||||||
use crate::state::navigation::TaskSnapshot;
|
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
|
// Styles
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -88,22 +56,52 @@ fn airplane_active(_theme: &Theme, status: button::Status) -> button::Style {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pick list style matching status bar.
|
/// Dropdown trigger button style.
|
||||||
fn locale_pick_list_style(_theme: &Theme, status: pick_list::Status) -> pick_list::Style {
|
fn dropdown_trigger(_theme: &Theme, status: button::Status) -> button::Style {
|
||||||
let bg = match status {
|
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,
|
_ => Color::TRANSPARENT,
|
||||||
};
|
};
|
||||||
pick_list::Style {
|
button::Style {
|
||||||
text_color: Color::from_rgb(0.70, 0.70, 0.75),
|
background: Some(Background::Color(bg)),
|
||||||
placeholder_color: Color::from_rgb(0.50, 0.50, 0.55),
|
text_color: Color::WHITE,
|
||||||
handle_color: Color::from_rgb(0.50, 0.50, 0.55),
|
|
||||||
background: Background::Color(bg),
|
|
||||||
border: Border {
|
border: Border {
|
||||||
color: Color::from_rgb(0.25, 0.25, 0.30),
|
color: Color::TRANSPARENT,
|
||||||
width: 1.0,
|
width: 0.0,
|
||||||
radius: 3.0.into(),
|
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,
|
media_count: usize,
|
||||||
locale: UiLocale,
|
locale: UiLocale,
|
||||||
offline_mode: bool,
|
offline_mode: bool,
|
||||||
|
locale_dropdown_open: bool,
|
||||||
task_snapshots: &[TaskSnapshot],
|
task_snapshots: &[TaskSnapshot],
|
||||||
) -> Element<'static, Message> {
|
) -> Element<'static, Message> {
|
||||||
let label_color = Color::from_rgb(0.60, 0.60, 0.65);
|
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 posts_label = tw(locale, "statusBar.posts", &[("count", &post_count.to_string())]);
|
||||||
let media_label = tw(locale, "statusBar.media", &[("count", &media_count.to_string())]);
|
let media_label = tw(locale, "statusBar.media", &[("count", &media_count.to_string())]);
|
||||||
|
|
||||||
// Airplane mode toggle — just the ✈ icon, like bDS
|
// Airplane mode toggle — ✈ icon
|
||||||
let airplane_btn = button(text("\u{2708}").size(13))
|
let airplane_btn = button(
|
||||||
|
text("\u{2708}").size(13).shaping(Shaping::Advanced),
|
||||||
|
)
|
||||||
.on_press(Message::SetOfflineMode(!offline_mode))
|
.on_press(Message::SetOfflineMode(!offline_mode))
|
||||||
.padding([2, 4])
|
.padding([2, 4])
|
||||||
.style(if offline_mode { airplane_active } else { airplane_inactive });
|
.style(if offline_mode { airplane_active } else { airplane_inactive });
|
||||||
|
|
||||||
// Language selector — "UI" label + pick_list dropdown, like bDS
|
// Language selector — current flag as trigger
|
||||||
let options: Vec<LocaleOption> = UiLocale::all()
|
let trigger_flag = text(locale.flag_emoji())
|
||||||
.iter()
|
.size(14)
|
||||||
.map(|l| LocaleOption::new(*l, locale))
|
.shaping(Shaping::Advanced);
|
||||||
.collect();
|
|
||||||
let selected = Some(LocaleOption::new(locale, locale));
|
|
||||||
|
|
||||||
let locale_picker = pick_list(
|
let locale_trigger = button(trigger_flag)
|
||||||
options,
|
.on_press(Message::ToggleLocaleDropdown)
|
||||||
selected,
|
.padding([1, 4])
|
||||||
|opt: LocaleOption| Message::SetUiLocale(opt.locale),
|
.style(dropdown_trigger);
|
||||||
)
|
|
||||||
.text_size(11)
|
|
||||||
.padding([2, 4])
|
|
||||||
.width(Length::Shrink)
|
|
||||||
.style(locale_pick_list_style);
|
|
||||||
|
|
||||||
let right = row![
|
let right = row![
|
||||||
text(posts_label).size(11).color(label_color),
|
text(posts_label).size(11).color(label_color),
|
||||||
text(media_label).size(11).color(label_color),
|
text(media_label).size(11).color(label_color),
|
||||||
airplane_btn,
|
airplane_btn,
|
||||||
text(t(locale, "statusBar.ui")).size(11).color(label_color),
|
locale_trigger,
|
||||||
locale_picker,
|
|
||||||
text("bDS").size(11).color(Color::from_rgb(0.45, 0.45, 0.50)),
|
text("bDS").size(11).color(Color::from_rgb(0.45, 0.45, 0.50)),
|
||||||
]
|
]
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.align_y(Alignment::Center);
|
.align_y(Alignment::Center);
|
||||||
|
|
||||||
container(
|
let status_row = container(
|
||||||
row![
|
row![
|
||||||
left,
|
left,
|
||||||
Space::with_width(Length::Fill),
|
Space::with_width(Length::Fill),
|
||||||
@@ -193,6 +186,43 @@ pub fn view(
|
|||||||
)
|
)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fixed(24.0))
|
.height(Length::Fixed(24.0))
|
||||||
.style(bar_style)
|
.style(bar_style);
|
||||||
.into()
|
|
||||||
|
// 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,
|
post_count: usize,
|
||||||
media_count: usize,
|
media_count: usize,
|
||||||
offline_mode: bool,
|
offline_mode: bool,
|
||||||
|
locale_dropdown_open: bool,
|
||||||
// i18n
|
// i18n
|
||||||
locale: UiLocale,
|
locale: UiLocale,
|
||||||
) -> Element<'static, Message> {
|
) -> Element<'static, Message> {
|
||||||
@@ -97,6 +98,7 @@ pub fn view(
|
|||||||
media_count,
|
media_count,
|
||||||
locale,
|
locale,
|
||||||
offline_mode,
|
offline_mode,
|
||||||
|
locale_dropdown_open,
|
||||||
task_snapshots,
|
task_snapshots,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user