account_login_view.rs (12291B)
1 use crate::app_style::NotedeckTextStyle; 2 use crate::key_parsing::LoginError; 3 use crate::login_manager::LoginManager; 4 use crate::ui::{Preview, PreviewConfig, View}; 5 use egui::{ 6 Align, Align2, Button, Color32, Frame, Id, LayerId, Margin, Pos2, Rect, RichText, Rounding, Ui, 7 Vec2, Window, 8 }; 9 use egui::{Image, TextEdit}; 10 11 pub struct AccountLoginView<'a> { 12 is_mobile: bool, 13 manager: &'a mut LoginManager, 14 generate_y_intercept: Option<f32>, 15 } 16 17 impl<'a> View for AccountLoginView<'a> { 18 fn ui(&mut self, ui: &mut egui::Ui) { 19 if let Some(_key) = self.manager.check_for_successful_login() { 20 // TODO: route to "home" 21 /* 22 return if self.mobile { 23 // route to "home" on mobile 24 } else { 25 // route to "home" on desktop 26 }; 27 */ 28 } 29 if self.is_mobile { 30 self.show_mobile(ui); 31 } else { 32 self.ui(ui); 33 } 34 } 35 } 36 37 impl<'a> AccountLoginView<'a> { 38 pub fn new(manager: &'a mut LoginManager, is_mobile: bool) -> Self { 39 AccountLoginView { 40 is_mobile, 41 manager, 42 generate_y_intercept: None, 43 } 44 } 45 46 fn ui(&mut self, ui: &mut egui::Ui) -> egui::Response { 47 let screen_width = ui.ctx().screen_rect().max.x; 48 let screen_height = ui.ctx().screen_rect().max.y; 49 50 let title_layer = LayerId::new(egui::Order::Background, Id::new("Title layer")); 51 52 let mut top_panel_height: Option<f32> = None; 53 ui.with_layer_id(title_layer, |ui| { 54 egui::TopBottomPanel::top("Top") 55 .resizable(false) 56 .default_height(340.0) 57 .frame(Frame::none()) 58 .show_separator_line(false) 59 .show_inside(ui, |ui| { 60 top_panel_height = Some(ui.available_rect_before_wrap().bottom()); 61 self.top_title_area(ui); 62 }); 63 }); 64 65 egui::TopBottomPanel::bottom("Bottom") 66 .resizable(false) 67 .frame(Frame::none()) 68 .show_separator_line(false) 69 .show_inside(ui, |ui| { 70 self.window(ui, top_panel_height.unwrap_or(0.0)); 71 }); 72 73 let top_rect = Rect { 74 min: Pos2::ZERO, 75 max: Pos2::new( 76 screen_width, 77 self.generate_y_intercept.unwrap_or(screen_height * 0.5), 78 ), 79 }; 80 81 let top_background_color = ui.visuals().noninteractive().bg_fill; 82 ui.painter_at(top_rect) 83 .with_layer_id(LayerId::background()) 84 .rect_filled(top_rect, Rounding::ZERO, top_background_color); 85 86 egui::CentralPanel::default() 87 .show(ui.ctx(), |_ui: &mut egui::Ui| {}) 88 .response 89 } 90 91 fn mobile_ui(&mut self, ui: &mut egui::Ui) -> egui::Response { 92 ui.vertical(|ui| { 93 ui.vertical_centered(|ui| { 94 ui.add(logo_unformatted().max_width(256.0)); 95 ui.add_space(64.0); 96 ui.label(login_info_text()); 97 ui.add_space(32.0); 98 ui.label(login_title_text()); 99 }); 100 101 ui.horizontal(|ui| { 102 ui.label(login_textedit_info_text()); 103 }); 104 105 ui.vertical_centered_justified(|ui| { 106 ui.add(login_textedit(self.manager)); 107 108 self.loading_and_error(ui); 109 110 if ui.add(login_button()).clicked() { 111 self.manager.apply_login(); 112 } 113 }); 114 115 ui.horizontal(|ui| { 116 ui.label( 117 RichText::new("New to Nostr?") 118 .color(ui.style().visuals.noninteractive().fg_stroke.color) 119 .text_style(NotedeckTextStyle::Body.text_style()), 120 ); 121 122 if ui 123 .add(Button::new(RichText::new("Create Account")).frame(false)) 124 .clicked() 125 { 126 // TODO: navigate to 'create account' screen 127 } 128 }); 129 }) 130 .response 131 } 132 133 pub fn show_mobile(&mut self, ui: &mut egui::Ui) -> egui::Response { 134 egui::CentralPanel::default() 135 .show(ui.ctx(), |_| { 136 Window::new("Login") 137 .movable(true) 138 .constrain(true) 139 .collapsible(false) 140 .drag_to_scroll(false) 141 .title_bar(false) 142 .resizable(false) 143 .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) 144 .frame(Frame::central_panel(&ui.ctx().style())) 145 .max_width(ui.ctx().screen_rect().width() - 32.0) // margin 146 .show(ui.ctx(), |ui| self.mobile_ui(ui)); 147 }) 148 .response 149 } 150 151 fn window(&mut self, ui: &mut Ui, top_panel_height: f32) { 152 let needed_height_over_top = (ui.ctx().screen_rect().bottom() / 2.0) - 230.0; 153 let y_offset = if top_panel_height > needed_height_over_top { 154 top_panel_height - needed_height_over_top 155 } else { 156 0.0 157 }; 158 Window::new("Account login") 159 .movable(false) 160 .constrain(true) 161 .collapsible(false) 162 .drag_to_scroll(false) 163 .title_bar(false) 164 .resizable(false) 165 .anchor(Align2::CENTER_CENTER, [0f32, y_offset]) 166 .max_width(538.0) 167 .frame(egui::Frame::window(ui.style()).inner_margin(Margin::ZERO)) 168 .show(ui.ctx(), |ui| { 169 ui.vertical_centered(|ui| { 170 ui.add_space(40.0); 171 172 ui.label(login_title_text()); 173 174 ui.add_space(16f32); 175 176 ui.label(login_window_info_text(ui)); 177 178 ui.add_space(24.0); 179 180 Frame::none() 181 .outer_margin(Margin::symmetric(48.0, 0.0)) 182 .show(ui, |ui| { 183 self.login_form(ui); 184 }); 185 186 ui.add_space(32.0); 187 188 let y_margin: f32 = 24.0; 189 let generate_frame = egui::Frame::default() 190 .fill(ui.style().noninteractive().bg_fill) // TODO: gradient 191 .rounding(ui.style().visuals.window_rounding) 192 .stroke(ui.style().noninteractive().bg_stroke) 193 .inner_margin(Margin::symmetric(48.0, y_margin)); 194 195 generate_frame.show(ui, |ui| { 196 self.generate_y_intercept = 197 Some(ui.available_rect_before_wrap().top() - y_margin); 198 self.generate_group(ui); 199 }); 200 }); 201 }); 202 } 203 204 fn top_title_area(&mut self, ui: &mut egui::Ui) { 205 ui.vertical_centered(|ui| { 206 ui.add(logo_unformatted().max_width(232.0)); 207 208 ui.add_space(48.0); 209 210 let welcome_data = egui::include_image!("../../assets/Welcome to Nostrdeck 2x.png"); 211 ui.add(egui::Image::new(welcome_data).max_width(528.0)); 212 213 ui.add_space(12.0); 214 215 // ui.label( 216 // RichText::new("Welcome to Nostrdeck") 217 // .size(48.0) 218 // .strong() 219 // .line_height(Some(72.0)), 220 // ); 221 ui.label(login_info_text()); 222 }); 223 } 224 225 fn login_form(&mut self, ui: &mut egui::Ui) { 226 ui.vertical_centered_justified(|ui| { 227 ui.horizontal(|ui| { 228 ui.label(login_textedit_info_text()); 229 }); 230 231 ui.add_space(8f32); 232 233 ui.add(login_textedit(self.manager).min_size(Vec2::new(440.0, 40.0))); 234 235 self.loading_and_error(ui); 236 237 let login_button = login_button().min_size(Vec2::new(442.0, 40.0)); 238 239 if ui.add(login_button).clicked() { 240 self.manager.apply_login() 241 } 242 }); 243 } 244 245 fn loading_and_error(&mut self, ui: &mut egui::Ui) { 246 ui.add_space(8.0); 247 248 ui.vertical_centered(|ui| { 249 if self.manager.is_awaiting_network() { 250 ui.add(egui::Spinner::new()); 251 } 252 }); 253 254 if let Some(err) = self.manager.check_for_error() { 255 show_error(ui, err); 256 } 257 258 ui.add_space(8.0); 259 } 260 261 fn generate_group(&mut self, ui: &mut egui::Ui) { 262 ui.horizontal(|ui| { 263 ui.label( 264 RichText::new("New in nostr?").text_style(NotedeckTextStyle::Heading3.text_style()), 265 ); 266 267 ui.label( 268 RichText::new(" — we got you!") 269 .text_style(NotedeckTextStyle::Heading3.text_style()) 270 .color(ui.visuals().noninteractive().fg_stroke.color), 271 ); 272 }); 273 274 ui.add_space(6.0); 275 276 ui.horizontal(|ui| { 277 ui.label(generate_info_text().color(ui.visuals().noninteractive().fg_stroke.color)); 278 }); 279 280 ui.add_space(16.0); 281 282 let generate_button = generate_keys_button().min_size(Vec2::new(442.0, 40.0)); 283 if ui.add(generate_button).clicked() { 284 // TODO: keygen 285 } 286 } 287 } 288 289 fn show_error(ui: &mut egui::Ui, err: &LoginError) { 290 ui.horizontal(|ui| { 291 let error_label = match err { 292 LoginError::InvalidKey => { 293 egui::Label::new(RichText::new("Invalid key.").color(ui.visuals().error_fg_color)) 294 } 295 LoginError::Nip05Failed(e) => { 296 egui::Label::new(RichText::new(e).color(ui.visuals().error_fg_color)) 297 } 298 }; 299 ui.add(error_label.truncate()); 300 }); 301 } 302 303 fn login_title_text() -> RichText { 304 RichText::new("Login") 305 .text_style(NotedeckTextStyle::Heading2.text_style()) 306 .strong() 307 } 308 309 fn login_info_text() -> RichText { 310 RichText::new("The best alternative to tweetDeck built in nostr protocol") 311 .text_style(NotedeckTextStyle::Heading3.text_style()) 312 } 313 314 fn login_window_info_text(ui: &Ui) -> RichText { 315 RichText::new("Enter your private key to start using Notedeck") 316 .text_style(NotedeckTextStyle::Body.text_style()) 317 .color(ui.visuals().noninteractive().fg_stroke.color) 318 } 319 320 fn login_textedit_info_text() -> RichText { 321 RichText::new("Enter your key") 322 .strong() 323 .text_style(NotedeckTextStyle::Body.text_style()) 324 } 325 326 fn logo_unformatted() -> Image<'static> { 327 let logo_gradient_data = egui::include_image!("../../assets/Logo-Gradient-2x.png"); 328 return egui::Image::new(logo_gradient_data); 329 } 330 331 fn generate_info_text() -> RichText { 332 RichText::new("Quickly generate your keys. Make sure you save them safely.") 333 .text_style(NotedeckTextStyle::Body.text_style()) 334 } 335 336 fn generate_keys_button() -> Button<'static> { 337 Button::new(RichText::new("Generate keys").text_style(NotedeckTextStyle::Body.text_style())) 338 } 339 340 fn login_button() -> Button<'static> { 341 Button::new( 342 RichText::new("Login now — let's do this!") 343 .text_style(NotedeckTextStyle::Body.text_style()) 344 .strong(), 345 ) 346 .fill(Color32::from_rgb(0xF8, 0x69, 0xB6)) // TODO: gradient 347 .min_size(Vec2::new(0.0, 40.0)) 348 } 349 350 fn login_textedit(manager: &mut LoginManager) -> TextEdit { 351 manager.get_login_textedit(|text| { 352 egui::TextEdit::singleline(text) 353 .hint_text( 354 RichText::new("Your key here...").text_style(NotedeckTextStyle::Body.text_style()), 355 ) 356 .vertical_align(Align::Center) 357 .min_size(Vec2::new(0.0, 40.0)) 358 .margin(Margin::same(12.0)) 359 }) 360 } 361 362 pub struct AccountLoginPreview { 363 is_mobile: bool, 364 manager: LoginManager, 365 } 366 367 impl View for AccountLoginPreview { 368 fn ui(&mut self, ui: &mut egui::Ui) { 369 AccountLoginView::new(&mut self.manager, self.is_mobile).ui(ui); 370 } 371 } 372 373 impl<'a> Preview for AccountLoginView<'a> { 374 type Prev = AccountLoginPreview; 375 376 fn preview(cfg: PreviewConfig) -> Self::Prev { 377 let manager = LoginManager::new(); 378 let is_mobile = cfg.is_mobile; 379 AccountLoginPreview { is_mobile, manager } 380 } 381 }