settings.rs (34201B)
1 use egui::{ 2 vec2, Button, Color32, ComboBox, CornerRadius, Frame, Layout, Margin, RichText, ScrollArea, 3 TextEdit, ThemePreference, 4 }; 5 use egui_extras::{Size, StripBuilder}; 6 use notedeck::{ 7 tr, ui::richtext_small, DragResponse, LanguageIdentifier, NoteContext, NotedeckTextStyle, 8 Settings, DEFAULT_MAX_HASHTAGS_PER_NOTE, 9 }; 10 use notedeck_ui::{ 11 app_images::{copy_to_clipboard_dark_image, copy_to_clipboard_image}, 12 AnimationHelper, NoteOptions, NoteView, 13 }; 14 15 use nostrdb::Transaction; 16 17 use crate::{nav::RouterAction, ui::account_login_view::eye_button, Damus, Route}; 18 19 const PREVIEW_NOTE_ID: [u8; 32] = [ 20 0xcb, 0x65, 0x83, 0xa1, 0x12, 0x7f, 0xae, 0xe6, 0x7b, 0xc8, 0x55, 0x5f, 0x4e, 0x5f, 0x52, 0x54, 21 0xad, 0x6a, 0x77, 0x8d, 0x52, 0x91, 0x65, 0x98, 0x6c, 0xc2, 0x9d, 0x31, 0xe6, 0xac, 0x6b, 0x90, 22 ]; 23 24 const MIN_ZOOM: f32 = 0.5; 25 const MAX_ZOOM: f32 = 3.0; 26 const ZOOM_STEP: f32 = 0.1; 27 const RESET_ZOOM: f32 = 1.0; 28 29 pub enum SettingsAction { 30 SetZoomFactor(f32), 31 SetTheme(ThemePreference), 32 SetLocale(LanguageIdentifier), 33 SetRepliestNewestFirst(bool), 34 SetAnimateNavTransitions(bool), 35 SetMaxHashtagsPerNote(usize), 36 OpenRelays, 37 OpenCacheFolder, 38 ClearCacheFolder, 39 CompactDatabase, 40 } 41 42 impl SettingsAction { 43 pub fn process_settings_action( 44 self, 45 app: &mut Damus, 46 app_ctx: &mut notedeck::AppContext<'_>, 47 egui_ctx: &egui::Context, 48 ) -> Option<RouterAction> { 49 let mut route_action: Option<RouterAction> = None; 50 51 match self { 52 Self::OpenRelays => { 53 route_action = Some(RouterAction::route_to(Route::Relays)); 54 } 55 Self::SetZoomFactor(zoom_factor) => { 56 egui_ctx.set_zoom_factor(zoom_factor); 57 app_ctx.settings.set_zoom_factor(zoom_factor); 58 } 59 Self::SetTheme(theme) => { 60 egui_ctx.set_theme(theme); 61 app_ctx.settings.set_theme(theme); 62 } 63 Self::SetLocale(language) => { 64 if app_ctx.i18n.set_locale(language.clone()).is_ok() { 65 app_ctx.settings.set_locale(language.to_string()); 66 } 67 } 68 Self::SetRepliestNewestFirst(value) => { 69 app.note_options.set(NoteOptions::RepliesNewestFirst, value); 70 app_ctx.settings.set_show_replies_newest_first(value); 71 } 72 Self::OpenCacheFolder => { 73 use opener; 74 let _ = opener::open(app_ctx.img_cache.base_path.clone()); 75 } 76 Self::ClearCacheFolder => { 77 let _ = app_ctx.img_cache.clear_folder_contents(); 78 } 79 Self::SetAnimateNavTransitions(value) => { 80 app_ctx.settings.set_animate_nav_transitions(value); 81 } 82 83 Self::SetMaxHashtagsPerNote(value) => { 84 app_ctx.settings.set_max_hashtags_per_note(value); 85 app_ctx.accounts.update_max_hashtags_per_note(value); 86 } 87 Self::CompactDatabase => { 88 let own_pubkeys: Vec<[u8; 32]> = app_ctx 89 .accounts 90 .cache 91 .accounts() 92 .map(|a| *a.key.pubkey.bytes()) 93 .collect(); 94 95 let db_path = app_ctx.args.db_path(app_ctx.path); 96 let compact_path = app_ctx.args.db_compact_path(app_ctx.path); 97 let _ = std::fs::create_dir_all(&compact_path); 98 99 let old_size = std::fs::metadata(db_path.join("data.mdb")) 100 .map(|m| m.len()) 101 .unwrap_or(0); 102 103 let compact_path_str = compact_path.to_str().unwrap_or("").to_string(); 104 let ndb = app_ctx.ndb.clone(); 105 106 let receiver = app_ctx.job_pool.schedule_receivable(move || { 107 ndb.compact(&compact_path_str, &own_pubkeys) 108 .map(|()| { 109 let new_size = 110 std::fs::metadata(format!("{compact_path_str}/data.mdb")) 111 .map(|m| m.len()) 112 .unwrap_or(0); 113 notedeck::compact::CompactResult { old_size, new_size } 114 }) 115 .map_err(|e| format!("{e}")) 116 }); 117 118 app.view_state.compact.status = notedeck::compact::CompactStatus::Running(receiver); 119 } 120 } 121 route_action 122 } 123 } 124 125 pub struct SettingsView<'a> { 126 settings: &'a mut Settings, 127 note_context: &'a mut NoteContext<'a>, 128 db_path: &'a std::path::Path, 129 compact: &'a mut notedeck::compact::CompactState, 130 } 131 132 fn settings_group<S>(ui: &mut egui::Ui, title: S, contents: impl FnOnce(&mut egui::Ui)) 133 where 134 S: Into<String>, 135 { 136 Frame::group(ui.style()) 137 .fill(ui.style().visuals.widgets.open.bg_fill) 138 .inner_margin(10.0) 139 .show(ui, |ui| { 140 ui.label(RichText::new(title).text_style(NotedeckTextStyle::Body.text_style())); 141 ui.separator(); 142 143 ui.vertical(|ui| { 144 ui.spacing_mut().item_spacing = vec2(10.0, 10.0); 145 146 contents(ui) 147 }); 148 }); 149 } 150 151 impl<'a> SettingsView<'a> { 152 pub fn new( 153 settings: &'a mut Settings, 154 note_context: &'a mut NoteContext<'a>, 155 db_path: &'a std::path::Path, 156 compact: &'a mut notedeck::compact::CompactState, 157 ) -> Self { 158 Self { 159 settings, 160 note_context, 161 db_path, 162 compact, 163 } 164 } 165 166 /// Get the localized name for a language identifier 167 fn get_selected_language_name(&mut self) -> String { 168 if let Ok(lang_id) = self.settings.locale.parse::<LanguageIdentifier>() { 169 self.note_context 170 .i18n 171 .get_locale_native_name(&lang_id) 172 .map(|s| s.to_owned()) 173 .unwrap_or_else(|| lang_id.to_string()) 174 } else { 175 self.settings.locale.clone() 176 } 177 } 178 179 pub fn appearance_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> { 180 let mut action = None; 181 let title = tr!( 182 self.note_context.i18n, 183 "Appearance", 184 "Label for appearance settings section", 185 ); 186 let txn = Transaction::new(self.note_context.ndb).unwrap(); 187 settings_group(ui, title, |ui| { 188 let current_zoom = ui.ctx().zoom_factor(); 189 190 ui.horizontal_wrapped(|ui| { 191 ui.label(richtext_small(tr!( 192 self.note_context.i18n, 193 "Zoom Level:", 194 "Label for zoom level, Appearance settings section", 195 ))); 196 197 let min_reached = current_zoom <= MIN_ZOOM; 198 let max_reached = current_zoom >= MAX_ZOOM; 199 200 if ui 201 .add_enabled( 202 !min_reached, 203 Button::new( 204 RichText::new("-").text_style(NotedeckTextStyle::Small.text_style()), 205 ), 206 ) 207 .clicked() 208 { 209 let new_zoom = (current_zoom - ZOOM_STEP).max(MIN_ZOOM); 210 action = Some(SettingsAction::SetZoomFactor(new_zoom)); 211 }; 212 213 ui.label( 214 RichText::new(format!("{:.0}%", current_zoom * 100.0)) 215 .text_style(NotedeckTextStyle::Small.text_style()), 216 ); 217 218 if ui 219 .add_enabled( 220 !max_reached, 221 Button::new( 222 RichText::new("+").text_style(NotedeckTextStyle::Small.text_style()), 223 ), 224 ) 225 .clicked() 226 { 227 let new_zoom = (current_zoom + ZOOM_STEP).min(MAX_ZOOM); 228 action = Some(SettingsAction::SetZoomFactor(new_zoom)); 229 }; 230 231 if ui 232 .button(richtext_small(tr!( 233 self.note_context.i18n, 234 "Reset", 235 "Label for reset zoom level, Appearance settings section", 236 ))) 237 .clicked() 238 { 239 action = Some(SettingsAction::SetZoomFactor(RESET_ZOOM)); 240 } 241 }); 242 243 ui.horizontal_wrapped(|ui| { 244 ui.label(richtext_small(tr!( 245 self.note_context.i18n, 246 "Language:", 247 "Label for language, Appearance settings section", 248 ))); 249 250 // 251 ComboBox::from_label("") 252 .selected_text(self.get_selected_language_name()) 253 .show_ui(ui, |ui| { 254 for lang in self.note_context.i18n.get_available_locales() { 255 let name = self 256 .note_context 257 .i18n 258 .get_locale_native_name(lang) 259 .map(|s| s.to_owned()) 260 .unwrap_or_else(|| lang.to_string()); 261 if ui 262 .selectable_value(&mut self.settings.locale, lang.to_string(), name) 263 .clicked() 264 { 265 action = Some(SettingsAction::SetLocale(lang.to_owned())) 266 } 267 } 268 }); 269 }); 270 271 ui.horizontal_wrapped(|ui| { 272 ui.label(richtext_small(tr!( 273 self.note_context.i18n, 274 "Theme:", 275 "Label for theme, Appearance settings section", 276 ))); 277 278 if ui 279 .selectable_value( 280 &mut self.settings.theme, 281 ThemePreference::Light, 282 richtext_small(tr!( 283 self.note_context.i18n, 284 "Light", 285 "Label for Theme Light, Appearance settings section", 286 )), 287 ) 288 .clicked() 289 { 290 action = Some(SettingsAction::SetTheme(ThemePreference::Light)); 291 } 292 293 if ui 294 .selectable_value( 295 &mut self.settings.theme, 296 ThemePreference::Dark, 297 richtext_small(tr!( 298 self.note_context.i18n, 299 "Dark", 300 "Label for Theme Dark, Appearance settings section", 301 )), 302 ) 303 .clicked() 304 { 305 action = Some(SettingsAction::SetTheme(ThemePreference::Dark)); 306 } 307 }); 308 309 if let Ok(preview_note) = self.note_context.ndb.get_note_by_id(&txn, &PREVIEW_NOTE_ID) { 310 notedeck_ui::padding(8.0, ui, |ui| { 311 ui.set_max_width(ui.available_width()); 312 313 NoteView::new(self.note_context, &preview_note, NoteOptions::default()) 314 .actionbar(false) 315 .options_button(false) 316 .show(ui); 317 }); 318 } 319 }); 320 321 action 322 } 323 324 pub fn storage_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> { 325 let id = ui.id(); 326 let mut action: Option<SettingsAction> = None; 327 let title = tr!( 328 self.note_context.i18n, 329 "Storage", 330 "Label for storage settings section" 331 ); 332 settings_group(ui, title, |ui| { 333 ui.horizontal_wrapped(|ui| { 334 let static_imgs_size = self 335 .note_context 336 .img_cache 337 .static_imgs 338 .cache_size 339 .lock() 340 .unwrap(); 341 342 let gifs_size = self.note_context.img_cache.gifs.cache_size.lock().unwrap(); 343 344 ui.label( 345 RichText::new(format!( 346 "{} {}", 347 tr!( 348 self.note_context.i18n, 349 "Image cache size:", 350 "Label for Image cache size, Storage settings section" 351 ), 352 format_size( 353 [static_imgs_size, gifs_size] 354 .iter() 355 .fold(0_u64, |acc, cur| acc + cur.unwrap_or_default()) 356 ) 357 )) 358 .text_style(NotedeckTextStyle::Small.text_style()), 359 ); 360 361 ui.end_row(); 362 363 if !notedeck::ui::is_compiled_as_mobile() 364 && ui 365 .button(richtext_small(tr!( 366 self.note_context.i18n, 367 "View folder", 368 "Label for view folder button, Storage settings section", 369 ))) 370 .clicked() 371 { 372 action = Some(SettingsAction::OpenCacheFolder); 373 } 374 375 let clearcache_resp = ui.button( 376 richtext_small(tr!( 377 self.note_context.i18n, 378 "Clear cache", 379 "Label for clear cache button, Storage settings section", 380 )) 381 .color(Color32::LIGHT_RED), 382 ); 383 384 let id_clearcache = id.with("clear_cache"); 385 if clearcache_resp.clicked() { 386 ui.data_mut(|d| d.insert_temp(id_clearcache, true)); 387 } 388 389 if ui.data_mut(|d| *d.get_temp_mut_or_default(id_clearcache)) { 390 let mut confirm_pressed = false; 391 clearcache_resp.show_tooltip_ui(|ui| { 392 let confirm_resp = ui.button(tr!( 393 self.note_context.i18n, 394 "Confirm", 395 "Label for confirm clear cache, Storage settings section" 396 )); 397 if confirm_resp.clicked() { 398 confirm_pressed = true; 399 } 400 401 if confirm_resp.clicked() 402 || ui 403 .button(tr!( 404 self.note_context.i18n, 405 "Cancel", 406 "Label for cancel clear cache, Storage settings section" 407 )) 408 .clicked() 409 { 410 ui.data_mut(|d| d.insert_temp(id_clearcache, false)); 411 } 412 }); 413 414 if confirm_pressed { 415 action = Some(SettingsAction::ClearCacheFolder); 416 } else if !confirm_pressed && clearcache_resp.clicked_elsewhere() { 417 ui.data_mut(|d| d.insert_temp(id_clearcache, false)); 418 } 419 }; 420 }); 421 }); 422 423 action 424 } 425 426 pub fn database_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> { 427 let id = ui.id(); 428 let mut action: Option<SettingsAction> = None; 429 430 // Poll compaction status; invalidate cached size when done 431 if self.compact.status.poll() { 432 self.compact.invalidate_size(); 433 } 434 435 let title = tr!( 436 self.note_context.i18n, 437 "Database", 438 "Label for database settings section" 439 ); 440 settings_group(ui, title, |ui| { 441 ui.horizontal_wrapped(|ui| { 442 let db_size = self.compact.db_size(self.db_path); 443 444 ui.label( 445 RichText::new(format!( 446 "{} {}", 447 tr!( 448 self.note_context.i18n, 449 "Database size:", 450 "Label for database size in settings" 451 ), 452 format_size(db_size) 453 )) 454 .text_style(NotedeckTextStyle::Small.text_style()), 455 ); 456 457 ui.end_row(); 458 459 match self.compact.status { 460 notedeck::compact::CompactStatus::Running(_) => { 461 ui.label( 462 richtext_small(tr!( 463 self.note_context.i18n, 464 "Compacting...", 465 "Status label while database compaction is running" 466 )), 467 ); 468 } 469 notedeck::compact::CompactStatus::Done(ref result) => { 470 ui.label(richtext_small(format!( 471 "{} {} → {}. {}", 472 tr!( 473 self.note_context.i18n, 474 "Compacted!", 475 "Status label after database compaction completes" 476 ), 477 format_size(result.old_size), 478 format_size(result.new_size), 479 tr!( 480 self.note_context.i18n, 481 "Restart to apply.", 482 "Instruction to restart after compaction" 483 ), 484 ))); 485 } 486 notedeck::compact::CompactStatus::Error(ref e) => { 487 ui.label( 488 richtext_small(format!( 489 "{} {e}", 490 tr!( 491 self.note_context.i18n, 492 "Compaction error:", 493 "Status label when database compaction fails" 494 ), 495 )) 496 .color(Color32::LIGHT_RED), 497 ); 498 } 499 notedeck::compact::CompactStatus::Idle => { 500 let compact_resp = ui.button(richtext_small(tr!( 501 self.note_context.i18n, 502 "Compact database", 503 "Button to compact the database" 504 ))); 505 506 let id_compact = id.with("compact_db"); 507 if compact_resp.clicked() { 508 ui.data_mut(|d| d.insert_temp(id_compact, true)); 509 } 510 511 if ui.data_mut(|d| *d.get_temp_mut_or_default(id_compact)) { 512 let mut confirm_pressed = false; 513 compact_resp.show_tooltip_ui(|ui| { 514 ui.label(tr!( 515 self.note_context.i18n, 516 "Keeps all profiles and your notes. The smaller database will be used on next restart.", 517 "Confirmation prompt for database compaction" 518 )); 519 let confirm_resp = ui.button(tr!( 520 self.note_context.i18n, 521 "Confirm", 522 "Label for confirm compact database" 523 )); 524 if confirm_resp.clicked() { 525 confirm_pressed = true; 526 } 527 528 if confirm_resp.clicked() 529 || ui 530 .button(tr!( 531 self.note_context.i18n, 532 "Cancel", 533 "Label for cancel compact database" 534 )) 535 .clicked() 536 { 537 ui.data_mut(|d| d.insert_temp(id_compact, false)); 538 } 539 }); 540 541 if confirm_pressed { 542 action = Some(SettingsAction::CompactDatabase); 543 } else if !confirm_pressed && compact_resp.clicked_elsewhere() { 544 ui.data_mut(|d| d.insert_temp(id_compact, false)); 545 } 546 } 547 } 548 } 549 }); 550 }); 551 552 action 553 } 554 555 fn other_options_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> { 556 let mut action = None; 557 558 let title = tr!( 559 self.note_context.i18n, 560 "Others", 561 "Label for others settings section" 562 ); 563 settings_group(ui, title, |ui| { 564 ui.horizontal_wrapped(|ui| { 565 ui.label(richtext_small(tr!( 566 self.note_context.i18n, 567 "Sort replies newest first:", 568 "Label for Sort replies newest first, others settings section", 569 ))); 570 571 if ui 572 .toggle_value( 573 &mut self.settings.show_replies_newest_first, 574 RichText::new(tr!( 575 self.note_context.i18n, 576 "On", 577 "Setting to turn on sorting replies so that the newest are shown first" 578 )) 579 .text_style(NotedeckTextStyle::Small.text_style()), 580 ) 581 .changed() 582 { 583 action = Some(SettingsAction::SetRepliestNewestFirst( 584 self.settings.show_replies_newest_first, 585 )); 586 } 587 }); 588 589 ui.horizontal_wrapped(|ui| { 590 ui.label(richtext_small("Animate view transitions:")); 591 592 if ui 593 .toggle_value( 594 &mut self.settings.animate_nav_transitions, 595 RichText::new("On").text_style(NotedeckTextStyle::Small.text_style()), 596 ) 597 .changed() 598 { 599 action = Some(SettingsAction::SetAnimateNavTransitions( 600 self.settings.animate_nav_transitions, 601 )); 602 } 603 }); 604 605 ui.horizontal_wrapped(|ui| { 606 ui.label(richtext_small(tr!( 607 self.note_context.i18n, 608 "Max hashtags per note:", 609 "Label for max hashtags per note, others settings section", 610 ))); 611 612 if ui 613 .add( 614 egui::Slider::new(&mut self.settings.max_hashtags_per_note, 0..=20) 615 .text("") 616 .step_by(1.0), 617 ) 618 .changed() 619 { 620 action = Some(SettingsAction::SetMaxHashtagsPerNote( 621 self.settings.max_hashtags_per_note, 622 )); 623 }; 624 625 if ui 626 .button(richtext_small(tr!( 627 self.note_context.i18n, 628 "Reset", 629 "Label for reset max hashtags per note, others settings section", 630 ))) 631 .clicked() 632 { 633 action = Some(SettingsAction::SetMaxHashtagsPerNote( 634 DEFAULT_MAX_HASHTAGS_PER_NOTE, 635 )); 636 } 637 }); 638 639 ui.horizontal_wrapped(|ui| { 640 let text = if self.settings.max_hashtags_per_note == 0 { 641 tr!( 642 self.note_context.i18n, 643 "Hashtag filter disabled", 644 "Info text when hashtag filter is disabled (set to 0)" 645 ) 646 } else { 647 format!( 648 "Hide posts with more than {} hashtags", 649 self.settings.max_hashtags_per_note 650 ) 651 }; 652 ui.label( 653 richtext_small(&text).color(ui.visuals().gray_out(ui.visuals().text_color())), 654 ); 655 }); 656 }); 657 658 action 659 } 660 661 fn keys_section(&mut self, ui: &mut egui::Ui) { 662 let title = tr!( 663 self.note_context.i18n, 664 "Keys", 665 "label for keys setting section" 666 ); 667 668 settings_group(ui, title, |ui| { 669 ui.horizontal_wrapped(|ui| { 670 ui.label( 671 richtext_small(tr!( 672 self.note_context.i18n, 673 "PUBLIC ACCOUNT ID", 674 "label describing public key" 675 )) 676 .color(ui.visuals().gray_out(ui.visuals().text_color())), 677 ); 678 }); 679 680 let copy_img = if ui.visuals().dark_mode { 681 copy_to_clipboard_image() 682 } else { 683 copy_to_clipboard_dark_image() 684 }; 685 let copy_max_size = vec2(16.0, 16.0); 686 687 if let Some(npub) = self.note_context.accounts.selected_account_pubkey().npub() { 688 item_frame(ui).show(ui, |ui| { 689 StripBuilder::new(ui) 690 .size(Size::exact(24.0)) 691 .cell_layout(Layout::left_to_right(egui::Align::Center)) 692 .vertical(|mut strip| { 693 strip.strip(|builder| { 694 builder 695 .size(Size::remainder()) 696 .size(Size::exact(16.0)) 697 .cell_layout(Layout::left_to_right(egui::Align::Center)) 698 .horizontal(|mut strip| { 699 strip.cell(|ui| { 700 ui.horizontal_wrapped(|ui| { 701 ui.label(richtext_small(&npub)); 702 }); 703 }); 704 705 strip.cell(|ui| { 706 let helper = AnimationHelper::new( 707 ui, 708 "copy-to-clipboard-npub", 709 copy_max_size, 710 ); 711 712 copy_img.paint_at(ui, helper.scaled_rect()); 713 714 if helper.take_animation_response().clicked() { 715 ui.ctx().copy_text(npub); 716 } 717 }); 718 }); 719 }); 720 }); 721 }); 722 } 723 724 let Some(filled) = self.note_context.accounts.selected_filled() else { 725 return; 726 }; 727 let Some(mut nsec) = bech32::encode::<bech32::Bech32>( 728 bech32::Hrp::parse_unchecked("nsec"), 729 &filled.secret_key.secret_bytes(), 730 ) 731 .ok() else { 732 return; 733 }; 734 735 ui.horizontal_wrapped(|ui| { 736 ui.label( 737 richtext_small(tr!( 738 self.note_context.i18n, 739 "SECRET ACCOUNT LOGIN KEY", 740 "label describing secret key" 741 )) 742 .color(ui.visuals().gray_out(ui.visuals().text_color())), 743 ); 744 }); 745 746 let is_password_id = ui.id().with("is-password"); 747 let is_password = ui 748 .ctx() 749 .data_mut(|d| d.get_temp(is_password_id)) 750 .unwrap_or(true); 751 752 item_frame(ui).show(ui, |ui| { 753 StripBuilder::new(ui) 754 .size(Size::exact(24.0)) 755 .cell_layout(Layout::left_to_right(egui::Align::Center)) 756 .vertical(|mut strip| { 757 strip.strip(|builder| { 758 builder 759 .size(Size::remainder()) 760 .size(Size::exact(48.0)) 761 .cell_layout(Layout::left_to_right(egui::Align::Center)) 762 .horizontal(|mut strip| { 763 strip.cell(|ui| { 764 if is_password { 765 ui.add( 766 TextEdit::singleline(&mut nsec) 767 .password(is_password) 768 .interactive(false) 769 .frame(false), 770 ); 771 } else { 772 ui.horizontal_wrapped(|ui| { 773 ui.label(richtext_small(&nsec)); 774 }); 775 } 776 }); 777 778 strip.cell(|ui| { 779 let helper = AnimationHelper::new( 780 ui, 781 "copy-to-clipboard-nsec", 782 copy_max_size, 783 ); 784 785 copy_img.paint_at(ui, helper.scaled_rect()); 786 787 if helper.take_animation_response().clicked() { 788 ui.ctx().copy_text(nsec); 789 } 790 791 if eye_button(ui, is_password).clicked() { 792 ui.ctx().data_mut(|d| { 793 d.insert_temp(is_password_id, !is_password) 794 }); 795 } 796 }); 797 }); 798 }); 799 }); 800 }); 801 }); 802 } 803 804 fn manage_relays_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> { 805 let mut action = None; 806 807 if ui 808 .add_sized( 809 [ui.available_width(), 30.0], 810 Button::new(richtext_small(tr!( 811 self.note_context.i18n, 812 "Configure relays", 813 "Label for configure relays, settings section", 814 ))), 815 ) 816 .clicked() 817 { 818 action = Some(SettingsAction::OpenRelays); 819 } 820 821 action 822 } 823 824 pub fn ui(&mut self, ui: &mut egui::Ui) -> DragResponse<SettingsAction> { 825 let scroll_out = Frame::default() 826 .inner_margin(Margin::symmetric(10, 10)) 827 .show(ui, |ui| { 828 ScrollArea::vertical().show(ui, |ui| { 829 let mut action = None; 830 if let Some(new_action) = self.appearance_section(ui) { 831 action = Some(new_action); 832 } 833 834 ui.add_space(5.0); 835 836 if let Some(new_action) = self.storage_section(ui) { 837 action = Some(new_action); 838 } 839 840 ui.add_space(5.0); 841 842 if let Some(new_action) = self.database_section(ui) { 843 action = Some(new_action); 844 } 845 846 ui.add_space(5.0); 847 848 self.keys_section(ui); 849 850 ui.add_space(5.0); 851 852 if let Some(new_action) = self.other_options_section(ui) { 853 action = Some(new_action); 854 } 855 856 ui.add_space(10.0); 857 858 if let Some(new_action) = self.manage_relays_section(ui) { 859 action = Some(new_action); 860 } 861 action 862 }) 863 }) 864 .inner; 865 866 DragResponse::scroll(scroll_out) 867 } 868 } 869 870 pub fn format_size(size_bytes: u64) -> String { 871 const KB: f64 = 1024.0; 872 const MB: f64 = KB * 1024.0; 873 const GB: f64 = MB * 1024.0; 874 875 let size = size_bytes as f64; 876 877 if size < KB { 878 format!("{size:.0} Bytes") 879 } else if size < MB { 880 format!("{:.1} KB", size / KB) 881 } else if size < GB { 882 format!("{:.1} MB", size / MB) 883 } else { 884 format!("{:.2} GB", size / GB) 885 } 886 } 887 888 fn item_frame(ui: &egui::Ui) -> egui::Frame { 889 Frame::new() 890 .inner_margin(Margin::same(8)) 891 .corner_radius(CornerRadius::same(8)) 892 .fill(ui.visuals().panel_fill) 893 }