notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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 }