mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-02-21 03:52:04 +00:00
feat(eckhart): Add full-screen device menu component
device menu cont
This commit is contained in:
parent
b84a983678
commit
4c7aec0635
@ -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;
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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<TString> = 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,
|
||||
|
@ -536,6 +536,14 @@ impl FirmwareUI for UIBolt {
|
||||
}
|
||||
}
|
||||
|
||||
fn device_menu(
|
||||
_failed_backup: bool,
|
||||
_low_battery: bool,
|
||||
_connections: TString<'static>,
|
||||
) -> Result<impl LayoutMaybeTrace, Error> {
|
||||
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
|
||||
}
|
||||
|
||||
fn flow_confirm_output(
|
||||
_title: Option<TString<'static>>,
|
||||
_subtitle: Option<TString<'static>>,
|
||||
|
@ -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<impl LayoutMaybeTrace, Error> {
|
||||
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
|
||||
}
|
||||
|
||||
fn flow_confirm_output(
|
||||
_title: Option<TString<'static>>,
|
||||
_subtitle: Option<TString<'static>>,
|
||||
|
@ -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<impl LayoutMaybeTrace, Error> {
|
||||
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
|
||||
}
|
||||
|
||||
fn flow_confirm_output(
|
||||
title: Option<TString<'static>>,
|
||||
subtitle: Option<TString<'static>>,
|
||||
|
@ -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<Button, MAX_MENUS>;
|
||||
|
||||
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<usize, MAX_MENUS>,
|
||||
/// Stack of menu screens and their children
|
||||
menu_stack: Vec<(VerticalMenuScreen, Vec<Option<usize>, 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<Option<usize>, 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<DeviceMenuMsg> {
|
||||
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<DeviceMenuMsg> {
|
||||
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<Self::Msg> {
|
||||
// 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");
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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<Obj, Error> {
|
||||
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<Obj, Error> {
|
||||
match msg {
|
||||
DeviceMenuMsg::Close => Ok(CANCELLED.as_obj()),
|
||||
DeviceMenuMsg::Selected(_) => Ok(CONFIRMED.as_obj()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
|
||||
}
|
||||
|
||||
fn device_menu(
|
||||
failed_backup: bool,
|
||||
low_battery: bool,
|
||||
connections: TString<'static>,
|
||||
) -> Result<impl LayoutMaybeTrace, Error> {
|
||||
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<TString<'static>>,
|
||||
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<Option<usize>, 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<TString<'static>>,
|
||||
_subtitle: Option<TString<'static>>,
|
||||
|
@ -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<impl LayoutMaybeTrace, Error>;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn flow_confirm_output(
|
||||
title: Option<TString<'static>>,
|
||||
|
@ -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(
|
||||
*,
|
||||
|
@ -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?"
|
||||
|
@ -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,
|
||||
|
@ -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?",
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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": [
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user