diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index 5810c6be62..f508f6c6bb 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -208,6 +208,7 @@ static void _librust_qstrs(void) { MP_QSTR_confirm_value; MP_QSTR_confirm_value_intro; MP_QSTR_confirm_with_info; + MP_QSTR_connections; MP_QSTR_continue_recovery_homepage; MP_QSTR_count; MP_QSTR_current; @@ -220,6 +221,19 @@ static void _librust_qstrs(void) { MP_QSTR_deinit; MP_QSTR_description; MP_QSTR_details_title; + MP_QSTR_device_menu; + MP_QSTR_device_menu__1_connection; + MP_QSTR_device_menu__about; + MP_QSTR_device_menu__active_connections; + MP_QSTR_device_menu__backup_failed_description; + MP_QSTR_device_menu__backup_failed_title; + MP_QSTR_device_menu__battery_low_description; + MP_QSTR_device_menu__battery_low_title; + MP_QSTR_device_menu__bluetooth; + MP_QSTR_device_menu__brightness; + MP_QSTR_device_menu__connections_title; + MP_QSTR_device_menu__fw_version; + MP_QSTR_device_menu__language; MP_QSTR_device_name__change_template; MP_QSTR_device_name__title; MP_QSTR_disable_animation; @@ -235,6 +249,7 @@ static void _librust_qstrs(void) { MP_QSTR_extra; MP_QSTR_extra_items; MP_QSTR_extra_title; + MP_QSTR_failed_backup; MP_QSTR_fee; MP_QSTR_fee_items; MP_QSTR_fee_label; @@ -317,6 +332,7 @@ static void _librust_qstrs(void) { MP_QSTR_lockscreen__tap_to_unlock; MP_QSTR_lockscreen__title_locked; MP_QSTR_lockscreen__title_not_connected; + MP_QSTR_low_battery; MP_QSTR_max_count; MP_QSTR_max_feerate; MP_QSTR_max_len; diff --git a/core/embed/rust/src/translations/generated/translated_string.rs b/core/embed/rust/src/translations/generated/translated_string.rs index 4371d2905b..5a0a3ae415 100644 --- a/core/embed/rust/src/translations/generated/translated_string.rs +++ b/core/embed/rust/src/translations/generated/translated_string.rs @@ -1383,6 +1383,18 @@ pub enum TranslatedString { #[cfg(feature = "universal_fw")] ethereum__unknown_contract_address_short = 974, // "Unknown contract address." reset__share_words_first = 975, // "Write down the first word from the backup." + device_menu__1_connection = 976, // "1 active connection" + device_menu__about = 977, // "About" + device_menu__active_connections = 978, // "{0} active connections" + device_menu__backup_failed_description = 979, // "Review" + device_menu__backup_failed_title = 980, // "Backup failed" + device_menu__battery_low_description = 981, // "Recharge soon" + device_menu__battery_low_title = 982, // "Battery low" + device_menu__bluetooth = 983, // "Bluetooth management" + device_menu__brightness = 984, // "Brightness" + device_menu__connections_title = 985, // "Pair & Connect" + device_menu__fw_version = 986, // "Firmware version" + device_menu__language = 987, // "Language" } impl TranslatedString { @@ -2776,6 +2788,18 @@ impl TranslatedString { #[cfg(feature = "universal_fw")] Self::ethereum__unknown_contract_address_short => "Unknown contract address.", Self::reset__share_words_first => "Write down the first word from the backup.", + Self::device_menu__1_connection => "1 active connection", + Self::device_menu__about => "About", + Self::device_menu__active_connections => "{0} active connections", + Self::device_menu__backup_failed_description => "Review", + Self::device_menu__backup_failed_title => "Backup failed", + Self::device_menu__battery_low_description => "Recharge soon", + Self::device_menu__battery_low_title => "Battery low", + Self::device_menu__bluetooth => "Bluetooth management", + Self::device_menu__brightness => "Brightness", + Self::device_menu__connections_title => "Pair & Connect", + Self::device_menu__fw_version => "Firmware version", + Self::device_menu__language => "Language", } } @@ -4154,6 +4178,18 @@ impl TranslatedString { #[cfg(feature = "universal_fw")] Qstr::MP_QSTR_ethereum__unknown_contract_address_short => Some(Self::ethereum__unknown_contract_address_short), Qstr::MP_QSTR_reset__share_words_first => Some(Self::reset__share_words_first), + Qstr::MP_QSTR_device_menu__1_connection => Some(Self::device_menu__1_connection), + Qstr::MP_QSTR_device_menu__about => Some(Self::device_menu__about), + Qstr::MP_QSTR_device_menu__active_connections => Some(Self::device_menu__active_connections), + Qstr::MP_QSTR_device_menu__backup_failed_description => Some(Self::device_menu__backup_failed_description), + Qstr::MP_QSTR_device_menu__backup_failed_title => Some(Self::device_menu__backup_failed_title), + Qstr::MP_QSTR_device_menu__battery_low_description => Some(Self::device_menu__battery_low_description), + Qstr::MP_QSTR_device_menu__battery_low_title => Some(Self::device_menu__battery_low_title), + Qstr::MP_QSTR_device_menu__bluetooth => Some(Self::device_menu__bluetooth), + Qstr::MP_QSTR_device_menu__brightness => Some(Self::device_menu__brightness), + Qstr::MP_QSTR_device_menu__connections_title => Some(Self::device_menu__connections_title), + Qstr::MP_QSTR_device_menu__fw_version => Some(Self::device_menu__fw_version), + Qstr::MP_QSTR_device_menu__language => Some(Self::device_menu__language), _ => None, } } diff --git a/core/embed/rust/src/ui/api/firmware_micropython.rs b/core/embed/rust/src/ui/api/firmware_micropython.rs index c20ede2ac2..4817ab97ad 100644 --- a/core/embed/rust/src/ui/api/firmware_micropython.rs +++ b/core/embed/rust/src/ui/api/firmware_micropython.rs @@ -419,6 +419,19 @@ extern "C" fn new_continue_recovery_homepage( unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } +extern "C" fn new_device_menu(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let failed_backup: bool = kwargs.get(Qstr::MP_QSTR_failed_backup)?.try_into()?; + let low_battery: bool = kwargs.get(Qstr::MP_QSTR_low_battery)?.try_into()?; + let connections: TString = kwargs.get(Qstr::MP_QSTR_connections)?.try_into()?; + + + let layout = ModelUI::device_menu(failed_backup, low_battery, connections)?; + Ok(LayoutObj::new_root(layout)?.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + extern "C" fn new_flow_confirm_output(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { let title: Option = kwargs.get(Qstr::MP_QSTR_title)?.try_into_option()?; @@ -1318,6 +1331,16 @@ pub static mp_module_trezorui_api: Module = obj_module! { /// """Device recovery homescreen.""" Qstr::MP_QSTR_continue_recovery_homepage => obj_fn_kw!(0, new_continue_recovery_homepage).as_obj(), + /// def device_menu( + /// *, + /// failed_backup: bool, + /// low_battery: bool, + /// connections: str | None, + /// ) -> LayoutObj[int]: + /// """Show eckhart device menu.""" + Qstr::MP_QSTR_device_menu => obj_fn_kw!(0, new_device_menu).as_obj(), + + /// def flow_confirm_output( /// *, /// title: str | None, diff --git a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs index 8ab1850100..718411262a 100644 --- a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs @@ -536,6 +536,14 @@ impl FirmwareUI for UIBolt { } } + fn device_menu( + _failed_backup: bool, + _low_battery: bool, + _connections: TString<'static>, + ) -> Result { + Err::, Error>(Error::ValueError(c"not implemented")) + } + fn flow_confirm_output( _title: Option>, _subtitle: Option>, diff --git a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs index 43901b7cab..de35ea03ae 100644 --- a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs @@ -671,6 +671,14 @@ impl FirmwareUI for UICaesar { LayoutObj::new_root(layout) } + fn device_menu( + _failed_backup: bool, + _low_battery: bool, + _connections: TString<'static>, + ) -> Result { + Err::, Error>(Error::ValueError(c"not implemented")) + } + fn flow_confirm_output( _title: Option>, _subtitle: Option>, diff --git a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs index a392c4affa..102074e9c0 100644 --- a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs @@ -507,6 +507,14 @@ impl FirmwareUI for UIDelizia { LayoutObj::new_root(flow) } + fn device_menu( + _failed_backup: bool, + _low_battery: bool, + _connections: TString<'static>, + ) -> Result { + Err::, Error>(Error::ValueError(c"not implemented")) + } + fn flow_confirm_output( title: Option>, subtitle: Option>, diff --git a/core/embed/rust/src/ui/layout_eckhart/component/device_menu_screen.rs b/core/embed/rust/src/ui/layout_eckhart/component/device_menu_screen.rs new file mode 100644 index 0000000000..c22cac20ab --- /dev/null +++ b/core/embed/rust/src/ui/layout_eckhart/component/device_menu_screen.rs @@ -0,0 +1,143 @@ +use crate::ui::{ + component::{base::AttachType, Component, Event, EventCtx}, + geometry::Rect, + layout_eckhart::{ + component::{Button, VerticalMenuScreen, VerticalMenuScreenMsg, MENU_MAX_ITEMS}, + constant::SCREEN, + theme, + }, + shape::{self, Renderer}, +}; + +use heapless::Vec; + +// Max number of menu screens +const MAX_MENUS: usize = 5; + +type VerticalMenus = Vec; + +pub enum DeviceMenuMsg { + Selected(usize), + /// Right header button clicked + Close, +} + +/// Linear map of vertical menus. +pub struct DeviceMenuScreen { + /// Bounds of the menu screen + bounds: Rect, + /// Index of the currently active menu + active_menu: usize, + /// Stack of parent menus for back navigation + menu_parents: Vec, + /// Stack of menu screens and their children + menu_stack: Vec<(VerticalMenuScreen, Vec, MENU_MAX_ITEMS>), 5>, +} + +impl DeviceMenuScreen { + pub fn empty() -> Self { + Self { + bounds: Rect::zero(), + active_menu: 0, // Start with the first menu by default + menu_stack: Vec::new(), + menu_parents: Vec::new(), + } + } + + // Add an internal menu screen with children and return the index of the new + // menu within the stack. The children are optional indices of the sub-menus in + // the stack + pub fn add_inner_menu( + &mut self, + menu: VerticalMenuScreen, + children: Vec, MENU_MAX_ITEMS>, + ) -> usize { + unwrap!(self.menu_stack.push((menu, children))); + self.menu_stack.len() - 1 + } + + // Add a leaf menu screen (without any children) + pub fn add_leaf_menu(&mut self, menu: VerticalMenuScreen) -> usize { + let mut children = Vec::new(); + for _ in 0..MENU_MAX_ITEMS { + unwrap!(children.push(None)); + } + + self.add_inner_menu(menu, children) + } + + // Set the index of the active menu + pub fn set_active_menu(&mut self, menu: usize) { + assert!(menu < self.menu_stack.len()); + self.active_menu = menu; + } + + // Navigate to a different menu based on selection. + fn handle_menu_selection(&mut self, ctx: &mut EventCtx, index: usize) -> Option { + if self.menu_stack[self.active_menu].1[index].is_some() { + unwrap!(self.menu_parents.push(self.active_menu)); + self.active_menu = self.menu_stack[self.active_menu].1[index].unwrap(); + self.place(self.bounds); + self.menu_stack[self.active_menu].0.update_menu(ctx); + } + None + } + + // Handle back navigation to previously active menu + fn handle_back_navigation(&mut self) -> Option { + if self.menu_parents.is_empty() { + Some(DeviceMenuMsg::Close) + } else { + self.active_menu = self.menu_parents.pop().unwrap(); + self.place(self.bounds); + None + } + } +} + +impl Component for DeviceMenuScreen { + type Msg = DeviceMenuMsg; + + fn place(&mut self, bounds: Rect) -> Rect { + // assert full screen + debug_assert_eq!(bounds.height(), SCREEN.height()); + debug_assert_eq!(bounds.width(), SCREEN.width()); + + self.bounds = bounds; + // Place the active menu + self.menu_stack[self.active_menu].0.place(bounds); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + // Update the menu when the screen is attached + if let Event::Attach(AttachType::Initial) = event {} + // Handle the event for the active menu + match self.menu_stack[self.active_menu].0.event(ctx, event) { + Some(VerticalMenuScreenMsg::Selected(index)) => { + // Navigate to the selected menu (if any) + return self.handle_menu_selection(ctx, index); + } + Some(VerticalMenuScreenMsg::Back) => { + return self.handle_back_navigation(); + } + Some(VerticalMenuScreenMsg::Close) => { + return Some(DeviceMenuMsg::Close); + } + _ => {} + } + None + } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + // Render the active menu if any + self.menu_stack[self.active_menu].0.render(target); + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for DeviceMenuScreen { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("DeviceMenuScreen"); + } +} diff --git a/core/embed/rust/src/ui/layout_eckhart/component/mod.rs b/core/embed/rust/src/ui/layout_eckhart/component/mod.rs index 317b132dd0..3c337bd1ec 100644 --- a/core/embed/rust/src/ui/layout_eckhart/component/mod.rs +++ b/core/embed/rust/src/ui/layout_eckhart/component/mod.rs @@ -1,6 +1,7 @@ mod action_bar; pub mod bl_confirm; mod button; +mod device_menu_screen; mod error; mod header; mod hint; @@ -14,6 +15,7 @@ mod welcome_screen; pub use action_bar::{ActionBar, ActionBarMsg}; pub use button::{Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, IconText}; +pub use device_menu_screen::{DeviceMenuMsg, DeviceMenuScreen}; pub use error::ErrorScreen; pub use header::{Header, HeaderMsg}; pub use hint::Hint; diff --git a/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs b/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs index 244d020b50..8c34c2a83e 100644 --- a/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs +++ b/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs @@ -14,7 +14,8 @@ use crate::{ }; use super::component::{ - AllowedTextContent, SelectWordMsg, SelectWordScreen, TextScreen, TextScreenMsg, + AllowedTextContent, DeviceMenuMsg, DeviceMenuScreen, SelectWordMsg, SelectWordScreen, + TextScreen, TextScreenMsg, VerticalMenuScreen, VerticalMenuScreenMsg, }; // Clippy/compiler complains about conflicting implementations @@ -61,3 +62,22 @@ impl ComponentMsgObj for SelectWordScreen { } } } + +impl ComponentMsgObj for VerticalMenuScreen { + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + match msg { + VerticalMenuScreenMsg::Back => Ok(CANCELLED.as_obj()), + VerticalMenuScreenMsg::Close => Ok(CANCELLED.as_obj()), + VerticalMenuScreenMsg::Selected(_) => Ok(CONFIRMED.as_obj()), + } + } +} + +impl ComponentMsgObj for DeviceMenuScreen { + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + match msg { + DeviceMenuMsg::Close => Ok(CANCELLED.as_obj()), + DeviceMenuMsg::Selected(_) => Ok(CONFIRMED.as_obj()), + } + } +} diff --git a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs index 6047a5db3a..5384cb1c72 100644 --- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs @@ -12,6 +12,7 @@ use crate::{ }, Empty, FormattedText, }, + geometry::Alignment, layout::{ obj::{LayoutMaybeTrace, LayoutObj, RootComponent}, util::{ConfirmValueParams, RecoveryType, StrOrBytes}, @@ -24,10 +25,15 @@ use crate::{ }; use super::{ - component::{ActionBar, Button, Header, HeaderMsg, Hint, SelectWordScreen, TextScreen}, + component::{ + ActionBar, Button, ButtonStyleSheet, DeviceMenuScreen, Header, HeaderMsg, Hint, + SelectWordScreen, TextScreen, VerticalMenu, VerticalMenuScreen, MENU_MAX_ITEMS, + }, flow, fonts, theme, UIEckhart, }; +use heapless::Vec; + impl FirmwareUI for UIEckhart { fn confirm_action( title: TString<'static>, @@ -285,6 +291,148 @@ impl FirmwareUI for UIEckhart { Err::, Error>(Error::ValueError(c"not implemented")) } + fn device_menu( + failed_backup: bool, + low_battery: bool, + connections: TString<'static>, + ) -> Result { + const BUTTON_RADIUS: u8 = 12; + const BUTTON_ALIGNMENT: Alignment = Alignment::Start; + + // Function to add a menu item with optional subtext to the VerticalMenu + fn add_menu_item( + menu: VerticalMenu, + text: TString<'static>, + subtext: Option>, + style: ButtonStyleSheet, + include: bool, + ) -> VerticalMenu { + if include { + let button = match subtext { + Some(sub) => Button::with_text_and_subtext(text, sub) + .with_text_align(BUTTON_ALIGNMENT) + .styled(style) + .with_radius(BUTTON_RADIUS), + None => Button::with_text(text) + .with_text_align(BUTTON_ALIGNMENT) + .styled(style) + .with_radius(BUTTON_RADIUS), + }; + menu.item(button) + } else { + menu + } + } + + // Create the device menu screen + let mut device_menu = DeviceMenuScreen::empty(); + + // Define menu items for settings + let settings_menu_items = [ + (TR::device_menu__language, None, true), + (TR::device_menu__bluetooth, None, true), + (TR::device_menu__brightness, None, true), + (TR::device_menu__fw_version, None, true), + (TR::device_menu__about, None, true), + ]; + + // Build the settings menu dynamically + let settings_menu = settings_menu_items + .iter() + .fold( + VerticalMenu::empty().with_separators(), + |menu, &(text, subtext, include)| { + add_menu_item( + menu, + text.into(), + subtext.into(), + theme::menu_item_title(), + include, + ) + }, + ) + .with_separators(); + + // Create the settings screen + let settings_screen = VerticalMenuScreen::new(settings_menu).with_header( + Header::new(TR::words__settings.into()) + .with_right_button(Button::with_icon(theme::ICON_CROSS), HeaderMsg::Cancelled) + .with_left_button(Button::with_icon(theme::ICON_CHEVRON_LEFT), HeaderMsg::Back), + ); + let setting_index = device_menu.add_leaf_menu(settings_screen); + + // Determine battery color based on low_battery flag + let battery_color = if low_battery { + theme::YELLOW + } else { + theme::GREEN_LIME + }; + + // Define root menu items + let root_menu_items = [ + ( + TR::device_menu__backup_failed_title, + Some(TR::device_menu__backup_failed_description.into()), + theme::menu_item_title_red(), + failed_backup, + ), + ( + TR::device_menu__battery_low_title, + Some(TR::device_menu__battery_low_description.into()), + theme::menu_item_title_yellow(), + low_battery, + ), + ( + TR::device_menu__connections_title, + Some(connections.into()), + theme::menu_item_title(), + true, + ), + (TR::words__settings, None, theme::menu_item_title(), true), + ]; + + // Build the root menu dynamically + let root_menu = root_menu_items.iter().fold( + VerticalMenu::empty().with_separators(), + |menu, &(text, subtext, style, include)| { + add_menu_item(menu, text.into(), subtext.into(), style, include) + }, + ); + + // Initialize root children + let mut root_children: Vec, MENU_MAX_ITEMS> = Vec::new(); + + // Optional failed backup child + if failed_backup { + root_children.push(None).unwrap(); + } + // Optional low battery child + if low_battery { + root_children.push(None).unwrap(); + } + + // Remaining children + root_children + .extend_from_slice(&[None, Some(setting_index)]) + .unwrap(); + + // Create the root screen + let root_screen = VerticalMenuScreen::new(root_menu).with_header( + Header::new("".into()) + .with_right_button(Button::with_icon(theme::ICON_CROSS), HeaderMsg::Cancelled) + .with_icon(theme::ICON_BATTERY_ZAP, battery_color), + ); + let root_index: usize = device_menu.add_inner_menu(root_screen, root_children); + + // Set root menu as active + device_menu.set_active_menu(root_index); + + // Create and return the layout + let layout = RootComponent::new(device_menu); + + Ok(layout) + } + fn flow_confirm_output( _title: Option>, _subtitle: Option>, diff --git a/core/embed/rust/src/ui/ui_firmware.rs b/core/embed/rust/src/ui/ui_firmware.rs index 927213041f..70b21b8a0a 100644 --- a/core/embed/rust/src/ui/ui_firmware.rs +++ b/core/embed/rust/src/ui/ui_firmware.rs @@ -156,6 +156,12 @@ pub trait FirmwareUI { fn check_homescreen_format(image: BinaryData, accept_toif: bool) -> bool; + fn device_menu( + failed_backup: bool, + low_battery: bool, + connections: TString<'static>, + ) -> Result; + #[allow(clippy::too_many_arguments)] fn flow_confirm_output( title: Option>, diff --git a/core/mocks/generated/trezorui_api.pyi b/core/mocks/generated/trezorui_api.pyi index 5dbefcd2ae..a92f7df7d0 100644 --- a/core/mocks/generated/trezorui_api.pyi +++ b/core/mocks/generated/trezorui_api.pyi @@ -306,6 +306,16 @@ def continue_recovery_homepage( """Device recovery homescreen.""" +# rust/src/ui/api/firmware_micropython.rs +def device_menu( + *, + failed_backup: bool, + low_battery: bool, + connections: str | None, +) -> LayoutObj[int]: + """Show eckhart device menu.""" + + # rust/src/ui/api/firmware_micropython.rs def flow_confirm_output( *, diff --git a/core/mocks/trezortranslate_keys.pyi b/core/mocks/trezortranslate_keys.pyi index f64599e59a..c46972b5f7 100644 --- a/core/mocks/trezortranslate_keys.pyi +++ b/core/mocks/trezortranslate_keys.pyi @@ -251,6 +251,18 @@ class TR: confirm_total__title_sending_from: str = "Sending from" debug__loading_seed: str = "Loading seed" debug__loading_seed_not_recommended: str = "Loading private seed is not recommended." + device_menu__1_connection: str = "1 active connection" + device_menu__about: str = "About" + device_menu__active_connections: str = "{0} active connections" + device_menu__backup_failed_description: str = "Review" + device_menu__backup_failed_title: str = "Backup failed" + device_menu__battery_low_description: str = "Recharge soon" + device_menu__battery_low_title: str = "Battery low" + device_menu__bluetooth: str = "Bluetooth management" + device_menu__brightness: str = "Brightness" + device_menu__connections_title: str = "Pair & Connect" + device_menu__fw_version: str = "Firmware version" + device_menu__language: str = "Language" device_name__change_template: str = "Change device name to {0}?" device_name__title: str = "Device name" entropy__send: str = "Do you really want to send entropy?" diff --git a/core/src/trezor/ui/layouts/eckhart/__init__.py b/core/src/trezor/ui/layouts/eckhart/__init__.py index 5468ea9412..ba4e758704 100644 --- a/core/src/trezor/ui/layouts/eckhart/__init__.py +++ b/core/src/trezor/ui/layouts/eckhart/__init__.py @@ -919,6 +919,35 @@ async def confirm_signverify( raise NotImplemented +async def device_menu( + failed_backup: bool, + battery_level: int, + connections: int, +) -> None: + + # Currently arbitrary value + if battery_level < 20: + low_battery = True + else: + low_battery = False + + if connections == 1: + connections_str = TR.device_menu__1_connection + elif connections == 0: + connections_str = TR.device_menu__connections_title.format("No") + else: + connections_str = TR.device_menu__connections_title.format(connections) + + await interact( + trezorui_api.device_menu( + failed_backup=failed_backup, + low_battery=low_battery, + connections=connections_str, + ), + None, + ) + + def error_popup( title: str, description: str, diff --git a/core/translations/en.json b/core/translations/en.json index 8d57acf2a8..9d364574ab 100644 --- a/core/translations/en.json +++ b/core/translations/en.json @@ -253,6 +253,18 @@ "confirm_total__title_sending_from": "Sending from", "debug__loading_seed": "Loading seed", "debug__loading_seed_not_recommended": "Loading private seed is not recommended.", + "device_menu__brightness": "Brightness", + "device_menu__language": "Language", + "device_menu__bluetooth": "Bluetooth management", + "device_menu__fw_version": "Firmware version", + "device_menu__about": "About", + "device_menu__backup_failed_title": "Backup failed", + "device_menu__backup_failed_description": "Review", + "device_menu__battery_low_title": "Battery low", + "device_menu__battery_low_description": "Recharge soon", + "device_menu__connections_title": "Pair & Connect", + "device_menu__1_connection": "1 active connection", + "device_menu__active_connections": "{0} active connections", "device_name__change_template": "Change device name to {0}?", "device_name__title": "Device name", "entropy__send": "Do you really want to send entropy?", diff --git a/core/translations/order.json b/core/translations/order.json index 95f2852ba8..5d382835b5 100644 --- a/core/translations/order.json +++ b/core/translations/order.json @@ -974,5 +974,17 @@ "972": "ethereum__interaction_contract", "973": "misc__enable_labeling", "974": "ethereum__unknown_contract_address_short", - "975": "reset__share_words_first" + "975": "reset__share_words_first", + "976": "device_menu__1_connection", + "977": "device_menu__about", + "978": "device_menu__active_connections", + "979": "device_menu__backup_failed_description", + "980": "device_menu__backup_failed_title", + "981": "device_menu__battery_low_description", + "982": "device_menu__battery_low_title", + "983": "device_menu__bluetooth", + "984": "device_menu__brightness", + "985": "device_menu__connections_title", + "986": "device_menu__fw_version", + "987": "device_menu__language" } diff --git a/core/translations/signatures.json b/core/translations/signatures.json index 3879d1ce46..e93777fc0e 100644 --- a/core/translations/signatures.json +++ b/core/translations/signatures.json @@ -1,8 +1,8 @@ { "current": { - "merkle_root": "43c08e81d71c1c28d77b1650fc96b0dfcd473fde0c922717e5588baeeb581bd3", - "datetime": "2025-02-14T14:18:48.730778", - "commit": "8ecb8718c89547bcc8c0bb2f2cf9e823a9a75be3" + "merkle_root": "f3c02816770393fd369280c0668d5d93812be27ba55413957d66a73abce201b5", + "datetime": "2025-02-16T11:06:40.774900", + "commit": "71e2c51c5b52f783f85a87f38d943750d7e2e332" }, "history": [ {