preview.rs (9963B)
1 use crate::ui::ProfilePic; 2 use crate::{colors, images, DisplayName}; 3 use egui::load::TexturePoll; 4 use egui::{Frame, Label, RichText, Sense, Widget}; 5 use egui_extras::Size; 6 use enostr::{NoteId, Pubkey}; 7 use nostrdb::{Ndb, ProfileRecord, Transaction}; 8 9 use notedeck::{DataPath, DataPathType, ImageCache, NotedeckTextStyle, UserAccount}; 10 11 pub struct ProfilePreview<'a, 'cache> { 12 profile: &'a ProfileRecord<'a>, 13 cache: &'cache mut ImageCache, 14 banner_height: Size, 15 } 16 17 impl<'a, 'cache> ProfilePreview<'a, 'cache> { 18 pub fn new(profile: &'a ProfileRecord<'a>, cache: &'cache mut ImageCache) -> Self { 19 let banner_height = Size::exact(80.0); 20 ProfilePreview { 21 profile, 22 cache, 23 banner_height, 24 } 25 } 26 27 pub fn banner_height(&mut self, size: Size) { 28 self.banner_height = size; 29 } 30 31 fn banner_texture( 32 ui: &mut egui::Ui, 33 profile: &ProfileRecord<'_>, 34 ) -> Option<egui::load::SizedTexture> { 35 // TODO: cache banner 36 let banner = profile.record().profile().and_then(|p| p.banner()); 37 38 if let Some(banner) = banner { 39 let texture_load_res = 40 egui::Image::new(banner).load_for_size(ui.ctx(), ui.available_size()); 41 if let Ok(texture_poll) = texture_load_res { 42 match texture_poll { 43 TexturePoll::Pending { .. } => {} 44 TexturePoll::Ready { texture, .. } => return Some(texture), 45 } 46 } 47 } 48 49 None 50 } 51 52 fn banner(ui: &mut egui::Ui, profile: &ProfileRecord<'_>) -> egui::Response { 53 if let Some(texture) = Self::banner_texture(ui, profile) { 54 images::aspect_fill( 55 ui, 56 Sense::hover(), 57 texture.id, 58 texture.size.x / texture.size.y, 59 ) 60 } else { 61 // TODO: default banner texture 62 ui.label("") 63 } 64 } 65 66 fn body(self, ui: &mut egui::Ui) { 67 let padding = 12.0; 68 crate::ui::padding(padding, ui, |ui| { 69 let mut pfp_rect = ui.available_rect_before_wrap(); 70 let size = 80.0; 71 pfp_rect.set_width(size); 72 pfp_rect.set_height(size); 73 let pfp_rect = pfp_rect.translate(egui::vec2(0.0, -(padding + 2.0 + (size / 2.0)))); 74 75 ui.put( 76 pfp_rect, 77 ProfilePic::new(self.cache, get_profile_url(Some(self.profile))).size(size), 78 ); 79 ui.add(display_name_widget( 80 get_display_name(Some(self.profile)), 81 false, 82 )); 83 ui.add(about_section_widget(self.profile)); 84 }); 85 } 86 } 87 88 impl egui::Widget for ProfilePreview<'_, '_> { 89 fn ui(self, ui: &mut egui::Ui) -> egui::Response { 90 ui.vertical(|ui| { 91 ui.add_sized([ui.available_size().x, 80.0], |ui: &mut egui::Ui| { 92 ProfilePreview::banner(ui, self.profile) 93 }); 94 95 self.body(ui); 96 }) 97 .response 98 } 99 } 100 101 pub struct SimpleProfilePreview<'a, 'cache> { 102 profile: Option<&'a ProfileRecord<'a>>, 103 cache: &'cache mut ImageCache, 104 is_nsec: bool, 105 } 106 107 impl<'a, 'cache> SimpleProfilePreview<'a, 'cache> { 108 pub fn new( 109 profile: Option<&'a ProfileRecord<'a>>, 110 cache: &'cache mut ImageCache, 111 is_nsec: bool, 112 ) -> Self { 113 SimpleProfilePreview { 114 profile, 115 cache, 116 is_nsec, 117 } 118 } 119 } 120 121 impl egui::Widget for SimpleProfilePreview<'_, '_> { 122 fn ui(self, ui: &mut egui::Ui) -> egui::Response { 123 Frame::none() 124 .show(ui, |ui| { 125 ui.add(ProfilePic::new(self.cache, get_profile_url(self.profile)).size(48.0)); 126 ui.vertical(|ui| { 127 ui.add(display_name_widget(get_display_name(self.profile), true)); 128 if !self.is_nsec { 129 ui.add( 130 Label::new( 131 RichText::new("Read only") 132 .size(notedeck::fonts::get_font_size( 133 ui.ctx(), 134 &NotedeckTextStyle::Tiny, 135 )) 136 .color(ui.visuals().warn_fg_color), 137 ) 138 .selectable(false), 139 ); 140 } 141 }); 142 }) 143 .response 144 } 145 } 146 147 mod previews { 148 use super::*; 149 use crate::test_data::test_profile_record; 150 use crate::ui::{Preview, PreviewConfig, View}; 151 152 pub struct ProfilePreviewPreview<'a> { 153 profile: ProfileRecord<'a>, 154 cache: ImageCache, 155 } 156 157 impl ProfilePreviewPreview<'_> { 158 pub fn new() -> Self { 159 let profile = test_profile_record(); 160 let path = DataPath::new("previews") 161 .path(DataPathType::Cache) 162 .join(ImageCache::rel_dir()); 163 let cache = ImageCache::new(path); 164 ProfilePreviewPreview { profile, cache } 165 } 166 } 167 168 impl Default for ProfilePreviewPreview<'_> { 169 fn default() -> Self { 170 ProfilePreviewPreview::new() 171 } 172 } 173 174 impl View for ProfilePreviewPreview<'_> { 175 fn ui(&mut self, ui: &mut egui::Ui) { 176 ProfilePreview::new(&self.profile, &mut self.cache).ui(ui); 177 } 178 } 179 180 impl<'a> Preview for ProfilePreview<'a, '_> { 181 /// A preview of the profile preview :D 182 type Prev = ProfilePreviewPreview<'a>; 183 184 fn preview(_cfg: PreviewConfig) -> Self::Prev { 185 ProfilePreviewPreview::new() 186 } 187 } 188 } 189 190 pub fn get_display_name<'a>(profile: Option<&ProfileRecord<'a>>) -> DisplayName<'a> { 191 if let Some(name) = profile.and_then(|p| crate::profile::get_profile_name(p)) { 192 name 193 } else { 194 DisplayName::One("??") 195 } 196 } 197 198 pub fn get_profile_url<'a>(profile: Option<&ProfileRecord<'a>>) -> &'a str { 199 if let Some(url) = profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())) { 200 url 201 } else { 202 ProfilePic::no_pfp_url() 203 } 204 } 205 206 pub fn get_profile_url_owned(profile: Option<ProfileRecord<'_>>) -> &str { 207 if let Some(url) = profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())) { 208 url 209 } else { 210 ProfilePic::no_pfp_url() 211 } 212 } 213 214 pub fn get_account_url<'a>( 215 txn: &'a nostrdb::Transaction, 216 ndb: &nostrdb::Ndb, 217 account: Option<&UserAccount>, 218 ) -> &'a str { 219 if let Some(selected_account) = account { 220 if let Ok(profile) = ndb.get_profile_by_pubkey(txn, selected_account.pubkey.bytes()) { 221 get_profile_url_owned(Some(profile)) 222 } else { 223 get_profile_url_owned(None) 224 } 225 } else { 226 get_profile_url(None) 227 } 228 } 229 230 fn display_name_widget( 231 display_name: DisplayName<'_>, 232 add_placeholder_space: bool, 233 ) -> impl egui::Widget + '_ { 234 move |ui: &mut egui::Ui| match display_name { 235 DisplayName::One(n) => { 236 let name_response = ui.add( 237 Label::new(RichText::new(n).text_style(NotedeckTextStyle::Heading3.text_style())) 238 .selectable(false), 239 ); 240 if add_placeholder_space { 241 ui.add_space(16.0); 242 } 243 name_response 244 } 245 246 DisplayName::Both { 247 display_name, 248 username, 249 } => { 250 ui.add( 251 Label::new( 252 RichText::new(display_name) 253 .text_style(NotedeckTextStyle::Heading3.text_style()), 254 ) 255 .selectable(false), 256 ); 257 258 ui.add( 259 Label::new( 260 RichText::new(format!("@{}", username)) 261 .size(12.0) 262 .color(colors::MID_GRAY), 263 ) 264 .selectable(false), 265 ) 266 } 267 } 268 } 269 270 pub fn one_line_display_name_widget<'a>( 271 visuals: &egui::Visuals, 272 display_name: DisplayName<'a>, 273 style: NotedeckTextStyle, 274 ) -> impl egui::Widget + 'a { 275 let text_style = style.text_style(); 276 let color = visuals.noninteractive().fg_stroke.color; 277 278 move |ui: &mut egui::Ui| match display_name { 279 DisplayName::One(n) => ui.label(RichText::new(n).text_style(text_style).color(color)), 280 281 DisplayName::Both { 282 display_name, 283 username: _, 284 } => ui.label( 285 RichText::new(display_name) 286 .text_style(text_style) 287 .color(color), 288 ), 289 } 290 } 291 292 fn about_section_widget<'a, 'b>(profile: &'b ProfileRecord<'a>) -> impl egui::Widget + 'b 293 where 294 'b: 'a, 295 { 296 move |ui: &mut egui::Ui| { 297 if let Some(about) = profile.record().profile().and_then(|p| p.about()) { 298 ui.label(about) 299 } else { 300 // need any Response so we dont need an Option 301 ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover()) 302 } 303 } 304 } 305 306 fn get_display_name_as_string<'a>(profile: Option<&ProfileRecord<'a>>) -> &'a str { 307 let display_name = get_display_name(profile); 308 match display_name { 309 DisplayName::One(n) => n, 310 DisplayName::Both { display_name, .. } => display_name, 311 } 312 } 313 314 pub fn get_profile_displayname_string<'a>(txn: &'a Transaction, ndb: &Ndb, pk: &Pubkey) -> &'a str { 315 let profile = ndb.get_profile_by_pubkey(txn, pk.bytes()).ok(); 316 get_display_name_as_string(profile.as_ref()) 317 } 318 319 pub fn get_note_users_displayname_string<'a>( 320 txn: &'a Transaction, 321 ndb: &Ndb, 322 id: &NoteId, 323 ) -> &'a str { 324 let note = ndb.get_note_by_id(txn, id.bytes()); 325 let profile = if let Ok(note) = note { 326 ndb.get_profile_by_pubkey(txn, note.pubkey()).ok() 327 } else { 328 None 329 }; 330 331 get_display_name_as_string(profile.as_ref()) 332 }