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