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