mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-29 19:08:12 +00:00
refactor(core): move selectors to UiFeatures
This commit is contained in:
parent
34c374b3bc
commit
5aeab55429
@ -14,7 +14,7 @@ use crate::{
|
|||||||
base::LAYOUT_STATE,
|
base::LAYOUT_STATE,
|
||||||
obj::{LayoutObj, ATTACH_TYPE_OBJ},
|
obj::{LayoutObj, ATTACH_TYPE_OBJ},
|
||||||
result::{CANCELLED, CONFIRMED, INFO},
|
result::{CANCELLED, CONFIRMED, INFO},
|
||||||
util::upy_disable_animation,
|
util::{upy_disable_animation, RecoveryType},
|
||||||
},
|
},
|
||||||
ui_features::ModelUI,
|
ui_features::ModelUI,
|
||||||
ui_features_fw::UIFeaturesFirmware,
|
ui_features_fw::UIFeaturesFirmware,
|
||||||
@ -118,6 +118,29 @@ extern "C" fn new_request_passphrase(n_args: usize, args: *const Obj, kwargs: *m
|
|||||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extern "C" fn new_select_word(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||||
|
let block = move |_args: &[Obj], kwargs: &Map| {
|
||||||
|
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
||||||
|
let description: TString = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
|
||||||
|
let words_iterable: Obj = kwargs.get(Qstr::MP_QSTR_words)?;
|
||||||
|
let words: [TString<'static>; 3] = util::iter_into_array(words_iterable)?;
|
||||||
|
|
||||||
|
let layout = ModelUI::select_word(title, description, words)?;
|
||||||
|
Ok(LayoutObj::new_root(layout)?.into())
|
||||||
|
};
|
||||||
|
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" fn new_select_word_count(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||||
|
let block = move |_args: &[Obj], kwargs: &Map| {
|
||||||
|
let recovery_type: RecoveryType = kwargs.get(Qstr::MP_QSTR_recovery_type)?.try_into()?;
|
||||||
|
|
||||||
|
let layout = ModelUI::select_word_count(recovery_type)?;
|
||||||
|
Ok(LayoutObj::new_root(layout)?.into())
|
||||||
|
};
|
||||||
|
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" fn show_info(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
extern "C" fn show_info(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||||
let block = move |_args: &[Obj], kwargs: &Map| {
|
let block = move |_args: &[Obj], kwargs: &Map| {
|
||||||
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
||||||
@ -299,6 +322,24 @@ pub static mp_module_trezorui_api: Module = obj_module! {
|
|||||||
/// """Passphrase input keyboard."""
|
/// """Passphrase input keyboard."""
|
||||||
Qstr::MP_QSTR_request_passphrase => obj_fn_kw!(0, new_request_passphrase).as_obj(),
|
Qstr::MP_QSTR_request_passphrase => obj_fn_kw!(0, new_request_passphrase).as_obj(),
|
||||||
|
|
||||||
|
/// def select_word(
|
||||||
|
/// *,
|
||||||
|
/// title: str,
|
||||||
|
/// description: str,
|
||||||
|
/// words: Iterable[str],
|
||||||
|
/// ) -> LayoutObj[int]:
|
||||||
|
/// """Select mnemonic word from three possibilities - seed check after backup. The
|
||||||
|
/// iterable must be of exact size. Returns index in range `0..3`."""
|
||||||
|
Qstr::MP_QSTR_select_word => obj_fn_kw!(0, new_select_word).as_obj(),
|
||||||
|
|
||||||
|
/// def select_word_count(
|
||||||
|
/// *,
|
||||||
|
/// recovery_type: RecoveryType,
|
||||||
|
/// ) -> LayoutObj[int | str]: # TR returns str
|
||||||
|
/// """Select a mnemonic word count from the options: 12, 18, 20, 24, or 33.
|
||||||
|
/// For unlocking a repeated backup, select from 20 or 33."""
|
||||||
|
Qstr::MP_QSTR_select_word_count => obj_fn_kw!(0, new_select_word_count).as_obj(),
|
||||||
|
|
||||||
/// def show_info(
|
/// def show_info(
|
||||||
/// *,
|
/// *,
|
||||||
/// title: str,
|
/// title: str,
|
||||||
|
@ -1162,20 +1162,6 @@ extern "C" fn new_request_number(n_args: usize, args: *const Obj, kwargs: *mut M
|
|||||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" fn new_select_word(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
|
||||||
let block = move |_args: &[Obj], kwargs: &Map| {
|
|
||||||
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
|
||||||
let description: TString = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
|
|
||||||
let words_iterable: Obj = kwargs.get(Qstr::MP_QSTR_words)?;
|
|
||||||
let words: [TString; 3] = util::iter_into_array(words_iterable)?;
|
|
||||||
|
|
||||||
let content = VerticalMenu::select_word(words);
|
|
||||||
let frame_with_menu = Frame::left_aligned(title, content).with_subtitle(description);
|
|
||||||
Ok(LayoutObj::new(frame_with_menu)?.into())
|
|
||||||
};
|
|
||||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" fn new_show_checklist(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
extern "C" fn new_show_checklist(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||||
let block = move |_args: &[Obj], kwargs: &Map| {
|
let block = move |_args: &[Obj], kwargs: &Map| {
|
||||||
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
||||||
@ -1226,23 +1212,6 @@ extern "C" fn new_show_tutorial() -> Obj {
|
|||||||
unsafe { util::try_or_raise(block) }
|
unsafe { util::try_or_raise(block) }
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" fn new_select_word_count(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
|
||||||
let block = move |_args: &[Obj], kwargs: &Map| {
|
|
||||||
let recovery_type: RecoveryType = kwargs.get(Qstr::MP_QSTR_recovery_type)?.try_into()?;
|
|
||||||
let content = if matches!(recovery_type, RecoveryType::UnlockRepeatedBackup) {
|
|
||||||
SelectWordCount::new_multishare()
|
|
||||||
} else {
|
|
||||||
SelectWordCount::new_all()
|
|
||||||
};
|
|
||||||
let obj = LayoutObj::new(Frame::left_aligned(
|
|
||||||
TR::recovery__num_of_words.into(),
|
|
||||||
content,
|
|
||||||
))?;
|
|
||||||
Ok(obj.into())
|
|
||||||
};
|
|
||||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" fn new_show_group_share_success(
|
extern "C" fn new_show_group_share_success(
|
||||||
n_args: usize,
|
n_args: usize,
|
||||||
args: *const Obj,
|
args: *const Obj,
|
||||||
@ -1617,16 +1586,6 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
|||||||
/// """Confirm coinjoin authorization."""
|
/// """Confirm coinjoin authorization."""
|
||||||
Qstr::MP_QSTR_confirm_coinjoin => obj_fn_kw!(0, new_confirm_coinjoin).as_obj(),
|
Qstr::MP_QSTR_confirm_coinjoin => obj_fn_kw!(0, new_confirm_coinjoin).as_obj(),
|
||||||
|
|
||||||
/// def select_word(
|
|
||||||
/// *,
|
|
||||||
/// title: str,
|
|
||||||
/// description: str,
|
|
||||||
/// words: Iterable[str],
|
|
||||||
/// ) -> LayoutObj[int]:
|
|
||||||
/// """Select mnemonic word from three possibilities - seed check after backup. The
|
|
||||||
/// iterable must be of exact size. Returns index in range `0..3`."""
|
|
||||||
Qstr::MP_QSTR_select_word => obj_fn_kw!(0, new_select_word).as_obj(),
|
|
||||||
|
|
||||||
/// def flow_prompt_backup() -> LayoutObj[UiResult]:
|
/// def flow_prompt_backup() -> LayoutObj[UiResult]:
|
||||||
/// """Prompt a user to create backup with an option to skip."""
|
/// """Prompt a user to create backup with an option to skip."""
|
||||||
Qstr::MP_QSTR_flow_prompt_backup => obj_fn_0!(new_prompt_backup).as_obj(),
|
Qstr::MP_QSTR_flow_prompt_backup => obj_fn_0!(new_prompt_backup).as_obj(),
|
||||||
@ -1688,14 +1647,6 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
|||||||
/// """Device recovery homescreen."""
|
/// """Device recovery homescreen."""
|
||||||
Qstr::MP_QSTR_flow_continue_recovery => obj_fn_kw!(0, new_continue_recovery).as_obj(),
|
Qstr::MP_QSTR_flow_continue_recovery => obj_fn_kw!(0, new_continue_recovery).as_obj(),
|
||||||
|
|
||||||
/// def select_word_count(
|
|
||||||
/// *,
|
|
||||||
/// recovery_type: RecoveryType,
|
|
||||||
/// ) -> LayoutObj[int | str]: # merucry returns int
|
|
||||||
/// """Select a mnemonic word count from the options: 12, 18, 20, 24, or 33.
|
|
||||||
/// For unlocking a repeated backup, select from 20 or 33."""
|
|
||||||
Qstr::MP_QSTR_select_word_count => obj_fn_kw!(0, new_select_word_count).as_obj(),
|
|
||||||
|
|
||||||
/// def show_group_share_success(
|
/// def show_group_share_success(
|
||||||
/// *,
|
/// *,
|
||||||
/// lines: Iterable[str]
|
/// lines: Iterable[str]
|
||||||
|
@ -10,14 +10,18 @@ use crate::{
|
|||||||
text::paragraphs::{Paragraph, Paragraphs},
|
text::paragraphs::{Paragraph, Paragraphs},
|
||||||
},
|
},
|
||||||
geometry::Direction,
|
geometry::Direction,
|
||||||
layout::obj::{LayoutMaybeTrace, LayoutObj, RootComponent},
|
layout::{
|
||||||
|
obj::{LayoutMaybeTrace, LayoutObj, RootComponent},
|
||||||
|
util::RecoveryType,
|
||||||
|
},
|
||||||
ui_features_fw::UIFeaturesFirmware,
|
ui_features_fw::UIFeaturesFirmware,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
component::{
|
component::{
|
||||||
Bip39Input, Frame, MnemonicKeyboard, PinKeyboard, Slip39Input, SwipeContent, SwipeUpScreen,
|
Bip39Input, Frame, MnemonicKeyboard, PinKeyboard, SelectWordCount, Slip39Input,
|
||||||
|
SwipeContent, SwipeUpScreen, VerticalMenu,
|
||||||
},
|
},
|
||||||
flow, theme, ModelMercuryFeatures,
|
flow, theme, ModelMercuryFeatures,
|
||||||
};
|
};
|
||||||
@ -105,6 +109,30 @@ impl UIFeaturesFirmware for ModelMercuryFeatures {
|
|||||||
Ok(flow)
|
Ok(flow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn select_word(
|
||||||
|
title: TString<'static>,
|
||||||
|
description: TString<'static>,
|
||||||
|
words: [TString<'static>; 3],
|
||||||
|
) -> Result<impl LayoutMaybeTrace, Error> {
|
||||||
|
let content = VerticalMenu::select_word(words);
|
||||||
|
let layout =
|
||||||
|
RootComponent::new(Frame::left_aligned(title, content).with_subtitle(description));
|
||||||
|
Ok(layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_word_count(recovery_type: RecoveryType) -> Result<impl LayoutMaybeTrace, Error> {
|
||||||
|
let content = if matches!(recovery_type, RecoveryType::UnlockRepeatedBackup) {
|
||||||
|
SelectWordCount::new_multishare()
|
||||||
|
} else {
|
||||||
|
SelectWordCount::new_all()
|
||||||
|
};
|
||||||
|
let layout = RootComponent::new(Frame::left_aligned(
|
||||||
|
TR::recovery__num_of_words.into(),
|
||||||
|
content,
|
||||||
|
));
|
||||||
|
Ok(layout)
|
||||||
|
}
|
||||||
|
|
||||||
fn show_info(
|
fn show_info(
|
||||||
title: TString<'static>,
|
title: TString<'static>,
|
||||||
description: TString<'static>,
|
description: TString<'static>,
|
||||||
|
@ -1197,29 +1197,6 @@ extern "C" fn new_confirm_coinjoin(n_args: usize, args: *const Obj, kwargs: *mut
|
|||||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" fn new_select_word(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
|
||||||
let block = |_args: &[Obj], kwargs: &Map| {
|
|
||||||
// we ignore passed in `title` and use `description` in its place
|
|
||||||
let description: TString = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
|
|
||||||
let words_iterable: Obj = kwargs.get(Qstr::MP_QSTR_words)?;
|
|
||||||
// There are only 3 words, but SimpleChoice requires 5 elements
|
|
||||||
let words: Vec<TString<'static>, 5> = util::iter_into_vec(words_iterable)?;
|
|
||||||
|
|
||||||
// Returning the index of the selected word, not the word itself
|
|
||||||
let obj = LayoutObj::new(
|
|
||||||
Frame::new(
|
|
||||||
description,
|
|
||||||
SimpleChoice::new(words, false)
|
|
||||||
.with_show_incomplete()
|
|
||||||
.with_return_index(),
|
|
||||||
)
|
|
||||||
.with_title_centered(),
|
|
||||||
)?;
|
|
||||||
Ok(obj.into())
|
|
||||||
};
|
|
||||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" fn new_show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
extern "C" fn new_show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||||
let block = |_args: &[Obj], kwargs: &Map| {
|
let block = |_args: &[Obj], kwargs: &Map| {
|
||||||
let share_words_obj: Obj = kwargs.get(Qstr::MP_QSTR_share_words)?;
|
let share_words_obj: Obj = kwargs.get(Qstr::MP_QSTR_share_words)?;
|
||||||
@ -1332,29 +1309,6 @@ extern "C" fn new_confirm_recovery(n_args: usize, args: *const Obj, kwargs: *mut
|
|||||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" fn new_select_word_count(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
|
||||||
let block = |_args: &[Obj], kwargs: &Map| {
|
|
||||||
let title: TString = TR::word_count__title.into();
|
|
||||||
let recovery_type: RecoveryType = kwargs.get(Qstr::MP_QSTR_recovery_type)?.try_into()?;
|
|
||||||
|
|
||||||
let choices: Vec<TString<'static>, 5> = {
|
|
||||||
let nums: &[&str] = if matches!(recovery_type, RecoveryType::UnlockRepeatedBackup) {
|
|
||||||
&["20", "33"]
|
|
||||||
} else {
|
|
||||||
&["12", "18", "20", "24", "33"]
|
|
||||||
};
|
|
||||||
|
|
||||||
nums.iter().map(|&num| num.into()).collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
let obj = LayoutObj::new(
|
|
||||||
Frame::new(title, SimpleChoice::new(choices, false)).with_title_centered(),
|
|
||||||
)?;
|
|
||||||
Ok(obj.into())
|
|
||||||
};
|
|
||||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" fn new_show_group_share_success(
|
extern "C" fn new_show_group_share_success(
|
||||||
n_args: usize,
|
n_args: usize,
|
||||||
args: *const Obj,
|
args: *const Obj,
|
||||||
@ -1753,16 +1707,6 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
|||||||
/// """Confirm coinjoin authorization."""
|
/// """Confirm coinjoin authorization."""
|
||||||
Qstr::MP_QSTR_confirm_coinjoin => obj_fn_kw!(0, new_confirm_coinjoin).as_obj(),
|
Qstr::MP_QSTR_confirm_coinjoin => obj_fn_kw!(0, new_confirm_coinjoin).as_obj(),
|
||||||
|
|
||||||
/// def select_word(
|
|
||||||
/// *,
|
|
||||||
/// title: str, # unused on TR
|
|
||||||
/// description: str,
|
|
||||||
/// words: Iterable[str],
|
|
||||||
/// ) -> LayoutObj[int]:
|
|
||||||
/// """Select mnemonic word from three possibilities - seed check after backup. The
|
|
||||||
/// iterable must be of exact size. Returns index in range `0..3`."""
|
|
||||||
Qstr::MP_QSTR_select_word => obj_fn_kw!(0, new_select_word).as_obj(),
|
|
||||||
|
|
||||||
/// def show_share_words(
|
/// def show_share_words(
|
||||||
/// *,
|
/// *,
|
||||||
/// share_words: Iterable[str],
|
/// share_words: Iterable[str],
|
||||||
@ -1804,14 +1748,6 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
|||||||
/// """Device recovery homescreen."""
|
/// """Device recovery homescreen."""
|
||||||
Qstr::MP_QSTR_confirm_recovery => obj_fn_kw!(0, new_confirm_recovery).as_obj(),
|
Qstr::MP_QSTR_confirm_recovery => obj_fn_kw!(0, new_confirm_recovery).as_obj(),
|
||||||
|
|
||||||
/// def select_word_count(
|
|
||||||
/// *,
|
|
||||||
/// recovery_type: RecoveryType,
|
|
||||||
/// ) -> LayoutObj[int | str]: # TR returns str
|
|
||||||
/// """Select a mnemonic word count from the options: 12, 18, 20, 24, or 33.
|
|
||||||
/// For unlocking a repeated backup, select from 20 or 33."""
|
|
||||||
Qstr::MP_QSTR_select_word_count => obj_fn_kw!(0, new_select_word_count).as_obj(),
|
|
||||||
|
|
||||||
/// def show_group_share_success(
|
/// def show_group_share_success(
|
||||||
/// *,
|
/// *,
|
||||||
/// lines: Iterable[str],
|
/// lines: Iterable[str],
|
||||||
|
@ -4,24 +4,30 @@ use crate::{
|
|||||||
maybe_trace::MaybeTrace,
|
maybe_trace::MaybeTrace,
|
||||||
micropython::gc::Gc,
|
micropython::gc::Gc,
|
||||||
strutil::TString,
|
strutil::TString,
|
||||||
|
translations::TR,
|
||||||
ui::{
|
ui::{
|
||||||
component::{
|
component::{
|
||||||
text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs, VecExt},
|
text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs, VecExt},
|
||||||
Component, ComponentExt, Paginate, Timeout,
|
Component, ComponentExt, Paginate, Timeout,
|
||||||
},
|
},
|
||||||
layout::obj::{LayoutMaybeTrace, LayoutObj, RootComponent},
|
layout::{
|
||||||
|
obj::{LayoutMaybeTrace, LayoutObj, RootComponent},
|
||||||
|
util::RecoveryType,
|
||||||
|
},
|
||||||
ui_features_fw::UIFeaturesFirmware,
|
ui_features_fw::UIFeaturesFirmware,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
component::{
|
component::{
|
||||||
ButtonDetails, ButtonPage, Frame, PassphraseEntry, PinEntry, ScrollableFrame,
|
ButtonDetails, ButtonPage, Frame, PassphraseEntry, PinEntry, ScrollableFrame, SimpleChoice,
|
||||||
WordlistEntry, WordlistType,
|
WordlistEntry, WordlistType,
|
||||||
},
|
},
|
||||||
theme, ModelTRFeatures,
|
theme, ModelTRFeatures,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use heapless::Vec;
|
||||||
|
|
||||||
impl UIFeaturesFirmware for ModelTRFeatures {
|
impl UIFeaturesFirmware for ModelTRFeatures {
|
||||||
fn confirm_action(
|
fn confirm_action(
|
||||||
title: TString<'static>,
|
title: TString<'static>,
|
||||||
@ -116,6 +122,43 @@ impl UIFeaturesFirmware for ModelTRFeatures {
|
|||||||
Ok(layout)
|
Ok(layout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn select_word(
|
||||||
|
title: TString<'static>,
|
||||||
|
description: TString<'static>,
|
||||||
|
words: [TString<'static>; 3],
|
||||||
|
) -> Result<impl LayoutMaybeTrace, Error> {
|
||||||
|
let words: Vec<TString<'static>, 5> = Vec::from_iter(words);
|
||||||
|
// Returning the index of the selected word, not the word itself
|
||||||
|
let layout = RootComponent::new(
|
||||||
|
Frame::new(
|
||||||
|
description,
|
||||||
|
SimpleChoice::new(words, false)
|
||||||
|
.with_show_incomplete()
|
||||||
|
.with_return_index(),
|
||||||
|
)
|
||||||
|
.with_title_centered(),
|
||||||
|
);
|
||||||
|
Ok(layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_word_count(recovery_type: RecoveryType) -> Result<impl LayoutMaybeTrace, Error> {
|
||||||
|
let title: TString = TR::word_count__title.into();
|
||||||
|
let choices: Vec<TString<'static>, 5> = {
|
||||||
|
let nums: &[&str] = if matches!(recovery_type, RecoveryType::UnlockRepeatedBackup) {
|
||||||
|
&["20", "33"]
|
||||||
|
} else {
|
||||||
|
&["12", "18", "20", "24", "33"]
|
||||||
|
};
|
||||||
|
|
||||||
|
nums.iter().map(|&num| num.into()).collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
let layout = RootComponent::new(
|
||||||
|
Frame::new(title, SimpleChoice::new(choices, false)).with_title_centered(),
|
||||||
|
);
|
||||||
|
Ok(layout)
|
||||||
|
}
|
||||||
|
|
||||||
fn show_info(
|
fn show_info(
|
||||||
title: TString<'static>,
|
title: TString<'static>,
|
||||||
description: TString<'static>,
|
description: TString<'static>,
|
||||||
|
@ -1150,24 +1150,6 @@ extern "C" fn new_confirm_coinjoin(n_args: usize, args: *const Obj, kwargs: *mut
|
|||||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" fn new_select_word(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
|
||||||
let block = move |_args: &[Obj], kwargs: &Map| {
|
|
||||||
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
|
||||||
let description: TString = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
|
|
||||||
let words_iterable: Obj = kwargs.get(Qstr::MP_QSTR_words)?;
|
|
||||||
let words: [TString<'static>; 3] = util::iter_into_array(words_iterable)?;
|
|
||||||
|
|
||||||
let paragraphs = Paragraphs::new([Paragraph::new(&theme::TEXT_DEMIBOLD, description)]);
|
|
||||||
let obj = LayoutObj::new(Frame::left_aligned(
|
|
||||||
theme::label_title(),
|
|
||||||
title,
|
|
||||||
Dialog::new(paragraphs, Button::select_word(words)),
|
|
||||||
))?;
|
|
||||||
Ok(obj.into())
|
|
||||||
};
|
|
||||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" fn new_show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
extern "C" fn new_show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||||
let block = move |_args: &[Obj], kwargs: &Map| {
|
let block = move |_args: &[Obj], kwargs: &Map| {
|
||||||
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
||||||
@ -1319,36 +1301,6 @@ extern "C" fn new_confirm_recovery(n_args: usize, args: *const Obj, kwargs: *mut
|
|||||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" fn new_select_word_count(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
|
||||||
let block = move |_args: &[Obj], kwargs: &Map| {
|
|
||||||
let recovery_type: RecoveryType = kwargs.get(Qstr::MP_QSTR_recovery_type)?.try_into()?;
|
|
||||||
let title: TString = match recovery_type {
|
|
||||||
RecoveryType::DryRun => TR::recovery__title_dry_run.into(),
|
|
||||||
RecoveryType::UnlockRepeatedBackup => TR::recovery__title_dry_run.into(),
|
|
||||||
_ => TR::recovery__title.into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let paragraphs = Paragraphs::new(Paragraph::new(
|
|
||||||
&theme::TEXT_DEMIBOLD,
|
|
||||||
TR::recovery__num_of_words,
|
|
||||||
));
|
|
||||||
|
|
||||||
let content = if matches!(recovery_type, RecoveryType::UnlockRepeatedBackup) {
|
|
||||||
SelectWordCount::new_multishare()
|
|
||||||
} else {
|
|
||||||
SelectWordCount::new_all()
|
|
||||||
};
|
|
||||||
|
|
||||||
let obj = LayoutObj::new(Frame::left_aligned(
|
|
||||||
theme::label_title(),
|
|
||||||
title,
|
|
||||||
Dialog::new(paragraphs, content),
|
|
||||||
))?;
|
|
||||||
Ok(obj.into())
|
|
||||||
};
|
|
||||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" fn new_show_group_share_success(
|
extern "C" fn new_show_group_share_success(
|
||||||
n_args: usize,
|
n_args: usize,
|
||||||
args: *const Obj,
|
args: *const Obj,
|
||||||
@ -1755,16 +1707,6 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
|||||||
/// """Confirm coinjoin authorization."""
|
/// """Confirm coinjoin authorization."""
|
||||||
Qstr::MP_QSTR_confirm_coinjoin => obj_fn_kw!(0, new_confirm_coinjoin).as_obj(),
|
Qstr::MP_QSTR_confirm_coinjoin => obj_fn_kw!(0, new_confirm_coinjoin).as_obj(),
|
||||||
|
|
||||||
/// def select_word(
|
|
||||||
/// *,
|
|
||||||
/// title: str,
|
|
||||||
/// description: str,
|
|
||||||
/// words: Iterable[str],
|
|
||||||
/// ) -> LayoutObj[int]:
|
|
||||||
/// """Select mnemonic word from three possibilities - seed check after backup. The
|
|
||||||
/// iterable must be of exact size. Returns index in range `0..3`."""
|
|
||||||
Qstr::MP_QSTR_select_word => obj_fn_kw!(0, new_select_word).as_obj(),
|
|
||||||
|
|
||||||
/// def show_share_words(
|
/// def show_share_words(
|
||||||
/// *,
|
/// *,
|
||||||
/// title: str,
|
/// title: str,
|
||||||
@ -1814,14 +1756,6 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
|||||||
/// """Device recovery homescreen."""
|
/// """Device recovery homescreen."""
|
||||||
Qstr::MP_QSTR_confirm_recovery => obj_fn_kw!(0, new_confirm_recovery).as_obj(),
|
Qstr::MP_QSTR_confirm_recovery => obj_fn_kw!(0, new_confirm_recovery).as_obj(),
|
||||||
|
|
||||||
/// def select_word_count(
|
|
||||||
/// *,
|
|
||||||
/// recovery_type: RecoveryType,
|
|
||||||
/// ) -> LayoutObj[int | str]: # TT returns int
|
|
||||||
/// """Select a mnemonic word count from the options: 12, 18, 20, 24, or 33.
|
|
||||||
/// For unlocking a repeated backup, select from 20 or 33."""
|
|
||||||
Qstr::MP_QSTR_select_word_count => obj_fn_kw!(0, new_select_word_count).as_obj(),
|
|
||||||
|
|
||||||
/// def show_group_share_success(
|
/// def show_group_share_success(
|
||||||
/// *,
|
/// *,
|
||||||
/// lines: Iterable[str]
|
/// lines: Iterable[str]
|
||||||
|
@ -7,18 +7,22 @@ use crate::{
|
|||||||
ui::{
|
ui::{
|
||||||
component::{
|
component::{
|
||||||
image::BlendedImage,
|
image::BlendedImage,
|
||||||
text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, VecExt},
|
text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs, VecExt},
|
||||||
ComponentExt, Empty, Timeout,
|
ComponentExt, Empty, Timeout,
|
||||||
},
|
},
|
||||||
layout::obj::{LayoutMaybeTrace, LayoutObj, RootComponent},
|
layout::{
|
||||||
|
obj::{LayoutMaybeTrace, LayoutObj, RootComponent},
|
||||||
|
util::RecoveryType,
|
||||||
|
},
|
||||||
ui_features_fw::UIFeaturesFirmware,
|
ui_features_fw::UIFeaturesFirmware,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
component::{
|
component::{
|
||||||
Bip39Input, Button, ButtonMsg, ButtonPage, ButtonStyleSheet, CancelConfirmMsg, Frame,
|
Bip39Input, Button, ButtonMsg, ButtonPage, ButtonStyleSheet, CancelConfirmMsg, Dialog,
|
||||||
IconDialog, MnemonicKeyboard, PassphraseKeyboard, PinKeyboard, Slip39Input,
|
Frame, IconDialog, MnemonicKeyboard, PassphraseKeyboard, PinKeyboard, SelectWordCount,
|
||||||
|
Slip39Input,
|
||||||
},
|
},
|
||||||
theme, ModelTTFeatures,
|
theme, ModelTTFeatures,
|
||||||
};
|
};
|
||||||
@ -119,6 +123,46 @@ impl UIFeaturesFirmware for ModelTTFeatures {
|
|||||||
Ok(layout)
|
Ok(layout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn select_word(
|
||||||
|
title: TString<'static>,
|
||||||
|
description: TString<'static>,
|
||||||
|
words: [TString<'static>; 3],
|
||||||
|
) -> Result<impl LayoutMaybeTrace, Error> {
|
||||||
|
let paragraphs = Paragraphs::new([Paragraph::new(&theme::TEXT_DEMIBOLD, description)]);
|
||||||
|
let layout = RootComponent::new(Frame::left_aligned(
|
||||||
|
theme::label_title(),
|
||||||
|
title,
|
||||||
|
Dialog::new(paragraphs, Button::select_word(words)),
|
||||||
|
));
|
||||||
|
Ok(layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_word_count(recovery_type: RecoveryType) -> Result<impl LayoutMaybeTrace, Error> {
|
||||||
|
let title: TString = match recovery_type {
|
||||||
|
RecoveryType::DryRun => TR::recovery__title_dry_run.into(),
|
||||||
|
RecoveryType::UnlockRepeatedBackup => TR::recovery__title_dry_run.into(),
|
||||||
|
_ => TR::recovery__title.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let paragraphs = Paragraphs::new(Paragraph::new(
|
||||||
|
&theme::TEXT_DEMIBOLD,
|
||||||
|
TR::recovery__num_of_words,
|
||||||
|
));
|
||||||
|
|
||||||
|
let content = if matches!(recovery_type, RecoveryType::UnlockRepeatedBackup) {
|
||||||
|
SelectWordCount::new_multishare()
|
||||||
|
} else {
|
||||||
|
SelectWordCount::new_all()
|
||||||
|
};
|
||||||
|
|
||||||
|
let layout = RootComponent::new(Frame::left_aligned(
|
||||||
|
theme::label_title(),
|
||||||
|
title,
|
||||||
|
Dialog::new(paragraphs, content),
|
||||||
|
));
|
||||||
|
Ok(layout)
|
||||||
|
}
|
||||||
|
|
||||||
fn show_info(
|
fn show_info(
|
||||||
title: TString<'static>,
|
title: TString<'static>,
|
||||||
description: TString<'static>,
|
description: TString<'static>,
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
use crate::{error::Error, io::BinaryData, micropython::gc::Gc, strutil::TString};
|
use crate::{error::Error, io::BinaryData, micropython::gc::Gc, strutil::TString};
|
||||||
|
|
||||||
use super::layout::obj::{LayoutMaybeTrace, LayoutObj};
|
use super::layout::{
|
||||||
|
obj::{LayoutMaybeTrace, LayoutObj},
|
||||||
|
util::RecoveryType,
|
||||||
|
};
|
||||||
|
|
||||||
pub trait UIFeaturesFirmware {
|
pub trait UIFeaturesFirmware {
|
||||||
fn confirm_action(
|
fn confirm_action(
|
||||||
@ -44,6 +46,14 @@ pub trait UIFeaturesFirmware {
|
|||||||
max_len: u32,
|
max_len: u32,
|
||||||
) -> Result<impl LayoutMaybeTrace, Error>;
|
) -> Result<impl LayoutMaybeTrace, Error>;
|
||||||
|
|
||||||
|
fn select_word(
|
||||||
|
title: TString<'static>,
|
||||||
|
description: TString<'static>,
|
||||||
|
words: [TString<'static>; 3],
|
||||||
|
) -> Result<impl LayoutMaybeTrace, Error>;
|
||||||
|
|
||||||
|
fn select_word_count(recovery_type: RecoveryType) -> Result<impl LayoutMaybeTrace, Error>;
|
||||||
|
|
||||||
fn show_info(
|
fn show_info(
|
||||||
title: TString<'static>,
|
title: TString<'static>,
|
||||||
description: TString<'static>,
|
description: TString<'static>,
|
||||||
|
@ -242,17 +242,6 @@ def confirm_coinjoin(
|
|||||||
"""Confirm coinjoin authorization."""
|
"""Confirm coinjoin authorization."""
|
||||||
|
|
||||||
|
|
||||||
# rust/src/ui/model_mercury/layout.rs
|
|
||||||
def select_word(
|
|
||||||
*,
|
|
||||||
title: str,
|
|
||||||
description: str,
|
|
||||||
words: Iterable[str],
|
|
||||||
) -> LayoutObj[int]:
|
|
||||||
"""Select mnemonic word from three possibilities - seed check after backup. The
|
|
||||||
iterable must be of exact size. Returns index in range `0..3`."""
|
|
||||||
|
|
||||||
|
|
||||||
# rust/src/ui/model_mercury/layout.rs
|
# rust/src/ui/model_mercury/layout.rs
|
||||||
def flow_prompt_backup() -> LayoutObj[UiResult]:
|
def flow_prompt_backup() -> LayoutObj[UiResult]:
|
||||||
"""Prompt a user to create backup with an option to skip."""
|
"""Prompt a user to create backup with an option to skip."""
|
||||||
@ -320,15 +309,6 @@ def flow_continue_recovery(
|
|||||||
"""Device recovery homescreen."""
|
"""Device recovery homescreen."""
|
||||||
|
|
||||||
|
|
||||||
# rust/src/ui/model_mercury/layout.rs
|
|
||||||
def select_word_count(
|
|
||||||
*,
|
|
||||||
recovery_type: RecoveryType,
|
|
||||||
) -> LayoutObj[int | str]: # merucry returns int
|
|
||||||
"""Select a mnemonic word count from the options: 12, 18, 20, 24, or 33.
|
|
||||||
For unlocking a repeated backup, select from 20 or 33."""
|
|
||||||
|
|
||||||
|
|
||||||
# rust/src/ui/model_mercury/layout.rs
|
# rust/src/ui/model_mercury/layout.rs
|
||||||
def show_group_share_success(
|
def show_group_share_success(
|
||||||
*,
|
*,
|
||||||
@ -728,17 +708,6 @@ def confirm_coinjoin(
|
|||||||
"""Confirm coinjoin authorization."""
|
"""Confirm coinjoin authorization."""
|
||||||
|
|
||||||
|
|
||||||
# rust/src/ui/model_tr/layout.rs
|
|
||||||
def select_word(
|
|
||||||
*,
|
|
||||||
title: str, # unused on TR
|
|
||||||
description: str,
|
|
||||||
words: Iterable[str],
|
|
||||||
) -> LayoutObj[int]:
|
|
||||||
"""Select mnemonic word from three possibilities - seed check after backup. The
|
|
||||||
iterable must be of exact size. Returns index in range `0..3`."""
|
|
||||||
|
|
||||||
|
|
||||||
# rust/src/ui/model_tr/layout.rs
|
# rust/src/ui/model_tr/layout.rs
|
||||||
def show_share_words(
|
def show_share_words(
|
||||||
*,
|
*,
|
||||||
@ -784,15 +753,6 @@ def confirm_recovery(
|
|||||||
"""Device recovery homescreen."""
|
"""Device recovery homescreen."""
|
||||||
|
|
||||||
|
|
||||||
# rust/src/ui/model_tr/layout.rs
|
|
||||||
def select_word_count(
|
|
||||||
*,
|
|
||||||
recovery_type: RecoveryType,
|
|
||||||
) -> LayoutObj[int | str]: # TR returns str
|
|
||||||
"""Select a mnemonic word count from the options: 12, 18, 20, 24, or 33.
|
|
||||||
For unlocking a repeated backup, select from 20 or 33."""
|
|
||||||
|
|
||||||
|
|
||||||
# rust/src/ui/model_tr/layout.rs
|
# rust/src/ui/model_tr/layout.rs
|
||||||
def show_group_share_success(
|
def show_group_share_success(
|
||||||
*,
|
*,
|
||||||
@ -1113,17 +1073,6 @@ def confirm_coinjoin(
|
|||||||
"""Confirm coinjoin authorization."""
|
"""Confirm coinjoin authorization."""
|
||||||
|
|
||||||
|
|
||||||
# rust/src/ui/model_tt/layout.rs
|
|
||||||
def select_word(
|
|
||||||
*,
|
|
||||||
title: str,
|
|
||||||
description: str,
|
|
||||||
words: Iterable[str],
|
|
||||||
) -> LayoutObj[int]:
|
|
||||||
"""Select mnemonic word from three possibilities - seed check after backup. The
|
|
||||||
iterable must be of exact size. Returns index in range `0..3`."""
|
|
||||||
|
|
||||||
|
|
||||||
# rust/src/ui/model_tt/layout.rs
|
# rust/src/ui/model_tt/layout.rs
|
||||||
def show_share_words(
|
def show_share_words(
|
||||||
*,
|
*,
|
||||||
@ -1178,15 +1127,6 @@ def confirm_recovery(
|
|||||||
"""Device recovery homescreen."""
|
"""Device recovery homescreen."""
|
||||||
|
|
||||||
|
|
||||||
# rust/src/ui/model_tt/layout.rs
|
|
||||||
def select_word_count(
|
|
||||||
*,
|
|
||||||
recovery_type: RecoveryType,
|
|
||||||
) -> LayoutObj[int | str]: # TT returns int
|
|
||||||
"""Select a mnemonic word count from the options: 12, 18, 20, 24, or 33.
|
|
||||||
For unlocking a repeated backup, select from 20 or 33."""
|
|
||||||
|
|
||||||
|
|
||||||
# rust/src/ui/model_tt/layout.rs
|
# rust/src/ui/model_tt/layout.rs
|
||||||
def show_group_share_success(
|
def show_group_share_success(
|
||||||
*,
|
*,
|
||||||
|
@ -136,6 +136,26 @@ def request_passphrase(
|
|||||||
"""Passphrase input keyboard."""
|
"""Passphrase input keyboard."""
|
||||||
|
|
||||||
|
|
||||||
|
# rust/src/ui/api/firmware_upy.rs
|
||||||
|
def select_word(
|
||||||
|
*,
|
||||||
|
title: str,
|
||||||
|
description: str,
|
||||||
|
words: Iterable[str],
|
||||||
|
) -> LayoutObj[int]:
|
||||||
|
"""Select mnemonic word from three possibilities - seed check after backup. The
|
||||||
|
iterable must be of exact size. Returns index in range `0..3`."""
|
||||||
|
|
||||||
|
|
||||||
|
# rust/src/ui/api/firmware_upy.rs
|
||||||
|
def select_word_count(
|
||||||
|
*,
|
||||||
|
recovery_type: RecoveryType,
|
||||||
|
) -> LayoutObj[int | str]: # TR returns str
|
||||||
|
"""Select a mnemonic word count from the options: 12, 18, 20, 24, or 33.
|
||||||
|
For unlocking a repeated backup, select from 20 or 33."""
|
||||||
|
|
||||||
|
|
||||||
# rust/src/ui/api/firmware_upy.rs
|
# rust/src/ui/api/firmware_upy.rs
|
||||||
def show_info(
|
def show_info(
|
||||||
*,
|
*,
|
||||||
|
@ -17,9 +17,10 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
async def request_word_count(recovery_type: RecoveryType) -> int:
|
async def request_word_count(recovery_type: RecoveryType) -> int:
|
||||||
selector = trezorui2.select_word_count(recovery_type=recovery_type)
|
|
||||||
count = await interact(
|
count = await interact(
|
||||||
selector, "recovery_word_count", ButtonRequestType.MnemonicWordCount
|
trezorui_api.select_word_count(recovery_type=recovery_type),
|
||||||
|
"recovery_word_count",
|
||||||
|
ButtonRequestType.MnemonicWordCount,
|
||||||
)
|
)
|
||||||
return int(count)
|
return int(count)
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ async def select_word(
|
|||||||
words.append(words[-1])
|
words.append(words[-1])
|
||||||
|
|
||||||
result = await interact(
|
result = await interact(
|
||||||
trezorui2.select_word(
|
trezorui_api.select_word(
|
||||||
title=title,
|
title=title,
|
||||||
description=TR.reset__select_word_x_of_y_template.format(
|
description=TR.reset__select_word_x_of_y_template.format(
|
||||||
checked_index + 1, count
|
checked_index + 1, count
|
||||||
|
@ -16,7 +16,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
async def request_word_count(recovery_type: RecoveryType) -> int:
|
async def request_word_count(recovery_type: RecoveryType) -> int:
|
||||||
count = await interact(
|
count = await interact(
|
||||||
trezorui2.select_word_count(recovery_type=recovery_type),
|
trezorui_api.select_word_count(recovery_type=recovery_type),
|
||||||
"recovery_word_count",
|
"recovery_word_count",
|
||||||
ButtonRequestType.MnemonicWordCount,
|
ButtonRequestType.MnemonicWordCount,
|
||||||
)
|
)
|
||||||
|
@ -88,7 +88,7 @@ async def select_word(
|
|||||||
|
|
||||||
word_ordinal = format_ordinal(checked_index + 1)
|
word_ordinal = format_ordinal(checked_index + 1)
|
||||||
result = await interact(
|
result = await interact(
|
||||||
trezorui2.select_word(
|
trezorui_api.select_word(
|
||||||
title="",
|
title="",
|
||||||
description=TR.reset__select_word_template.format(word_ordinal),
|
description=TR.reset__select_word_template.format(word_ordinal),
|
||||||
words=(words[0].lower(), words[1].lower(), words[2].lower()),
|
words=(words[0].lower(), words[1].lower(), words[2].lower()),
|
||||||
|
@ -17,7 +17,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
async def request_word_count(recovery_type: RecoveryType) -> int:
|
async def request_word_count(recovery_type: RecoveryType) -> int:
|
||||||
count = await interact(
|
count = await interact(
|
||||||
trezorui2.select_word_count(recovery_type=recovery_type),
|
trezorui_api.select_word_count(recovery_type=recovery_type),
|
||||||
"recovery_word_count",
|
"recovery_word_count",
|
||||||
ButtonRequestType.MnemonicWordCount,
|
ButtonRequestType.MnemonicWordCount,
|
||||||
)
|
)
|
||||||
|
@ -84,7 +84,7 @@ async def select_word(
|
|||||||
words.append(words[-1])
|
words.append(words[-1])
|
||||||
|
|
||||||
result = await interact(
|
result = await interact(
|
||||||
trezorui2.select_word(
|
trezorui_api.select_word(
|
||||||
title=title,
|
title=title,
|
||||||
description=TR.reset__select_word_x_of_y_template.format(
|
description=TR.reset__select_word_x_of_y_template.format(
|
||||||
checked_index + 1, count
|
checked_index + 1, count
|
||||||
|
Loading…
Reference in New Issue
Block a user