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