picture.rs (7009B)
1 use crate::images::ImageType; 2 use crate::ui::{Preview, PreviewConfig}; 3 use egui::{vec2, Sense, TextureHandle}; 4 use nostrdb::{Ndb, Transaction}; 5 use tracing::info; 6 7 use notedeck::{AppContext, ImageCache}; 8 9 pub struct ProfilePic<'cache, 'url> { 10 cache: &'cache mut ImageCache, 11 url: &'url str, 12 size: f32, 13 } 14 15 impl egui::Widget for ProfilePic<'_, '_> { 16 fn ui(self, ui: &mut egui::Ui) -> egui::Response { 17 render_pfp(ui, self.cache, self.url, self.size) 18 } 19 } 20 21 impl<'cache, 'url> ProfilePic<'cache, 'url> { 22 pub fn new(cache: &'cache mut ImageCache, url: &'url str) -> Self { 23 let size = Self::default_size(); 24 ProfilePic { cache, url, size } 25 } 26 27 pub fn from_profile( 28 cache: &'cache mut ImageCache, 29 profile: &nostrdb::ProfileRecord<'url>, 30 ) -> Option<Self> { 31 profile 32 .record() 33 .profile() 34 .and_then(|p| p.picture()) 35 .map(|url| ProfilePic::new(cache, url)) 36 } 37 38 #[inline] 39 pub fn default_size() -> f32 { 40 38.0 41 } 42 43 #[inline] 44 pub fn medium_size() -> f32 { 45 32.0 46 } 47 48 #[inline] 49 pub fn small_size() -> f32 { 50 24.0 51 } 52 53 #[inline] 54 pub fn no_pfp_url() -> &'static str { 55 "https://damus.io/img/no-profile.svg" 56 } 57 58 #[inline] 59 pub fn size(mut self, size: f32) -> Self { 60 self.size = size; 61 self 62 } 63 } 64 65 fn render_pfp( 66 ui: &mut egui::Ui, 67 img_cache: &mut ImageCache, 68 url: &str, 69 ui_size: f32, 70 ) -> egui::Response { 71 #[cfg(feature = "profiling")] 72 puffin::profile_function!(); 73 74 // We will want to downsample these so it's not blurry on hi res displays 75 let img_size = 128u32; 76 77 let m_cached_promise = img_cache.map().get(url); 78 if m_cached_promise.is_none() { 79 let res = crate::images::fetch_img(img_cache, ui.ctx(), url, ImageType::Profile(img_size)); 80 img_cache.map_mut().insert(url.to_owned(), res); 81 } 82 83 match img_cache.map()[url].ready() { 84 None => paint_circle(ui, ui_size), 85 86 // Failed to fetch profile! 87 Some(Err(_err)) => { 88 let m_failed_promise = img_cache.map().get(url); 89 if m_failed_promise.is_none() { 90 let no_pfp = crate::images::fetch_img( 91 img_cache, 92 ui.ctx(), 93 ProfilePic::no_pfp_url(), 94 ImageType::Profile(img_size), 95 ); 96 img_cache.map_mut().insert(url.to_owned(), no_pfp); 97 } 98 99 match img_cache.map().get(url).unwrap().ready() { 100 None => paint_circle(ui, ui_size), 101 Some(Err(_e)) => { 102 //error!("Image load error: {:?}", e); 103 paint_circle(ui, ui_size) 104 } 105 Some(Ok(img)) => pfp_image(ui, img, ui_size), 106 } 107 } 108 Some(Ok(img)) => pfp_image(ui, img, ui_size), 109 } 110 } 111 112 fn pfp_image(ui: &mut egui::Ui, img: &TextureHandle, size: f32) -> egui::Response { 113 #[cfg(feature = "profiling")] 114 puffin::profile_function!(); 115 116 //img.show_max_size(ui, egui::vec2(size, size)) 117 ui.add(egui::Image::new(img).max_width(size)) 118 //.with_options() 119 } 120 121 fn paint_circle(ui: &mut egui::Ui, size: f32) -> egui::Response { 122 let (rect, response) = ui.allocate_at_least(vec2(size, size), Sense::hover()); 123 ui.painter() 124 .circle_filled(rect.center(), size / 2.0, ui.visuals().weak_text_color()); 125 126 response 127 } 128 129 mod preview { 130 use super::*; 131 use crate::ui; 132 use nostrdb::*; 133 use std::collections::HashSet; 134 135 pub struct ProfilePicPreview { 136 keys: Option<Vec<ProfileKey>>, 137 } 138 139 impl ProfilePicPreview { 140 fn new() -> Self { 141 ProfilePicPreview { keys: None } 142 } 143 144 fn show(&mut self, app: &mut AppContext<'_>, ui: &mut egui::Ui) { 145 egui::ScrollArea::both().show(ui, |ui| { 146 ui.horizontal_wrapped(|ui| { 147 let txn = Transaction::new(app.ndb).unwrap(); 148 149 let keys = if let Some(keys) = &self.keys { 150 keys 151 } else { 152 return; 153 }; 154 155 for key in keys { 156 let profile = app.ndb.get_profile_by_key(&txn, *key).unwrap(); 157 let url = profile 158 .record() 159 .profile() 160 .expect("should have profile") 161 .picture() 162 .expect("should have picture"); 163 164 let expand_size = 10.0; 165 let anim_speed = 0.05; 166 167 let (rect, size, _resp) = ui::anim::hover_expand( 168 ui, 169 egui::Id::new(profile.key().unwrap()), 170 ui::ProfilePic::default_size(), 171 expand_size, 172 anim_speed, 173 ); 174 175 ui.put(rect, ui::ProfilePic::new(app.img_cache, url).size(size)) 176 .on_hover_ui_at_pointer(|ui| { 177 ui.set_max_width(300.0); 178 ui.add(ui::ProfilePreview::new(&profile, app.img_cache)); 179 }); 180 } 181 }); 182 }); 183 } 184 185 fn setup(&mut self, ndb: &Ndb) { 186 let txn = Transaction::new(ndb).unwrap(); 187 let filters = vec![Filter::new().kinds(vec![0]).build()]; 188 let mut pks = HashSet::new(); 189 let mut keys = HashSet::new(); 190 191 for query_result in ndb.query(&txn, &filters, 20000).unwrap() { 192 pks.insert(query_result.note.pubkey()); 193 } 194 195 for pk in pks { 196 let profile = if let Ok(profile) = ndb.get_profile_by_pubkey(&txn, pk) { 197 profile 198 } else { 199 continue; 200 }; 201 202 if profile 203 .record() 204 .profile() 205 .and_then(|p| p.picture()) 206 .is_none() 207 { 208 continue; 209 } 210 211 keys.insert(profile.key().expect("should not be owned")); 212 } 213 214 let keys: Vec<ProfileKey> = keys.into_iter().collect(); 215 info!("Loaded {} profiles", keys.len()); 216 self.keys = Some(keys); 217 } 218 } 219 220 impl notedeck::App for ProfilePicPreview { 221 fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) { 222 if self.keys.is_none() { 223 self.setup(ctx.ndb); 224 } 225 226 self.show(ctx, ui) 227 } 228 } 229 230 impl Preview for ProfilePic<'_, '_> { 231 type Prev = ProfilePicPreview; 232 233 fn preview(_cfg: PreviewConfig) -> Self::Prev { 234 ProfilePicPreview::new() 235 } 236 } 237 }