accounts.rs (7598B)
1 use crate::colors::PINK; 2 use crate::imgcache::ImageCache; 3 use crate::{ 4 accounts::Accounts, 5 route::{Route, Router}, 6 ui::{Preview, PreviewConfig, View}, 7 Damus, 8 }; 9 use egui::{ 10 Align, Button, Frame, Image, InnerResponse, Layout, RichText, ScrollArea, Ui, UiBuilder, Vec2, 11 }; 12 use nostrdb::{Ndb, Transaction}; 13 14 use super::profile::preview::SimpleProfilePreview; 15 16 pub struct AccountsView<'a> { 17 ndb: &'a Ndb, 18 accounts: &'a Accounts, 19 img_cache: &'a mut ImageCache, 20 } 21 22 #[derive(Clone, Debug)] 23 pub enum AccountsViewResponse { 24 SelectAccount(usize), 25 RemoveAccount(usize), 26 RouteToLogin, 27 } 28 29 #[derive(Debug)] 30 enum ProfilePreviewAction { 31 RemoveAccount, 32 SwitchTo, 33 } 34 35 impl<'a> AccountsView<'a> { 36 pub fn new(ndb: &'a Ndb, accounts: &'a Accounts, img_cache: &'a mut ImageCache) -> Self { 37 AccountsView { 38 ndb, 39 accounts, 40 img_cache, 41 } 42 } 43 44 pub fn ui(&mut self, ui: &mut Ui) -> InnerResponse<Option<AccountsViewResponse>> { 45 Frame::none().outer_margin(12.0).show(ui, |ui| { 46 if let Some(resp) = Self::top_section_buttons_widget(ui).inner { 47 return Some(resp); 48 } 49 50 ui.add_space(8.0); 51 scroll_area() 52 .show(ui, |ui| { 53 Self::show_accounts(ui, self.accounts, self.ndb, self.img_cache) 54 }) 55 .inner 56 }) 57 } 58 59 fn show_accounts( 60 ui: &mut Ui, 61 accounts: &Accounts, 62 ndb: &Ndb, 63 img_cache: &mut ImageCache, 64 ) -> Option<AccountsViewResponse> { 65 let mut return_op: Option<AccountsViewResponse> = None; 66 ui.allocate_ui_with_layout( 67 Vec2::new(ui.available_size_before_wrap().x, 32.0), 68 Layout::top_down(egui::Align::Min), 69 |ui| { 70 let txn = if let Ok(txn) = Transaction::new(ndb) { 71 txn 72 } else { 73 return; 74 }; 75 76 for i in 0..accounts.num_accounts() { 77 let (account_pubkey, has_nsec) = match accounts.get_account(i) { 78 Some(acc) => (acc.pubkey.bytes(), acc.secret_key.is_some()), 79 None => continue, 80 }; 81 82 let profile = ndb.get_profile_by_pubkey(&txn, account_pubkey).ok(); 83 let is_selected = if let Some(selected) = accounts.get_selected_account_index() 84 { 85 i == selected 86 } else { 87 false 88 }; 89 90 let profile_peview_view = { 91 let max_size = egui::vec2(ui.available_width(), 77.0); 92 let resp = ui.allocate_response(max_size, egui::Sense::click()); 93 ui.allocate_new_ui(UiBuilder::new().max_rect(resp.rect), |ui| { 94 let preview = 95 SimpleProfilePreview::new(profile.as_ref(), img_cache, has_nsec); 96 show_profile_card(ui, preview, max_size, is_selected, resp) 97 }) 98 .inner 99 }; 100 101 if let Some(op) = profile_peview_view { 102 return_op = Some(match op { 103 ProfilePreviewAction::SwitchTo => { 104 AccountsViewResponse::SelectAccount(i) 105 } 106 ProfilePreviewAction::RemoveAccount => { 107 AccountsViewResponse::RemoveAccount(i) 108 } 109 }); 110 } 111 } 112 }, 113 ); 114 return_op 115 } 116 117 fn top_section_buttons_widget( 118 ui: &mut egui::Ui, 119 ) -> InnerResponse<Option<AccountsViewResponse>> { 120 ui.allocate_ui_with_layout( 121 Vec2::new(ui.available_size_before_wrap().x, 32.0), 122 Layout::left_to_right(egui::Align::Center), 123 |ui| { 124 if ui.add(add_account_button()).clicked() { 125 Some(AccountsViewResponse::RouteToLogin) 126 } else { 127 None 128 } 129 }, 130 ) 131 } 132 } 133 134 fn show_profile_card( 135 ui: &mut egui::Ui, 136 preview: SimpleProfilePreview, 137 max_size: egui::Vec2, 138 is_selected: bool, 139 card_resp: egui::Response, 140 ) -> Option<ProfilePreviewAction> { 141 let mut op: Option<ProfilePreviewAction> = None; 142 143 ui.add_sized(max_size, |ui: &mut egui::Ui| { 144 let mut frame = Frame::none(); 145 if is_selected || card_resp.hovered() { 146 frame = frame.fill(ui.visuals().noninteractive().weak_bg_fill); 147 } 148 if is_selected { 149 frame = frame.stroke(ui.visuals().noninteractive().fg_stroke); 150 } 151 frame 152 .rounding(8.0) 153 .inner_margin(8.0) 154 .show(ui, |ui| { 155 ui.horizontal(|ui| { 156 ui.add(preview); 157 158 ui.with_layout(Layout::right_to_left(Align::Center), |ui| { 159 if card_resp.clicked() { 160 op = Some(ProfilePreviewAction::SwitchTo); 161 } 162 if ui 163 .add_sized(egui::Vec2::new(84.0, 32.0), sign_out_button()) 164 .clicked() 165 { 166 op = Some(ProfilePreviewAction::RemoveAccount) 167 } 168 }); 169 }); 170 }) 171 .response 172 }); 173 ui.add_space(8.0); 174 op 175 } 176 177 fn scroll_area() -> ScrollArea { 178 egui::ScrollArea::vertical() 179 .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden) 180 .auto_shrink([false; 2]) 181 } 182 183 fn add_account_button() -> Button<'static> { 184 let img_data = egui::include_image!("../../assets/icons/add_account_icon_4x.png"); 185 let img = Image::new(img_data).fit_to_exact_size(Vec2::new(48.0, 48.0)); 186 Button::image_and_text( 187 img, 188 RichText::new(" Add account") 189 .size(16.0) 190 // TODO: this color should not be hard coded. Find some way to add it to the visuals 191 .color(PINK), 192 ) 193 .frame(false) 194 } 195 196 fn sign_out_button() -> egui::Button<'static> { 197 egui::Button::new(RichText::new("Sign out")) 198 } 199 200 // PREVIEWS 201 mod preview { 202 203 use super::*; 204 use crate::{accounts::process_accounts_view_response, test_data}; 205 206 pub struct AccountsPreview { 207 app: Damus, 208 router: Router<Route>, 209 } 210 211 impl AccountsPreview { 212 fn new() -> Self { 213 let app = test_data::test_app(); 214 let router = Router::new(vec![Route::accounts()]); 215 216 AccountsPreview { app, router } 217 } 218 } 219 220 impl View for AccountsPreview { 221 fn ui(&mut self, ui: &mut egui::Ui) { 222 ui.add_space(24.0); 223 // TODO(jb55): maybe just use render_nav here so we can step through routes 224 if let Some(response) = 225 AccountsView::new(&self.app.ndb, &self.app.accounts, &mut self.app.img_cache) 226 .ui(ui) 227 .inner 228 { 229 process_accounts_view_response(self.app.accounts_mut(), response, &mut self.router); 230 } 231 } 232 } 233 234 impl Preview for AccountsView<'_> { 235 type Prev = AccountsPreview; 236 237 fn preview(_cfg: PreviewConfig) -> Self::Prev { 238 AccountsPreview::new() 239 } 240 } 241 }