From 7d90552d81f62ea9cb4233c85687dc3cb2a92c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ioan=20Biz=C4=83u?= Date: Mon, 15 Jul 2024 10:00:05 +0200 Subject: [PATCH] refactor(core/ui): new Mercury design for number input slider --- core/embed/rust/librust_qstr.h | 5 + .../generated/translated_string.rs | 19 ++- .../src/ui/model_mercury/component/mod.rs | 1 + .../component/number_input_slider.rs | 138 +++++++++++++----- .../ui/model_mercury/flow/set_brightness.rs | 75 +++++----- .../embed/rust/src/ui/model_mercury/layout.rs | 4 +- core/mocks/trezortranslate_keys.pyi | 7 +- core/translations/en.json | 7 +- core/translations/order.json | 7 +- core/translations/signatures.json | 6 +- 10 files changed, 185 insertions(+), 84 deletions(-) diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index b752c9917d..b38c6e4ceb 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -121,6 +121,8 @@ static void _librust_qstrs(void) { MP_QSTR_bootscreen; MP_QSTR_br_code; MP_QSTR_br_type; + MP_QSTR_brightness__change_title; + MP_QSTR_brightness__changed_title; MP_QSTR_brightness__title; MP_QSTR_button; MP_QSTR_button_event; @@ -295,6 +297,7 @@ static void _librust_qstrs(void) { MP_QSTR_instructions__learn_more; MP_QSTR_instructions__shares_continue_with_x_template; MP_QSTR_instructions__shares_start_with_1; + MP_QSTR_instructions__swipe_horizontally; MP_QSTR_instructions__swipe_up; MP_QSTR_instructions__tap_to_confirm; MP_QSTR_instructions__tap_to_start; @@ -618,6 +621,8 @@ static void _librust_qstrs(void) { MP_QSTR_send__transaction_signed; MP_QSTR_send__you_are_contributing; MP_QSTR_set_brightness; + MP_QSTR_setting__adjust; + MP_QSTR_setting__apply; MP_QSTR_share_words; MP_QSTR_share_words__words_in_order; MP_QSTR_share_words__wrote_down_all; diff --git a/core/embed/rust/src/translations/generated/translated_string.rs b/core/embed/rust/src/translations/generated/translated_string.rs index 49c9c54481..6f56926ecb 100644 --- a/core/embed/rust/src/translations/generated/translated_string.rs +++ b/core/embed/rust/src/translations/generated/translated_string.rs @@ -1330,7 +1330,7 @@ pub enum TranslatedString { words__settings = 929, // "Settings" words__try_again = 930, // "Try again." reset__slip39_checklist_num_groups_x_template = 931, // "Number of groups: {0}" - brightness__title = 932, // "Change display brightness" + brightness__title = 932, // "Display brightness" recovery__title_unlock_repeated_backup = 933, // "Multi-share backup" recovery__unlock_repeated_backup = 934, // "Create additional backup?" recovery__unlock_repeated_backup_verb = 935, // "Create backup" @@ -1349,6 +1349,11 @@ pub enum TranslatedString { tutorial__title_well_done = 948, // "Well done!" tutorial__lets_begin = 949, // "Learn how to use and navigate this device with ease." tutorial__get_started = 950, // "Get started!" + instructions__swipe_horizontally = 951, // "Swipe horizontally" + setting__adjust = 952, // "Adjust" + setting__apply = 953, // "Apply" + brightness__changed_title = 954, // "Display brightness changed" + brightness__change_title = 955, // "Change display brightness" } impl TranslatedString { @@ -2673,7 +2678,7 @@ impl TranslatedString { Self::words__settings => "Settings", Self::words__try_again => "Try again.", Self::reset__slip39_checklist_num_groups_x_template => "Number of groups: {0}", - Self::brightness__title => "Change display brightness", + Self::brightness__title => "Display brightness", Self::recovery__title_unlock_repeated_backup => "Multi-share backup", Self::recovery__unlock_repeated_backup => "Create additional backup?", Self::recovery__unlock_repeated_backup_verb => "Create backup", @@ -2692,6 +2697,11 @@ impl TranslatedString { Self::tutorial__title_well_done => "Well done!", Self::tutorial__lets_begin => "Learn how to use and navigate this device with ease.", Self::tutorial__get_started => "Get started!", + Self::instructions__swipe_horizontally => "Swipe horizontally", + Self::setting__adjust => "Adjust", + Self::setting__apply => "Apply", + Self::brightness__changed_title => "Display brightness changed", + Self::brightness__change_title => "Change display brightness", } } @@ -4036,6 +4046,11 @@ impl TranslatedString { Qstr::MP_QSTR_tutorial__title_well_done => Some(Self::tutorial__title_well_done), Qstr::MP_QSTR_tutorial__lets_begin => Some(Self::tutorial__lets_begin), Qstr::MP_QSTR_tutorial__get_started => Some(Self::tutorial__get_started), + Qstr::MP_QSTR_instructions__swipe_horizontally => Some(Self::instructions__swipe_horizontally), + Qstr::MP_QSTR_setting__adjust => Some(Self::setting__adjust), + Qstr::MP_QSTR_setting__apply => Some(Self::setting__apply), + Qstr::MP_QSTR_brightness__changed_title => Some(Self::brightness__changed_title), + Qstr::MP_QSTR_brightness__change_title => Some(Self::brightness__change_title), _ => None, } } diff --git a/core/embed/rust/src/ui/model_mercury/component/mod.rs b/core/embed/rust/src/ui/model_mercury/component/mod.rs index b51299c331..9a11533bfe 100644 --- a/core/embed/rust/src/ui/model_mercury/component/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/component/mod.rs @@ -20,6 +20,7 @@ mod keyboard; mod loader; #[cfg(feature = "translations")] mod number_input; +#[cfg(feature = "translations")] pub mod number_input_slider; mod progress; #[cfg(feature = "translations")] diff --git a/core/embed/rust/src/ui/model_mercury/component/number_input_slider.rs b/core/embed/rust/src/ui/model_mercury/component/number_input_slider.rs index 3f8b71999e..723a36c27e 100644 --- a/core/embed/rust/src/ui/model_mercury/component/number_input_slider.rs +++ b/core/embed/rust/src/ui/model_mercury/component/number_input_slider.rs @@ -1,16 +1,17 @@ use crate::{ - strutil::ShortString, + strutil::{ShortString, TString}, + translations::TR, ui::{ component::{base::ComponentExt, Child, Component, Event, EventCtx}, constant::screen, display, event::TouchEvent, - geometry::{Alignment, Insets, Point, Rect}, + geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect}, shape::{self, Renderer}, }, }; -use super::theme; +use super::{theme, Footer}; pub enum NumberInputSliderDialogMsg { Changed(u16), @@ -18,22 +19,25 @@ pub enum NumberInputSliderDialogMsg { pub struct NumberInputSliderDialog { area: Rect, - text_area: Rect, input: Child, + footer: Footer<'static>, min: u16, max: u16, val: u16, + init_val: u16, } impl NumberInputSliderDialog { pub fn new(min: u16, max: u16, init_value: u16) -> Self { Self { area: Rect::zero(), - text_area: Rect::zero(), input: NumberInputSlider::new(min, max, init_value).into_child(), + footer: Footer::new::>(TR::instructions__swipe_horizontally.into()) + .with_description::>(TR::setting__adjust.into()), min, max, val: init_value, + init_val: init_value, } } @@ -42,25 +46,50 @@ impl NumberInputSliderDialog { } } +const INPUT_AREA_HEIGHT: i16 = 91; + impl Component for NumberInputSliderDialog { type Msg = NumberInputSliderDialogMsg; fn place(&mut self, bounds: Rect) -> Rect { self.area = bounds; - let content_area = self.area.inset(Insets::top(2 * theme::BUTTON_SPACING)); - let (_, content_area) = content_area.split_top(30); - let (input_area, _) = content_area.split_top(15); - let (text_area, _) = content_area.split_bottom(theme::BUTTON_HEIGHT); - self.text_area = text_area; + let whole_area = self.area.inset(Insets::bottom(theme::SPACING)); + let (remaining, footer_area) = whole_area.split_bottom(self.footer.height()); + self.footer.place(footer_area); + let content_area = remaining; + + let used_area = content_area + .inset(Insets::sides(theme::SPACING)) + .inset(Insets::bottom(theme::SPACING)); + + let input_area = Rect::snap( + used_area.center(), + Offset::new(used_area.width(), INPUT_AREA_HEIGHT), + Alignment2D::CENTER, + ); self.input.place(input_area.inset(Insets::sides(20))); + bounds } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { if let Some(value) = self.input.event(ctx, event) { self.val = value; + + if self.val == self.init_val || self.input.inner().touching { + self.footer + .update_instruction(ctx, TR::instructions__swipe_horizontally); + self.footer.update_description(ctx, TR::setting__adjust); + ctx.request_paint(); + } else { + self.footer + .update_instruction(ctx, TR::instructions__swipe_up); + self.footer.update_description(ctx, TR::setting__apply); + ctx.request_paint(); + } + return Some(Self::Msg::Changed(value)); } None @@ -72,17 +101,7 @@ impl Component for NumberInputSliderDialog { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { self.input.render(target); - - let mut str = ShortString::new(); - let val_pct = (100 * (self.val - self.min)) / (self.max - self.min); - - unwrap!(ufmt::uwrite!(str, "{} %", val_pct)); - - shape::Text::new(self.text_area.center(), &str) - .with_font(theme::TEXT_NORMAL.text_font) - .with_fg(theme::TEXT_NORMAL.text_color) - .with_align(Alignment::Center) - .render(target); + self.footer.render(target); } } @@ -97,9 +116,11 @@ impl crate::trace::Trace for NumberInputSliderDialog { pub struct NumberInputSlider { area: Rect, touch_area: Rect, + text_area: Rect, min: u16, max: u16, value: u16, + touching: bool, } impl NumberInputSlider { @@ -108,20 +129,27 @@ impl NumberInputSlider { Self { area: Rect::zero(), touch_area: Rect::zero(), + text_area: Rect::zero(), min, max, value, + touching: false, } } - pub fn slider_eval(&mut self, pos: Point, ctx: &mut EventCtx) -> Option { - if self.touch_area.contains(pos) { + pub fn touch_eval( + &mut self, + pos: Point, + ctx: &mut EventCtx, + force_bubble_up: bool, + ) -> Option { + if self.touching { let filled = pos.x - self.area.x0; let filled = filled.clamp(0, self.area.width()); let val_pct = (filled as u16 * 100) / self.area.width() as u16; let val = (val_pct * (self.max - self.min)) / 100 + self.min; - if val != self.value { + if val != self.value || force_bubble_up { self.value = val; ctx.request_paint(); return Some(self.value); @@ -136,16 +164,26 @@ impl Component for NumberInputSlider { fn place(&mut self, bounds: Rect) -> Rect { self.area = bounds; - self.touch_area = bounds.outset(Insets::new(40, 20, 40, 20)).clamp(screen()); + self.touch_area = bounds.outset(Insets::new(0, 20, 0, 20)).clamp(screen()); bounds } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { if let Event::Touch(touch_event) = event { return match touch_event { - TouchEvent::TouchStart(pos) => self.slider_eval(pos, ctx), - TouchEvent::TouchMove(pos) => self.slider_eval(pos, ctx), - TouchEvent::TouchEnd(pos) => self.slider_eval(pos, ctx), + TouchEvent::TouchStart(pos) => { + if self.touch_area.contains(pos) { + self.touching = true; + ctx.request_paint(); + } + self.touch_eval(pos, ctx, true) + } + TouchEvent::TouchMove(pos) => self.touch_eval(pos, ctx, false), + TouchEvent::TouchEnd(pos) => { + self.touching = false; + ctx.request_paint(); + self.touch_eval(pos, ctx, true) + } TouchEvent::TouchAbort => None, }; } @@ -160,16 +198,24 @@ impl Component for NumberInputSlider { } fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + let (top_left_shape, top_right_shape, bot_left_shape, bot_right_shape) = + shape::CornerHighlight::from_rect( + self.area, + if self.touching { + theme::GREY_DARK + } else { + theme::WHITE + }, + theme::BG, + ); + top_left_shape.render(target); + top_right_shape.render(target); + bot_left_shape.render(target); + bot_right_shape.render(target); + let val_pct = (100 * (self.value - self.min)) / (self.max - self.min); - shape::Bar::new(self.area) - .with_radius(2) - .with_thickness(2) - .with_bg(theme::BG) - .with_fg(theme::FG) - .render(target); - - let inner = self.area.inset(Insets::uniform(1)); + let inner = self.area.inset(Insets::uniform(10)); let fill_to = (val_pct as i16 * inner.width()) / 100; @@ -177,8 +223,26 @@ impl Component for NumberInputSlider { shape::Bar::new(inner) .with_radius(1) - .with_bg(theme::FG) + .with_bg(if self.touching { + theme::WHITE + } else { + theme::GREY_EXTRA_DARK + }) .render(target); + + let mut str = ShortString::new(); + let val_pct = (100 * (self.value - self.min)) / (self.max - self.min); + + unwrap!(ufmt::uwrite!(str, "{} %", val_pct)); + + if !self.touching { + let text_height = theme::TEXT_BOLD.text_font.line_height(); + shape::Text::new(self.area.center() + Offset::new(0, text_height / 2), &str) + .with_font(theme::TEXT_BOLD.text_font) + .with_fg(theme::TEXT_BOLD.text_color) + .with_align(Alignment::Center) + .render(target); + } } } diff --git a/core/embed/rust/src/ui/model_mercury/flow/set_brightness.rs b/core/embed/rust/src/ui/model_mercury/flow/set_brightness.rs index f85714450b..48baf45dac 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/set_brightness.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/set_brightness.rs @@ -9,8 +9,8 @@ use crate::{ ui::{ component::{base::ComponentExt, swipe_detect::SwipeSettings, SwipeDirection}, flow::{ - base::{Decision, FlowMsg}, - flow_store, FlowState, FlowStore, SwipeFlow, + base::{DecisionBuilder as _, FlowMsg, StateChange}, + FlowState, SwipeFlow, }, layout::obj::LayoutObj, model_mercury::component::{ @@ -21,7 +21,7 @@ use crate::{ }; use super::super::{ - component::{Frame, FrameMsg, PromptScreen, VerticalMenu, VerticalMenuChoiceMsg}, + component::{Frame, FrameMsg, PromptScreen, StatusScreen, VerticalMenu, VerticalMenuChoiceMsg}, theme, }; @@ -30,41 +30,35 @@ pub enum SetBrightness { Slider, Menu, Confirm, + Confirmed, } impl FlowState for SetBrightness { - fn handle_swipe(&self, direction: SwipeDirection) -> Decision { + #[inline] + fn index(&'static self) -> usize { + *self as usize + } + + fn handle_swipe(&'static self, direction: SwipeDirection) -> StateChange { match (self, direction) { - (SetBrightness::Menu, SwipeDirection::Right) => { - Decision::Goto(SetBrightness::Slider, direction) - } - (SetBrightness::Slider, SwipeDirection::Up) => { - Decision::Goto(SetBrightness::Confirm, direction) - } - (SetBrightness::Confirm, SwipeDirection::Down) => { - Decision::Goto(SetBrightness::Slider, direction) - } - (SetBrightness::Confirm, SwipeDirection::Left) => { - Decision::Goto(SetBrightness::Menu, direction) - } - _ => Decision::Nothing, + (Self::Menu, SwipeDirection::Right) => Self::Slider.swipe(direction), + (Self::Slider, SwipeDirection::Up) => Self::Confirm.swipe(direction), + (Self::Confirm, SwipeDirection::Down) => Self::Slider.swipe(direction), + (Self::Confirm, SwipeDirection::Left) => Self::Menu.swipe(direction), + (Self::Confirmed, SwipeDirection::Up) => self.return_msg(FlowMsg::Confirmed), + _ => self.do_nothing(), } } - fn handle_event(&self, msg: FlowMsg) -> Decision { + fn handle_event(&'static self, msg: FlowMsg) -> StateChange { match (self, msg) { - (SetBrightness::Slider, FlowMsg::Info) => { - Decision::Goto(SetBrightness::Menu, SwipeDirection::Left) - } - (SetBrightness::Menu, FlowMsg::Cancelled) => { - Decision::Goto(SetBrightness::Slider, SwipeDirection::Right) - } - (SetBrightness::Menu, FlowMsg::Choice(0)) => Decision::Return(FlowMsg::Cancelled), - (SetBrightness::Confirm, FlowMsg::Confirmed) => Decision::Return(FlowMsg::Confirmed), - (SetBrightness::Confirm, FlowMsg::Info) => { - Decision::Goto(SetBrightness::Menu, SwipeDirection::Left) - } - _ => Decision::Nothing, + (Self::Slider, FlowMsg::Info) => Self::Menu.swipe_left(), + (Self::Menu, FlowMsg::Cancelled) => Self::Slider.swipe_right(), + (Self::Menu, FlowMsg::Choice(0)) => self.return_msg(FlowMsg::Cancelled), + (Self::Confirm, FlowMsg::Confirmed) => Self::Confirmed.swipe_up(), + (Self::Confirm, FlowMsg::Info) => Self::Menu.swipe_left(), + (Self::Confirmed, FlowMsg::Confirmed) => self.return_msg(FlowMsg::Confirmed), + _ => self.do_nothing(), } } } @@ -87,8 +81,8 @@ impl SetBrightness { current.unwrap_or(theme::backlight::get_backlight_normal()), ), ) + .with_subtitle(TR::homescreen__settings_subtitle.into()) .with_menu_button() - .with_footer(TR::instructions__swipe_up.into(), None) .with_swipe(SwipeDirection::Up, SwipeSettings::default()) .map(|msg| match msg { FrameMsg::Content(NumberInputSliderDialogMsg::Changed(n)) => { @@ -111,7 +105,7 @@ impl SetBrightness { }); let content_confirm = Frame::left_aligned( - TR::brightness__title.into(), + TR::brightness__change_title.into(), SwipeContent::new(PromptScreen::new_tap_to_confirm()), ) .with_footer(TR::instructions__tap_to_confirm.into(), None) @@ -126,12 +120,19 @@ impl SetBrightness { FrameMsg::Button(_) => Some(FlowMsg::Info), }); - let store = flow_store() - .add(content_slider)? - .add(content_menu)? - .add(content_confirm)?; + let content_confirmed = Frame::left_aligned( + TR::brightness__changed_title.into(), + SwipeContent::new(StatusScreen::new_success()).with_no_attach_anim(), + ) + .with_footer(TR::instructions__swipe_up.into(), None) + .with_swipe(SwipeDirection::Up, SwipeSettings::default()) + .map(move |_msg| Some(FlowMsg::Confirmed)); - let res = SwipeFlow::new(SetBrightness::Slider, store)?; + let res = SwipeFlow::new(&SetBrightness::Slider)? + .with_page(&SetBrightness::Slider, content_slider)? + .with_page(&SetBrightness::Menu, content_menu)? + .with_page(&SetBrightness::Confirm, content_confirm)? + .with_page(&SetBrightness::Confirmed, content_confirmed)?; Ok(LayoutObj::new(res)?.into()) } diff --git a/core/embed/rust/src/ui/model_mercury/layout.rs b/core/embed/rust/src/ui/model_mercury/layout.rs index 18e95b387a..7726e5fe6f 100644 --- a/core/embed/rust/src/ui/model_mercury/layout.rs +++ b/core/embed/rust/src/ui/model_mercury/layout.rs @@ -6,8 +6,8 @@ use super::{ CoinJoinProgress, FidoConfirm, FidoMsg, Frame, FrameMsg, Homescreen, HomescreenMsg, Lockscreen, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboard, PinKeyboardMsg, Progress, PromptScreen, - SelectWordCount, SelectWordCountMsg, SetBrightnessDialog, Slip39Input, StatusScreen, - SwipeUpScreen, SwipeUpScreenMsg, VerticalMenu, VerticalMenuChoiceMsg, + SelectWordCount, SelectWordCountMsg, Slip39Input, StatusScreen, SwipeUpScreen, + SwipeUpScreenMsg, VerticalMenu, VerticalMenuChoiceMsg, }, flow, theme, }; diff --git a/core/mocks/trezortranslate_keys.pyi b/core/mocks/trezortranslate_keys.pyi index 48ed2143a2..327a930862 100644 --- a/core/mocks/trezortranslate_keys.pyi +++ b/core/mocks/trezortranslate_keys.pyi @@ -79,7 +79,9 @@ class TR: bitcoin__unverified_external_inputs: str = "The transaction contains unverified external inputs." bitcoin__valid_signature: str = "The signature is valid." bitcoin__voting_rights: str = "Voting rights to:" - brightness__title: str = "Change display brightness" + brightness__change_title: str = "Change display brightness" + brightness__changed_title: str = "Display brightness changed" + brightness__title: str = "Display brightness" buttons__abort: str = "Abort" buttons__access: str = "Access" buttons__again: str = "Again" @@ -385,6 +387,7 @@ class TR: instructions__learn_more: str = "Learn more" instructions__shares_continue_with_x_template: str = "Continue with Share #{0}" instructions__shares_start_with_1: str = "Start with share #1" + instructions__swipe_horizontally: str = "Swipe horizontally" instructions__swipe_up: str = "Swipe up" instructions__tap_to_confirm: str = "Tap to confirm" instructions__tap_to_start: str = "Tap to start" @@ -742,6 +745,8 @@ class TR: send__transaction_id: str = "Transaction ID:" send__transaction_signed: str = "Transaction signed" send__you_are_contributing: str = "You are contributing:" + setting__adjust: str = "Adjust" + setting__apply: str = "Apply" share_words__words_in_order: str = " words in order." share_words__wrote_down_all: str = "I wrote down all " sign_message__bytes_template: str = "{0} Bytes" diff --git a/core/translations/en.json b/core/translations/en.json index 3bb11ce1e8..f9ddb412ef 100644 --- a/core/translations/en.json +++ b/core/translations/en.json @@ -81,7 +81,9 @@ "bitcoin__unverified_external_inputs": "The transaction contains unverified external inputs.", "bitcoin__valid_signature": "The signature is valid.", "bitcoin__voting_rights": "Voting rights to:", - "brightness__title": "Change display brightness", + "brightness__title": "Display brightness", + "brightness__change_title": "Change display brightness", + "brightness__changed_title": "Display brightness changed", "buttons__abort": "Abort", "buttons__access": "Access", "buttons__again": "Again", @@ -388,6 +390,7 @@ "instructions__shares_continue_with_x_template": "Continue with Share #{0}", "instructions__shares_start_with_1": "Start with share #1", "instructions__swipe_up": "Swipe up", + "instructions__swipe_horizontally": "Swipe horizontally", "instructions__tap_to_confirm": "Tap to confirm", "instructions__tap_to_start": "Tap to start", "joint__title": "Joint transaction", @@ -744,6 +747,8 @@ "send__transaction_id": "Transaction ID:", "send__transaction_signed": "Transaction signed", "send__you_are_contributing": "You are contributing:", + "setting__adjust": "Adjust", + "setting__apply": "Apply", "share_words__words_in_order": " words in order.", "share_words__wrote_down_all": "I wrote down all ", "sign_message__bytes_template": "{0} Bytes", diff --git a/core/translations/order.json b/core/translations/order.json index f3a3c15e15..ad52d92b62 100644 --- a/core/translations/order.json +++ b/core/translations/order.json @@ -949,5 +949,10 @@ "947": "tutorial__title_hold", "948": "tutorial__title_well_done", "949": "tutorial__lets_begin", - "950": "tutorial__get_started" + "950": "tutorial__get_started", + "951": "instructions__swipe_horizontally", + "952": "setting__adjust", + "953": "setting__apply", + "954": "brightness__changed_title", + "955": "brightness__change_title" } diff --git a/core/translations/signatures.json b/core/translations/signatures.json index 377f95e5d2..d21071bec2 100644 --- a/core/translations/signatures.json +++ b/core/translations/signatures.json @@ -1,8 +1,8 @@ { "current": { - "merkle_root": "f8fe7da5605cc8f5136d8d77b526df530daddbb6795e7c7fa1906822d3dd2a10", - "datetime": "2024-07-08T22:50:20.632650", - "commit": "c9c4bf63623d411384737662ec2aefc88824812d" + "merkle_root": "c94efabb9554eea2adb7f312469fa8b366503bbdb57dd7f92fa67b23dc63bb97", + "datetime": "2024-07-15T09:41:37.793874", + "commit": "5935708ac86d336364e5ab78fc5fd67b19402eaf" }, "history": [ {