From 497b967fd0deb3bf132d82cac7bc4ed74444a20e Mon Sep 17 00:00:00 2001 From: ardocrat Date: Mon, 23 Mar 2026 04:46:29 +0300 Subject: [PATCH] node: scan and share connection with qr code --- src/gui/views/network/connections.rs | 73 ++++++-- src/gui/views/network/metrics.rs | 2 +- src/gui/views/network/modals/ext_conn.rs | 170 +++++++++++++----- src/gui/views/network/modals/mod.rs | 5 +- src/gui/views/network/modals/share_conn.rs | 57 ++++++ src/gui/views/network/node.rs | 4 +- src/gui/views/network/setup/node.rs | 2 +- src/gui/views/network/setup/p2p.rs | 3 +- src/gui/views/network/types.rs | 10 ++ src/gui/views/qr.rs | 73 ++++++-- src/gui/views/views.rs | 2 +- src/gui/views/wallets/creation/content.rs | 14 +- src/gui/views/wallets/wallet/message.rs | 7 +- .../wallets/wallet/settings/connection.rs | 1 + src/wallet/connections/config.rs | 2 +- src/wallet/connections/external.rs | 10 +- 16 files changed, 336 insertions(+), 99 deletions(-) create mode 100644 src/gui/views/network/modals/share_conn.rs diff --git a/src/gui/views/network/connections.rs b/src/gui/views/network/connections.rs index 7d59c7be..03c3df09 100644 --- a/src/gui/views/network/connections.rs +++ b/src/gui/views/network/connections.rs @@ -15,9 +15,10 @@ use eframe::epaint::RectShape; use egui::{Align, Color32, CornerRadius, CursorIcon, Layout, RichText, Sense, StrokeKind, UiBuilder}; -use crate::gui::icons::{CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PLUS_CIRCLE, POWER, TRASH, WARNING_CIRCLE, X_CIRCLE}; +use crate::gui::icons::{CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PLUS_CIRCLE, POWER, QR_CODE, TRASH, WARNING_CIRCLE, X_CIRCLE}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::network::modals::ExternalConnectionModal; +use crate::gui::views::network::modals::{ExternalConnectionModal, ShareConnectionContent}; +use crate::gui::views::network::types::ShareConnection; use crate::gui::views::network::NodeSetup; use crate::gui::views::types::{ContentContainer, ModalPosition}; use crate::gui::views::{Modal, View}; @@ -30,23 +31,32 @@ use crate::AppConfig; pub struct ConnectionsContent { /// Flag to check connections state on first draw. first_draw: bool, + /// External connection [`Modal`] content. - ext_conn_modal: ExternalConnectionModal, + ext_conn_modal_content: ExternalConnectionModal, + + /// [`Modal`] content to share connection with QR code. + share_conn_modal_content: Option } impl Default for ConnectionsContent { fn default() -> Self { Self { first_draw: true, - ext_conn_modal: ExternalConnectionModal::new(None), + ext_conn_modal_content: ExternalConnectionModal::new(None), + share_conn_modal_content: None } } } +/// Identifier for [`Modal`] to share connection. +const SHARE_CONN_QR_MODAL: &'static str = "share_conn_qr_modal"; + impl ContentContainer for ConnectionsContent { fn modal_ids(&self) -> Vec<&'static str> { vec![ - ExternalConnectionModal::NETWORK_ID + ExternalConnectionModal::NETWORK_ID, + SHARE_CONN_QR_MODAL ] } @@ -56,8 +66,13 @@ impl ContentContainer for ConnectionsContent { cb: &dyn PlatformCallbacks) { match modal.id { ExternalConnectionModal::NETWORK_ID => { - self.ext_conn_modal.ui(ui, cb, modal, |_| {}); + self.ext_conn_modal_content.ui(ui, cb, modal, |_| {}); }, + SHARE_CONN_QR_MODAL => { + if let Some(c) = self.share_conn_modal_content.as_mut() { + c.ui(ui, modal, cb); + } + } _ => {} } } @@ -84,7 +99,24 @@ impl ContentContainer for ConnectionsContent { // Show integrated node info content. Self::integrated_node_item_ui(ui, Colors::fill_lite(), (true, || { AppConfig::toggle_show_connections_network_panel(); - }), |_| false); + }), |ui| { + let r = View::item_rounding(0, 1, true); + View::item_button(ui, r, QR_CODE, None, || { + if let Ok(c) = ShareConnectionContent::new(ShareConnection { + url: format!("http://{}", NodeConfig::get_api_address()), + username: "grin".to_string(), + secret: NodeConfig::get_api_secret(true).unwrap_or("".to_string()), + }) { + self.share_conn_modal_content = Some(c); + // Show QR code to share integrated node connection. + Modal::new(SHARE_CONN_QR_MODAL) + .position(ModalPosition::Center) + .title(t!("network.node")) + .show(); + } + }); + true + }); // Show external connections. ui.add_space(8.0); @@ -105,16 +137,37 @@ impl ContentContainer for ConnectionsContent { ui.add_space(8.0); for (i, c) in ext_conn_list.iter().enumerate() { ui.horizontal_wrapped(|ui| { + let mut show_qr_content: Option = None; // Draw external connection list item. let bg = Colors::fill_lite(); Self::ext_conn_item_ui(ui, bg, c, i, len, (true, || { self.show_add_ext_conn_modal(Some(c.clone())); }), |ui| { - let button_rounding = View::item_rounding(i, len, true); - View::item_button(ui, button_rounding, TRASH, None, || { + // Draw button to delete connection. + let r = View::item_rounding(i, len, true); + View::item_button(ui, r, TRASH, Some(Colors::inactive_text()), || { ConnectionsConfig::remove_ext_conn(c.id); }); + // Draw button to share connection + let r = CornerRadius::default(); + View::item_button(ui, r, QR_CODE, None, || { + if let Ok(c) = ShareConnectionContent::new(ShareConnection { + url: c.url.clone(), + username: "grin".to_string(), + secret: c.secret.clone().unwrap_or("".to_string()), + }) { + show_qr_content = Some(c); + } + }); }); + if let Some(c) = show_qr_content { + self.share_conn_modal_content = Some(c); + // Show QR code to share external connection. + Modal::new(SHARE_CONN_QR_MODAL) + .position(ModalPosition::Center) + .title(t!("wallets.ext_conn").replace(":", "")) + .show(); + } }); } } @@ -277,7 +330,7 @@ impl ConnectionsContent { /// Show [`Modal`] to add external connection. pub fn show_add_ext_conn_modal(&mut self, conn: Option) { - self.ext_conn_modal = ExternalConnectionModal::new(conn); + self.ext_conn_modal_content = ExternalConnectionModal::new(conn); // Show modal. Modal::new(ExternalConnectionModal::NETWORK_ID) .position(ModalPosition::CenterTop) diff --git a/src/gui/views/network/metrics.rs b/src/gui/views/network/metrics.rs index 202c965b..4fb921b3 100644 --- a/src/gui/views/network/metrics.rs +++ b/src/gui/views/network/metrics.rs @@ -152,7 +152,7 @@ fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: CornerRadius) { rect.max -= vec2(8.0, 0.0); ui.painter().rect(rect, rounding, - Colors::white_or_black(false), + Colors::fill(), View::item_stroke(), StrokeKind::Outside); diff --git a/src/gui/views/network/modals/ext_conn.rs b/src/gui/views/network/modals/ext_conn.rs index 99bb92b2..570b3261 100644 --- a/src/gui/views/network/modals/ext_conn.rs +++ b/src/gui/views/network/modals/ext_conn.rs @@ -15,9 +15,11 @@ use egui::{Id, RichText}; use url::Url; -use crate::gui::Colors; +use crate::gui::icons::SCAN; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, TextEdit, View}; +use crate::gui::views::network::types::ShareConnection; +use crate::gui::views::{CameraContent, Modal, TextEdit, View}; +use crate::gui::Colors; use crate::wallet::{ConnectionsConfig, ExternalConnection}; /// Content to create or update external wallet connection. @@ -25,14 +27,21 @@ pub struct ExternalConnectionModal { /// Flag to check if content was just rendered. first_draw: bool, - /// External connection URL value for [`Modal`]. - ext_node_url_edit: String, - /// External connection API secret value for [`Modal`]. - ext_node_secret_edit: String, - /// Flag to show URL format error at [`Modal`]. - ext_node_url_error: bool, - /// Editing external connection identifier for [`Modal`]. - ext_conn_id: Option, + /// Editing external connection identifier. + id: Option, + + /// External connection URL. + url_edit: String, + /// Flag to show URL format error. + url_error: bool, + + /// External connection username. + username_edit: String, + /// External connection API secret. + secret_edit: String, + + /// QR code scanner content. + scan_qr_content: Option, } impl ExternalConnectionModal { @@ -43,17 +52,21 @@ impl ExternalConnectionModal { /// Create new instance from optional provided connection to update. pub fn new(conn: Option) -> Self { - let (ext_node_url_edit, ext_node_secret_edit, ext_conn_id) = if let Some(c) = conn { - (c.url, c.secret.unwrap_or("".to_string()), Some(c.id)) + let (url_edit, username_edit, secret_edit, id) = if let Some(c) = conn { + let username = c.username.unwrap_or("grin".to_string()); + let secret = c.secret.unwrap_or("".to_string()); + (c.url, username, secret, Some(c.id)) } else { - ("".to_string(), "".to_string(), None) + ("".to_string(), "grin".to_string(), "".to_string(), None) }; Self { first_draw: true, - ext_node_url_edit, - ext_node_secret_edit, - ext_node_url_error: false, - ext_conn_id, + url_edit, + url_error: false, + username_edit, + secret_edit, + id, + scan_qr_content: None } } @@ -63,25 +76,69 @@ impl ExternalConnectionModal { cb: &dyn PlatformCallbacks, modal: &Modal, on_save: impl Fn(ExternalConnection)) { + // Show QR code scanner content. + if let Some(scan_content) = self.scan_qr_content.as_mut() { + if let Some(result) = scan_content.qr_scan_result() { + cb.stop_camera(); + modal.enable_closing(); + self.scan_qr_content = None; + // Parse scan result. + if let Ok(c) = serde_json::from_str::(&result.text()) { + let ext_conn = ExternalConnection::new(c.url, Some(c.username), Some(c.secret)); + ConnectionsConfig::add_ext_conn(ext_conn.clone()); + ExternalConnection::check(Some(ext_conn.id), ui.ctx()); + Modal::close(); + } + } else { + scan_content.ui(ui, cb); + } + ui.add_space(8.0); + + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + // Show buttons to close modal or scanner. + ui.columns(2, |cols| { + cols[0].vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + cb.stop_camera(); + self.scan_qr_content = None; + Modal::close(); + }); + }); + cols[1].vertical_centered_justified(|ui| { + View::button(ui, t!("back"), Colors::white_or_black(false), || { + cb.stop_camera(); + self.scan_qr_content = None; + modal.enable_closing(); + }); + }); + }); + return; + } // Add connection button callback. let on_add = |ui: &mut egui::Ui, m: &mut ExternalConnectionModal| { - let url = if !m.ext_node_url_edit.starts_with("http") { - format!("https://{}", m.ext_node_url_edit) + let url = if !m.url_edit.starts_with("http") { + format!("https://{}", m.url_edit) } else { - m.ext_node_url_edit.clone() + m.url_edit.clone() }; let error = Url::parse(url.trim()).is_err(); - m.ext_node_url_error = error; + m.url_error = error; if !error { - let secret = if m.ext_node_secret_edit.is_empty() { + let username = if m.secret_edit.is_empty() { + Some("grin".to_string()) + } else { + Some(m.secret_edit.clone()) + }; + let secret = if m.secret_edit.is_empty() { None } else { - Some(m.ext_node_secret_edit.clone()) + Some(m.secret_edit.clone()) }; - // Update or create new connection. - let mut ext_conn = ExternalConnection::new(url, secret); - if let Some(id) = m.ext_conn_id { + let mut ext_conn = ExternalConnection::new(url, username, secret); + if let Some(id) = m.id { ext_conn.id = id; } ConnectionsConfig::add_ext_conn(ext_conn.clone()); @@ -89,9 +146,9 @@ impl ExternalConnectionModal { on_save(ext_conn); // Close modal. - m.ext_node_url_edit = "".to_string(); - m.ext_node_secret_edit = "".to_string(); - m.ext_node_url_error = false; + m.url_edit = "".to_string(); + m.secret_edit = "".to_string(); + m.url_error = false; Modal::close(); } }; @@ -104,11 +161,24 @@ impl ExternalConnectionModal { ui.add_space(8.0); // Draw node URL text edit. - let url_edit_id = Id::from(modal.id).with(self.ext_conn_id).with("node_url"); - let mut url_edit = TextEdit::new(url_edit_id) - .paste() - .focus(self.first_draw); - url_edit.ui(ui, &mut self.ext_node_url_edit, cb); + let url_edit_id = Id::from(modal.id).with(self.id).with("node_url"); + let mut url_edit = TextEdit::new(url_edit_id).paste().focus(self.first_draw); + let url_edit_before = self.url_edit.clone(); + url_edit.ui(ui, &mut self.url_edit, cb); + if self.url_edit != url_edit_before { + self.url_error = false; + } + + ui.add_space(8.0); + ui.label(RichText::new(t!("wallets.name")) + .size(17.0) + .color(Colors::gray())); + ui.add_space(8.0); + + // Draw node username text edit (disabled by default). + let username_edit_id = Id::from(modal.id).with(self.id).with("node_username"); + let mut username_edit = TextEdit::new(username_edit_id).focus(false).disable(); + username_edit.ui(ui, &mut self.username_edit, cb); ui.add_space(8.0); ui.label(RichText::new(t!("wallets.node_secret")) @@ -117,29 +187,37 @@ impl ExternalConnectionModal { ui.add_space(8.0); // Draw node API secret text edit. - let secret_edit_id = Id::from(modal.id).with(self.ext_conn_id).with("node_secret"); - let mut secret_edit = TextEdit::new(secret_edit_id) - .password() - .paste() - .focus(false); + let secret_edit_id = Id::from(modal.id).with(self.id).with("node_secret"); + let mut secret_edit = TextEdit::new(secret_edit_id).password().paste().focus(false); if url_edit.enter_pressed { secret_edit.focus_request(); } - secret_edit.ui(ui, &mut self.ext_node_secret_edit, cb); + secret_edit.ui(ui, &mut self.secret_edit, cb); if secret_edit.enter_pressed { - (on_add)(ui, self); + on_add(ui, self); } // Show error when specified URL is not valid. - if self.ext_node_url_error { + if self.url_error { ui.add_space(12.0); ui.label(RichText::new(t!("wallets.invalid_url")) .size(17.0) .color(Colors::red())); } ui.add_space(12.0); + + let scan_text = format!("{} {}", SCAN, t!("scan")); + View::button(ui, scan_text, Colors::white_or_black(false), || { + modal.disable_closing(); + self.scan_qr_content = Some(CameraContent::default()); + cb.start_camera(); + }); }); + ui.add_space(8.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); + // Show modal buttons. ui.scope(|ui| { // Setup spacing between buttons. @@ -149,14 +227,14 @@ impl ExternalConnectionModal { columns[0].vertical_centered_justified(|ui| { View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { // Close modal. - self.ext_node_url_edit = "".to_string(); - self.ext_node_secret_edit = "".to_string(); - self.ext_node_url_error = false; + self.url_edit = "".to_string(); + self.secret_edit = "".to_string(); + self.url_error = false; Modal::close(); }); }); columns[1].vertical_centered_justified(|ui| { - View::button_ui(ui, if self.ext_conn_id.is_some() { + View::button_ui(ui, if self.id.is_some() { t!("modal.save") } else { t!("modal.add") diff --git a/src/gui/views/network/modals/mod.rs b/src/gui/views/network/modals/mod.rs index 5ec63fed..c3e841d4 100644 --- a/src/gui/views/network/modals/mod.rs +++ b/src/gui/views/network/modals/mod.rs @@ -13,4 +13,7 @@ // limitations under the License. mod ext_conn; -pub use ext_conn::*; \ No newline at end of file +pub use ext_conn::*; + +mod share_conn; +pub use share_conn::*; \ No newline at end of file diff --git a/src/gui/views/network/modals/share_conn.rs b/src/gui/views/network/modals/share_conn.rs new file mode 100644 index 00000000..6393873d --- /dev/null +++ b/src/gui/views/network/modals/share_conn.rs @@ -0,0 +1,57 @@ +// Copyright 2024 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::AppConfig; +use crate::gui::Colors; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::network::types::ShareConnection; +use crate::gui::views::{Modal, QrCodeContent, View}; + +/// [`Modal`] content to share connection with QR code. +pub struct ShareConnectionContent { + /// QR code content. + pub qr_details_content: QrCodeContent, +} + +impl ShareConnectionContent { + /// Create new content instance from connection details. + pub fn new(details: ShareConnection) -> Result { + let details = serde_json::to_string_pretty(&details)?; + let c = Self { + qr_details_content: QrCodeContent::new(details, false).hide_text().no_copy(), + }; + Ok(c) + } + + /// Draw QR code content. + pub fn ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + let dark_theme = AppConfig::dark_theme().unwrap_or(false); + // Set light theme for better scanning. + AppConfig::set_dark_theme(false); + modal.set_background_color(Colors::FILL_DEEP); + crate::setup_visuals(ui.ctx()); + // Draw QR code content. + ui.add_space(6.0); + self.qr_details_content.ui(ui, cb); + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + Modal::close(); + }); + }); + ui.add_space(6.0); + // Set color theme back. + AppConfig::set_dark_theme(dark_theme); + crate::setup_visuals(ui.ctx()); + } +} \ No newline at end of file diff --git a/src/gui/views/network/node.rs b/src/gui/views/network/node.rs index edd1d811..8c00ba83 100644 --- a/src/gui/views/network/node.rs +++ b/src/gui/views/network/node.rs @@ -176,7 +176,7 @@ fn node_stats_ui(ui: &mut egui::Ui) { const PEER_ITEM_HEIGHT: f32 = 77.0; /// Draw connected peer info item. -fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, rounding: CornerRadius) { +fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, r: CornerRadius) { let mut rect = ui.available_rect_before_wrap(); rect.set_height(PEER_ITEM_HEIGHT); ui.allocate_ui(rect.size(), |ui| { @@ -184,7 +184,7 @@ fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, rounding: CornerRadius) { ui.add_space(4.0); // Draw round background. - ui.painter().rect(rect, rounding, Colors::fill_lite(), View::item_stroke(), StrokeKind::Outside); + ui.painter().rect(rect, r, Colors::fill(), View::item_stroke(), StrokeKind::Outside); // Draw IP address. ui.horizontal(|ui| { diff --git a/src/gui/views/network/setup/node.rs b/src/gui/views/network/setup/node.rs index d0942a69..8affd2e8 100644 --- a/src/gui/views/network/setup/node.rs +++ b/src/gui/views/network/setup/node.rs @@ -17,7 +17,7 @@ use eframe::epaint::{RectShape, StrokeKind}; use egui::{CursorIcon, Id, Layout, RichText, Sense, UiBuilder}; use grin_core::global::ChainTypes; -use crate::gui::icons::{CLOCK_CLOCKWISE, COMPUTER_TOWER, FOLDERS, PENCIL, PLUG, POWER, SHIELD, SHIELD_SLASH}; +use crate::gui::icons::{CLOCK_CLOCKWISE, COMPUTER_TOWER, FOLDERS, PLUG, POWER, SHIELD, SHIELD_SLASH}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::network::settings::NetworkSettings; use crate::gui::views::network::NetworkContent; diff --git a/src/gui/views/network/setup/p2p.rs b/src/gui/views/network/setup/p2p.rs index 21dd8ba4..5cdcfd0f 100644 --- a/src/gui/views/network/setup/p2p.rs +++ b/src/gui/views/network/setup/p2p.rs @@ -794,7 +794,8 @@ fn peer_item_ui(ui: &mut egui::Ui, ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { // Draw delete button for non-default seed peers. if peer_type != &PeerType::DefaultSeed { - View::item_button(ui, View::item_rounding(index, len, true), TRASH, None, || { + let r = View::item_rounding(index, len, true); + View::item_button(ui, r, TRASH, Some(Colors::inactive_text()), || { match peer_type { PeerType::CustomSeed => { NodeConfig::remove_custom_seed(peer_addr); diff --git a/src/gui/views/network/types.rs b/src/gui/views/network/types.rs index 6777203b..c8d75448 100644 --- a/src/gui/views/network/types.rs +++ b/src/gui/views/network/types.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use serde_derive::{Deserialize, Serialize}; use crate::gui::platform::PlatformCallbacks; /// Integrated node tab content interface. @@ -38,4 +39,13 @@ impl NodeTabType { NodeTabType::Settings => t!("network.settings").into() } } +} + +/// Connection details to share. +#[derive(Serialize, Deserialize, Clone)] +pub struct ShareConnection { + #[serde(rename(serialize = "ipPort", deserialize = "ipPort"))] + pub url: String, + pub username: String, + pub secret: String } \ No newline at end of file diff --git a/src/gui/views/qr.rs b/src/gui/views/qr.rs index e4a2a3fc..82746dc4 100644 --- a/src/gui/views/qr.rs +++ b/src/gui/views/qr.rs @@ -32,6 +32,10 @@ use crate::gui::Colors; pub struct QrCodeContent { /// QR code text. pub text: String, + /// Flag to show text below QR code. + show_text: bool, + /// Flag to copy text below QR code. + can_copy_text: bool, /// Maximum QR code size. max_size: f32, @@ -56,6 +60,8 @@ impl QrCodeContent { pub fn new(text: String, animated: bool) -> Self { Self { text, + show_text: true, + can_copy_text: true, max_size: DEFAULT_QR_SIZE as f32, animated, animated_index: None, @@ -71,6 +77,18 @@ impl QrCodeContent { self } + /// Hide text below QR code. + pub fn hide_text(mut self) -> Self { + self.show_text = false; + self + } + + /// Do not show button to copy QR code text. + pub fn no_copy(mut self) -> Self { + self.can_copy_text = false; + self + } + /// Draw QR code. pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { if self.animated { @@ -123,7 +141,9 @@ impl QrCodeContent { self.qr_image_ui(svg, ui); // Show QR code text. - self.text_ui(ui); + if self.show_text { + self.text_ui(ui); + } ui.vertical_centered(|ui| { let sharing = { @@ -202,32 +222,47 @@ impl QrCodeContent { self.qr_image_ui(svg, ui); // Show QR code text. - self.text_ui(ui); + if self.show_text { + self.text_ui(ui); + } else { + ui.add_space(8.0); + } - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0); + if self.can_copy_text { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - // Draw copy button. - let copy_text = format!("{} {}", COPY, t!("copy")); - View::button(ui, copy_text, Colors::white_or_black(false), || { - cb.copy_string_to_buffer(self.text.clone()); + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + // Draw copy button. + let copy_text = format!("{} {}", COPY, t!("copy")); + View::button(ui, copy_text, Colors::white_or_black(false), || { + cb.copy_string_to_buffer(self.text.clone()); + }); + }); + columns[1].vertical_centered_justified(|ui| { + self.share_static_button_ui(ui, cb); }); }); - columns[1].vertical_centered_justified(|ui| { - let share_text = format!("{} {}", IMAGES_SQUARE, t!("share")); - View::colored_text_button(ui, - share_text, - Colors::blue(), - Colors::white_or_black(false), || { - self.share_static(cb); - }); + } else { + ui.vertical_centered(|ui| { + self.share_static_button_ui(ui, cb); }); - }); + } } } + /// Draw button to share static QR code. + fn share_static_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + let share_text = format!("{} {}", IMAGES_SQUARE, t!("share")); + View::colored_text_button(ui, + share_text, + Colors::blue(), + Colors::white_or_black(false), || { + self.share_static(cb); + }); + } + /// Share static QR code image. fn share_static(&self, cb: &dyn PlatformCallbacks) { let text = self.text.as_str(); diff --git a/src/gui/views/views.rs b/src/gui/views/views.rs index c8a31615..17631302 100644 --- a/src/gui/views/views.rs +++ b/src/gui/views/views.rs @@ -458,7 +458,7 @@ impl View { ne: if r[1] { 8.0 as u8 } else { 0.0 as u8 }, sw: if r[2] { 8.0 as u8 } else { 0.0 as u8 }, se: if r[3] { 8.0 as u8 } else { 0.0 as u8 }, - }, Colors::fill_lite(), Self::item_stroke(), StrokeKind::Outside); + }, Colors::fill(), Self::item_stroke(), StrokeKind::Outside); let bg_idx = ui.painter().add(bg_shape.clone()); // Draw box content. diff --git a/src/gui/views/wallets/creation/content.rs b/src/gui/views/wallets/creation/content.rs index 44c0edec..bf9a76ea 100644 --- a/src/gui/views/wallets/creation/content.rs +++ b/src/gui/views/wallets/creation/content.rs @@ -232,9 +232,7 @@ impl WalletCreationContent { columns[0].vertical_centered_justified(|ui| { match self.mnemonic_setup.mnemonic.mode() { PhraseMode::Generate => { - let c_t = format!("{} {}", - COPY, - t!("copy").to_uppercase()); + let c_t = format!("{} {}", COPY, t!("copy")); View::button(ui, c_t, Colors::white_or_black(false), || { cb.copy_string_to_buffer(self.mnemonic_setup .mnemonic @@ -242,9 +240,7 @@ impl WalletCreationContent { }); } PhraseMode::Import => { - let p_t = format!("{} {}", - CLIPBOARD_TEXT, - t!("paste").to_uppercase()); + let p_t = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); View::button(ui, p_t, Colors::white_or_black(false), || { let data = ZeroingString::from(cb.get_string_from_buffer()); self.mnemonic_setup.mnemonic.import(&data); @@ -257,9 +253,7 @@ impl WalletCreationContent { if next { self.next_step_button_ui(ui, on_create); } else { - let scan_text = format!("{} {}", - SCAN, - t!("scan").to_uppercase()); + let scan_text = format!("{} {}", SCAN, t!("scan")); View::button(ui, scan_text, Colors::white_or_black(false), || { self.scan_modal_content = Some(CameraScanContent::default()); // Show QR code scan modal. @@ -279,7 +273,7 @@ impl WalletCreationContent { if next { self.next_step_button_ui(ui, on_create); } else { - let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase()); + let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); View::button(ui, paste_text, Colors::white_or_black(false), || { let data = ZeroingString::from(cb.get_string_from_buffer()); self.mnemonic_setup.mnemonic.import(&data); diff --git a/src/gui/views/wallets/wallet/message.rs b/src/gui/views/wallets/wallet/message.rs index 4c585b46..0f60a65d 100644 --- a/src/gui/views/wallets/wallet/message.rs +++ b/src/gui/views/wallets/wallet/message.rs @@ -28,11 +28,12 @@ pub struct MessageInputContent { message_edit: String, /// Flag to check if error happened at Slatepack message parsing. parse_error: bool, - /// QR code scanner content. - scan_qr_content: Option, /// Button to parse picked file content. file_pick_button: FilePickContent, + /// QR code scanner content. + scan_qr_content: Option, + /// Payment proof input content. pub proof_content: Option, } @@ -45,10 +46,10 @@ impl Default for MessageInputContent { Self { message_edit: "".to_string(), parse_error: false, - scan_qr_content: None, file_pick_button: FilePickContent::new( FilePickContentType::Button(t!("choose_file").into()) ), + scan_qr_content: None, proof_content: None, } } diff --git a/src/gui/views/wallets/wallet/settings/connection.rs b/src/gui/views/wallets/wallet/settings/connection.rs index 5af2a74c..4064f513 100644 --- a/src/gui/views/wallets/wallet/settings/connection.rs +++ b/src/gui/views/wallets/wallet/settings/connection.rs @@ -115,6 +115,7 @@ impl ContentContainer for ConnectionSettings { ext_conn_list.push(ExternalConnection { id: *id, url: url.clone(), + username: Some("grin".to_string()), secret: None, available: Some(true), }) diff --git a/src/wallet/connections/config.rs b/src/wallet/connections/config.rs index 12e38a94..dcab4b42 100644 --- a/src/wallet/connections/config.rs +++ b/src/wallet/connections/config.rs @@ -64,7 +64,7 @@ impl ConnectionsConfig { pub fn add_ext_conn(conn: ExternalConnection) { let mut w_config = Settings::conn_config_to_update(); if let Some(pos) = w_config.external.iter().position(|c| { - c.id == conn.id + c.id == conn.id || c.url == conn.url }) { w_config.external.remove(pos); w_config.external.insert(pos, conn); diff --git a/src/wallet/connections/external.rs b/src/wallet/connections/external.rs index e1ee5277..7775704a 100644 --- a/src/wallet/connections/external.rs +++ b/src/wallet/connections/external.rs @@ -28,6 +28,8 @@ pub struct ExternalConnection { pub id: i64, /// Node URL. pub url: String, + /// Optional username. + pub username: Option, /// Optional API secret key. pub secret: Option, @@ -61,6 +63,7 @@ impl ExternalConnection { ExternalConnection { id: index as i64, url: url.to_string(), + username: Some("grin".to_string()), secret: None, available: None, } @@ -68,11 +71,12 @@ impl ExternalConnection { } /// Create new external connection. - pub fn new(url: String, secret: Option) -> Self { + pub fn new(url: String, username: Option, secret: Option) -> Self { let id = chrono::Utc::now().timestamp(); Self { id, url, + username, secret, available: None, } @@ -128,7 +132,6 @@ fn check_ext_conn(conn: &ExternalConnection, ui_ctx: &egui::Context) { return; } let url = url_res.unwrap(); - if let Ok(_) = url.socket_addrs(|| None) { let addr = format!("{}v2/foreign", url.to_string()); let mut req_setup = hyper::Request::builder() @@ -136,9 +139,10 @@ fn check_ext_conn(conn: &ExternalConnection, ui_ctx: &egui::Context) { .uri(addr.clone()); // Setup secret key auth. if let Some(key) = conn.secret { + let username = conn.username.unwrap_or("grin".to_string()); let basic_auth = format!( "Basic {}", - to_base64(&format!("grin:{}", key)) + to_base64(&format!("{}:{}", username, key)) ); req_setup = req_setup .header(hyper::header::AUTHORIZATION, basic_auth.clone());