diff --git a/Cargo.lock b/Cargo.lock index c83a5b7..5b08863 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -540,6 +540,18 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.7.3" @@ -652,9 +664,13 @@ dependencies = [ "log", "password-hash", "rand 0.9.0", + "resvg 0.45.0", "serde", "serde_json", "simple_logger", + "tiny-skia", + "toml", + "usvg 0.45.0", "zeroize", ] @@ -1057,6 +1073,15 @@ dependencies = [ "libc", ] +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1132,6 +1157,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" +[[package]] +name = "data-url" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" + [[package]] name = "deranged" version = "0.4.0" @@ -1328,6 +1359,7 @@ dependencies = [ "enum-map", "log", "mime_guess2", + "resvg 0.37.0", "serde", ] @@ -1551,6 +1583,35 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "fontconfig-parser" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fcfcd44ca6e90c921fee9fa665d530b21ef1327a4c1a6c5250ea44b776ada7" +dependencies = [ + "roxmltree 0.20.0", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -2140,6 +2201,18 @@ dependencies = [ "quick-error", ] +[[package]] +name = "imagesize" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" + +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + [[package]] name = "imgref" version = "1.11.0" @@ -2275,6 +2348,25 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "kurbo" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "kurbo" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89234b2cc610a7dd927ebde6b41dd1a5d4214cffaef4cf1fb2195d592f92518f" +dependencies = [ + "arrayvec", + "smallvec", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -2323,6 +2415,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + [[package]] name = "libredox" version = "0.1.3" @@ -2923,6 +3021,12 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -3257,6 +3361,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rctree" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f" + [[package]] name = "redox_syscall" version = "0.3.5" @@ -3321,11 +3431,57 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "resvg" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadccb3d99a9efb8e5e00c16fbb732cbe400db2ec7fc004697ee7d97d86cf1f4" +dependencies = [ + "log", + "pico-args", + "rgb", + "svgtypes 0.13.0", + "tiny-skia", + "usvg 0.37.0", +] + +[[package]] +name = "resvg" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd43d1c474e9dadf09a8fdf22d713ba668b499b5117b9b9079500224e26b5b29" +dependencies = [ + "gif", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes 0.15.3", + "tiny-skia", + "usvg 0.45.0", + "zune-jpeg", +] + [[package]] name = "rgb" version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "roxmltree" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" [[package]] name = "rustc-hash" @@ -3379,6 +3535,24 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.9.0", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + [[package]] name = "ryu" version = "1.0.20" @@ -3524,6 +3698,27 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -3654,6 +3849,9 @@ name = "strict-num" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] [[package]] name = "subtle" @@ -3661,6 +3859,26 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "svgtypes" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e44e288cd960318917cbd540340968b90becc8bc81f171345d706e7a89d9d70" +dependencies = [ + "kurbo 0.9.5", + "siphasher 0.3.11", +] + +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo 0.11.1", + "siphasher 1.0.1", +] + [[package]] name = "syn" version = "1.0.109" @@ -3810,6 +4028,7 @@ dependencies = [ "bytemuck", "cfg-if", "log", + "png", "tiny-skia-path", ] @@ -3930,6 +4149,9 @@ name = "ttf-parser" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] [[package]] name = "type-map" @@ -3963,6 +4185,24 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -3978,12 +4218,30 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-script" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" + [[package]] name = "unicode-segmentation" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + [[package]] name = "unicode-width" version = "0.1.14" @@ -4017,6 +4275,77 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "usvg" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b0a51b72ab80ca511d126b77feeeb4fb1e972764653e61feac30adc161a756" +dependencies = [ + "base64 0.21.7", + "log", + "pico-args", + "usvg-parser", + "usvg-tree", + "xmlwriter", +] + +[[package]] +name = "usvg" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac8e0e3e4696253dc06167990b3fe9a2668ab66270adf949a464db4088cb354" +dependencies = [ + "base64 0.22.1", + "data-url", + "flate2", + "fontdb", + "imagesize 0.13.0", + "kurbo 0.11.1", + "log", + "pico-args", + "roxmltree 0.20.0", + "rustybuzz", + "simplecss", + "siphasher 1.0.1", + "strict-num", + "svgtypes 0.15.3", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "usvg-parser" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd4e3c291f45d152929a31f0f6c819245e2921bfd01e7bd91201a9af39a2bdc" +dependencies = [ + "data-url", + "flate2", + "imagesize 0.12.0", + "kurbo 0.9.5", + "log", + "roxmltree 0.19.0", + "simplecss", + "siphasher 0.3.11", + "svgtypes 0.13.0", + "usvg-tree", +] + +[[package]] +name = "usvg-tree" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee3d202ebdb97a6215604b8f5b4d6ef9024efd623cf2e373a6416ba976ec7d3" +dependencies = [ + "rctree", + "strict-num", + "svgtypes 0.13.0", + "tiny-skia-path", +] + [[package]] name = "utf16_iter" version = "1.0.5" @@ -4888,6 +5217,12 @@ version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + [[package]] name = "yoke" version = "0.7.5" diff --git a/bitvault-ui/Cargo.toml b/bitvault-ui/Cargo.toml index 9ac6cb4..250a947 100644 --- a/bitvault-ui/Cargo.toml +++ b/bitvault-ui/Cargo.toml @@ -22,7 +22,7 @@ getrandom.workspace = true # egui dependencies eframe = "0.26.2" egui = "0.26.2" -egui_extras = "0.26.2" +egui_extras = { version = "0.26.2", features = ["svg"] } egui_plot = "0.26.2" # Logging @@ -40,3 +40,7 @@ image = "0.25.5" # Internal dependencies bitvault-core = { path = "../bitvault-core" } +toml = "0.8.20" +usvg = "0.45.0" +resvg = "0.45.0" +tiny-skia = "0.11.4" diff --git a/bitvault-ui/assets/NotoSans-Regular.ttf b/bitvault-ui/assets/NotoSans-Regular.ttf new file mode 100644 index 0000000..d552209 Binary files /dev/null and b/bitvault-ui/assets/NotoSans-Regular.ttf differ diff --git a/bitvault-ui/assets/onboarding1.svg b/bitvault-ui/assets/onboarding1.svg new file mode 100644 index 0000000..fc043e8 --- /dev/null +++ b/bitvault-ui/assets/onboarding1.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bitvault-ui/assets/onboarding2.svg b/bitvault-ui/assets/onboarding2.svg new file mode 100644 index 0000000..4ace4b2 --- /dev/null +++ b/bitvault-ui/assets/onboarding2.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/bitvault-ui/assets/onboarding3.svg b/bitvault-ui/assets/onboarding3.svg new file mode 100644 index 0000000..1833cd1 --- /dev/null +++ b/bitvault-ui/assets/onboarding3.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/bitvault-ui/assets/telegram.png b/bitvault-ui/assets/telegram.png new file mode 100644 index 0000000..bc912dd Binary files /dev/null and b/bitvault-ui/assets/telegram.png differ diff --git a/bitvault-ui/src/app.rs b/bitvault-ui/src/app.rs index edae925..c817578 100644 --- a/bitvault-ui/src/app.rs +++ b/bitvault-ui/src/app.rs @@ -6,12 +6,13 @@ use std::sync::OnceLock; use anyhow::Result; use eframe::{ - egui::{self, Color32, Context, Id, Rect, RichText, Ui}, + egui::{self, Color32, Context, RichText, Ui}, CreationContext, }; use serde::{Deserialize, Serialize}; use std::sync::RwLock; +use crate::config::Settings; use crate::wallet; use bitvault_core::crypto; @@ -41,6 +42,9 @@ pub enum View { Wallet, LockScreen, SplashScreen, + OnboardingOne, + OnboardingTwo, + OnboardingThree, } // Define a struct to hold the global state @@ -58,6 +62,8 @@ pub struct AppState { pub encrypted_wallet_data: Option, // Encrypted wallet data stored on disk pub lock_error: Option, // Error message when unlocking fails pub splash_timer: Option, // Timer for splash screen (in seconds) + pub testing_mode: bool, // Flag for testing mode to bypass lock screen + pub onboarding_completed: bool, // Flag to track if onboarding has been completed } // Create a type alias for a thread-safe, shared reference to the state @@ -65,17 +71,166 @@ pub type SharedAppState = Arc>; pub struct BitVaultApp { state: SharedAppState, + settings: Settings, +} + +// Add this helper module for asset management at the top level before BitVaultApp struct +mod assets { + use eframe::egui; + use std::path::PathBuf; + use std::sync::OnceLock; + + // Base paths to try for asset loading + const BASE_PATHS: [&str; 3] = ["bitvault-ui", ".", ".."]; + + // Find the correct base path once + fn get_base_path() -> &'static PathBuf { + static BASE_PATH: OnceLock = OnceLock::new(); + + BASE_PATH.get_or_init(|| { + for base in BASE_PATHS { + let path = PathBuf::from(base); + if path.exists() { + return path; + } + } + // Default to current directory if nothing found + PathBuf::from(".") + }) + } + + // Load a font file + pub fn load_font(font_name: &str) -> Option> { + let base = get_base_path(); + let font_path = base.join("assets").join(font_name); + + std::fs::read(&font_path).ok() + } + + // Load an image file + pub fn load_image(path: &str) -> Option> { + let base = get_base_path(); + let img_path = base.join(path); + + std::fs::read(&img_path).ok() + } + + // SVG loading function that works with the existing dependencies + pub fn load_svg_as_texture( + ctx: &egui::Context, + name: &str, + path: &str, + ) -> Option { + let base = get_base_path(); + let svg_path = base.join(path); + + log::debug!("Loading SVG from: {:?}", svg_path); + + // First read the SVG file + let svg_data = std::fs::read_to_string(&svg_path).ok()?; + + // Parse SVG with usvg + let opt = usvg::Options { + ..Default::default() + }; + + let tree = usvg::Tree::from_str(&svg_data, &opt).ok()?; + + // Get the size and create a pixmap + let size = tree.size(); + + // Apply a scale factor to increase resolution (2.0 = double resolution) + let scale_factor = 2.0; + let scaled_width = (size.width() * scale_factor) as u32; + let scaled_height = (size.height() * scale_factor) as u32; + + let pixmap_size = tiny_skia::IntSize::from_wh(scaled_width, scaled_height)?; + + // Create a pixmap (tiny-skia's bitmap for rendering) + let mut pixmap = tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height())?; + + // Render the SVG tree to the pixmap with the scale transform + resvg::render( + &tree, + tiny_skia::Transform::from_scale(scale_factor, scale_factor), + &mut pixmap.as_mut(), + ); + + // Convert to egui texture + let image_size = [pixmap_size.width() as _, pixmap_size.height() as _]; + let image_data = pixmap.data(); + + // Create the color image and texture + let color_image = egui::ColorImage::from_rgba_unmultiplied(image_size, image_data); + + Some(ctx.load_texture(name, color_image, Default::default())) + } + + // Get a texture handle for an image + pub fn get_image_texture( + ctx: &egui::Context, + name: &str, + path: &str, + ) -> Option { + load_image(path).and_then(|image_data| { + image::load_from_memory(&image_data).ok().map(|image| { + let size = [image.width() as _, image.height() as _]; + let image_buffer = image.to_rgba8(); + let pixels = image_buffer.as_flat_samples(); + + let color_image = egui::ColorImage::from_rgba_unmultiplied(size, pixels.as_slice()); + ctx.load_texture(name, color_image, Default::default()) + }) + }) + } } impl BitVaultApp { - pub fn new(_cc: &CreationContext<'_>) -> Self { + pub fn new(cc: &CreationContext<'_>) -> Self { + // Attempt to configure a font with good Unicode support + let mut fonts = egui::FontDefinitions::default(); + + // Try to add Noto Sans which has good Unicode character support + if let Some(font_data) = assets::load_font("NotoSans-Regular.ttf") { + log::info!("Successfully loaded Noto Sans font"); + + // Add font data + fonts + .font_data + .insert("noto".to_owned(), egui::FontData::from_owned(font_data)); + + // Set as primary font + fonts + .families + .get_mut(&egui::FontFamily::Proportional) + .unwrap() + .insert(0, "noto".to_owned()); + + // Apply the font configuration + cc.egui_ctx.set_fonts(fonts); + log::info!("Applied custom font configuration"); + } else { + log::warn!("Could not load Noto Sans font - using default fonts"); + } + + // Check for testing mode environment variable + let testing_mode = std::env::var("TESTING").unwrap_or_default() == "1"; + if testing_mode { + log::info!("Running in TESTING mode - lock screen will be bypassed"); + } + + // Load settings or use defaults + let settings = Settings::load(); + // Create the app with default state let app = Self { state: Arc::new(RwLock::new(AppState { current_view: View::SplashScreen, splash_timer: Some(1.0), // 1 second splash screen + testing_mode, ..Default::default() })), + settings, }; // Check if a wallet file exists and load it @@ -187,15 +342,12 @@ impl BitVaultApp { ui.label("Choose a secure PIN to protect your wallet"); ui.add_space(10.0); - let mut pin_input = String::new(); - let mut pin_confirm = String::new(); - let mut back_button_clicked = false; - - // Read current state values - if let Ok(state) = self.state.read() { - pin_input = state.pin_input.clone(); - pin_confirm = state.pin_confirm.clone(); - } + // Read current state values once + let (mut pin_input, mut pin_confirm) = if let Ok(state) = self.state.read() { + (state.pin_input.clone(), state.pin_confirm.clone()) + } else { + (String::new(), String::new()) + }; // PIN input fields ui.horizontal(|ui| { @@ -207,7 +359,7 @@ impl BitVaultApp { .desired_width(200.0), ); - // Update state with new input + // Update state with new input if changed if response.changed() { if let Ok(mut state) = self.state.write() { state.pin_input = pin_input.clone(); @@ -226,22 +378,12 @@ impl BitVaultApp { .desired_width(200.0), ); - // Update state with new input + // Update state with new input if changed if response.changed() { if let Ok(mut state) = self.state.write() { state.pin_confirm = pin_confirm.clone(); } } - - // Check for Enter key press - let enter_pressed = - response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)); - if enter_pressed { - // Store the enter key state in memory for use outside this scope - ui.memory_mut(|mem| { - mem.data.insert_temp(Id::new("pin_enter_pressed"), true); - }); - } }); ui.add_space(20.0); @@ -249,51 +391,35 @@ impl BitVaultApp { // Calculate pin_valid based on current values let pin_valid = !pin_input.is_empty() && pin_input == pin_confirm; - // Get the enter key state from memory - let enter_pressed = ui - .memory(|mem| mem.data.get_temp::(Id::new("pin_enter_pressed"))) - .unwrap_or(false); - // Set PIN button if ui .add_enabled(pin_valid, egui::Button::new("Set PIN")) .clicked() - || (enter_pressed && pin_valid) + && pin_valid { - log::info!("Set PIN button clicked, PIN is valid: {}", pin_valid); + if let Ok(mut state) = self.state.write() { + // Store the PIN + state.user_pin = Some(pin_input); + log::info!("PIN set successfully"); - if pin_valid { - if let Ok(mut state) = self.state.write() { - // Store the PIN - state.user_pin = Some(pin_input.clone()); - log::info!("PIN set successfully"); - - // Clear the input fields for security - state.pin_input.clear(); - state.pin_confirm.clear(); - - // Move to the next step - if state.wallet_state == WalletState::Creating { - log::info!("Moving to Seed view for new wallet creation"); - // For creating a new wallet, move to the seed view - state.current_view = View::Seed; - } else if state.wallet_state == WalletState::Restoring { - log::info!("Moving to Seed view for wallet restoration"); - // For restoring, go to the seed view where the user can enter their seed phrase - state.current_view = View::Seed; - } + // Clear the input fields for security + state.pin_input.clear(); + state.pin_confirm.clear(); + + // Move to the next step + if state.wallet_state == WalletState::Creating { + log::info!("Moving to Seed view for new wallet creation"); + state.current_view = View::Seed; + } else if state.wallet_state == WalletState::Restoring { + log::info!("Moving to Seed view for wallet restoration"); + state.current_view = View::Seed; } } } - // Back button + // Back button with simpler structure let back_response = ui.button("Go Back"); if back_response.clicked() { - back_button_clicked = true; - } - - // Handle back button click outside the state read lock - if back_button_clicked { if let Ok(mut state) = self.state.write() { state.current_view = View::Disclaimer; } @@ -457,16 +583,14 @@ impl BitVaultApp { ); // Read state once to get the values we need - let (original_seed, mut verification_input, mut verification_result) = - if let Ok(state) = self.state.read() { - ( - state.seed_phrase.clone().unwrap_or_default(), - state.verification_input.clone(), - None, - ) - } else { - (String::new(), String::new(), None) - }; + let (original_seed, mut verification_input) = if let Ok(state) = self.state.read() { + ( + state.seed_phrase.clone().unwrap_or_default(), + state.verification_input.clone(), + ) + } else { + (String::new(), String::new()) + }; ui.add_space(20.0); @@ -485,10 +609,6 @@ impl BitVaultApp { } } - // Check for Enter key press - let enter_pressed = - response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)); - ui.add_space(20.0); // Check if the entered text matches the original seed phrase @@ -505,53 +625,25 @@ impl BitVaultApp { ui.add_space(10.0); - let verify_clicked = ui.button("Verify").clicked(); - let back_button = ui.add(egui::Button::new("Go Back")); - let back_clicked = back_button.clicked(); - - // Store the back button rect for the icon - if back_button.rect.width() > 0.0 { - ui.memory_mut(|mem| { - mem.data - .insert_temp(Id::new("back_button"), back_button.rect) - }); - } - - // Handle button clicks outside of any locks - if verify_clicked || enter_pressed { - if is_correct { - // Verification successful - update state once - if let Ok(mut state) = self.state.write() { - log::info!("Seed verification successful, moving to wallet view"); - state.current_view = View::Wallet; - state.wallet_state = WalletState::Unlocked; - } - } else { - // Verification failed - log::warn!("Seed verification failed - phrases don't match"); - verification_result = Some(false); + // Verify button + if ui.button("Verify").clicked() && is_correct { + if let Ok(mut state) = self.state.write() { + log::info!("Seed verification successful, moving to wallet view"); + state.current_view = View::Wallet; + state.wallet_state = WalletState::Unlocked; } } - if back_clicked { + // Back button with simpler structure + let back_button = ui.button("Go Back"); + if back_button.clicked() { if let Ok(mut state) = self.state.write() { state.current_view = View::Seed; } } - // Show verification result if needed - if let Some(false) = verification_result { - ui.add_space(5.0); - ui.label( - RichText::new("Verification failed. Please check your recovery phrase.") - .color(Color32::RED), - ); - } - // Draw back button icon - if let Some(rect) = ui.memory(|m| m.data.get_temp::(Id::new("back_button"))) { - crate::icons::draw_caret_left(ui, rect, Color32::WHITE); - } + crate::icons::draw_caret_left(ui, back_button.rect, Color32::WHITE); }); } @@ -778,55 +870,32 @@ impl BitVaultApp { // Center the logo ui.vertical_centered(|ui| { - // Center vertically - push down less to make it more centered - ui.add_space(screen_rect.height() / 4.0); - // Use a static texture handle to avoid reloading on every frame static TEXTURE_ID: OnceLock> = OnceLock::new(); let texture_id = TEXTURE_ID.get_or_init(|| { - log::debug!("Loading image - this should only happen once"); - - // Try to load the image from the file system - try multiple paths - let possible_paths = [ - "public/splash_logo.png", - "./public/splash_logo.png", - "../public/splash_logo.png", - "bitvault-ui/public/splash_logo.png", - ]; - - for path in possible_paths { - log::debug!("Trying to load image from: {}", path); - if let Ok(image_data) = std::fs::read(path) { - // Use the image crate to decode the image - if let Ok(image) = image::load_from_memory(&image_data) { - let size = [image.width() as _, image.height() as _]; - let image_buffer = image.to_rgba8(); - let pixels = image_buffer.as_flat_samples(); - - let color_image = - egui::ColorImage::from_rgba_unmultiplied(size, pixels.as_slice()); - - let texture = ui.ctx().load_texture( - "splash_logo", - color_image, - Default::default(), - ); - - log::debug!("Image loaded successfully from {}", path); - return Some(texture); - } - } - } - - log::error!("Failed to read image file from any path"); - None + log::debug!("Loading splash logo - this should only happen once"); + assets::get_image_texture(ui.ctx(), "splash_logo", "public/splash_logo.png") }); match texture_id { Some(texture) => { - // Make the image smaller (50% of original size) - let scale = 0.5; + // Get texture size and available space + let available_size = ui.available_size(); + let texture_size = texture.size_vec2(); + + // Calculate appropriate scale - use a smaller maximum to prevent oversizing + // Use a target width of 50-60% of screen width, but never larger than original + let target_width_ratio = 0.5; + let desired_width = available_size.x * target_width_ratio; + let scale = (desired_width / texture_size.x).min(1.0); + + let display_size = texture_size * scale; + + // Ensure vertical centering by adjusting spacing + let vertical_center_offset = (available_size.y - display_size.y) / 2.0; + ui.add_space(vertical_center_offset); + ui.add(egui::Image::new(texture).fit_to_original_size(scale)); log::trace!("Image added to frame {}", count); } @@ -841,6 +910,593 @@ impl BitVaultApp { ui.ctx().request_repaint(); } + // Common function to render a centered onboarding container + fn render_onboarding_container(&self, ui: &mut Ui, render_content: impl FnOnce(&mut Ui)) { + // Set the background to white + let screen_rect = ui.max_rect(); + ui.painter().rect_filled(screen_rect, 0.0, Color32::WHITE); + + // Calculate the available space + let available_width = screen_rect.width(); + let available_height = screen_rect.height(); + + // Content width (fixed at 328px for mobile designs) + let content_width: f32 = 328.0; + let content_height: f32 = 650.0; // Approximate height of content + + // Calculate vertical padding to center content + let min_padding: f32 = 10.0; + let vertical_padding = (available_height - content_height).max(min_padding) / 2.0; + + // Add container that centers content both horizontally and vertically + egui::CentralPanel::default() + .frame(egui::Frame::none()) + .show_inside(ui, |ui| { + ui.vertical_centered(|ui| { + ui.add_space(vertical_padding); + + // Create a container with fixed width but centered horizontally + let min_side_margin: f32 = 20.0; + let container_width = content_width.min(available_width - min_side_margin); + ui.allocate_ui_with_layout( + egui::vec2(container_width, content_height), + egui::Layout::top_down(egui::Align::Center), + render_content, + ); + + ui.add_space(vertical_padding); + }); + }); + } + + fn render_onboarding_one(&self, ui: &mut Ui) { + self.render_onboarding_container(ui, |ui| { + // Upper panel with illustration + ui.add_space(80.0); // Status bar + top spacing + + // Illustration frame - use SVG + ui.allocate_ui(egui::vec2(328.0, 249.0), |ui| { + ui.vertical_centered(|ui| { + // Load and display the SVG image + static TEXTURE_ID: OnceLock> = OnceLock::new(); + + let texture = TEXTURE_ID.get_or_init(|| { + log::debug!("Loading onboarding1.svg - this should only happen once"); + assets::load_svg_as_texture(ui.ctx(), "onboarding1", "assets/onboarding1.svg") + }); + + if let Some(texture) = texture { + // Get texture size and available space + let available_size = ui.available_size(); + let texture_size = texture.size_vec2(); + + // Scale to fit within the available space while preserving aspect ratio + let scale = (available_size.x / texture_size.x) + .min(available_size.y / texture_size.y) + .min(1.0); // Don't scale up if image is smaller + + let display_size = texture_size * scale; + + ui.add_space((available_size.y - display_size.y) / 2.0); // Center vertically + ui.add(egui::Image::new(texture).fit_to_original_size(scale)); + } else { + ui.colored_label(Color32::RED, "Failed to load SVG image"); + + // Fallback to drawn elements if SVG fails to load + ui.add_space(20.0); + + // Draw a shield with keys icon (for multisig) + let center = ui.available_rect_before_wrap().center(); + let shield_size = 120.0; + + // Shield background + ui.painter().circle_filled( + center, + shield_size/2.0, + Color32::from_rgb(240, 240, 240), + ); + + // Shield border + ui.painter().circle_stroke( + center, + shield_size/2.0 + 1.0, + egui::Stroke::new(1.0, Color32::from_rgb(200, 200, 200)), + ); + + // Draw three key symbols + let key_color = Color32::from_rgb(50, 50, 50); + let key_spacing = shield_size * 0.3; + + // Draw three symbolic keys + for i in -1..=1 { + let key_center = center + egui::vec2(i as f32 * key_spacing, 0.0); + + // Key head (circle) + ui.painter().circle_filled( + key_center - egui::vec2(0.0, shield_size * 0.15), + shield_size * 0.08, + key_color, + ); + + // Key shaft + let shaft_rect = egui::Rect::from_min_size( + key_center + egui::vec2(-shield_size * 0.03, -shield_size * 0.05), + egui::vec2(shield_size * 0.06, shield_size * 0.25), + ); + + ui.painter().rect_filled( + shaft_rect, + 2.0, + key_color, + ); + + // Key teeth + let teeth_top = key_center.y + shield_size * 0.08; + let teeth_width = shield_size * 0.04; + let teeth_height = shield_size * 0.06; + + ui.painter().rect_filled( + egui::Rect::from_min_size( + egui::pos2(key_center.x - teeth_width/2.0, teeth_top), + egui::vec2(teeth_width, teeth_height) + ), + 1.0, + key_color, + ); + } + } + }); + }); + + // Content + ui.add_space(32.0); + ui.heading(RichText::new("Multisig security").color(Color32::BLACK).size(24.0)); + ui.add_space(8.0); + ui.label( + RichText::new("Secure your funds with 2-of-3 multisig vaults. For extra security spread the 3 keys across 3 different geolocation and store an extra copy of one key in a physical vault or similar.") + .color(Color32::from_rgb(82, 82, 82)) + .size(14.0) + ); + + // Indicators + ui.add_space(24.0); + self.draw_navigation_arrows(ui, 1); + + // Buttons at the bottom + ui.add_space(32.0); + + if ui.add(egui::Button::new( + RichText::new("Create a new wallet") + .color(Color32::WHITE) + .size(16.0)) + .min_size(egui::vec2(328.0, 48.0)) + .fill(Color32::BLACK) + .rounding(16.0) + ).clicked() { + if let Ok(mut state) = self.state.write() { + state.current_view = View::OnboardingTwo; + } + } + + ui.add_space(8.0); + + if ui.add(egui::Button::new( + RichText::new("I already have a wallet") + .color(Color32::BLACK) + .size(16.0)) + .min_size(egui::vec2(328.0, 48.0)) + .frame(false) + ).clicked() { + if let Ok(mut state) = self.state.write() { + state.current_view = View::Home; + state.onboarding_completed = true; + } + } + + ui.add_space(8.0); + ui.label( + RichText::new("By continuing, I agree to the Terms of Service") + .color(Color32::from_rgb(82, 82, 82)) + .size(12.0) + ); + + // Navigation hint + ui.add_space(4.0); + ui.label( + RichText::new("Tip: Use Left/Right arrow keys to navigate") + .color(Color32::from_rgb(150, 150, 150)) + .size(10.0) + ); + }); + } + + fn render_onboarding_two(&self, ui: &mut Ui) { + self.render_onboarding_container(ui, |ui| { + // Upper panel with illustration + ui.add_space(80.0); // Status bar + top spacing + + // Illustration frame - use SVG + ui.allocate_ui(egui::vec2(328.0, 249.0), |ui| { + ui.vertical_centered(|ui| { + // Load and display the SVG image + static TEXTURE_ID: OnceLock> = OnceLock::new(); + + let texture = TEXTURE_ID.get_or_init(|| { + log::debug!("Loading onboarding2.svg - this should only happen once"); + assets::load_svg_as_texture(ui.ctx(), "onboarding2", "assets/onboarding2.svg") + }); + + if let Some(texture) = texture { + // Get texture size and available space + let available_size = ui.available_size(); + let texture_size = texture.size_vec2(); + + // Scale to fit within the available space while preserving aspect ratio + let scale = (available_size.x / texture_size.x) + .min(available_size.y / texture_size.y) + .min(1.0); // Don't scale up if image is smaller + + let display_size = texture_size * scale; + + ui.add_space((available_size.y - display_size.y) / 2.0); // Center vertically + ui.add(egui::Image::new(texture).fit_to_original_size(scale)); + } else { + ui.colored_label(Color32::RED, "Failed to load SVG image"); + + // Fallback to drawn elements if SVG fails to load + ui.add_space(20.0); + + // Draw a clock (for time delay) + let center = ui.available_rect_before_wrap().center(); + let clock_size = 120.0; + + // Clock face + ui.painter().circle_filled( + center, + clock_size/2.0, + Color32::from_rgb(240, 240, 240), + ); + + // Clock border + ui.painter().circle_stroke( + center, + clock_size/2.0, + egui::Stroke::new(2.0, Color32::from_rgb(50, 50, 50)), + ); + + // Clock hands + let hour_hand = center + egui::vec2(0.0, -clock_size * 0.25); + let minute_hand = center + egui::vec2(clock_size * 0.3, 0.0); + + ui.painter().line_segment( + [center, hour_hand], + egui::Stroke::new(3.0, Color32::BLACK), + ); + + ui.painter().line_segment( + [center, minute_hand], + egui::Stroke::new(3.0, Color32::BLACK), + ); + + // Clock center dot + ui.painter().circle_filled( + center, + 4.0, + Color32::BLACK, + ); + } + }); + }); + + // Content + ui.add_space(32.0); + ui.heading(RichText::new("Time-delay protection").color(Color32::BLACK).size(28.0)); + ui.add_space(8.0); + ui.label( + RichText::new("Set time-delays and prevent unauthorised withdrawals. The xPUB is of VITAL importance to recover your multisig vault. Keep AT LEAST a copy of the xPUB together with each key.") + .color(Color32::from_rgb(82, 82, 82)) + .size(14.0) + ); + + // Indicators + ui.add_space(24.0); + self.draw_navigation_arrows(ui, 2); + + // Buttons at the bottom + ui.add_space(32.0); + + if ui.add(egui::Button::new( + RichText::new("Continue") + .color(Color32::WHITE) + .size(16.0)) + .min_size(egui::vec2(328.0, 48.0)) + .fill(Color32::BLACK) + .rounding(16.0) + ).clicked() { + if let Ok(mut state) = self.state.write() { + state.current_view = View::OnboardingThree; + } + } + + ui.add_space(8.0); + + if ui.add(egui::Button::new( + RichText::new("Back") + .color(Color32::BLACK) + .size(16.0)) + .min_size(egui::vec2(328.0, 48.0)) + .frame(false) + ).clicked() { + if let Ok(mut state) = self.state.write() { + state.current_view = View::OnboardingOne; + } + } + + ui.add_space(8.0); + ui.label( + RichText::new("By continuing, I agree to the Terms of Service") + .color(Color32::from_rgb(82, 82, 82)) + .size(12.0) + ); + + // Navigation hint + ui.add_space(4.0); + ui.label( + RichText::new("Tip: Use Left/Right arrow keys to navigate") + .color(Color32::from_rgb(150, 150, 150)) + .size(10.0) + ); + }); + } + + fn render_onboarding_three(&self, ui: &mut Ui) { + self.render_onboarding_container(ui, |ui| { + // Upper panel with illustration + ui.add_space(80.0); // Status bar + top spacing + + // Illustration frame - use SVG + ui.allocate_ui(egui::vec2(328.0, 249.0), |ui| { + ui.vertical_centered(|ui| { + // Load and display the SVG image + static TEXTURE_ID: OnceLock> = OnceLock::new(); + + let texture = TEXTURE_ID.get_or_init(|| { + log::debug!("Loading onboarding3.svg - this should only happen once"); + assets::load_svg_as_texture(ui.ctx(), "onboarding3", "assets/onboarding3.svg") + }); + + if let Some(texture) = texture { + // Get texture size and available space + let available_size = ui.available_size(); + let texture_size = texture.size_vec2(); + + // Scale to fit within the available space while preserving aspect ratio + let scale = (available_size.x / texture_size.x) + .min(available_size.y / texture_size.y) + .min(1.0); // Don't scale up if image is smaller + + let display_size = texture_size * scale; + + ui.add_space((available_size.y - display_size.y) / 2.0); // Center vertically + ui.add(egui::Image::new(texture).fit_to_original_size(scale)); + } else { + ui.colored_label(Color32::RED, "Failed to load SVG image"); + + // Fallback to the shield rendering if SVG fails + // Center position + let center = ui.min_rect().center(); + + // Draw a shield shape for the notification icon + let shield_size = 100.0; + let shield_radius = shield_size / 2.0; + + // Draw shield background (light gray) + ui.painter().circle_filled( + center, + shield_radius, + Color32::from_rgb(245, 245, 245), + ); + + // Draw shield outline + ui.painter().circle_stroke( + center, + shield_radius, + egui::Stroke::new(1.0, Color32::from_rgb(200, 200, 200)), + ); + + // Draw lock icon inside the shield + let lock_size = 40.0; + let lock_top = center.y - lock_size * 0.2; + let lock_bottom = center.y + lock_size * 0.5; + let lock_left = center.x - lock_size * 0.3; + let lock_right = center.x + lock_size * 0.3; + + // Lock body + let lock_body = egui::Rect::from_min_max( + egui::pos2(lock_left, lock_top), + egui::pos2(lock_right, lock_bottom), + ); + ui.painter().rect_filled( + lock_body, + 5.0, + Color32::from_rgb(30, 30, 30), + ); + + // Lock shackle (arc) + let shackle_radius = lock_size * 0.4; + let shackle_center = egui::pos2(center.x, lock_top - shackle_radius * 0.3); + let shackle_stroke = egui::Stroke::new(6.0, Color32::from_rgb(30, 30, 30)); + + // Draw a semi-circle for the shackle + ui.painter().circle_stroke(shackle_center, shackle_radius, shackle_stroke); + } + }); + }); + + // Content + ui.add_space(32.0); + ui.heading(RichText::new("Secret notifications").color(Color32::BLACK).size(28.0)); + ui.add_space(8.0); + ui.label( + RichText::new("Stay informed about important wallet events and security updates. Secret notifications are end-to-end encrypted to protect your privacy and security.") + .color(Color32::from_rgb(82, 82, 82)) + .size(14.0) + ); + + // Indicators + ui.add_space(24.0); + self.draw_navigation_arrows(ui, 3); + + // Buttons at the bottom + ui.add_space(32.0); + + if ui.add(egui::Button::new( + RichText::new("Let's go!") + .color(Color32::WHITE) + .size(16.0)) + .min_size(egui::vec2(328.0, 48.0)) + .fill(Color32::BLACK) + .rounding(16.0) + ).clicked() { + if let Ok(mut state) = self.state.write() { + state.current_view = View::Home; + state.onboarding_completed = true; + } + } + + ui.add_space(8.0); + + if ui.add(egui::Button::new( + RichText::new("Back") + .color(Color32::BLACK) + .size(16.0)) + .min_size(egui::vec2(328.0, 48.0)) + .frame(false) + ).clicked() { + if let Ok(mut state) = self.state.write() { + state.current_view = View::OnboardingTwo; + } + } + + ui.add_space(8.0); + ui.label( + RichText::new("By continuing, I agree to the Terms of Service") + .color(Color32::from_rgb(82, 82, 82)) + .size(12.0) + ); + + // Navigation hint + ui.add_space(4.0); + ui.label( + RichText::new("Tip: Use Left/Right arrow keys to navigate") + .color(Color32::from_rgb(150, 150, 150)) + .size(10.0) + ); + }); + } + + // Helper function to draw arrow navigation indicators + fn draw_navigation_arrows(&self, ui: &mut Ui, screen_number: usize) { + // Available width needed for centering calculation + let available_width = ui.available_width(); + + // Create fixed-width dots + let active_width = 15.0; + let inactive_width = 5.0; + let dot_height = 4.0; // Slightly thicker for better visibility while still bead-like + let dot_spacing = 3.0; + let click_padding = 12.0; // Larger click area padding for better usability + + // Calculate total width of all dots + let total_dot_width = match screen_number { + 1 => active_width + 2.0 * inactive_width + 2.0 * dot_spacing, + 2 => inactive_width + active_width + inactive_width + 2.0 * dot_spacing, + 3 => 2.0 * inactive_width + active_width + 2.0 * dot_spacing, + _ => active_width + 2.0 * inactive_width + 2.0 * dot_spacing, + }; + + // Add space for centering + let left_padding = (available_width - total_dot_width) / 2.0; + + ui.horizontal(|ui| { + ui.add_space(left_padding); + + // Create a container for our dots with extra height for easier clicking + let response = ui.allocate_rect( + egui::Rect::from_min_size( + ui.cursor().min, + egui::vec2(total_dot_width, dot_height + click_padding), + ), + egui::Sense::click(), // Make the entire area clickable + ); + + // Draw the dots directly using the painter + let painter = ui.painter(); + let mut current_x = response.rect.min.x; + let center_y = response.rect.center().y; + + // Store click positions for later processing + let mut click_areas = Vec::new(); + + // Draw all dots + for i in 1..=3 { + if i > 1 { + current_x += dot_spacing; + } + + // Determine dot properties based on state + let (width, color) = if i == screen_number { + (active_width, Color32::from_rgb(17, 165, 238)) + } else { + (inactive_width, Color32::from_rgb(217, 217, 217)) + }; + + // Calculate the dot rectangle + let dot_rect = egui::Rect::from_min_size( + egui::pos2(current_x, center_y - dot_height / 2.0), + egui::vec2(width, dot_height), + ); + + // Draw the dot + painter.rect_filled(dot_rect, dot_height / 2.0, color); + + // Store click area if this is an inactive dot + if i != screen_number { + // Create a larger clickable area + let click_rect = egui::Rect::from_min_max( + egui::pos2(current_x - 2.0, center_y - (click_padding / 2.0)), + egui::pos2(current_x + width + 2.0, center_y + (click_padding / 2.0)), + ); + + click_areas.push((click_rect, i)); + } + + // Move to the next dot position + current_x += width; + } + + // Handle clicks for navigation + if response.clicked() { + if let Some(mouse_pos) = ui.ctx().pointer_latest_pos() { + // Handle clicks directly on dots + for (rect, idx) in click_areas { + if rect.contains(mouse_pos) { + if let Ok(mut state) = self.state.write() { + state.current_view = match idx { + 1 => View::OnboardingOne, + 2 => View::OnboardingTwo, + 3 => View::OnboardingThree, + _ => View::OnboardingOne, + }; + } + break; + } + } + } + } + }); + } + // Helper function to get the wallet file path fn get_wallet_file_path() -> Option { if let Some(config_dir) = dirs::config_dir() { @@ -883,6 +1539,13 @@ impl BitVaultApp { impl eframe::App for BitVaultApp { fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { + // Check for window resize events and save the new size + let screen_rect = ctx.input(|i| i.screen_rect); + let size = screen_rect.size(); + if size.x != self.settings.window_width || size.y != self.settings.window_height { + self.settings.update_window_size(size.x, size.y); + } + // Always request a repaint when in splash screen mode to ensure timer updates if let Ok(state) = self.state.read() { if state.current_view == View::SplashScreen { @@ -891,6 +1554,45 @@ impl eframe::App for BitVaultApp { } } + // Check for arrow key navigation in onboarding screens + if let Ok(state) = self.state.read() { + if matches!( + state.current_view, + View::OnboardingOne | View::OnboardingTwo | View::OnboardingThree + ) { + // Check for left/right arrow keys + let right_pressed = ctx.input(|i| i.key_pressed(egui::Key::ArrowRight)); + let left_pressed = ctx.input(|i| i.key_pressed(egui::Key::ArrowLeft)); + + // Store current view for use outside of the read lock + let current_view = state.current_view.clone(); + + // Release the read lock before attempting to acquire a write lock + drop(state); + + if right_pressed || left_pressed { + // Now get a write lock to potentially change the view + if let Ok(mut state) = self.state.write() { + match (current_view, right_pressed, left_pressed) { + (View::OnboardingOne, true, _) => { + state.current_view = View::OnboardingTwo + } + (View::OnboardingTwo, true, _) => { + state.current_view = View::OnboardingThree + } + (View::OnboardingTwo, _, true) => { + state.current_view = View::OnboardingOne + } + (View::OnboardingThree, _, true) => { + state.current_view = View::OnboardingTwo + } + _ => {} // No change for other combinations + } + } + } + } + } + // Update the splash timer if active if let Ok(mut state) = self.state.write() { if let Some(timer) = state.splash_timer { @@ -898,8 +1600,15 @@ impl eframe::App for BitVaultApp { if new_timer <= 0.0 { state.splash_timer = None; - // Transition to the appropriate view after splash screen - if state.wallet_state == WalletState::Locked { + // When in testing mode, bypass lock screen and go to onboarding + if state.testing_mode { + state.current_view = View::OnboardingOne; + log::info!( + "Testing mode active: Bypassing lock screen and showing onboarding" + ); + } + // Normal flow - go to lock screen or home + else if state.wallet_state == WalletState::Locked { state.current_view = View::LockScreen; } else { state.current_view = View::Home; @@ -941,6 +1650,9 @@ impl eframe::App for BitVaultApp { View::Wallet => self.render_wallet(ui), View::LockScreen => self.render_lock_screen(ui), View::SplashScreen => self.render_splash_screen(ui), + View::OnboardingOne => self.render_onboarding_one(ui), + View::OnboardingTwo => self.render_onboarding_two(ui), + View::OnboardingThree => self.render_onboarding_three(ui), } }); } diff --git a/bitvault-ui/src/config.rs b/bitvault-ui/src/config.rs new file mode 100644 index 0000000..2216997 --- /dev/null +++ b/bitvault-ui/src/config.rs @@ -0,0 +1,96 @@ +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; + +// Settings struct to persist application settings +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Settings { + pub window_width: f32, + pub window_height: f32, +} + +impl Default for Settings { + fn default() -> Self { + Settings { + window_width: 1440.0, + window_height: 900.0, + } + } +} + +impl Settings { + // Helper function to get the settings file path + pub fn get_settings_file_path() -> Option { + if let Some(config_dir) = dirs::config_dir() { + let app_config_dir = config_dir.join("bitvault"); + + // Create directory if it doesn't exist + if !app_config_dir.exists() && fs::create_dir_all(&app_config_dir).is_err() { + return None; + } + + return Some(app_config_dir.join("settings.toml")); + } + None + } + + // Load settings from TOML file + pub fn load() -> Self { + if let Some(file_path) = Self::get_settings_file_path() { + if file_path.exists() { + match fs::read_to_string(&file_path) { + Ok(toml_str) => match toml::from_str::(&toml_str) { + Ok(settings) => { + log::info!("Settings loaded successfully"); + return settings; + } + Err(e) => { + log::error!("Failed to parse settings file: {}", e); + } + }, + Err(e) => { + log::error!("Failed to read settings file: {}", e); + } + } + } else { + log::info!("No settings file found, using defaults"); + } + } else { + log::error!("Could not determine settings file path"); + } + + // Return default settings if we couldn't load from file + Settings::default() + } + + // Save settings to TOML file + pub fn save(&self) -> Result<(), String> { + if let Some(file_path) = Self::get_settings_file_path() { + match toml::to_string(self) { + Ok(toml_str) => fs::write(file_path, toml_str) + .map_err(|e| format!("Failed to save settings: {}", e)), + Err(e) => Err(format!("Failed to serialize settings: {}", e)), + } + } else { + Err("Could not determine settings file path".to_string()) + } + } + + // Update window size settings + pub fn update_window_size(&mut self, width: f32, height: f32) -> bool { + if self.window_width != width || self.window_height != height { + self.window_width = width; + self.window_height = height; + + // Save settings right away + if let Err(e) = self.save() { + log::error!("Failed to save window size: {}", e); + return false; + } + + log::debug!("Window size saved: {}x{}", width, height); + return true; + } + false + } +} diff --git a/bitvault-ui/src/main.rs b/bitvault-ui/src/main.rs index 72c4e5d..65018eb 100644 --- a/bitvault-ui/src/main.rs +++ b/bitvault-ui/src/main.rs @@ -1,4 +1,5 @@ mod app; +mod config; mod icons; mod wallet; @@ -12,9 +13,12 @@ fn main() { .init() .unwrap(); + // Load settings for the initial window size + let settings = config::Settings::load(); + let native_options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() - .with_inner_size([800.0, 600.0]) + .with_inner_size([settings.window_width, settings.window_height]) .with_min_inner_size([400.0, 300.0]), centered: true, ..Default::default()