accounts.rs (6650B)
1 use egui::{ 2 Align, Button, Frame, InnerResponse, Layout, RichText, ScrollArea, Ui, UiBuilder, Vec2, 3 }; 4 use enostr::Pubkey; 5 use nostrdb::{Ndb, Transaction}; 6 use notedeck::{tr, Accounts, Images, Localization}; 7 use notedeck_ui::colors::PINK; 8 use notedeck_ui::profile::preview::SimpleProfilePreview; 9 10 use notedeck_ui::app_images; 11 12 pub struct AccountsView<'a> { 13 ndb: &'a Ndb, 14 accounts: &'a Accounts, 15 img_cache: &'a mut Images, 16 i18n: &'a mut Localization, 17 } 18 19 #[derive(Clone, Debug)] 20 pub enum AccountsViewResponse { 21 SelectAccount(Pubkey), 22 RemoveAccount(Pubkey), 23 RouteToLogin, 24 } 25 26 #[derive(Debug)] 27 enum ProfilePreviewAction { 28 RemoveAccount, 29 SwitchTo, 30 } 31 32 impl<'a> AccountsView<'a> { 33 pub fn new( 34 ndb: &'a Ndb, 35 accounts: &'a Accounts, 36 img_cache: &'a mut Images, 37 i18n: &'a mut Localization, 38 ) -> Self { 39 AccountsView { 40 ndb, 41 accounts, 42 img_cache, 43 i18n, 44 } 45 } 46 47 pub fn ui(&mut self, ui: &mut Ui) -> InnerResponse<Option<AccountsViewResponse>> { 48 Frame::new().outer_margin(12.0).show(ui, |ui| { 49 if let Some(resp) = Self::top_section_buttons_widget(ui, self.i18n).inner { 50 return Some(resp); 51 } 52 53 ui.add_space(8.0); 54 scroll_area() 55 .id_salt(AccountsView::scroll_id()) 56 .show(ui, |ui| { 57 Self::show_accounts(ui, self.accounts, self.ndb, self.img_cache, self.i18n) 58 }) 59 .inner 60 }) 61 } 62 63 pub fn scroll_id() -> egui::Id { 64 egui::Id::new("accounts") 65 } 66 67 fn show_accounts( 68 ui: &mut Ui, 69 accounts: &Accounts, 70 ndb: &Ndb, 71 img_cache: &mut Images, 72 i18n: &mut Localization, 73 ) -> Option<AccountsViewResponse> { 74 let mut return_op: Option<AccountsViewResponse> = None; 75 ui.allocate_ui_with_layout( 76 Vec2::new(ui.available_size_before_wrap().x, 32.0), 77 Layout::top_down(egui::Align::Min), 78 |ui| { 79 let txn = if let Ok(txn) = Transaction::new(ndb) { 80 txn 81 } else { 82 return; 83 }; 84 85 let selected = accounts.cache.selected(); 86 for (pk, account) in &accounts.cache { 87 let profile = ndb.get_profile_by_pubkey(&txn, pk).ok(); 88 let is_selected = *pk == selected.key.pubkey; 89 let has_nsec = account.key.secret_key.is_some(); 90 91 let profile_peview_view = { 92 let max_size = egui::vec2(ui.available_width(), 77.0); 93 let resp = ui.allocate_response(max_size, egui::Sense::click()); 94 ui.allocate_new_ui(UiBuilder::new().max_rect(resp.rect), |ui| { 95 let preview = SimpleProfilePreview::new( 96 profile.as_ref(), 97 img_cache, 98 i18n, 99 has_nsec, 100 ); 101 show_profile_card(ui, preview, max_size, is_selected, resp) 102 }) 103 .inner 104 }; 105 106 if let Some(op) = profile_peview_view { 107 return_op = Some(match op { 108 ProfilePreviewAction::SwitchTo => { 109 AccountsViewResponse::SelectAccount(*pk) 110 } 111 ProfilePreviewAction::RemoveAccount => { 112 AccountsViewResponse::RemoveAccount(*pk) 113 } 114 }); 115 } 116 } 117 }, 118 ); 119 return_op 120 } 121 122 fn top_section_buttons_widget( 123 ui: &mut egui::Ui, 124 i18n: &mut Localization, 125 ) -> InnerResponse<Option<AccountsViewResponse>> { 126 ui.allocate_ui_with_layout( 127 Vec2::new(ui.available_size_before_wrap().x, 32.0), 128 Layout::left_to_right(egui::Align::Center), 129 |ui| { 130 if ui.add(add_account_button(i18n)).clicked() { 131 Some(AccountsViewResponse::RouteToLogin) 132 } else { 133 None 134 } 135 }, 136 ) 137 } 138 } 139 140 fn show_profile_card( 141 ui: &mut egui::Ui, 142 preview: SimpleProfilePreview, 143 max_size: egui::Vec2, 144 is_selected: bool, 145 card_resp: egui::Response, 146 ) -> Option<ProfilePreviewAction> { 147 let mut op: Option<ProfilePreviewAction> = None; 148 149 ui.add_sized(max_size, |ui: &mut egui::Ui| { 150 let mut frame = Frame::new(); 151 if is_selected || card_resp.hovered() { 152 frame = frame.fill(ui.visuals().noninteractive().weak_bg_fill); 153 } 154 if is_selected { 155 frame = frame.stroke(ui.visuals().noninteractive().fg_stroke); 156 } 157 frame 158 .corner_radius(8.0) 159 .inner_margin(8.0) 160 .show(ui, |ui| { 161 ui.horizontal(|ui| { 162 let btn = sign_out_button(preview.i18n); 163 ui.add(preview); 164 165 ui.with_layout(Layout::right_to_left(Align::Center), |ui| { 166 if card_resp.clicked() { 167 op = Some(ProfilePreviewAction::SwitchTo); 168 } 169 if ui.add_sized(egui::Vec2::new(84.0, 32.0), btn).clicked() { 170 op = Some(ProfilePreviewAction::RemoveAccount) 171 } 172 }); 173 }); 174 }) 175 .response 176 }); 177 ui.add_space(8.0); 178 op 179 } 180 181 fn scroll_area() -> ScrollArea { 182 egui::ScrollArea::vertical() 183 .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden) 184 .auto_shrink([false; 2]) 185 } 186 187 fn add_account_button(i18n: &mut Localization) -> Button<'static> { 188 Button::image_and_text( 189 app_images::add_account_image().fit_to_exact_size(Vec2::new(48.0, 48.0)), 190 RichText::new(tr!( 191 i18n, 192 "Add account", 193 "Button label to add a new account" 194 )) 195 .size(16.0) 196 // TODO: this color should not be hard coded. Find some way to add it to the visuals 197 .color(PINK), 198 ) 199 .frame(false) 200 } 201 202 fn sign_out_button(i18n: &mut Localization) -> egui::Button<'static> { 203 egui::Button::new(RichText::new(tr!( 204 i18n, 205 "Sign out", 206 "Button label to sign out of account" 207 ))) 208 }