edit.rs (8385B)
1 use core::f32; 2 3 use egui::{vec2, Button, CornerRadius, Layout, Margin, RichText, ScrollArea, TextEdit}; 4 use egui_winit::clipboard::Clipboard; 5 use enostr::ProfileState; 6 use notedeck::DragResponse; 7 use notedeck::{ 8 profile::unwrap_profile_url, tr, Images, Localization, MediaJobSender, NotedeckTextStyle, 9 }; 10 use notedeck_ui::context_menu::{input_context, PasteBehavior}; 11 use notedeck_ui::{profile::banner, ProfilePic}; 12 13 pub struct EditProfileView<'a> { 14 state: &'a mut ProfileState, 15 clipboard: &'a mut Clipboard, 16 img_cache: &'a mut Images, 17 i18n: &'a mut Localization, 18 jobs: &'a MediaJobSender, 19 } 20 21 impl<'a> EditProfileView<'a> { 22 pub fn new( 23 i18n: &'a mut Localization, 24 state: &'a mut ProfileState, 25 img_cache: &'a mut Images, 26 clipboard: &'a mut Clipboard, 27 jobs: &'a MediaJobSender, 28 ) -> Self { 29 Self { 30 i18n, 31 state, 32 img_cache, 33 clipboard, 34 jobs, 35 } 36 } 37 38 pub fn scroll_id() -> egui::Id { 39 egui::Id::new("edit_profile") 40 } 41 42 // return true to save 43 pub fn ui(&mut self, ui: &mut egui::Ui) -> DragResponse<bool> { 44 let scroll_out = ScrollArea::vertical() 45 .id_salt(EditProfileView::scroll_id()) 46 .stick_to_bottom(true) 47 .show(ui, |ui| { 48 banner(ui, self.state.banner(), 188.0); 49 50 let padding = 24.0; 51 notedeck_ui::padding(padding, ui, |ui| { 52 self.inner(ui, padding); 53 }); 54 55 ui.separator(); 56 57 let mut save = false; 58 notedeck_ui::padding(padding, ui, |ui| { 59 ui.with_layout(Layout::right_to_left(egui::Align::Center), |ui| { 60 if ui 61 .add( 62 button( 63 tr!( 64 self.i18n, 65 "Save changes", 66 "Button label to save profile changes" 67 ) 68 .as_str(), 69 119.0, 70 ) 71 .fill(notedeck_ui::colors::PINK), 72 ) 73 .clicked() 74 { 75 save = true; 76 } 77 }); 78 }); 79 80 Some(save) 81 }); 82 DragResponse::scroll(scroll_out) 83 } 84 85 fn inner(&mut self, ui: &mut egui::Ui, padding: f32) { 86 ui.spacing_mut().item_spacing = egui::vec2(0.0, 16.0); 87 let mut pfp_rect = ui.available_rect_before_wrap(); 88 let size = 80.0; 89 pfp_rect.set_width(size); 90 pfp_rect.set_height(size); 91 let pfp_rect = pfp_rect.translate(egui::vec2(0.0, -(padding + 2.0 + (size / 2.0)))); 92 93 let pfp_url = unwrap_profile_url(self.state.picture()); 94 ui.put( 95 pfp_rect, 96 &mut ProfilePic::new(self.img_cache, self.jobs, pfp_url) 97 .size(size) 98 .border(ProfilePic::border_stroke(ui)), 99 ); 100 101 in_frame(ui, |ui| { 102 ui.add(label( 103 tr!( 104 self.i18n, 105 "Display name", 106 "Profile display name field label" 107 ) 108 .as_str(), 109 )); 110 singleline_textedit(ui, self.state.str_mut("display_name"), self.clipboard); 111 }); 112 113 in_frame(ui, |ui| { 114 ui.add(label( 115 tr!(self.i18n, "Username", "Profile username field label").as_str(), 116 )); 117 singleline_textedit(ui, self.state.str_mut("name"), self.clipboard); 118 }); 119 120 in_frame(ui, |ui| { 121 ui.add(label( 122 tr!( 123 self.i18n, 124 "Profile picture", 125 "Profile picture URL field label" 126 ) 127 .as_str(), 128 )); 129 multiline_textedit(ui, self.state.str_mut("picture"), self.clipboard); 130 }); 131 132 in_frame(ui, |ui| { 133 ui.add(label( 134 tr!(self.i18n, "Banner", "Profile banner URL field label").as_str(), 135 )); 136 multiline_textedit(ui, self.state.str_mut("banner"), self.clipboard); 137 }); 138 139 in_frame(ui, |ui| { 140 ui.add(label( 141 tr!(self.i18n, "About", "Profile about/bio field label").as_str(), 142 )); 143 multiline_textedit(ui, self.state.str_mut("about"), self.clipboard); 144 }); 145 146 in_frame(ui, |ui| { 147 ui.add(label( 148 tr!(self.i18n, "Website", "Profile website field label").as_str(), 149 )); 150 singleline_textedit(ui, self.state.str_mut("website"), self.clipboard); 151 }); 152 153 in_frame(ui, |ui| { 154 ui.add(label( 155 tr!( 156 self.i18n, 157 "Lightning network address (lud16)", 158 "Bitcoin Lightning network address field label" 159 ) 160 .as_str(), 161 )); 162 multiline_textedit(ui, self.state.str_mut("lud16"), self.clipboard); 163 }); 164 165 in_frame(ui, |ui| { 166 ui.add(label( 167 tr!( 168 self.i18n, 169 "Nostr address (NIP-05 identity)", 170 "NIP-05 identity field label" 171 ) 172 .as_str(), 173 )); 174 175 singleline_textedit(ui, self.state.str_mut("nip05"), self.clipboard); 176 177 let Some(nip05) = self.state.nip05() else { 178 return; 179 }; 180 181 let mut split = nip05.split('@'); 182 183 let Some(prefix) = split.next() else { 184 return; 185 }; 186 let Some(suffix) = split.next() else { 187 return; 188 }; 189 190 let use_domain = if let Some(f) = prefix.chars().next() { 191 f == '_' 192 } else { 193 false 194 }; 195 ui.colored_label( 196 ui.visuals().noninteractive().fg_stroke.color, 197 RichText::new(if use_domain { 198 tr!( 199 self.i18n, 200 "\"{domain}\" will be used for identification", 201 "Domain identification message", 202 domain = suffix 203 ) 204 } else { 205 tr!( 206 self.i18n, 207 "\"{username}\" at \"{domain}\" will be used for identification", 208 "Username and domain identification message", 209 username = prefix, 210 domain = suffix 211 ) 212 }), 213 ); 214 }); 215 } 216 } 217 218 fn label(text: &str) -> impl egui::Widget + '_ { 219 move |ui: &mut egui::Ui| -> egui::Response { 220 ui.label(RichText::new(text).font(NotedeckTextStyle::Body.get_bolded_font(ui.ctx()))) 221 } 222 } 223 224 fn singleline_textedit(ui: &mut egui::Ui, data: &mut String, clipboard: &mut Clipboard) { 225 let r = ui.add( 226 TextEdit::singleline(data) 227 .min_size(vec2(0.0, 40.0)) 228 .vertical_align(egui::Align::Center) 229 .margin(Margin::symmetric(12, 10)) 230 .desired_width(f32::INFINITY), 231 ); 232 233 input_context(ui, &r, clipboard, data, PasteBehavior::Clear); 234 } 235 236 fn multiline_textedit(ui: &mut egui::Ui, data: &mut String, clipboard: &mut Clipboard) { 237 let r = ui.add( 238 TextEdit::multiline(data) 239 // .min_size(vec2(0.0, 40.0)) 240 .vertical_align(egui::Align::TOP) 241 .margin(Margin::symmetric(12, 10)) 242 .desired_width(f32::INFINITY) 243 .desired_rows(1), 244 ); 245 246 input_context(ui, &r, clipboard, data, PasteBehavior::Clear); 247 } 248 249 fn in_frame(ui: &mut egui::Ui, contents: impl FnOnce(&mut egui::Ui)) { 250 egui::Frame::new().show(ui, |ui| { 251 ui.spacing_mut().item_spacing = egui::vec2(0.0, 8.0); 252 contents(ui); 253 }); 254 } 255 256 fn button(text: &str, width: f32) -> egui::Button<'static> { 257 Button::new(text) 258 .corner_radius(CornerRadius::same(8)) 259 .min_size(vec2(width, 40.0)) 260 }