preview.rs (8850B)
1 use crate::app_style::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, RichText, Sense, Widget}; 9 use egui_extras::Size; 10 use enostr::NoteId; 11 use nostrdb::ProfileRecord; 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<'a, 'cache> egui::Widget for ProfilePreview<'a, 'cache> { 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 } 97 98 impl<'a, 'cache> SimpleProfilePreview<'a, 'cache> { 99 pub fn new(profile: Option<&'a ProfileRecord<'a>>, cache: &'cache mut ImageCache) -> Self { 100 SimpleProfilePreview { profile, cache } 101 } 102 } 103 104 impl<'a, 'cache> egui::Widget for SimpleProfilePreview<'a, 'cache> { 105 fn ui(self, ui: &mut egui::Ui) -> egui::Response { 106 Frame::none() 107 .show(ui, |ui| { 108 ui.add(ProfilePic::new(self.cache, get_profile_url(self.profile)).size(48.0)); 109 ui.vertical(|ui| { 110 ui.add(display_name_widget(get_display_name(self.profile), true)); 111 }); 112 }) 113 .response 114 } 115 } 116 117 mod previews { 118 use super::*; 119 use crate::test_data::test_profile_record; 120 use crate::ui::{Preview, PreviewConfig, View}; 121 122 pub struct ProfilePreviewPreview<'a> { 123 profile: ProfileRecord<'a>, 124 cache: ImageCache, 125 } 126 127 impl<'a> ProfilePreviewPreview<'a> { 128 pub fn new() -> Self { 129 let profile = test_profile_record(); 130 let path = DataPath::new("previews") 131 .path(DataPathType::Cache) 132 .join(ImageCache::rel_dir()); 133 let cache = ImageCache::new(path); 134 ProfilePreviewPreview { profile, cache } 135 } 136 } 137 138 impl<'a> Default for ProfilePreviewPreview<'a> { 139 fn default() -> Self { 140 ProfilePreviewPreview::new() 141 } 142 } 143 144 impl<'a> View for ProfilePreviewPreview<'a> { 145 fn ui(&mut self, ui: &mut egui::Ui) { 146 ProfilePreview::new(&self.profile, &mut self.cache).ui(ui); 147 } 148 } 149 150 impl<'a, 'cache> Preview for ProfilePreview<'a, 'cache> { 151 /// A preview of the profile preview :D 152 type Prev = ProfilePreviewPreview<'a>; 153 154 fn preview(_cfg: PreviewConfig) -> Self::Prev { 155 ProfilePreviewPreview::new() 156 } 157 } 158 } 159 160 pub fn get_display_name<'a>(profile: Option<&'a ProfileRecord<'a>>) -> DisplayName<'a> { 161 if let Some(name) = profile.and_then(|p| crate::profile::get_profile_name(p)) { 162 name 163 } else { 164 DisplayName::One("??") 165 } 166 } 167 168 pub fn get_profile_url<'a>(profile: Option<&'a ProfileRecord<'a>>) -> &'a str { 169 if let Some(url) = profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())) { 170 url 171 } else { 172 ProfilePic::no_pfp_url() 173 } 174 } 175 176 pub fn get_profile_url_owned(profile: Option<ProfileRecord<'_>>) -> &str { 177 if let Some(url) = profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())) { 178 url 179 } else { 180 ProfilePic::no_pfp_url() 181 } 182 } 183 184 pub fn get_account_url<'a>( 185 txn: &'a nostrdb::Transaction, 186 ndb: &nostrdb::Ndb, 187 account: Option<&UserAccount>, 188 ) -> &'a str { 189 if let Some(selected_account) = account { 190 if let Ok(profile) = ndb.get_profile_by_pubkey(txn, selected_account.pubkey.bytes()) { 191 get_profile_url_owned(Some(profile)) 192 } else { 193 get_profile_url_owned(None) 194 } 195 } else { 196 get_profile_url(None) 197 } 198 } 199 200 fn display_name_widget( 201 display_name: DisplayName<'_>, 202 add_placeholder_space: bool, 203 ) -> impl egui::Widget + '_ { 204 move |ui: &mut egui::Ui| match display_name { 205 DisplayName::One(n) => { 206 let name_response = 207 ui.label(RichText::new(n).text_style(NotedeckTextStyle::Heading3.text_style())); 208 if add_placeholder_space { 209 ui.add_space(16.0); 210 } 211 name_response 212 } 213 214 DisplayName::Both { 215 display_name, 216 username, 217 } => { 218 ui.label( 219 RichText::new(display_name).text_style(NotedeckTextStyle::Heading3.text_style()), 220 ); 221 222 ui.label( 223 RichText::new(format!("@{}", username)) 224 .size(12.0) 225 .color(colors::MID_GRAY), 226 ) 227 } 228 } 229 } 230 231 pub fn one_line_display_name_widget( 232 display_name: DisplayName<'_>, 233 style: NotedeckTextStyle, 234 ) -> impl egui::Widget + '_ { 235 let text_style = style.text_style(); 236 move |ui: &mut egui::Ui| match display_name { 237 DisplayName::One(n) => ui.label( 238 RichText::new(n) 239 .text_style(text_style) 240 .color(colors::GRAY_SECONDARY), 241 ), 242 243 DisplayName::Both { 244 display_name, 245 username: _, 246 } => ui.label( 247 RichText::new(display_name) 248 .text_style(text_style) 249 .color(colors::GRAY_SECONDARY), 250 ), 251 } 252 } 253 254 fn about_section_widget<'a>(profile: &'a ProfileRecord<'a>) -> impl egui::Widget + 'a { 255 |ui: &mut egui::Ui| { 256 if let Some(about) = profile.record().profile().and_then(|p| p.about()) { 257 ui.label(about) 258 } else { 259 // need any Response so we dont need an Option 260 ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover()) 261 } 262 } 263 } 264 265 fn get_display_name_as_string(profile: Option<&'_ ProfileRecord<'_>>) -> String { 266 let display_name = get_display_name(profile); 267 match display_name { 268 DisplayName::One(n) => n.to_string(), 269 DisplayName::Both { display_name, .. } => display_name.to_string(), 270 } 271 } 272 273 pub fn get_profile_displayname_string(ndb: &nostrdb::Ndb, pk: &enostr::Pubkey) -> String { 274 let txn = nostrdb::Transaction::new(ndb).expect("Transaction should have worked"); 275 let profile = ndb.get_profile_by_pubkey(&txn, pk.bytes()).ok(); 276 get_display_name_as_string(profile.as_ref()) 277 } 278 279 pub fn get_note_users_displayname_string(ndb: &nostrdb::Ndb, id: &NoteId) -> String { 280 let txn = nostrdb::Transaction::new(ndb).expect("Transaction should have worked"); 281 let note = ndb.get_note_by_id(&txn, id.bytes()); 282 let profile = if let Ok(note) = note { 283 ndb.get_profile_by_pubkey(&txn, note.pubkey()).ok() 284 } else { 285 None 286 }; 287 get_display_name_as_string(profile.as_ref()) 288 }