mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-02-18 02:22:01 +00:00
chore(core): show the last passphrase character for a while
This commit is contained in:
parent
997c27adb3
commit
02b2bf99e3
1
core/.changelog.d/3959.added
Normal file
1
core/.changelog.d/3959.added
Normal file
@ -0,0 +1 @@
|
||||
Show last typed passphrase character for short period of time
|
@ -1,12 +1,14 @@
|
||||
use crate::{
|
||||
strutil::{ShortString, TString},
|
||||
time::Duration,
|
||||
translations::TR,
|
||||
ui::{
|
||||
component::{
|
||||
base::ComponentExt, swipe_detect::SwipeConfig, text::common::TextBox, Component, Event,
|
||||
EventCtx, Label, Maybe, Never, Swipe,
|
||||
EventCtx, Label, Maybe, Never, Swipe, Timer,
|
||||
},
|
||||
display,
|
||||
event::TouchEvent,
|
||||
geometry::{Alignment, Direction, Grid, Insets, Offset, Rect},
|
||||
model_mercury::{
|
||||
component::{
|
||||
@ -22,7 +24,7 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
use core::cell::Cell;
|
||||
use core::{cell::Cell, mem};
|
||||
use num_traits::ToPrimitive;
|
||||
|
||||
pub enum PassphraseKeyboardMsg {
|
||||
@ -30,6 +32,14 @@ pub enum PassphraseKeyboardMsg {
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
#[cfg_attr(feature = "ui_debug", derive(ufmt::derive::uDebug))]
|
||||
enum DisplayStyle {
|
||||
Dots,
|
||||
Chars,
|
||||
LastChar,
|
||||
}
|
||||
|
||||
/// Enum keeping track of which keyboard is shown and which comes next. Keep the
|
||||
/// number of values and the constant PAGE_COUNT in synch.
|
||||
#[repr(u32)]
|
||||
@ -105,6 +115,8 @@ const MAX_LENGTH: usize = 50;
|
||||
const CONFIRM_BTN_INSETS: Insets = Insets::new(5, 0, 5, 0);
|
||||
const CONFIRM_EMPTY_BTN_MARGIN_RIGHT: i16 = 7;
|
||||
const CONFIRM_EMPTY_BTN_INSETS: Insets = Insets::new(5, CONFIRM_EMPTY_BTN_MARGIN_RIGHT, 5, 0);
|
||||
const INPUT_INSETS: Insets = Insets::new(10, 2, 10, 4);
|
||||
const LAST_DIGIT_TIMEOUT_S: u32 = 1;
|
||||
|
||||
impl PassphraseKeyboard {
|
||||
pub fn new() -> Self {
|
||||
@ -272,7 +284,6 @@ impl Component for PassphraseKeyboard {
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
const CONFIRM_BTN_WIDTH: i16 = 78;
|
||||
const CONFIRM_EMPTY_BTN_WIDTH: i16 = 32;
|
||||
const INPUT_INSETS: Insets = Insets::new(10, 2, 10, 4);
|
||||
|
||||
let bounds = bounds.inset(theme::borders());
|
||||
let (top_area, keypad_area) =
|
||||
@ -284,7 +295,7 @@ impl Component for PassphraseKeyboard {
|
||||
.1;
|
||||
|
||||
let top_area = top_area.inset(INPUT_INSETS);
|
||||
let input_area = input_area.inset(INPUT_INSETS);
|
||||
// let input_area = input_area.inset(INPUT_INSETS);
|
||||
let confirm_btn_area = confirm_btn_area.inset(CONFIRM_BTN_INSETS);
|
||||
let confirm_empty_btn_area = confirm_empty_btn_area.inset(CONFIRM_EMPTY_BTN_INSETS);
|
||||
|
||||
@ -323,8 +334,14 @@ impl Component for PassphraseKeyboard {
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if self.input.multi_tap.timeout_event(event) {
|
||||
self.input.multi_tap.clear_pending_state(ctx);
|
||||
self.input
|
||||
.last_char_timer
|
||||
.start(ctx, Duration::from_secs(LAST_DIGIT_TIMEOUT_S));
|
||||
return None;
|
||||
}
|
||||
|
||||
self.input.event(ctx, event);
|
||||
|
||||
if let Some(swipe) = self.page_swipe.event(ctx, event) {
|
||||
// We have detected a horizontal swipe. Change the keyboard page.
|
||||
self.on_page_change(ctx, swipe);
|
||||
@ -383,6 +400,17 @@ impl Component for PassphraseKeyboard {
|
||||
let edit = text.map(|c| self.input.multi_tap.click_key(ctx, key, c));
|
||||
self.input.textbox.apply(ctx, edit);
|
||||
self.after_edit(ctx);
|
||||
if text.len() == 1 {
|
||||
// If the key has just one character, it is immediately applied and the last
|
||||
// digit timer should be started
|
||||
self.input
|
||||
.last_char_timer
|
||||
.start(ctx, Duration::from_secs(LAST_DIGIT_TIMEOUT_S));
|
||||
} else {
|
||||
// multi tap timer is runnig, the last digit timer should be stopped
|
||||
self.input.last_char_timer.stop();
|
||||
}
|
||||
self.input.display_style = DisplayStyle::LastChar;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
@ -416,14 +444,46 @@ struct Input {
|
||||
area: Rect,
|
||||
textbox: TextBox,
|
||||
multi_tap: MultiTapKeyboard,
|
||||
display_style: DisplayStyle,
|
||||
last_char_timer: Timer,
|
||||
}
|
||||
|
||||
impl Input {
|
||||
const TWITCH: i16 = 4;
|
||||
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
area: Rect::zero(),
|
||||
textbox: TextBox::empty(MAX_LENGTH),
|
||||
multi_tap: MultiTapKeyboard::new(),
|
||||
display_style: DisplayStyle::LastChar,
|
||||
last_char_timer: Timer::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_display_style(&self, passphrase: &ShortString) -> ShortString {
|
||||
if passphrase.len() == 0 {
|
||||
ShortString::new()
|
||||
} else {
|
||||
match self.display_style {
|
||||
DisplayStyle::Dots => {
|
||||
let mut dots = ShortString::new();
|
||||
for _ in 0..(passphrase.len()) {
|
||||
dots.push('*').unwrap();
|
||||
}
|
||||
dots
|
||||
}
|
||||
DisplayStyle::Chars => passphrase.clone(),
|
||||
DisplayStyle::LastChar => {
|
||||
let mut dots = ShortString::new();
|
||||
for _ in 0..(passphrase.len() - 1) {
|
||||
dots.push('*').unwrap();
|
||||
}
|
||||
// This should not fail because the passphrase is not empty.
|
||||
dots.push(passphrase.chars().last().unwrap()).unwrap();
|
||||
dots
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -436,26 +496,55 @@ impl Component for Input {
|
||||
self.area
|
||||
}
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
match event {
|
||||
Event::Timer(_) if self.last_char_timer.expire(event) => {
|
||||
self.display_style = DisplayStyle::Dots;
|
||||
ctx.request_paint();
|
||||
}
|
||||
Event::Touch(TouchEvent::TouchStart(pos)) if self.area.contains(pos) => {
|
||||
self.display_style = DisplayStyle::Chars;
|
||||
ctx.request_paint();
|
||||
}
|
||||
Event::Touch(TouchEvent::TouchEnd(_)) => {
|
||||
if mem::replace(&mut self.display_style, DisplayStyle::Dots) == DisplayStyle::Chars
|
||||
{
|
||||
ctx.request_paint();
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||
let style = theme::label_keyboard();
|
||||
let style = theme::label_keyboard_mono();
|
||||
let text_area = self.area.inset(INPUT_INSETS);
|
||||
|
||||
let text_baseline = self.area.top_left() + Offset::y(style.text_font.text_height())
|
||||
let mut text_baseline = text_area.top_left() + Offset::y(style.text_font.text_height())
|
||||
- Offset::y(style.text_font.text_baseline());
|
||||
|
||||
let text = self.textbox.content();
|
||||
|
||||
shape::Bar::new(self.area).with_bg(theme::BG).render(target);
|
||||
shape::Bar::new(text_area).with_bg(theme::BG).render(target);
|
||||
|
||||
// Find out how much text can fit into the textbox.
|
||||
// Accounting for the pending marker, which draws itself one pixel longer than
|
||||
// the last character
|
||||
let available_area_width = self.area.width() - 1;
|
||||
let text_to_display =
|
||||
long_line_content_with_ellipsis(text, "...", style.text_font, available_area_width);
|
||||
let available_area_width = text_area.width() - 1;
|
||||
let truncated =
|
||||
long_line_content_with_ellipsis(text, "", style.text_font, available_area_width);
|
||||
|
||||
// Jiggle hidden passphrase when overflowed.
|
||||
if text.len() > truncated.len()
|
||||
&& text.len() % 2 == 0
|
||||
&& (self.display_style == DisplayStyle::Dots
|
||||
|| self.display_style == DisplayStyle::LastChar)
|
||||
{
|
||||
text_baseline.x += Self::TWITCH;
|
||||
}
|
||||
|
||||
let text_to_display = self.apply_display_style(&truncated);
|
||||
|
||||
shape::Text::new(text_baseline, &text_to_display)
|
||||
.with_font(style.text_font)
|
||||
|
@ -136,6 +136,10 @@ pub const fn label_keyboard() -> TextStyle {
|
||||
TextStyle::new(Font::DEMIBOLD, GREY_EXTRA_LIGHT, BG, GREY_LIGHT, GREY_LIGHT)
|
||||
}
|
||||
|
||||
pub const fn label_keyboard_mono() -> TextStyle {
|
||||
TextStyle::new(Font::MONO, GREY_EXTRA_LIGHT, BG, GREY_LIGHT, GREY_LIGHT)
|
||||
}
|
||||
|
||||
pub const fn label_keyboard_prompt() -> TextStyle {
|
||||
TextStyle::new(Font::DEMIBOLD, GREY_LIGHT, BG, GREY_LIGHT, GREY_LIGHT)
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
use crate::{
|
||||
strutil::{ShortString, TString},
|
||||
time::Duration,
|
||||
translations::TR,
|
||||
trezorhal::random,
|
||||
ui::{
|
||||
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
|
||||
component::{
|
||||
text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx, Timer,
|
||||
},
|
||||
display::Icon,
|
||||
geometry::Rect,
|
||||
shape::Renderer,
|
||||
@ -43,6 +46,8 @@ const DIGITS_INDEX: usize = 5;
|
||||
const SPECIAL_INDEX: usize = 6;
|
||||
const SPACE_INDEX: usize = 7;
|
||||
|
||||
const LAST_DIGIT_TIMEOUT_S: u32 = 1;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MenuItem {
|
||||
text: TString<'static>,
|
||||
@ -273,6 +278,7 @@ pub struct PassphraseEntry {
|
||||
show_last_digit: bool,
|
||||
textbox: TextBox,
|
||||
current_category: ChoiceCategory,
|
||||
last_char_timer: Timer,
|
||||
}
|
||||
|
||||
impl PassphraseEntry {
|
||||
@ -286,6 +292,7 @@ impl PassphraseEntry {
|
||||
show_last_digit: false,
|
||||
textbox: TextBox::empty(MAX_PASSPHRASE_LENGTH),
|
||||
current_category: ChoiceCategory::Menu,
|
||||
last_char_timer: Timer::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -383,17 +390,22 @@ impl Component for PassphraseEntry {
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
// Any non-timer event when showing real passphrase should hide it
|
||||
// Same with showing last digit
|
||||
if !matches!(event, Event::Timer(_)) {
|
||||
if self.show_plain_passphrase {
|
||||
match event {
|
||||
// Timeout for showing the last digit.
|
||||
Event::Timer(_) if self.last_char_timer.expire(event) => {
|
||||
if self.show_last_digit {
|
||||
self.show_last_digit = false;
|
||||
self.update_passphrase_dots(ctx);
|
||||
}
|
||||
}
|
||||
// Other timers are ignored.
|
||||
Event::Timer(_) => {}
|
||||
// Any non-timer event when showing plain passphrase should hide it
|
||||
_ if self.show_plain_passphrase => {
|
||||
self.show_plain_passphrase = false;
|
||||
self.update_passphrase_dots(ctx);
|
||||
}
|
||||
if self.show_last_digit {
|
||||
self.show_last_digit = false;
|
||||
self.update_passphrase_dots(ctx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some((action, long_press)) = self.choice_page.event(ctx, event) {
|
||||
@ -437,6 +449,8 @@ impl Component for PassphraseEntry {
|
||||
PassphraseAction::Character(ch) if !self.is_full() => {
|
||||
self.append_char(ctx, ch);
|
||||
self.show_last_digit = true;
|
||||
self.last_char_timer
|
||||
.start(ctx, Duration::from_secs(LAST_DIGIT_TIMEOUT_S));
|
||||
self.update_passphrase_dots(ctx);
|
||||
self.randomize_category_position(ctx);
|
||||
ctx.request_paint();
|
||||
|
@ -1,10 +1,13 @@
|
||||
use crate::{
|
||||
strutil::TString,
|
||||
strutil::{ShortString, TString},
|
||||
time::Duration,
|
||||
ui::{
|
||||
component::{
|
||||
base::ComponentExt, text::common::TextBox, Child, Component, Event, EventCtx, Never,
|
||||
Timer,
|
||||
},
|
||||
display,
|
||||
event::TouchEvent,
|
||||
geometry::{Grid, Offset, Rect},
|
||||
model_tt::component::{
|
||||
button::{Button, ButtonContent, ButtonMsg},
|
||||
@ -12,13 +15,11 @@ use crate::{
|
||||
swipe::{Swipe, SwipeDirection},
|
||||
theme, ScrollBar,
|
||||
},
|
||||
shape,
|
||||
shape::Renderer,
|
||||
shape::{self, Renderer},
|
||||
util::long_line_content_with_ellipsis,
|
||||
},
|
||||
};
|
||||
|
||||
use core::cell::Cell;
|
||||
use core::{cell::Cell, mem};
|
||||
|
||||
#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))]
|
||||
pub enum PassphraseKeyboardMsg {
|
||||
@ -26,6 +27,14 @@ pub enum PassphraseKeyboardMsg {
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
#[cfg_attr(feature = "ui_debug", derive(ufmt::derive::uDebug))]
|
||||
enum DisplayStyle {
|
||||
Dots,
|
||||
Chars,
|
||||
LastChar,
|
||||
}
|
||||
|
||||
pub struct PassphraseKeyboard {
|
||||
page_swipe: Swipe,
|
||||
input: Child<Input>,
|
||||
@ -50,6 +59,8 @@ const KEYBOARD: [[&str; KEY_COUNT]; PAGE_COUNT] = [
|
||||
const MAX_LENGTH: usize = 50;
|
||||
const INPUT_AREA_HEIGHT: i16 = ScrollBar::DOT_SIZE + 9;
|
||||
|
||||
const LAST_DIGIT_TIMEOUT_S: u32 = 1;
|
||||
|
||||
impl PassphraseKeyboard {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@ -189,7 +200,7 @@ impl Component for PassphraseKeyboard {
|
||||
let (input_area, key_grid_area) =
|
||||
bounds.split_bottom(4 * theme::PIN_BUTTON_HEIGHT + 3 * theme::BUTTON_SPACING);
|
||||
|
||||
let (input_area, scroll_area) = input_area.split_bottom(INPUT_AREA_HEIGHT);
|
||||
let (_, scroll_area) = input_area.split_bottom(INPUT_AREA_HEIGHT);
|
||||
let (scroll_area, _) = scroll_area.split_top(ScrollBar::DOT_SIZE);
|
||||
|
||||
let key_grid = Grid::new(key_grid_area, 4, 3).with_spacing(theme::BUTTON_SPACING);
|
||||
@ -231,8 +242,14 @@ impl Component for PassphraseKeyboard {
|
||||
}
|
||||
});
|
||||
if multitap_timeout {
|
||||
// the character has been added, show it for a bit and then hide it
|
||||
self.input
|
||||
.mutate(ctx, |ctx, t| t.start_timer(ctx, LAST_DIGIT_TIMEOUT_S));
|
||||
return None;
|
||||
}
|
||||
|
||||
self.input.event(ctx, event);
|
||||
|
||||
if let Some(swipe) = self.page_swipe.event(ctx, event) {
|
||||
// We have detected a horizontal swipe. Change the keyboard page.
|
||||
self.on_page_swipe(ctx, swipe);
|
||||
@ -288,6 +305,17 @@ impl Component for PassphraseKeyboard {
|
||||
i.textbox.apply(ctx, edit);
|
||||
});
|
||||
self.after_edit(ctx);
|
||||
if text.len() == 1 {
|
||||
// If the key has just one character, it is immediately applied and the last
|
||||
// digit timer should be started
|
||||
self.input
|
||||
.mutate(ctx, |ctx, t| t.start_timer(ctx, LAST_DIGIT_TIMEOUT_S));
|
||||
} else {
|
||||
// multi tap timer is runnig, the last digit timer should be stopped
|
||||
self.input.mutate(ctx, |_ctx, t| t.stop_timer());
|
||||
}
|
||||
self.input
|
||||
.mutate(ctx, |_ctx, t| t.set_display_style(DisplayStyle::LastChar));
|
||||
return None;
|
||||
}
|
||||
}
|
||||
@ -313,14 +341,59 @@ struct Input {
|
||||
area: Rect,
|
||||
textbox: TextBox,
|
||||
multi_tap: MultiTapKeyboard,
|
||||
display_style: DisplayStyle,
|
||||
last_char_timer: Timer,
|
||||
}
|
||||
|
||||
impl Input {
|
||||
const TWITCH: i16 = 4;
|
||||
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
area: Rect::zero(),
|
||||
textbox: TextBox::empty(MAX_LENGTH),
|
||||
multi_tap: MultiTapKeyboard::new(),
|
||||
display_style: DisplayStyle::LastChar,
|
||||
last_char_timer: Timer::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_display_style(&mut self, display_style: DisplayStyle) {
|
||||
self.display_style = display_style;
|
||||
}
|
||||
|
||||
fn start_timer(&mut self, ctx: &mut EventCtx, timeout_ms: u32) {
|
||||
self.last_char_timer
|
||||
.start(ctx, Duration::from_secs(timeout_ms));
|
||||
}
|
||||
|
||||
fn stop_timer(&mut self) {
|
||||
self.last_char_timer.stop();
|
||||
}
|
||||
|
||||
fn apply_display_style(&self, passphrase: &ShortString) -> ShortString {
|
||||
if passphrase.len() == 0 {
|
||||
ShortString::new()
|
||||
} else {
|
||||
match self.display_style {
|
||||
DisplayStyle::Dots => {
|
||||
let mut dots = ShortString::new();
|
||||
for _ in 0..(passphrase.len()) {
|
||||
dots.push('*').unwrap();
|
||||
}
|
||||
dots
|
||||
}
|
||||
DisplayStyle::Chars => passphrase.clone(),
|
||||
DisplayStyle::LastChar => {
|
||||
let mut dots = ShortString::new();
|
||||
for _ in 0..(passphrase.len() - 1) {
|
||||
dots.push('*').unwrap();
|
||||
}
|
||||
// This should not fail because the passphrase is not empty.
|
||||
dots.push(passphrase.chars().last().unwrap()).unwrap();
|
||||
dots
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -333,14 +406,31 @@ impl Component for Input {
|
||||
self.area
|
||||
}
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
match event {
|
||||
Event::Timer(_) if self.last_char_timer.expire(event) => {
|
||||
self.display_style = DisplayStyle::Dots;
|
||||
ctx.request_paint();
|
||||
}
|
||||
Event::Touch(TouchEvent::TouchStart(pos)) if self.area.contains(pos) => {
|
||||
self.display_style = DisplayStyle::Chars;
|
||||
ctx.request_paint();
|
||||
}
|
||||
Event::Touch(TouchEvent::TouchEnd(_)) => {
|
||||
if mem::replace(&mut self.display_style, DisplayStyle::Dots) == DisplayStyle::Chars
|
||||
{
|
||||
ctx.request_paint();
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||
let style = theme::label_keyboard();
|
||||
let style = theme::label_keyboard_mono();
|
||||
|
||||
let text_baseline = self.area.top_left() + Offset::y(style.text_font.text_height())
|
||||
let mut text_baseline = self.area.top_left() + Offset::y(style.text_font.text_height())
|
||||
- Offset::y(style.text_font.text_baseline());
|
||||
|
||||
let text = self.textbox.content();
|
||||
@ -351,8 +441,19 @@ impl Component for Input {
|
||||
// Accounting for the pending marker, which draws itself one pixel longer than
|
||||
// the last character
|
||||
let available_area_width = self.area.width() - 1;
|
||||
let text_to_display =
|
||||
long_line_content_with_ellipsis(text, "...", style.text_font, available_area_width);
|
||||
let truncated =
|
||||
long_line_content_with_ellipsis(text, "", style.text_font, available_area_width);
|
||||
|
||||
// Jiggle hidden passphrase when overflowed.
|
||||
if text.len() > truncated.len()
|
||||
&& text.len() % 2 == 0
|
||||
&& (self.display_style == DisplayStyle::Dots
|
||||
|| self.display_style == DisplayStyle::LastChar)
|
||||
{
|
||||
text_baseline.x += Self::TWITCH;
|
||||
}
|
||||
|
||||
let text_to_display = self.apply_display_style(&truncated);
|
||||
|
||||
shape::Text::new(text_baseline, &text_to_display)
|
||||
.with_font(style.text_font)
|
||||
|
@ -120,6 +120,10 @@ pub const fn label_keyboard() -> TextStyle {
|
||||
TextStyle::new(Font::DEMIBOLD, OFF_WHITE, BG, GREY_LIGHT, GREY_LIGHT)
|
||||
}
|
||||
|
||||
pub const fn label_keyboard_mono() -> TextStyle {
|
||||
TextStyle::new(Font::MONO, OFF_WHITE, BG, GREY_LIGHT, GREY_LIGHT)
|
||||
}
|
||||
|
||||
pub const fn label_keyboard_prompt() -> TextStyle {
|
||||
TextStyle::new(Font::DEMIBOLD, GREY_LIGHT, BG, GREY_LIGHT, GREY_LIGHT)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user