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