From 0dcf70bc15c04ee22a665587bafba97725e6c799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Lo=CC=81pez=20Guevara?= Date: Tue, 29 Jul 2025 21:02:18 -0300 Subject: [PATCH 01/11] feat(settings): persist settings to storage --- crates/notedeck_columns/src/app.rs | 8 ++-- crates/notedeck_columns/src/nav.rs | 6 +-- crates/notedeck_columns/src/ui/mod.rs | 2 +- crates/notedeck_columns/src/ui/settings.rs | 46 +++++++++++----------- 4 files changed, 29 insertions(+), 33 deletions(-) diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs index 7d20acd7..39195ae3 100644 --- a/crates/notedeck_columns/src/app.rs +++ b/crates/notedeck_columns/src/app.rs @@ -10,13 +10,11 @@ use crate::{ subscriptions::{SubKind, Subscriptions}, support::Support, timeline::{self, kind::ListKind, thread::Threads, TimelineCache, TimelineKind}, - ui::{self, DesktopSidePanel, SidePanelAction}, + ui::{self, DesktopSidePanel, ShowSourceClientOption, SidePanelAction}, view_state::ViewState, Result, }; -use crate::ui::settings::ShowNoteClientOption; - use egui_extras::{Size, StripBuilder}; use enostr::{ClientMessage, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool}; use nostrdb::Transaction; @@ -506,12 +504,12 @@ impl Damus { ); note_options.set( NoteOptions::ShowNoteClientTop, - ShowNoteClientOption::Top == app_context.settings_handler.show_source_client().into() + ShowSourceClientOption::Top == app_context.settings_handler.show_source_client().into() || parsed_args.is_flag_set(ColumnsFlag::ShowNoteClientTop), ); note_options.set( NoteOptions::ShowNoteClientBottom, - ShowNoteClientOption::Bottom + ShowSourceClientOption::Bottom == app_context.settings_handler.show_source_client().into() || parsed_args.is_flag_set(ColumnsFlag::ShowNoteClientBottom), ); diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs index b34df97b..89138356 100644 --- a/crates/notedeck_columns/src/nav.rs +++ b/crates/notedeck_columns/src/nav.rs @@ -21,7 +21,7 @@ use crate::{ note::{custom_zap::CustomZapView, NewPostAction, PostAction, PostType, QuoteRepostView}, profile::EditProfileView, search::{FocusState, SearchView}, - settings::SettingsAction, + settings::{SettingsAction, ShowSourceClientOption}, support::SupportView, wallet::{get_default_zap_state, WalletAction, WalletState, WalletView}, AccountsView, PostReplyView, PostView, ProfileView, RelayView, SettingsView, ThreadView, @@ -30,8 +30,6 @@ use crate::{ Damus, }; -use crate::ui::settings::ShowNoteClientOption; - use egui_nav::{Nav, NavAction, NavResponse, NavUiType, Percent, PopupResponse, PopupSheet}; use enostr::ProfileState; use nostrdb::{Filter, Ndb, Transaction}; @@ -588,7 +586,7 @@ fn render_nav_body( .map(RenderNavAction::RelayAction), Route::Settings => { - let mut show_note_client: ShowNoteClientOption = app.note_options.into(); + let mut show_note_client: ShowSourceClientOption = app.note_options.into(); let mut theme: String = (if ui.visuals().dark_mode { "Dark" diff --git a/crates/notedeck_columns/src/ui/mod.rs b/crates/notedeck_columns/src/ui/mod.rs index 46f9a882..cadb9750 100644 --- a/crates/notedeck_columns/src/ui/mod.rs +++ b/crates/notedeck_columns/src/ui/mod.rs @@ -26,7 +26,7 @@ pub use preview::{Preview, PreviewApp, PreviewConfig}; pub use profile::ProfileView; pub use relay::RelayView; pub use settings::SettingsView; -pub use settings::ShowNoteClientOption; +pub use settings::ShowSourceClientOption; pub use side_panel::{DesktopSidePanel, SidePanelAction}; pub use thread::ThreadView; pub use timeline::TimelineView; diff --git a/crates/notedeck_columns/src/ui/settings.rs b/crates/notedeck_columns/src/ui/settings.rs index 126d4774..e8abdc32 100644 --- a/crates/notedeck_columns/src/ui/settings.rs +++ b/crates/notedeck_columns/src/ui/settings.rs @@ -6,35 +6,35 @@ use strum::Display; use crate::{nav::RouterAction, Damus, Route}; #[derive(Clone, Copy, PartialEq, Eq, Display)] -pub enum ShowNoteClientOption { +pub enum ShowSourceClientOption { Hide, Top, Bottom, } -impl From for String { - fn from(value: ShowNoteClientOption) -> Self { - match value { - ShowNoteClientOption::Hide => "hide".to_string(), - ShowNoteClientOption::Top => "top".to_string(), - ShowNoteClientOption::Bottom => "bottom".to_string(), +impl Into for ShowSourceClientOption { + fn into(self) -> String { + match self { + Self::Hide => "hide".to_string(), + Self::Top => "top".to_string(), + Self::Bottom => "bottom".to_string(), } } } -impl From for ShowNoteClientOption { +impl From for ShowSourceClientOption { fn from(note_options: NoteOptions) -> Self { if note_options.contains(NoteOptions::ShowNoteClientTop) { - ShowNoteClientOption::Top + ShowSourceClientOption::Top } else if note_options.contains(NoteOptions::ShowNoteClientBottom) { - ShowNoteClientOption::Bottom + ShowSourceClientOption::Bottom } else { - ShowNoteClientOption::Hide + ShowSourceClientOption::Hide } } } -impl From for ShowNoteClientOption { +impl From for ShowSourceClientOption { fn from(s: String) -> Self { match s.to_lowercase().as_str() { "hide" => Self::Hide, @@ -45,7 +45,7 @@ impl From for ShowNoteClientOption { } } -impl ShowNoteClientOption { +impl ShowSourceClientOption { pub fn set_note_options(self, note_options: &mut NoteOptions) { match self { Self::Hide => { @@ -67,7 +67,7 @@ impl ShowNoteClientOption { pub enum SettingsAction { SetZoomFactor(f32), SetTheme(ThemePreference), - SetShowSourceClient(ShowNoteClientOption), + SetShowSourceClient(ShowSourceClientOption), SetLocale(LanguageIdentifier), OpenRelays, OpenCacheFolder, @@ -125,7 +125,7 @@ impl SettingsAction { pub struct SettingsView<'a> { theme: &'a mut String, selected_language: &'a mut String, - show_note_client: &'a mut ShowNoteClientOption, + show_note_client: &'a mut ShowSourceClientOption, i18n: &'a mut Localization, img_cache: &'a mut Images, } @@ -135,7 +135,7 @@ impl<'a> SettingsView<'a> { img_cache: &'a mut Images, selected_language: &'a mut String, theme: &'a mut String, - show_note_client: &'a mut ShowNoteClientOption, + show_note_client: &'a mut ShowSourceClientOption, i18n: &'a mut Localization, ) -> Self { Self { @@ -160,19 +160,19 @@ impl<'a> SettingsView<'a> { } /// Get the localized label for ShowNoteClientOption - fn get_show_note_client_label(&mut self, option: ShowNoteClientOption) -> String { + fn get_show_note_client_label(&mut self, option: ShowSourceClientOption) -> String { match option { - ShowNoteClientOption::Hide => tr!( + ShowSourceClientOption::Hide => tr!( self.i18n, "Hide", "Option in settings section to hide the source client label in note display" ), - ShowNoteClientOption::Top => tr!( + ShowSourceClientOption::Top => tr!( self.i18n, "Top", "Option in settings section to show the source client label at the top of the note" ), - ShowNoteClientOption::Bottom => tr!( + ShowSourceClientOption::Bottom => tr!( self.i18n, "Bottom", "Option in settings section to show the source client label at the bottom of the note" @@ -463,9 +463,9 @@ impl<'a> SettingsView<'a> { ); for option in [ - ShowNoteClientOption::Hide, - ShowNoteClientOption::Top, - ShowNoteClientOption::Bottom, + ShowSourceClientOption::Hide, + ShowSourceClientOption::Top, + ShowSourceClientOption::Bottom, ] { let label = self.get_show_note_client_label(option); From 5848f1c355a24a8a2a84936bda7c889667825ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Lo=CC=81pez=20Guevara?= Date: Tue, 29 Jul 2025 21:09:33 -0300 Subject: [PATCH 02/11] refactor(settings): add settings sections methods --- crates/notedeck_columns/src/ui/settings.rs | 619 ++++++++++----------- 1 file changed, 305 insertions(+), 314 deletions(-) diff --git a/crates/notedeck_columns/src/ui/settings.rs b/crates/notedeck_columns/src/ui/settings.rs index e8abdc32..ad38b0cc 100644 --- a/crates/notedeck_columns/src/ui/settings.rs +++ b/crates/notedeck_columns/src/ui/settings.rs @@ -130,6 +130,29 @@ pub struct SettingsView<'a> { img_cache: &'a mut Images, } +fn small_richtext(i18n: &'_ mut Localization, text: &str, comment: &str) -> RichText { + RichText::new(tr!(i18n, text, comment)).text_style(NotedeckTextStyle::Small.text_style()) +} + +fn settings_group(ui: &mut egui::Ui, title: S, contents: impl FnOnce(&mut egui::Ui)) +where + S: Into, +{ + Frame::group(ui.style()) + .fill(ui.style().visuals.widgets.open.bg_fill) + .inner_margin(10.0) + .show(ui, |ui| { + ui.label(RichText::new(title).text_style(NotedeckTextStyle::Body.text_style())); + ui.separator(); + + ui.vertical(|ui| { + ui.spacing_mut().item_spacing = vec2(10.0, 10.0); + + contents(ui) + }); + }); +} + impl<'a> SettingsView<'a> { pub fn new( img_cache: &'a mut Images, @@ -180,328 +203,296 @@ impl<'a> SettingsView<'a> { }.to_string() } - pub fn ui(&mut self, ui: &mut egui::Ui) -> Option { - let id = ui.id(); + pub fn appearance_section(&mut self, ui: &mut egui::Ui) -> Option { let mut action = None; + let title = tr!( + self.i18n, + "Appearance", + "Label for appearance settings section", + ); + settings_group(ui, title, |ui| { + let current_zoom = ui.ctx().zoom_factor(); - Frame::default() - .inner_margin(Margin::symmetric(10, 10)) - .show(ui, |ui| { - Frame::group(ui.style()) - .fill(ui.style().visuals.widgets.open.bg_fill) - .inner_margin(10.0) - .show(ui, |ui| { - ui.vertical(|ui| { - ui.label( - RichText::new(tr!( - self.i18n, - "Appearance", - "Label for appearance settings section" - )) - .text_style(NotedeckTextStyle::Body.text_style()), - ); - ui.separator(); - ui.spacing_mut().item_spacing = vec2(10.0, 10.0); - - let current_zoom = ui.ctx().zoom_factor(); - - ui.horizontal(|ui| { - ui.label( - RichText::new(tr!( - self.i18n, - "Zoom Level:", - "Label for zoom level, Appearance settings section" - )) - .text_style(NotedeckTextStyle::Small.text_style()), - ); - - if ui - .button( - RichText::new("-") - .text_style(NotedeckTextStyle::Small.text_style()), - ) - .clicked() - { - let new_zoom = (current_zoom - 0.1).max(0.1); - action = Some(SettingsAction::SetZoomFactor(new_zoom)); - }; - - ui.label( - RichText::new(format!("{:.0}%", current_zoom * 100.0)) - .text_style(NotedeckTextStyle::Small.text_style()), - ); - - if ui - .button( - RichText::new("+") - .text_style(NotedeckTextStyle::Small.text_style()), - ) - .clicked() - { - let new_zoom = (current_zoom + 0.1).min(10.0); - action = Some(SettingsAction::SetZoomFactor(new_zoom)); - }; - - if ui - .button( - RichText::new(tr!( - self.i18n, - "Reset", - "Label for reset zoom level, Appearance settings section" - )) - .text_style(NotedeckTextStyle::Small.text_style()), - ) - .clicked() - { - action = Some(SettingsAction::SetZoomFactor(1.0)); - } - }); - - ui.horizontal(|ui| { - ui.label( - RichText::new(tr!( - self.i18n, - "Language:", - "Label for language, Appearance settings section" - )) - .text_style(NotedeckTextStyle::Small.text_style()), - ); - ComboBox::from_label("") - .selected_text(self.get_selected_language_name()) - .show_ui(ui, |ui| { - for lang in self.i18n.get_available_locales() { - let name = self.i18n - .get_locale_native_name(lang) - .map(|s| s.to_owned()) - .unwrap_or_else(|| lang.to_string()); - if ui - .selectable_value( - self.selected_language, - lang.to_string(), - name, - ) - .clicked() - { - action = Some(SettingsAction::SetLocale(lang.to_owned())) - } - } - }) - }); - - ui.horizontal(|ui| { - ui.label( - RichText::new(tr!( - self.i18n, - "Theme:", - "Label for theme, Appearance settings section" - )) - .text_style(NotedeckTextStyle::Small.text_style()), - ); - if ui - .selectable_value( - self.theme, - "Light".into(), - RichText::new(tr!( - self.i18n, - "Light", - "Label for Theme Light, Appearance settings section" - )) - .text_style(NotedeckTextStyle::Small.text_style()), - ) - .clicked() - { - action = Some(SettingsAction::SetTheme(ThemePreference::Light)); - } - if ui - .selectable_value( - self.theme, - "Dark".into(), - RichText::new(tr!( - self.i18n, - "Dark", - "Label for Theme Dark, Appearance settings section" - )) - .text_style(NotedeckTextStyle::Small.text_style()), - ) - .clicked() - { - action = Some(SettingsAction::SetTheme(ThemePreference::Dark)); - } - }); - }); - }); - - ui.add_space(5.0); - - Frame::group(ui.style()) - .fill(ui.style().visuals.widgets.open.bg_fill) - .inner_margin(10.0) - .show(ui, |ui| { - ui.label( - RichText::new(tr!( - self.i18n, - "Storage", - "Label for storage settings section" - )) - .text_style(NotedeckTextStyle::Body.text_style()), - ); - ui.separator(); - - ui.vertical(|ui| { - ui.spacing_mut().item_spacing = vec2(10.0, 10.0); - - ui.horizontal_wrapped(|ui| { - let static_imgs_size = self - .img_cache - .static_imgs - .cache_size - .lock() - .unwrap(); - - let gifs_size = self.img_cache.gifs.cache_size.lock().unwrap(); - - ui.label( - RichText::new(format!("{} {}", - tr!( - self.i18n, - "Image cache size:", - "Label for Image cache size, Storage settings section" - ), - format_size( - [static_imgs_size, gifs_size] - .iter() - .fold(0_u64, |acc, cur| acc - + cur.unwrap_or_default()) - ) - )) - .text_style(NotedeckTextStyle::Small.text_style()), - ); - - ui.end_row(); - - if !notedeck::ui::is_compiled_as_mobile() && - ui.button(RichText::new(tr!(self.i18n, "View folder", "Label for view folder button, Storage settings section")) - .text_style(NotedeckTextStyle::Small.text_style())).clicked() { - action = Some(SettingsAction::OpenCacheFolder); - } - - let clearcache_resp = ui.button( - RichText::new(tr!( - self.i18n, - "Clear cache", - "Label for clear cache button, Storage settings section" - )) - .text_style(NotedeckTextStyle::Small.text_style()) - .color(Color32::LIGHT_RED), - ); - - let id_clearcache = id.with("clear_cache"); - if clearcache_resp.clicked() { - ui.data_mut(|d| d.insert_temp(id_clearcache, true)); - } - - if ui.data_mut(|d| *d.get_temp_mut_or_default(id_clearcache)) { - let mut confirm_pressed = false; - clearcache_resp.show_tooltip_ui(|ui| { - let confirm_resp = ui.button(tr!( - self.i18n, - "Confirm", - "Label for confirm clear cache, Storage settings section" - )); - if confirm_resp.clicked() { - confirm_pressed = true; - } - - if confirm_resp.clicked() || ui.button(tr!( - self.i18n, - "Cancel", - "Label for cancel clear cache, Storage settings section" - )).clicked() { - ui.data_mut(|d| d.insert_temp(id_clearcache, false)); - } - }); - - if confirm_pressed { - action = Some(SettingsAction::ClearCacheFolder); - } else if !confirm_pressed - && clearcache_resp.clicked_elsewhere() - { - ui.data_mut(|d| d.insert_temp(id_clearcache, false)); - } - }; - }); - }); - }); - - ui.add_space(5.0); - - Frame::group(ui.style()) - .fill(ui.style().visuals.widgets.open.bg_fill) - .inner_margin(10.0) - .show(ui, |ui| { - ui.label( - RichText::new(tr!( - self.i18n, - "Others", - "Label for others settings section" - )) - .text_style(NotedeckTextStyle::Body.text_style()), - ); - ui.separator(); - ui.vertical(|ui| { - ui.spacing_mut().item_spacing = vec2(10.0, 10.0); - - ui.horizontal_wrapped(|ui| { - ui.label( - RichText::new( - tr!( - self.i18n, - "Show source client", - "Label for Show source client, others settings section" - )) - .text_style(NotedeckTextStyle::Small.text_style()), - ); - - for option in [ - ShowSourceClientOption::Hide, - ShowSourceClientOption::Top, - ShowSourceClientOption::Bottom, - ] { - let label = self.get_show_note_client_label(option); - - if ui - .selectable_value( - self.show_note_client, - option, - RichText::new(label) - .text_style(NotedeckTextStyle::Small.text_style()), - ) - .changed() - { - action = Some(SettingsAction::SetShowSourceClient(option)); - } - } - }); - }); - }); - - ui.add_space(10.0); + ui.horizontal(|ui| { + ui.label(small_richtext( + self.i18n, + "Zoom Level:", + "Label for zoom level, Appearance settings section", + )); if ui - .add_sized( - [ui.available_width(), 30.0], - Button::new( - RichText::new(tr!( - self.i18n, - "Configure relays", - "Label for configure relays, settings section" - )) - .text_style(NotedeckTextStyle::Small.text_style()), + .button(RichText::new("-").text_style(NotedeckTextStyle::Small.text_style())) + .clicked() + { + let new_zoom = (current_zoom - 0.1).max(0.1); + action = Some(SettingsAction::SetZoomFactor(new_zoom)); + }; + + ui.label( + RichText::new(format!("{:.0}%", current_zoom * 100.0)) + .text_style(NotedeckTextStyle::Small.text_style()), + ); + + if ui + .button(RichText::new("+").text_style(NotedeckTextStyle::Small.text_style())) + .clicked() + { + let new_zoom = (current_zoom + 0.1).min(10.0); + action = Some(SettingsAction::SetZoomFactor(new_zoom)); + }; + + if ui + .button(small_richtext( + self.i18n, + "Reset", + "Label for reset zoom level, Appearance settings section", + )) + .clicked() + { + action = Some(SettingsAction::SetZoomFactor(1.0)); + } + }); + + ui.horizontal(|ui| { + ui.label(small_richtext( + self.i18n, + "Language:", + "Label for language, Appearance settings section", + )); + + ComboBox::from_label("") + .selected_text(self.get_selected_language_name()) + .show_ui(ui, |ui| { + for lang in self.i18n.get_available_locales() { + let name = self + .i18n + .get_locale_native_name(lang) + .map(|s| s.to_owned()) + .unwrap_or_else(|| lang.to_string()); + if ui + .selectable_value(self.selected_language, lang.to_string(), name) + .clicked() + { + action = Some(SettingsAction::SetLocale(lang.to_owned())) + } + } + }) + }); + + ui.horizontal(|ui| { + ui.label(small_richtext( + self.i18n, + "Theme:", + "Label for theme, Appearance settings section", + )); + if ui + .selectable_value( + self.theme, + "Light".into(), + small_richtext( + self.i18n, + "Light", + "Label for Theme Light, Appearance settings section", ), ) .clicked() { - action = Some(SettingsAction::OpenRelays); + action = Some(SettingsAction::SetTheme(ThemePreference::Light)); + } + if ui + .selectable_value( + self.theme, + "Dark".into(), + small_richtext( + self.i18n, + "Dark", + "Label for Theme Dark, Appearance settings section", + ), + ) + .clicked() + { + action = Some(SettingsAction::SetTheme(ThemePreference::Dark)); + } + }); + }); + + action + } + + pub fn storage_section(&mut self, ui: &mut egui::Ui) -> Option { + let id = ui.id(); + let mut action: Option = None; + let title = tr!(self.i18n, "Storage", "Label for storage settings section"); + settings_group(ui, title, |ui| { + ui.horizontal_wrapped(|ui| { + let static_imgs_size = self.img_cache.static_imgs.cache_size.lock().unwrap(); + + let gifs_size = self.img_cache.gifs.cache_size.lock().unwrap(); + + ui.label( + RichText::new(format!( + "{} {}", + tr!( + self.i18n, + "Image cache size:", + "Label for Image cache size, Storage settings section" + ), + format_size( + [static_imgs_size, gifs_size] + .iter() + .fold(0_u64, |acc, cur| acc + cur.unwrap_or_default()) + ) + )) + .text_style(NotedeckTextStyle::Small.text_style()), + ); + + ui.end_row(); + + if !notedeck::ui::is_compiled_as_mobile() + && ui + .button(small_richtext( + self.i18n, + "View folder", + "Label for view folder button, Storage settings section", + )) + .clicked() + { + action = Some(SettingsAction::OpenCacheFolder); + } + + let clearcache_resp = ui.button( + small_richtext( + self.i18n, + "Clear cache", + "Label for clear cache button, Storage settings section", + ) + .color(Color32::LIGHT_RED), + ); + + let id_clearcache = id.with("clear_cache"); + if clearcache_resp.clicked() { + ui.data_mut(|d| d.insert_temp(id_clearcache, true)); + } + + if ui.data_mut(|d| *d.get_temp_mut_or_default(id_clearcache)) { + let mut confirm_pressed = false; + clearcache_resp.show_tooltip_ui(|ui| { + let confirm_resp = ui.button(tr!( + self.i18n, + "Confirm", + "Label for confirm clear cache, Storage settings section" + )); + if confirm_resp.clicked() { + confirm_pressed = true; + } + + if confirm_resp.clicked() + || ui + .button(tr!( + self.i18n, + "Cancel", + "Label for cancel clear cache, Storage settings section" + )) + .clicked() + { + ui.data_mut(|d| d.insert_temp(id_clearcache, false)); + } + }); + + if confirm_pressed { + action = Some(SettingsAction::ClearCacheFolder); + } else if !confirm_pressed && clearcache_resp.clicked_elsewhere() { + ui.data_mut(|d| d.insert_temp(id_clearcache, false)); + } + }; + }); + }); + + action + } + + fn other_options_section(&mut self, ui: &mut egui::Ui) -> Option { + let mut action = None; + + let title = tr!(self.i18n, "Others", "Label for others settings section"); + settings_group(ui, title, |ui| { + ui.horizontal_wrapped(|ui| { + ui.label(small_richtext( + self.i18n, + "Show source client", + "Label for Show source client, others settings section", + )); + + for option in [ + ShowSourceClientOption::Hide, + ShowSourceClientOption::Top, + ShowSourceClientOption::Bottom, + ] { + let label = self.get_show_note_client_label(option); + + if ui + .selectable_value( + self.show_note_client, + option, + RichText::new(label).text_style(NotedeckTextStyle::Small.text_style()), + ) + .changed() + { + action = Some(SettingsAction::SetShowSourceClient(option)); + } + } + }); + }); + + action + } + + fn manage_relays_section(&mut self, ui: &mut egui::Ui) -> Option { + let mut action = None; + + if ui + .add_sized( + [ui.available_width(), 30.0], + Button::new(small_richtext( + self.i18n, + "Configure relays", + "Label for configure relays, settings section", + )), + ) + .clicked() + { + action = Some(SettingsAction::OpenRelays); + } + + action + } + + pub fn ui(&mut self, ui: &mut egui::Ui) -> Option { + let mut action: Option = None; + + Frame::default() + .inner_margin(Margin::symmetric(10, 10)) + .show(ui, |ui| { + if let Some(new_action) = self.appearance_section(ui) { + action = Some(new_action); + } + + ui.add_space(5.0); + + if let Some(new_action) = self.storage_section(ui) { + action = Some(new_action); + } + + ui.add_space(5.0); + + if let Some(new_action) = self.other_options_section(ui) { + action = Some(new_action); + } + + ui.add_space(10.0); + + if let Some(new_action) = self.manage_relays_section(ui) { + action = Some(new_action); } }); From be720c0f763979ba169c977a505429261181716a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Lo=CC=81pez=20Guevara?= Date: Tue, 29 Jul 2025 21:21:06 -0300 Subject: [PATCH 03/11] fix(settings): use localization --- crates/notedeck/src/app.rs | 4 +- .../notedeck/src/persist/settings_handler.rs | 6 +- crates/notedeck_chrome/src/chrome.rs | 4 +- crates/notedeck_columns/src/ui/settings.rs | 103 ++++++++++-------- 4 files changed, 66 insertions(+), 51 deletions(-) diff --git a/crates/notedeck/src/app.rs b/crates/notedeck/src/app.rs index 36c5004f..c6e54e83 100644 --- a/crates/notedeck/src/app.rs +++ b/crates/notedeck/src/app.rs @@ -159,9 +159,7 @@ impl Notedeck { 1024usize * 1024usize * 1024usize * 1024usize }; - let mut settings_handler = SettingsHandler::new(&path); - - settings_handler.load(); + let settings_handler = SettingsHandler::new(&path).load(); let config = Config::new().set_ingester_threads(2).set_mapsize(map_size); diff --git a/crates/notedeck/src/persist/settings_handler.rs b/crates/notedeck/src/persist/settings_handler.rs index eb1b267d..6d2ed7b3 100644 --- a/crates/notedeck/src/persist/settings_handler.rs +++ b/crates/notedeck/src/persist/settings_handler.rs @@ -84,9 +84,9 @@ impl SettingsHandler { } } - pub fn load(&mut self) { + pub fn load(mut self) -> Self { if self.migrate_to_settings_file().is_ok() { - return; + return self; } match self.directory.get_file(SETTINGS_FILE.to_string()) { @@ -107,6 +107,8 @@ impl SettingsHandler { self.current_settings = Some(Settings::default()); } } + + self } pub fn save(&self) { diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs index ac2b113a..4213eaf1 100644 --- a/crates/notedeck_chrome/src/chrome.rs +++ b/crates/notedeck_chrome/src/chrome.rs @@ -112,9 +112,7 @@ impl ChromePanelAction { fn process(&self, ctx: &mut AppContext, chrome: &mut Chrome, ui: &mut egui::Ui) { match self { Self::SaveTheme(theme) => { - ui.ctx().options_mut(|o| { - o.theme_preference = *theme; - }); + ui.ctx().set_theme(*theme); ctx.settings_handler.set_theme(*theme); ctx.settings_handler.save(); } diff --git a/crates/notedeck_columns/src/ui/settings.rs b/crates/notedeck_columns/src/ui/settings.rs index ad38b0cc..82379389 100644 --- a/crates/notedeck_columns/src/ui/settings.rs +++ b/crates/notedeck_columns/src/ui/settings.rs @@ -5,6 +5,14 @@ use strum::Display; use crate::{nav::RouterAction, Damus, Route}; +const THEME_LIGHT: &str = "Light"; +const THEME_DARK: &str = "Dark"; + +const MIN_ZOOM: f32 = 0.5; +const MAX_ZOOM: f32 = 3.0; +const ZOOM_STEP: f32 = 0.1; +const RESET_ZOOM: f32 = 1.0; + #[derive(Clone, Copy, PartialEq, Eq, Display)] pub enum ShowSourceClientOption { Hide, @@ -62,6 +70,26 @@ impl ShowSourceClientOption { } } } + + fn label<'a>(&self, i18n: &'a mut Localization) -> String { + match self { + ShowSourceClientOption::Hide => tr!( + i18n, + "Hide", + "Option in settings section to hide the source client label in note display" + ), + ShowSourceClientOption::Top => tr!( + i18n, + "Top", + "Option in settings section to show the source client label at the top of the note" + ), + ShowSourceClientOption::Bottom => tr!( + i18n, + "Bottom", + "Option in settings section to show the source client label at the bottom of the note" + ), + } + } } pub enum SettingsAction { @@ -86,34 +114,32 @@ impl SettingsAction { let mut route_action: Option = None; match self { - SettingsAction::OpenRelays => { + Self::OpenRelays => { route_action = Some(RouterAction::route_to(Route::Relays)); } - SettingsAction::SetZoomFactor(zoom_factor) => { + Self::SetZoomFactor(zoom_factor) => { ctx.set_zoom_factor(zoom_factor); settings_handler.set_zoom_factor(zoom_factor); } - SettingsAction::SetShowSourceClient(option) => { + Self::SetShowSourceClient(option) => { option.set_note_options(&mut app.note_options); settings_handler.set_show_source_client(option); } - SettingsAction::SetTheme(theme) => { - ctx.options_mut(|o| { - o.theme_preference = theme; - }); + Self::SetTheme(theme) => { + ctx.set_theme(theme); settings_handler.set_theme(theme); } - SettingsAction::SetLocale(language) => { + Self::SetLocale(language) => { if i18n.set_locale(language.clone()).is_ok() { settings_handler.set_locale(language.to_string()); } } - SettingsAction::OpenCacheFolder => { + Self::OpenCacheFolder => { use opener; let _ = opener::open(img_cache.base_path.clone()); } - SettingsAction::ClearCacheFolder => { + Self::ClearCacheFolder => { let _ = img_cache.clear_folder_contents(); } } @@ -182,27 +208,6 @@ impl<'a> SettingsView<'a> { } } - /// Get the localized label for ShowNoteClientOption - fn get_show_note_client_label(&mut self, option: ShowSourceClientOption) -> String { - match option { - ShowSourceClientOption::Hide => tr!( - self.i18n, - "Hide", - "Option in settings section to hide the source client label in note display" - ), - ShowSourceClientOption::Top => tr!( - self.i18n, - "Top", - "Option in settings section to show the source client label at the top of the note" - ), - ShowSourceClientOption::Bottom => tr!( - self.i18n, - "Bottom", - "Option in settings section to show the source client label at the bottom of the note" - ), - }.to_string() - } - pub fn appearance_section(&mut self, ui: &mut egui::Ui) -> Option { let mut action = None; let title = tr!( @@ -220,11 +225,19 @@ impl<'a> SettingsView<'a> { "Label for zoom level, Appearance settings section", )); + let min_reached = current_zoom <= MIN_ZOOM; + let max_reached = current_zoom >= MAX_ZOOM; + if ui - .button(RichText::new("-").text_style(NotedeckTextStyle::Small.text_style())) + .add_enabled( + !min_reached, + Button::new( + RichText::new("-").text_style(NotedeckTextStyle::Small.text_style()), + ), + ) .clicked() { - let new_zoom = (current_zoom - 0.1).max(0.1); + let new_zoom = (current_zoom - ZOOM_STEP).max(MIN_ZOOM); action = Some(SettingsAction::SetZoomFactor(new_zoom)); }; @@ -234,10 +247,15 @@ impl<'a> SettingsView<'a> { ); if ui - .button(RichText::new("+").text_style(NotedeckTextStyle::Small.text_style())) + .add_enabled( + !max_reached, + Button::new( + RichText::new("+").text_style(NotedeckTextStyle::Small.text_style()), + ), + ) .clicked() { - let new_zoom = (current_zoom + 0.1).min(10.0); + let new_zoom = (current_zoom + ZOOM_STEP).min(MAX_ZOOM); action = Some(SettingsAction::SetZoomFactor(new_zoom)); }; @@ -249,7 +267,7 @@ impl<'a> SettingsView<'a> { )) .clicked() { - action = Some(SettingsAction::SetZoomFactor(1.0)); + action = Some(SettingsAction::SetZoomFactor(RESET_ZOOM)); } }); @@ -288,10 +306,10 @@ impl<'a> SettingsView<'a> { if ui .selectable_value( self.theme, - "Light".into(), + THEME_LIGHT.into(), small_richtext( self.i18n, - "Light", + THEME_LIGHT.into(), "Label for Theme Light, Appearance settings section", ), ) @@ -302,10 +320,10 @@ impl<'a> SettingsView<'a> { if ui .selectable_value( self.theme, - "Dark".into(), + THEME_DARK.into(), small_richtext( self.i18n, - "Dark", + THEME_DARK.into(), "Label for Theme Dark, Appearance settings section", ), ) @@ -428,13 +446,12 @@ impl<'a> SettingsView<'a> { ShowSourceClientOption::Top, ShowSourceClientOption::Bottom, ] { - let label = self.get_show_note_client_label(option); - if ui .selectable_value( self.show_note_client, option, - RichText::new(label).text_style(NotedeckTextStyle::Small.text_style()), + RichText::new(option.label(self.i18n)) + .text_style(NotedeckTextStyle::Small.text_style()), ) .changed() { From 40764d736809438d571b5cc77442f1f069ce92ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Lo=CC=81pez=20Guevara?= Date: Tue, 29 Jul 2025 21:22:39 -0300 Subject: [PATCH 04/11] fix(settings): use localization --- crates/notedeck_columns/src/ui/settings.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/notedeck_columns/src/ui/settings.rs b/crates/notedeck_columns/src/ui/settings.rs index 82379389..27ea00af 100644 --- a/crates/notedeck_columns/src/ui/settings.rs +++ b/crates/notedeck_columns/src/ui/settings.rs @@ -278,6 +278,7 @@ impl<'a> SettingsView<'a> { "Label for language, Appearance settings section", )); + // ComboBox::from_label("") .selected_text(self.get_selected_language_name()) .show_ui(ui, |ui| { @@ -294,7 +295,7 @@ impl<'a> SettingsView<'a> { action = Some(SettingsAction::SetLocale(lang.to_owned())) } } - }) + }); }); ui.horizontal(|ui| { From f2153f53dcf9b0fee85ff61c6bea7b9b92478aaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Lo=CC=81pez=20Guevara?= Date: Tue, 29 Jul 2025 21:30:35 -0300 Subject: [PATCH 05/11] feat(settings): allow sorting thread replies newest first --- crates/notedeck/src/persist/mod.rs | 1 + .../notedeck/src/persist/settings_handler.rs | 19 ++- crates/notedeck_columns/src/app.rs | 80 +++++++------ crates/notedeck_columns/src/nav.rs | 25 +--- crates/notedeck_columns/src/ui/settings.rs | 108 ++++++++++++------ crates/notedeck_columns/src/ui/thread.rs | 16 ++- crates/notedeck_ui/src/note/options.rs | 2 + 7 files changed, 154 insertions(+), 97 deletions(-) diff --git a/crates/notedeck/src/persist/mod.rs b/crates/notedeck/src/persist/mod.rs index 45bddafd..20d8bef1 100644 --- a/crates/notedeck/src/persist/mod.rs +++ b/crates/notedeck/src/persist/mod.rs @@ -5,6 +5,7 @@ mod token_handler; mod zoom; pub use app_size::AppSizeHandler; +pub use settings_handler::Settings; pub use settings_handler::SettingsHandler; pub use theme_handler::ThemeHandler; pub use token_handler::TokenHandler; diff --git a/crates/notedeck/src/persist/settings_handler.rs b/crates/notedeck/src/persist/settings_handler.rs index 6d2ed7b3..94631ee8 100644 --- a/crates/notedeck/src/persist/settings_handler.rs +++ b/crates/notedeck/src/persist/settings_handler.rs @@ -29,6 +29,7 @@ pub struct Settings { pub locale: String, pub zoom_factor: f32, pub show_source_client: String, + pub show_replies_newest_first: bool, } impl Default for Settings { @@ -38,10 +39,12 @@ impl Default for Settings { theme: DEFAULT_THEME, locale: DEFAULT_LOCALE.to_string(), zoom_factor: DEFAULT_ZOOM_FACTOR, - show_source_client: "Hide".to_string(), + show_source_client: DEFAULT_SHOW_SOURCE_CLIENT.to_string(), + show_replies_newest_first: false, } } } + pub struct SettingsHandler { directory: Directory, current_settings: Option, @@ -129,7 +132,7 @@ impl SettingsHandler { }; } - fn get_settings_mut(&mut self) -> &mut Settings { + pub fn get_settings_mut(&mut self) -> &mut Settings { if self.current_settings.is_none() { self.current_settings = Some(Settings::default()); } @@ -162,6 +165,11 @@ impl SettingsHandler { self.save(); } + pub fn set_show_replies_newest_first(&mut self, value: bool) { + self.get_settings_mut().show_replies_newest_first = value; + self.save(); + } + pub fn update_batch(&mut self, update_fn: F) where F: FnOnce(&mut Settings), @@ -204,6 +212,13 @@ impl SettingsHandler { .unwrap_or(DEFAULT_SHOW_SOURCE_CLIENT.to_string()) } + pub fn show_replies_newest_first(&self) -> bool { + self.current_settings + .as_ref() + .map(|s| s.show_replies_newest_first) + .unwrap_or(false) + } + pub fn is_loaded(&self) -> bool { self.current_settings.is_some() } diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs index 39195ae3..036dccbb 100644 --- a/crates/notedeck_columns/src/app.rs +++ b/crates/notedeck_columns/src/app.rs @@ -14,18 +14,13 @@ use crate::{ view_state::ViewState, Result, }; - use egui_extras::{Size, StripBuilder}; use enostr::{ClientMessage, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool}; use nostrdb::Transaction; use notedeck::{ - tr, ui::is_narrow, Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState, - Images, JobsCache, Localization, UnknownIds, -}; -use notedeck_ui::{ - media::{MediaViewer, MediaViewerFlags, MediaViewerState}, - NoteOptions, + tr, ui::is_narrow, Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState, Images, JobsCache, Localization, SettingsHandler, UnknownIds }; +use notedeck_ui::{media::{MediaViewer, MediaViewerFlags, MediaViewerState}, NoteOptions}; use std::collections::{BTreeSet, HashMap}; use std::path::Path; use std::time::Duration; @@ -443,6 +438,11 @@ impl Damus { let mut options = AppOptions::default(); let tmp_columns = !parsed_args.columns.is_empty(); options.set(AppOptions::TmpColumns, tmp_columns); + options.set(AppOptions::Debug, app_context.args.debug); + options.set( + AppOptions::SinceOptimize, + parsed_args.is_flag_set(ColumnsFlag::SinceOptimize), + ); let decks_cache = if tmp_columns { info!("DecksCache: loading from command line arguments"); @@ -487,37 +487,11 @@ impl Damus { // cache.add_deck_default(*pk); //} }; + let settings_handler = &app_context.settings_handler; let support = Support::new(app_context.path); - let mut note_options = NoteOptions::default(); - note_options.set( - NoteOptions::Textmode, - parsed_args.is_flag_set(ColumnsFlag::Textmode), - ); - note_options.set( - NoteOptions::ScrambleText, - parsed_args.is_flag_set(ColumnsFlag::Scramble), - ); - note_options.set( - NoteOptions::HideMedia, - parsed_args.is_flag_set(ColumnsFlag::NoMedia), - ); - note_options.set( - NoteOptions::ShowNoteClientTop, - ShowSourceClientOption::Top == app_context.settings_handler.show_source_client().into() - || parsed_args.is_flag_set(ColumnsFlag::ShowNoteClientTop), - ); - note_options.set( - NoteOptions::ShowNoteClientBottom, - ShowSourceClientOption::Bottom - == app_context.settings_handler.show_source_client().into() - || parsed_args.is_flag_set(ColumnsFlag::ShowNoteClientBottom), - ); - options.set(AppOptions::Debug, app_context.args.debug); - options.set( - AppOptions::SinceOptimize, - parsed_args.is_flag_set(ColumnsFlag::SinceOptimize), - ); + + let note_options = get_note_options(parsed_args, settings_handler); let jobs = JobsCache::default(); @@ -599,6 +573,39 @@ impl Damus { } } +fn get_note_options(args: ColumnsArgs, settings_handler: &&mut SettingsHandler) -> NoteOptions { + let mut note_options = NoteOptions::default(); + + note_options.set( + NoteOptions::Textmode, + args.is_flag_set(ColumnsFlag::Textmode), + ); + note_options.set( + NoteOptions::ScrambleText, + args.is_flag_set(ColumnsFlag::Scramble), + ); + note_options.set( + NoteOptions::HideMedia, + args.is_flag_set(ColumnsFlag::NoMedia), + ); + note_options.set( + NoteOptions::ShowNoteClientTop, + ShowSourceClientOption::Top == settings_handler.show_source_client().into() + || args.is_flag_set(ColumnsFlag::ShowNoteClientTop), + ); + note_options.set( + NoteOptions::ShowNoteClientBottom, + ShowSourceClientOption::Bottom == settings_handler.show_source_client().into() + || args.is_flag_set(ColumnsFlag::ShowNoteClientBottom), + ); + + note_options.set( + NoteOptions::RepliesNewestFirst, + settings_handler.show_replies_newest_first(), + ); + note_options +} + /* fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) { let stroke = ui.style().interact(&response).fg_stroke; @@ -620,6 +627,7 @@ fn render_damus_mobile( let mut app_action: Option = None; let active_col = app.columns_mut(app_ctx.i18n, app_ctx.accounts).selected as usize; + if !app.columns(app_ctx.accounts).columns().is_empty() { let r = nav::render_nav( active_col, diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs index 89138356..6dae444d 100644 --- a/crates/notedeck_columns/src/nav.rs +++ b/crates/notedeck_columns/src/nav.rs @@ -21,7 +21,7 @@ use crate::{ note::{custom_zap::CustomZapView, NewPostAction, PostAction, PostType, QuoteRepostView}, profile::EditProfileView, search::{FocusState, SearchView}, - settings::{SettingsAction, ShowSourceClientOption}, + settings::SettingsAction, support::SupportView, wallet::{get_default_zap_state, WalletAction, WalletState, WalletView}, AccountsView, PostReplyView, PostView, ProfileView, RelayView, SettingsView, ThreadView, @@ -586,26 +586,11 @@ fn render_nav_body( .map(RenderNavAction::RelayAction), Route::Settings => { - let mut show_note_client: ShowSourceClientOption = app.note_options.into(); + let mut settings = ctx.settings_handler.get_settings_mut(); - let mut theme: String = (if ui.visuals().dark_mode { - "Dark" - } else { - "Light" - }) - .into(); - - let mut selected_language: String = ctx.i18n.get_current_locale().to_string(); - - SettingsView::new( - ctx.img_cache, - &mut selected_language, - &mut theme, - &mut show_note_client, - ctx.i18n, - ) - .ui(ui) - .map(RenderNavAction::SettingsAction) + SettingsView::new(ctx.i18n, ctx.img_cache, &mut settings) + .ui(ui) + .map(RenderNavAction::SettingsAction) } Route::Reply(id) => { let txn = if let Ok(txn) = Transaction::new(ctx.ndb) { diff --git a/crates/notedeck_columns/src/ui/settings.rs b/crates/notedeck_columns/src/ui/settings.rs index 27ea00af..71d16d14 100644 --- a/crates/notedeck_columns/src/ui/settings.rs +++ b/crates/notedeck_columns/src/ui/settings.rs @@ -1,5 +1,7 @@ -use egui::{vec2, Button, Color32, ComboBox, Frame, Margin, RichText, ThemePreference}; -use notedeck::{tr, Images, LanguageIdentifier, Localization, NotedeckTextStyle, SettingsHandler}; +use egui::{vec2, Button, Color32, ComboBox, Frame, Margin, RichText, ScrollArea, ThemePreference}; +use notedeck::{ + tr, Images, LanguageIdentifier, Localization, NotedeckTextStyle, Settings, SettingsHandler, +}; use notedeck_ui::NoteOptions; use strum::Display; @@ -97,6 +99,7 @@ pub enum SettingsAction { SetTheme(ThemePreference), SetShowSourceClient(ShowSourceClientOption), SetLocale(LanguageIdentifier), + SetRepliestNewestFirst(bool), OpenRelays, OpenCacheFolder, ClearCacheFolder, @@ -135,6 +138,11 @@ impl SettingsAction { settings_handler.set_locale(language.to_string()); } } + Self::SetRepliestNewestFirst(value) => { + app.note_options.set(NoteOptions::RepliesNewestFirst, value); + settings_handler.set_show_replies_newest_first(value); + settings_handler.save(); + } Self::OpenCacheFolder => { use opener; let _ = opener::open(img_cache.base_path.clone()); @@ -149,9 +157,7 @@ impl SettingsAction { } pub struct SettingsView<'a> { - theme: &'a mut String, - selected_language: &'a mut String, - show_note_client: &'a mut ShowSourceClientOption, + settings: &'a mut Settings, i18n: &'a mut Localization, img_cache: &'a mut Images, } @@ -181,30 +187,30 @@ where impl<'a> SettingsView<'a> { pub fn new( - img_cache: &'a mut Images, - selected_language: &'a mut String, - theme: &'a mut String, - show_note_client: &'a mut ShowSourceClientOption, i18n: &'a mut Localization, + img_cache: &'a mut Images, + settings: &'a mut Settings, + // theme: &'a mut String, + // show_note_client: &'a mut ShowSourceClientOption, + // show_wide: &'a mut bool, + // show_replies_newest_first: &'a mut bool, ) -> Self { Self { - show_note_client, - theme, + settings, img_cache, - selected_language, i18n, } } /// Get the localized name for a language identifier fn get_selected_language_name(&mut self) -> String { - if let Ok(lang_id) = self.selected_language.parse::() { + if let Ok(lang_id) = self.settings.locale.parse::() { self.i18n .get_locale_native_name(&lang_id) .map(|s| s.to_owned()) .unwrap_or_else(|| lang_id.to_string()) } else { - self.selected_language.clone() + self.settings.locale.clone() } } @@ -289,7 +295,7 @@ impl<'a> SettingsView<'a> { .map(|s| s.to_owned()) .unwrap_or_else(|| lang.to_string()); if ui - .selectable_value(self.selected_language, lang.to_string(), name) + .selectable_value(&mut self.settings.locale, lang.to_string(), name) .clicked() { action = Some(SettingsAction::SetLocale(lang.to_owned())) @@ -304,10 +310,11 @@ impl<'a> SettingsView<'a> { "Theme:", "Label for theme, Appearance settings section", )); + if ui .selectable_value( - self.theme, - THEME_LIGHT.into(), + &mut self.settings.theme, + ThemePreference::Light, small_richtext( self.i18n, THEME_LIGHT.into(), @@ -318,10 +325,11 @@ impl<'a> SettingsView<'a> { { action = Some(SettingsAction::SetTheme(ThemePreference::Light)); } + if ui .selectable_value( - self.theme, - THEME_DARK.into(), + &mut self.settings.theme, + ThemePreference::Dark, small_richtext( self.i18n, THEME_DARK.into(), @@ -435,11 +443,32 @@ impl<'a> SettingsView<'a> { let title = tr!(self.i18n, "Others", "Label for others settings section"); settings_group(ui, title, |ui| { + ui.horizontal(|ui| { + ui.label(small_richtext( + self.i18n, + "Sort replies newest first", + "Label for Sort replies newest first, others settings section", + )); + + if ui + .toggle_value( + &mut self.settings.show_replies_newest_first, + RichText::new(tr!(self.i18n, "ON", "ON")) + .text_style(NotedeckTextStyle::Small.text_style()), + ) + .changed() + { + action = Some(SettingsAction::SetRepliestNewestFirst( + self.settings.show_replies_newest_first, + )); + } + }); + ui.horizontal_wrapped(|ui| { ui.label(small_richtext( self.i18n, - "Show source client", - "Label for Show source client, others settings section", + "Source client", + "Label for Source client, others settings section", )); for option in [ @@ -447,9 +476,12 @@ impl<'a> SettingsView<'a> { ShowSourceClientOption::Top, ShowSourceClientOption::Bottom, ] { + let mut current: ShowSourceClientOption = + self.settings.show_source_client.clone().into(); + if ui .selectable_value( - self.show_note_client, + &mut current, option, RichText::new(option.label(self.i18n)) .text_style(NotedeckTextStyle::Small.text_style()), @@ -491,27 +523,29 @@ impl<'a> SettingsView<'a> { Frame::default() .inner_margin(Margin::symmetric(10, 10)) .show(ui, |ui| { - if let Some(new_action) = self.appearance_section(ui) { - action = Some(new_action); - } + ScrollArea::vertical().show(ui, |ui| { + if let Some(new_action) = self.appearance_section(ui) { + action = Some(new_action); + } - ui.add_space(5.0); + ui.add_space(5.0); - if let Some(new_action) = self.storage_section(ui) { - action = Some(new_action); - } + if let Some(new_action) = self.storage_section(ui) { + action = Some(new_action); + } - ui.add_space(5.0); + ui.add_space(5.0); - if let Some(new_action) = self.other_options_section(ui) { - action = Some(new_action); - } + if let Some(new_action) = self.other_options_section(ui) { + action = Some(new_action); + } - ui.add_space(10.0); + ui.add_space(10.0); - if let Some(new_action) = self.manage_relays_section(ui) { - action = Some(new_action); - } + if let Some(new_action) = self.manage_relays_section(ui) { + action = Some(new_action); + } + }); }); action diff --git a/crates/notedeck_columns/src/ui/thread.rs b/crates/notedeck_columns/src/ui/thread.rs index 91094fc4..1bcc3574 100644 --- a/crates/notedeck_columns/src/ui/thread.rs +++ b/crates/notedeck_columns/src/ui/thread.rs @@ -115,7 +115,10 @@ impl<'a, 'd> ThreadView<'a, 'd> { .unwrap() .list; - let notes = note_builder.into_notes(&mut self.threads.seen_flags); + let notes = note_builder.into_notes( + self.note_options.contains(NoteOptions::RepliesNewestFirst), + &mut self.threads.seen_flags, + ); if !full_chain { // TODO(kernelkind): insert UI denoting we don't have the full chain yet @@ -223,7 +226,11 @@ impl<'a> ThreadNoteBuilder<'a> { self.replies.push(note); } - pub fn into_notes(mut self, seen_flags: &mut NoteSeenFlags) -> ThreadNotes<'a> { + pub fn into_notes( + mut self, + replies_newer_first: bool, + seen_flags: &mut NoteSeenFlags, + ) -> ThreadNotes<'a> { let mut notes = Vec::new(); let selected_is_root = self.chain.is_empty(); @@ -246,6 +253,11 @@ impl<'a> ThreadNoteBuilder<'a> { unread_and_have_replies: false, }); + if replies_newer_first { + self.replies + .sort_by(|a, b| b.created_at().cmp(&a.created_at())); + } + for reply in self.replies { notes.push(ThreadNote { unread_and_have_replies: *seen_flags.get(reply.id()).unwrap_or(&false), diff --git a/crates/notedeck_ui/src/note/options.rs b/crates/notedeck_ui/src/note/options.rs index a87f0e65..0fdb6811 100644 --- a/crates/notedeck_ui/src/note/options.rs +++ b/crates/notedeck_ui/src/note/options.rs @@ -25,6 +25,8 @@ bitflags! { /// Show note's client in the note header const ShowNoteClientTop = 1 << 12; const ShowNoteClientBottom = 1 << 13; + + const RepliesNewestFirst = 1 << 14; } } From 692f4889cfc648e63c431b1d84eaa20a2c2e4130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Lo=CC=81pez=20Guevara?= Date: Tue, 29 Jul 2025 21:31:36 -0300 Subject: [PATCH 06/11] update i18n comments for source client options Co-authored-by: Terry Yiu <963907+tyiu@users.noreply.github.com> --- crates/notedeck_columns/src/ui/settings.rs | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/crates/notedeck_columns/src/ui/settings.rs b/crates/notedeck_columns/src/ui/settings.rs index 71d16d14..5386f4e2 100644 --- a/crates/notedeck_columns/src/ui/settings.rs +++ b/crates/notedeck_columns/src/ui/settings.rs @@ -75,21 +75,9 @@ impl ShowSourceClientOption { fn label<'a>(&self, i18n: &'a mut Localization) -> String { match self { - ShowSourceClientOption::Hide => tr!( - i18n, - "Hide", - "Option in settings section to hide the source client label in note display" - ), - ShowSourceClientOption::Top => tr!( - i18n, - "Top", - "Option in settings section to show the source client label at the top of the note" - ), - ShowSourceClientOption::Bottom => tr!( - i18n, - "Bottom", - "Option in settings section to show the source client label at the bottom of the note" - ), + Self::Hide => tr!(i18n, "Hide", "Option in settings section to hide the source client label in note display"), + Self::Top => tr!(i18n, "Top", "Option in settings section to show the source client label at the top of the note"), + Self::Bottom => tr!(i18n, "Bottom", "Option in settings section to show the source client label at the bottom of the note"), } } } From 1163dd8461eb0182d7b2e1a5e1940b451355b964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Lo=CC=81pez=20Guevara?= Date: Tue, 29 Jul 2025 21:33:05 -0300 Subject: [PATCH 07/11] feat(settings): persist settings to storage --- crates/notedeck_columns/src/ui/settings.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/notedeck_columns/src/ui/settings.rs b/crates/notedeck_columns/src/ui/settings.rs index 5386f4e2..97b46c47 100644 --- a/crates/notedeck_columns/src/ui/settings.rs +++ b/crates/notedeck_columns/src/ui/settings.rs @@ -178,10 +178,6 @@ impl<'a> SettingsView<'a> { i18n: &'a mut Localization, img_cache: &'a mut Images, settings: &'a mut Settings, - // theme: &'a mut String, - // show_note_client: &'a mut ShowSourceClientOption, - // show_wide: &'a mut bool, - // show_replies_newest_first: &'a mut bool, ) -> Self { Self { settings, From d1a9e0020ec94d4126f6b331fa18801de1a8ee07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Lo=CC=81pez=20Guevara?= Date: Tue, 29 Jul 2025 21:34:28 -0300 Subject: [PATCH 08/11] fix(note-content): avoid empty text blocks (cherry picked from commit baa7031c25d0f3d3e8952f49f6625252413559a3) --- crates/notedeck_ui/src/note/contents.rs | 31 +++++++++++++++++-------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/crates/notedeck_ui/src/note/contents.rs b/crates/notedeck_ui/src/note/contents.rs index ca231a67..1d7f762b 100644 --- a/crates/notedeck_ui/src/note/contents.rs +++ b/crates/notedeck_ui/src/note/contents.rs @@ -4,12 +4,12 @@ use crate::{ }; use notedeck::{JobsCache, RenderableMedia}; -use egui::{Color32, Hyperlink, RichText}; +use egui::{vec2, Color32, Hyperlink, RichText}; use nostrdb::{BlockType, Mention, Note, NoteKey, Transaction}; use tracing::warn; use super::media::image_carousel; -use notedeck::{update_imeta_blurhashes, IsFollowing, NoteCache, NoteContext}; +use notedeck::{update_imeta_blurhashes, IsFollowing, NoteCache, NoteContext, NotedeckTextStyle}; pub struct NoteContents<'a, 'd> { note_context: &'a mut NoteContext<'d>, @@ -158,9 +158,9 @@ pub fn render_note_contents<'a>( return; }; - ui.spacing_mut().item_spacing.x = 0.0; + ui.spacing_mut().item_spacing = vec2(0.0, 0.0); - for block in blocks.iter(note) { + 'block_loop: for block in blocks.iter(note) { match block.blocktype() { BlockType::MentionBech32 => match block.as_mention().unwrap() { Mention::Profile(profile) => { @@ -258,17 +258,28 @@ pub fn render_note_contents<'a>( current_len += block_str.len(); block_str }; - + if block_str.trim().len() == 0 { + continue 'block_loop; + } if options.contains(NoteOptions::ScrambleText) { ui.add( - egui::Label::new(rot13(block_str)) - .wrap() - .selectable(selectable), + egui::Label::new( + RichText::new(rot13(block_str)) + .text_style(NotedeckTextStyle::Body.text_style()), + ) + .wrap() + .selectable(selectable), ); } else { - ui.add(egui::Label::new(block_str).wrap().selectable(selectable)); + ui.add( + egui::Label::new( + RichText::new(block_str) + .text_style(NotedeckTextStyle::Body.text_style()), + ) + .wrap() + .selectable(selectable), + ); } - // don't render any more blocks if truncate { break; From b9e2fe5dd150eff5484b0c05fcd14ab6a3f69fd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Lo=CC=81pez=20Guevara?= Date: Tue, 29 Jul 2025 21:38:04 -0300 Subject: [PATCH 09/11] fix(media): add spacing --- crates/notedeck_ui/src/note/media.rs | 116 ++++++++++++++------------- 1 file changed, 61 insertions(+), 55 deletions(-) diff --git a/crates/notedeck_ui/src/note/media.rs b/crates/notedeck_ui/src/note/media.rs index 249d9d31..dcf7b228 100644 --- a/crates/notedeck_ui/src/note/media.rs +++ b/crates/notedeck_ui/src/note/media.rs @@ -50,67 +50,73 @@ pub fn image_carousel( .drag_to_scroll(false) .id_salt(carousel_id) .show(ui, |ui| { - ui.horizontal(|ui| { - let mut media_infos: Vec = Vec::with_capacity(medias.len()); - let mut media_action: Option<(usize, MediaUIAction)> = None; + let response = ui + .horizontal(|ui| { + let spacing = ui.spacing_mut(); + spacing.item_spacing.x = 8.0; - for (i, media) in medias.iter().enumerate() { - let RenderableMedia { - url, - media_type, - obfuscation_type: blur_type, - } = media; + let mut media_infos: Vec = Vec::with_capacity(medias.len()); + let mut media_action: Option<(usize, MediaUIAction)> = None; - let cache = match media_type { - MediaCacheType::Image => &mut img_cache.static_imgs, - MediaCacheType::Gif => &mut img_cache.gifs, - }; - let media_state = get_content_media_render_state( - ui, - job_pool, - jobs, - trusted_media, - size, - &mut cache.textures_cache, - url, - *media_type, - &cache.cache_dir, - blur_type, - ); + for (i, media) in medias.iter().enumerate() { + let RenderableMedia { + url, + media_type, + obfuscation_type: blur_type, + } = media; - let media_response = render_media( - ui, - &mut img_cache.gif_states, - media_state, - url, - size, - i18n, - note_options.contains(NoteOptions::Wide), - ); + let cache = match media_type { + MediaCacheType::Image => &mut img_cache.static_imgs, + MediaCacheType::Gif => &mut img_cache.gifs, + }; + let media_state = get_content_media_render_state( + ui, + job_pool, + jobs, + trusted_media, + size, + &mut cache.textures_cache, + url, + *media_type, + &cache.cache_dir, + blur_type, + ); - if let Some(action) = media_response.inner { - media_action = Some((i, action)) + let media_response = render_media( + ui, + &mut img_cache.gif_states, + media_state, + url, + size, + i18n, + note_options.contains(NoteOptions::Wide), + ); + + if let Some(action) = media_response.inner { + media_action = Some((i, action)) + } + + let rect = media_response.response.rect; + media_infos.push(MediaInfo { + url: url.clone(), + original_position: rect, + }) } - let rect = media_response.response.rect; - media_infos.push(MediaInfo { - url: url.clone(), - original_position: rect, - }) - } - - if let Some((i, media_action)) = media_action { - action = media_action.into_media_action( - ui.ctx(), - medias, - media_infos, - i, - img_cache, - ImageType::Content(Some((size.x as u32, size.y as u32))), - ); - } - }) - .response + if let Some((i, media_action)) = media_action { + action = media_action.into_media_action( + ui.ctx(), + medias, + media_infos, + i, + img_cache, + ImageType::Content(Some((size.x as u32, size.y as u32))), + ); + } + }) + .response; + ui.add_space(8.0); + response }) .inner }); From 9ff5753bcaead14d7d992eb03ee9ff96b93562a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Lo=CC=81pez=20Guevara?= Date: Tue, 29 Jul 2025 21:41:03 -0300 Subject: [PATCH 10/11] settings: use timed serializer, handle zoom properly, use custom text style for note body font size, added font size slider, added preview note --- crates/notedeck/src/app.rs | 42 ++-- crates/notedeck/src/context.rs | 2 +- crates/notedeck/src/fonts.rs | 2 + crates/notedeck/src/persist/mod.rs | 5 +- .../notedeck/src/persist/settings_handler.rs | 116 +++++---- crates/notedeck/src/persist/theme_handler.rs | 76 ------ crates/notedeck/src/persist/zoom.rs | 26 -- crates/notedeck/src/style.rs | 3 + crates/notedeck/src/timed_serializer.rs | 16 +- crates/notedeck/src/ui.rs | 13 +- crates/notedeck_chrome/src/android.rs | 8 +- crates/notedeck_chrome/src/chrome.rs | 3 +- crates/notedeck_chrome/src/notedeck.rs | 8 +- crates/notedeck_chrome/src/preview.rs | 8 +- crates/notedeck_chrome/src/setup.rs | 22 +- crates/notedeck_columns/src/app.rs | 12 +- crates/notedeck_columns/src/nav.rs | 25 +- crates/notedeck_columns/src/ui/settings.rs | 228 ++++++++++++------ crates/notedeck_ui/src/mention.rs | 6 +- crates/notedeck_ui/src/note/contents.rs | 34 ++- crates/notedeck_ui/src/note/mod.rs | 7 +- 21 files changed, 379 insertions(+), 283 deletions(-) delete mode 100644 crates/notedeck/src/persist/theme_handler.rs delete mode 100644 crates/notedeck/src/persist/zoom.rs diff --git a/crates/notedeck/src/app.rs b/crates/notedeck/src/app.rs index c6e54e83..76a6411b 100644 --- a/crates/notedeck/src/app.rs +++ b/crates/notedeck/src/app.rs @@ -1,13 +1,13 @@ use crate::account::FALLBACK_PUBKEY; use crate::i18n::Localization; -use crate::persist::{AppSizeHandler, ZoomHandler}; +use crate::persist::{AppSizeHandler, SettingsHandler}; use crate::wallet::GlobalWallet; use crate::zaps::Zaps; +use crate::JobPool; use crate::{ frame_history::FrameHistory, AccountStorage, Accounts, AppContext, Args, DataPath, DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, UnknownIds, }; -use crate::{JobPool, SettingsHandler}; use egui::Margin; use egui::ThemePreference; use egui_winit::clipboard::Clipboard; @@ -40,9 +40,8 @@ pub struct Notedeck { global_wallet: GlobalWallet, path: DataPath, args: Args, - settings_handler: SettingsHandler, + settings: SettingsHandler, app: Option>>, - zoom: ZoomHandler, app_size: AppSizeHandler, unrecognized_args: BTreeSet, clipboard: Clipboard, @@ -99,7 +98,15 @@ impl eframe::App for Notedeck { render_notedeck(self, ctx); - self.zoom.try_save_zoom_factor(ctx); + self.settings.update_batch(|settings| { + settings.zoom_factor = ctx.zoom_factor(); + settings.locale = self.i18n.get_current_locale().to_string(); + settings.theme = if ctx.style().visuals.dark_mode { + ThemePreference::Dark + } else { + ThemePreference::Light + }; + }); self.app_size.try_save_app_size(ctx); if self.args.relay_debug { @@ -159,7 +166,7 @@ impl Notedeck { 1024usize * 1024usize * 1024usize * 1024usize }; - let settings_handler = SettingsHandler::new(&path).load(); + let settings = SettingsHandler::new(&path).load(); let config = Config::new().set_ingester_threads(2).set_mapsize(map_size); @@ -214,12 +221,8 @@ impl Notedeck { let img_cache = Images::new(img_cache_dir); let note_cache = NoteCache::default(); - let zoom = ZoomHandler::new(&path); - let app_size = AppSizeHandler::new(&path); - if let Some(z) = zoom.get_zoom_factor() { - ctx.set_zoom_factor(z); - } + let app_size = AppSizeHandler::new(&path); // migrate if let Err(e) = img_cache.migrate_v0() { @@ -234,7 +237,7 @@ impl Notedeck { let mut i18n = Localization::new(); let setting_locale: Result = - settings_handler.locale().parse(); + settings.locale().parse(); if setting_locale.is_ok() { if let Err(err) = i18n.set_locale(setting_locale.unwrap()) { @@ -261,9 +264,8 @@ impl Notedeck { global_wallet, path: path.clone(), args: parsed_args, - settings_handler, + settings, app: None, - zoom, app_size, unrecognized_args, frame_history: FrameHistory::default(), @@ -290,7 +292,7 @@ impl Notedeck { global_wallet: &mut self.global_wallet, path: &self.path, args: &self.args, - settings_handler: &mut self.settings_handler, + settings: &mut self.settings, clipboard: &mut self.clipboard, zaps: &mut self.zaps, frame_history: &mut self.frame_history, @@ -308,7 +310,15 @@ impl Notedeck { } pub fn theme(&self) -> ThemePreference { - self.settings_handler.theme() + self.settings.theme() + } + + pub fn note_body_font_size(&self) -> f32 { + self.settings.note_body_font_size() + } + + pub fn zoom_factor(&self) -> f32 { + self.settings.zoom_factor() } pub fn unrecognized_args(&self) -> &BTreeSet { diff --git a/crates/notedeck/src/context.rs b/crates/notedeck/src/context.rs index 4d3af5b5..feac9eda 100644 --- a/crates/notedeck/src/context.rs +++ b/crates/notedeck/src/context.rs @@ -20,7 +20,7 @@ pub struct AppContext<'a> { pub global_wallet: &'a mut GlobalWallet, pub path: &'a DataPath, pub args: &'a Args, - pub settings_handler: &'a mut SettingsHandler, + pub settings: &'a mut SettingsHandler, pub clipboard: &'a mut Clipboard, pub zaps: &'a mut Zaps, pub frame_history: &'a mut FrameHistory, diff --git a/crates/notedeck/src/fonts.rs b/crates/notedeck/src/fonts.rs index 5fed62d2..6673f917 100644 --- a/crates/notedeck/src/fonts.rs +++ b/crates/notedeck/src/fonts.rs @@ -31,6 +31,7 @@ pub fn desktop_font_size(text_style: &NotedeckTextStyle) -> f32 { NotedeckTextStyle::Button => 13.0, NotedeckTextStyle::Small => 12.0, NotedeckTextStyle::Tiny => 10.0, + NotedeckTextStyle::NoteBody => 16.0, } } @@ -46,6 +47,7 @@ pub fn mobile_font_size(text_style: &NotedeckTextStyle) -> f32 { NotedeckTextStyle::Button => 13.0, NotedeckTextStyle::Small => 12.0, NotedeckTextStyle::Tiny => 10.0, + NotedeckTextStyle::NoteBody => 13.0, } } diff --git a/crates/notedeck/src/persist/mod.rs b/crates/notedeck/src/persist/mod.rs index 20d8bef1..c3d84c6c 100644 --- a/crates/notedeck/src/persist/mod.rs +++ b/crates/notedeck/src/persist/mod.rs @@ -1,12 +1,9 @@ mod app_size; mod settings_handler; -mod theme_handler; mod token_handler; -mod zoom; pub use app_size::AppSizeHandler; pub use settings_handler::Settings; pub use settings_handler::SettingsHandler; -pub use theme_handler::ThemeHandler; +pub use settings_handler::DEFAULT_NOTE_BODY_FONT_SIZE; pub use token_handler::TokenHandler; -pub use zoom::ZoomHandler; diff --git a/crates/notedeck/src/persist/settings_handler.rs b/crates/notedeck/src/persist/settings_handler.rs index 94631ee8..94b17e82 100644 --- a/crates/notedeck/src/persist/settings_handler.rs +++ b/crates/notedeck/src/persist/settings_handler.rs @@ -1,18 +1,23 @@ use crate::{ - storage::{self, delete_file}, - DataPath, DataPathType, Directory, + storage::delete_file, timed_serializer::TimedSerializer, DataPath, DataPathType, Directory, }; use egui::ThemePreference; use serde::{Deserialize, Serialize}; use tracing::{error, info}; const THEME_FILE: &str = "theme.txt"; +const ZOOM_FACTOR_FILE: &str = "zoom_level.json"; const SETTINGS_FILE: &str = "settings.json"; const DEFAULT_THEME: ThemePreference = ThemePreference::Dark; const DEFAULT_LOCALE: &str = "es-US"; const DEFAULT_ZOOM_FACTOR: f32 = 1.0; const DEFAULT_SHOW_SOURCE_CLIENT: &str = "hide"; +const DEFAULT_SHOW_REPLIES_NEWEST_FIRST: bool = false; +#[cfg(any(target_os = "android", target_os = "ios"))] +pub const DEFAULT_NOTE_BODY_FONT_SIZE: f32 = 13.0; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub const DEFAULT_NOTE_BODY_FONT_SIZE: f32 = 16.0; fn deserialize_theme(serialized_theme: &str) -> Option { match serialized_theme { @@ -23,72 +28,96 @@ fn deserialize_theme(serialized_theme: &str) -> Option { } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, PartialEq, Clone)] pub struct Settings { pub theme: ThemePreference, pub locale: String, pub zoom_factor: f32, pub show_source_client: String, pub show_replies_newest_first: bool, + pub note_body_font_size: f32, } impl Default for Settings { fn default() -> Self { - // Use the same fallback theme as before Self { theme: DEFAULT_THEME, locale: DEFAULT_LOCALE.to_string(), zoom_factor: DEFAULT_ZOOM_FACTOR, show_source_client: DEFAULT_SHOW_SOURCE_CLIENT.to_string(), - show_replies_newest_first: false, + show_replies_newest_first: DEFAULT_SHOW_REPLIES_NEWEST_FIRST, + note_body_font_size: DEFAULT_NOTE_BODY_FONT_SIZE, } } } pub struct SettingsHandler { directory: Directory, + serializer: TimedSerializer, current_settings: Option, } impl SettingsHandler { - fn read_legacy_theme(&self) -> Option { + fn read_from_theme_file(&self) -> Option { match self.directory.get_file(THEME_FILE.to_string()) { Ok(contents) => deserialize_theme(contents.trim()), Err(_) => None, } } - fn migrate_to_settings_file(&mut self) -> Result<(), ()> { + fn read_from_zomfactor_file(&self) -> Option { + match self.directory.get_file(ZOOM_FACTOR_FILE.to_string()) { + Ok(contents) => serde_json::from_str::(&contents).ok(), + Err(_) => None, + } + } + + fn migrate_to_settings_file(&mut self) -> bool { + let mut settings = Settings::default(); + let mut migrated = false; // if theme.txt exists migrate - if let Some(theme_from_file) = self.read_legacy_theme() { + if let Some(theme_from_file) = self.read_from_theme_file() { info!("migrating theme preference from theme.txt file"); _ = delete_file(&self.directory.file_path, THEME_FILE.to_string()); - self.current_settings = Some(Settings { - theme: theme_from_file, - ..Settings::default() - }); - - self.save(); - - Ok(()) + settings.theme = theme_from_file; + migrated = true; } else { - Err(()) + info!("theme.txt file not found, using default theme"); + }; + + // if zoom_factor.txt exists migrate + if let Some(zom_factor) = self.read_from_zomfactor_file() { + info!("migrating theme preference from zom_factor file"); + _ = delete_file(&self.directory.file_path, ZOOM_FACTOR_FILE.to_string()); + + settings.zoom_factor = zom_factor; + migrated = true; + } else { + info!("zoom_factor.txt exists migrate file not found, using default zoom factor"); + }; + + if migrated { + self.current_settings = Some(settings); + self.try_save_settings(); } + migrated } pub fn new(path: &DataPath) -> Self { let directory = Directory::new(path.path(DataPathType::Setting)); - let current_settings: Option = None; + let serializer = + TimedSerializer::new(path, DataPathType::Setting, "settings.json".to_owned()); Self { directory, - current_settings, + serializer, + current_settings: None, } } pub fn load(mut self) -> Self { - if self.migrate_to_settings_file().is_ok() { + if self.migrate_to_settings_file() { return self; } @@ -114,22 +143,9 @@ impl SettingsHandler { self } - pub fn save(&self) { - let settings = self.current_settings.as_ref().unwrap(); - match serde_json::to_string(settings) { - Ok(serialized) => { - if let Err(e) = storage::write_file( - &self.directory.file_path, - SETTINGS_FILE.to_string(), - &serialized, - ) { - error!("Could not save settings: {}", e); - } else { - info!("Settings saved successfully"); - } - } - Err(e) => error!("Failed to serialize settings: {}", e), - }; + pub(crate) fn try_save_settings(&mut self) { + let settings = self.get_settings_mut().clone(); + self.serializer.try_save(settings); } pub fn get_settings_mut(&mut self) -> &mut Settings { @@ -141,7 +157,7 @@ impl SettingsHandler { pub fn set_theme(&mut self, theme: ThemePreference) { self.get_settings_mut().theme = theme; - self.save(); + self.try_save_settings(); } pub fn set_locale(&mut self, locale: S) @@ -149,12 +165,12 @@ impl SettingsHandler { S: Into, { self.get_settings_mut().locale = locale.into(); - self.save(); + self.try_save_settings(); } pub fn set_zoom_factor(&mut self, zoom_factor: f32) { self.get_settings_mut().zoom_factor = zoom_factor; - self.save(); + self.try_save_settings(); } pub fn set_show_source_client(&mut self, option: S) @@ -162,12 +178,17 @@ impl SettingsHandler { S: Into, { self.get_settings_mut().show_source_client = option.into(); - self.save(); + self.try_save_settings(); } pub fn set_show_replies_newest_first(&mut self, value: bool) { self.get_settings_mut().show_replies_newest_first = value; - self.save(); + self.try_save_settings(); + } + + pub fn set_note_body_font_size(&mut self, value: f32) { + self.get_settings_mut().note_body_font_size = value; + self.try_save_settings(); } pub fn update_batch(&mut self, update_fn: F) @@ -176,12 +197,12 @@ impl SettingsHandler { { let settings = self.get_settings_mut(); update_fn(settings); - self.save(); + self.try_save_settings(); } pub fn update_settings(&mut self, new_settings: Settings) { self.current_settings = Some(new_settings); - self.save(); + self.try_save_settings(); } pub fn theme(&self) -> ThemePreference { @@ -216,10 +237,17 @@ impl SettingsHandler { self.current_settings .as_ref() .map(|s| s.show_replies_newest_first) - .unwrap_or(false) + .unwrap_or(DEFAULT_SHOW_REPLIES_NEWEST_FIRST) } pub fn is_loaded(&self) -> bool { self.current_settings.is_some() } + + pub fn note_body_font_size(&self) -> f32 { + self.current_settings + .as_ref() + .map(|s| s.note_body_font_size) + .unwrap_or(DEFAULT_NOTE_BODY_FONT_SIZE) + } } diff --git a/crates/notedeck/src/persist/theme_handler.rs b/crates/notedeck/src/persist/theme_handler.rs deleted file mode 100644 index 90a0dca4..00000000 --- a/crates/notedeck/src/persist/theme_handler.rs +++ /dev/null @@ -1,76 +0,0 @@ -use egui::ThemePreference; -use tracing::{error, info}; - -use crate::{storage, DataPath, DataPathType, Directory}; - -pub struct ThemeHandler { - directory: Directory, - fallback_theme: ThemePreference, -} - -const THEME_FILE: &str = "theme.txt"; - -impl ThemeHandler { - pub fn new(path: &DataPath) -> Self { - let directory = Directory::new(path.path(DataPathType::Setting)); - let fallback_theme = ThemePreference::Dark; - Self { - directory, - fallback_theme, - } - } - - pub fn load(&self) -> ThemePreference { - match self.directory.get_file(THEME_FILE.to_owned()) { - Ok(contents) => match deserialize_theme(contents) { - Some(theme) => theme, - None => { - error!( - "Could not deserialize theme. Using fallback {:?} instead", - self.fallback_theme - ); - self.fallback_theme - } - }, - Err(e) => { - error!( - "Could not read {} file: {:?}\nUsing fallback {:?} instead", - THEME_FILE, e, self.fallback_theme - ); - self.fallback_theme - } - } - } - - pub fn save(&self, theme: ThemePreference) { - match storage::write_file( - &self.directory.file_path, - THEME_FILE.to_owned(), - &theme_to_serialized(&theme), - ) { - Ok(_) => info!( - "Successfully saved {:?} theme change to {}", - theme, THEME_FILE - ), - Err(_) => error!("Could not save {:?} theme change to {}", theme, THEME_FILE), - } - } -} - -fn theme_to_serialized(theme: &ThemePreference) -> String { - match theme { - ThemePreference::Dark => "dark", - ThemePreference::Light => "light", - ThemePreference::System => "system", - } - .to_owned() -} - -fn deserialize_theme(serialized_theme: String) -> Option { - match serialized_theme.as_str() { - "dark" => Some(ThemePreference::Dark), - "light" => Some(ThemePreference::Light), - "system" => Some(ThemePreference::System), - _ => None, - } -} diff --git a/crates/notedeck/src/persist/zoom.rs b/crates/notedeck/src/persist/zoom.rs deleted file mode 100644 index eba62553..00000000 --- a/crates/notedeck/src/persist/zoom.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::{DataPath, DataPathType}; -use egui::Context; - -use crate::timed_serializer::TimedSerializer; - -pub struct ZoomHandler { - serializer: TimedSerializer, -} - -impl ZoomHandler { - pub fn new(path: &DataPath) -> Self { - let serializer = - TimedSerializer::new(path, DataPathType::Setting, "zoom_level.json".to_owned()); - - Self { serializer } - } - - pub fn try_save_zoom_factor(&mut self, ctx: &Context) { - let cur_zoom_level = ctx.zoom_factor(); - self.serializer.try_save(cur_zoom_level); - } - - pub fn get_zoom_factor(&self) -> Option { - self.serializer.get_item() - } -} diff --git a/crates/notedeck/src/style.rs b/crates/notedeck/src/style.rs index d54ff572..7c047bf0 100644 --- a/crates/notedeck/src/style.rs +++ b/crates/notedeck/src/style.rs @@ -15,6 +15,7 @@ pub enum NotedeckTextStyle { Button, Small, Tiny, + NoteBody, } impl NotedeckTextStyle { @@ -29,6 +30,7 @@ impl NotedeckTextStyle { Self::Button => TextStyle::Button, Self::Small => TextStyle::Small, Self::Tiny => TextStyle::Name("Tiny".into()), + Self::NoteBody => TextStyle::Name("NoteBody".into()), } } @@ -43,6 +45,7 @@ impl NotedeckTextStyle { Self::Button => FontFamily::Proportional, Self::Small => FontFamily::Proportional, Self::Tiny => FontFamily::Proportional, + Self::NoteBody => FontFamily::Proportional, } } diff --git a/crates/notedeck/src/timed_serializer.rs b/crates/notedeck/src/timed_serializer.rs index 5dda904d..7decd226 100644 --- a/crates/notedeck/src/timed_serializer.rs +++ b/crates/notedeck/src/timed_serializer.rs @@ -2,16 +2,16 @@ use crate::debouncer::Debouncer; use crate::{storage, DataPath, DataPathType, Directory}; use serde::{Deserialize, Serialize}; use std::time::Duration; -use tracing::info; // Adjust this import path as needed +use tracing::info; -pub struct TimedSerializer Deserialize<'de>> { +pub struct TimedSerializer Deserialize<'de>> { directory: Directory, file_name: String, debouncer: Debouncer, saved_item: Option, } -impl Deserialize<'de>> TimedSerializer { +impl Deserialize<'de>> TimedSerializer { pub fn new(path: &DataPath, path_type: DataPathType, file_name: String) -> Self { let directory = Directory::new(path.path(path_type)); let delay = Duration::from_millis(1000); @@ -30,11 +30,11 @@ impl Deserialize<'de>> TimedSerialize self } - // returns whether successful + /// Returns whether it actually wrote the new value pub fn try_save(&mut self, cur_item: T) -> bool { if self.debouncer.should_act() { - if let Some(saved_item) = self.saved_item { - if saved_item != cur_item { + if let Some(ref saved_item) = self.saved_item { + if *saved_item != cur_item { return self.save(cur_item); } } else { @@ -45,8 +45,8 @@ impl Deserialize<'de>> TimedSerialize } pub fn get_item(&self) -> Option { - if self.saved_item.is_some() { - return self.saved_item; + if let Some(ref item) = self.saved_item { + return Some(item.clone()); } if let Ok(file_contents) = self.directory.get_file(self.file_name.clone()) { if let Ok(item) = serde_json::from_str::(&file_contents) { diff --git a/crates/notedeck/src/ui.rs b/crates/notedeck/src/ui.rs index 9024e31c..87620b93 100644 --- a/crates/notedeck/src/ui.rs +++ b/crates/notedeck/src/ui.rs @@ -1,8 +1,19 @@ +use crate::NotedeckTextStyle; + +pub const NARROW_SCREEN_WIDTH: f32 = 550.0; /// Determine if the screen is narrow. This is useful for detecting mobile /// contexts, but with the nuance that we may also have a wide android tablet. + +pub fn richtext_small(text: S) -> egui::RichText +where + S: Into, +{ + egui::RichText::new(text).text_style(NotedeckTextStyle::Small.text_style()) +} + pub fn is_narrow(ctx: &egui::Context) -> bool { let screen_size = ctx.input(|c| c.screen_rect().size()); - screen_size.x < 550.0 + screen_size.x < NARROW_SCREEN_WIDTH } pub fn is_oled() -> bool { diff --git a/crates/notedeck_chrome/src/android.rs b/crates/notedeck_chrome/src/android.rs index fdb87f05..b4e07047 100644 --- a/crates/notedeck_chrome/src/android.rs +++ b/crates/notedeck_chrome/src/android.rs @@ -69,7 +69,13 @@ pub async fn android_main(app: AndroidApp) { Box::new(move |cc| { let ctx = &cc.egui_ctx; let mut notedeck = Notedeck::new(ctx, path, &app_args); - setup_chrome(ctx, ¬edeck.args(), notedeck.theme()); + setup_chrome( + ctx, + ¬edeck.args(), + notedeck.theme(), + notedeck.note_body_font_size(), + notedeck.zoom_factor(), + ); let context = &mut notedeck.app_context(); let dave = Dave::new(cc.wgpu_render_state.as_ref()); diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs index 4213eaf1..bc64b313 100644 --- a/crates/notedeck_chrome/src/chrome.rs +++ b/crates/notedeck_chrome/src/chrome.rs @@ -113,8 +113,7 @@ impl ChromePanelAction { match self { Self::SaveTheme(theme) => { ui.ctx().set_theme(*theme); - ctx.settings_handler.set_theme(*theme); - ctx.settings_handler.save(); + ctx.settings.set_theme(*theme); } Self::Toolbar(toolbar_action) => match toolbar_action { diff --git a/crates/notedeck_chrome/src/notedeck.rs b/crates/notedeck_chrome/src/notedeck.rs index 9ccc1051..a8f026bf 100644 --- a/crates/notedeck_chrome/src/notedeck.rs +++ b/crates/notedeck_chrome/src/notedeck.rs @@ -98,7 +98,13 @@ async fn main() { let columns = Damus::new(&mut notedeck.app_context(), &args); let dave = Dave::new(cc.wgpu_render_state.as_ref()); - setup_chrome(ctx, notedeck.args(), notedeck.theme()); + setup_chrome( + ctx, + notedeck.args(), + notedeck.theme(), + notedeck.note_body_font_size(), + notedeck.zoom_factor(), + ); // ensure we recognized all the arguments let completely_unrecognized: Vec = notedeck diff --git a/crates/notedeck_chrome/src/preview.rs b/crates/notedeck_chrome/src/preview.rs index 4bd8a84f..3107bc99 100644 --- a/crates/notedeck_chrome/src/preview.rs +++ b/crates/notedeck_chrome/src/preview.rs @@ -38,7 +38,13 @@ impl PreviewRunner { "unrecognized args: {:?}", notedeck.unrecognized_args() ); - setup_chrome(ctx, notedeck.args(), notedeck.theme()); + setup_chrome( + ctx, + notedeck.args(), + notedeck.theme(), + notedeck.note_body_font_size(), + notedeck.zoom_factor(), + ); notedeck.set_app(PreviewApp::new(preview)); diff --git a/crates/notedeck_chrome/src/setup.rs b/crates/notedeck_chrome/src/setup.rs index 06b7f204..a61b9cf7 100644 --- a/crates/notedeck_chrome/src/setup.rs +++ b/crates/notedeck_chrome/src/setup.rs @@ -1,12 +1,18 @@ use crate::{fonts, theme}; use eframe::NativeOptions; -use egui::ThemePreference; -use notedeck::{AppSizeHandler, DataPath}; +use egui::{FontId, ThemePreference}; +use notedeck::{AppSizeHandler, DataPath, NotedeckTextStyle}; use notedeck_ui::app_images; use tracing::info; -pub fn setup_chrome(ctx: &egui::Context, args: ¬edeck::Args, theme: ThemePreference) { +pub fn setup_chrome( + ctx: &egui::Context, + args: ¬edeck::Args, + theme: ThemePreference, + note_body_font_size: f32, + zoom_factor: f32, +) { let is_mobile = args .is_mobile .unwrap_or(notedeck::ui::is_compiled_as_mobile()); @@ -31,6 +37,15 @@ pub fn setup_chrome(ctx: &egui::Context, args: ¬edeck::Args, theme: ThemePref ctx.set_visuals_of(egui::Theme::Light, theme::light_mode()); setup_cc(ctx, is_mobile); + + ctx.set_zoom_factor(zoom_factor); + + let mut style = (*ctx.style()).clone(); + style.text_styles.insert( + NotedeckTextStyle::NoteBody.text_style(), + FontId::proportional(note_body_font_size), + ); + ctx.set_style(style); } pub fn setup_cc(ctx: &egui::Context, is_mobile: bool) { @@ -39,7 +54,6 @@ pub fn setup_cc(ctx: &egui::Context, is_mobile: bool) { if notedeck::ui::is_compiled_as_mobile() { ctx.set_pixels_per_point(ctx.pixels_per_point() + 0.2); } - //ctx.set_pixels_per_point(1.0); // // diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs index 036dccbb..d1dae9b5 100644 --- a/crates/notedeck_columns/src/app.rs +++ b/crates/notedeck_columns/src/app.rs @@ -18,9 +18,13 @@ use egui_extras::{Size, StripBuilder}; use enostr::{ClientMessage, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool}; use nostrdb::Transaction; use notedeck::{ - tr, ui::is_narrow, Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState, Images, JobsCache, Localization, SettingsHandler, UnknownIds + tr, ui::is_narrow, Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState, + Images, JobsCache, Localization, SettingsHandler, UnknownIds, +}; +use notedeck_ui::{ + media::{MediaViewer, MediaViewerFlags, MediaViewerState}, + NoteOptions, }; -use notedeck_ui::{media::{MediaViewer, MediaViewerFlags, MediaViewerState}, NoteOptions}; use std::collections::{BTreeSet, HashMap}; use std::path::Path; use std::time::Duration; @@ -487,11 +491,11 @@ impl Damus { // cache.add_deck_default(*pk); //} }; - let settings_handler = &app_context.settings_handler; + let settings = &app_context.settings; let support = Support::new(app_context.path); - let note_options = get_note_options(parsed_args, settings_handler); + let note_options = get_note_options(parsed_args, settings); let jobs = JobsCache::default(); diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs index 6dae444d..461bae04 100644 --- a/crates/notedeck_columns/src/nav.rs +++ b/crates/notedeck_columns/src/nav.rs @@ -485,13 +485,9 @@ fn process_render_nav_action( .process_relay_action(ui.ctx(), ctx.pool, action); None } - RenderNavAction::SettingsAction(action) => action.process_settings_action( - app, - ctx.settings_handler, - ctx.i18n, - ctx.img_cache, - ui.ctx(), - ), + RenderNavAction::SettingsAction(action) => { + action.process_settings_action(app, ctx.settings, ctx.i18n, ctx.img_cache, ui.ctx()) + } }; if let Some(action) = router_action { @@ -585,13 +581,14 @@ fn render_nav_body( .ui(ui) .map(RenderNavAction::RelayAction), - Route::Settings => { - let mut settings = ctx.settings_handler.get_settings_mut(); - - SettingsView::new(ctx.i18n, ctx.img_cache, &mut settings) - .ui(ui) - .map(RenderNavAction::SettingsAction) - } + Route::Settings => SettingsView::new( + &mut ctx.settings.get_settings_mut(), + &mut note_context, + &mut app.note_options, + &mut app.jobs, + ) + .ui(ui) + .map(RenderNavAction::SettingsAction), Route::Reply(id) => { let txn = if let Ok(txn) = Transaction::new(ctx.ndb) { txn diff --git a/crates/notedeck_columns/src/ui/settings.rs b/crates/notedeck_columns/src/ui/settings.rs index 97b46c47..3b381dcf 100644 --- a/crates/notedeck_columns/src/ui/settings.rs +++ b/crates/notedeck_columns/src/ui/settings.rs @@ -1,12 +1,21 @@ -use egui::{vec2, Button, Color32, ComboBox, Frame, Margin, RichText, ScrollArea, ThemePreference}; -use notedeck::{ - tr, Images, LanguageIdentifier, Localization, NotedeckTextStyle, Settings, SettingsHandler, +use egui::{ + vec2, Button, Color32, ComboBox, FontId, Frame, Margin, RichText, ScrollArea, ThemePreference, }; -use notedeck_ui::NoteOptions; +use enostr::NoteId; +use nostrdb::Transaction; +use notedeck::{ + tr, + ui::{is_narrow, richtext_small}, + Images, JobsCache, LanguageIdentifier, Localization, NoteContext, NotedeckTextStyle, Settings, + SettingsHandler, DEFAULT_NOTE_BODY_FONT_SIZE, +}; +use notedeck_ui::{NoteOptions, NoteView}; use strum::Display; use crate::{nav::RouterAction, Damus, Route}; +const PREVIEW_NOTE_ID: &str = "note1edjc8ggj07hwv77g2405uh6j2jkk5aud22gktxrvc2wnre4vdwgqzlv2gw"; + const THEME_LIGHT: &str = "Light"; const THEME_DARK: &str = "Dark"; @@ -88,6 +97,7 @@ pub enum SettingsAction { SetShowSourceClient(ShowSourceClientOption), SetLocale(LanguageIdentifier), SetRepliestNewestFirst(bool), + SetNoteBodyFontSize(f32), OpenRelays, OpenCacheFolder, ClearCacheFolder, @@ -97,7 +107,7 @@ impl SettingsAction { pub fn process_settings_action<'a>( self, app: &mut Damus, - settings_handler: &'a mut SettingsHandler, + settings: &'a mut SettingsHandler, i18n: &'a mut Localization, img_cache: &mut Images, ctx: &egui::Context, @@ -110,26 +120,25 @@ impl SettingsAction { } Self::SetZoomFactor(zoom_factor) => { ctx.set_zoom_factor(zoom_factor); - settings_handler.set_zoom_factor(zoom_factor); + settings.set_zoom_factor(zoom_factor); } Self::SetShowSourceClient(option) => { option.set_note_options(&mut app.note_options); - settings_handler.set_show_source_client(option); + settings.set_show_source_client(option); } Self::SetTheme(theme) => { ctx.set_theme(theme); - settings_handler.set_theme(theme); + settings.set_theme(theme); } Self::SetLocale(language) => { if i18n.set_locale(language.clone()).is_ok() { - settings_handler.set_locale(language.to_string()); + settings.set_locale(language.to_string()); } } Self::SetRepliestNewestFirst(value) => { app.note_options.set(NoteOptions::RepliesNewestFirst, value); - settings_handler.set_show_replies_newest_first(value); - settings_handler.save(); + settings.set_show_replies_newest_first(value); } Self::OpenCacheFolder => { use opener; @@ -138,20 +147,26 @@ impl SettingsAction { Self::ClearCacheFolder => { let _ = img_cache.clear_folder_contents(); } + Self::SetNoteBodyFontSize(size) => { + let mut style = (*ctx.style()).clone(); + style.text_styles.insert( + NotedeckTextStyle::NoteBody.text_style(), + FontId::proportional(size), + ); + ctx.set_style(style); + + settings.set_note_body_font_size(size); + } } - settings_handler.save(); route_action } } pub struct SettingsView<'a> { settings: &'a mut Settings, - i18n: &'a mut Localization, - img_cache: &'a mut Images, -} - -fn small_richtext(i18n: &'_ mut Localization, text: &str, comment: &str) -> RichText { - RichText::new(tr!(i18n, text, comment)).text_style(NotedeckTextStyle::Small.text_style()) + note_context: &'a mut NoteContext<'a>, + note_options: &'a mut NoteOptions, + jobs: &'a mut JobsCache, } fn settings_group(ui: &mut egui::Ui, title: S, contents: impl FnOnce(&mut egui::Ui)) @@ -175,21 +190,24 @@ where impl<'a> SettingsView<'a> { pub fn new( - i18n: &'a mut Localization, - img_cache: &'a mut Images, settings: &'a mut Settings, + note_context: &'a mut NoteContext<'a>, + note_options: &'a mut NoteOptions, + jobs: &'a mut JobsCache, ) -> Self { Self { settings, - img_cache, - i18n, + note_context, + note_options, + jobs, } } /// Get the localized name for a language identifier fn get_selected_language_name(&mut self) -> String { if let Ok(lang_id) = self.settings.locale.parse::() { - self.i18n + self.note_context + .i18n .get_locale_native_name(&lang_id) .map(|s| s.to_owned()) .unwrap_or_else(|| lang_id.to_string()) @@ -201,19 +219,76 @@ impl<'a> SettingsView<'a> { pub fn appearance_section(&mut self, ui: &mut egui::Ui) -> Option { let mut action = None; let title = tr!( - self.i18n, + self.note_context.i18n, "Appearance", "Label for appearance settings section", ); settings_group(ui, title, |ui| { + ui.horizontal(|ui| { + ui.label(richtext_small(tr!( + self.note_context.i18n, + "Font size:", + "Label for font size, Appearance settings section", + ))); + + if ui + .add( + egui::Slider::new(&mut self.settings.note_body_font_size, 8.0..=32.0) + .text(""), + ) + .changed() + { + action = Some(SettingsAction::SetNoteBodyFontSize( + self.settings.note_body_font_size, + )); + }; + + if ui + .button(richtext_small(tr!( + self.note_context.i18n, + "Reset", + "Label for reset note body font size, Appearance settings section", + ))) + .clicked() + { + action = Some(SettingsAction::SetNoteBodyFontSize( + DEFAULT_NOTE_BODY_FONT_SIZE, + )); + } + }); + + let txn = Transaction::new(self.note_context.ndb).unwrap(); + if let Some(note_id) = NoteId::from_bech(PREVIEW_NOTE_ID) { + if let Ok(preview_note) = + self.note_context.ndb.get_note_by_id(&txn, ¬e_id.bytes()) + { + notedeck_ui::padding(8.0, ui, |ui| { + if is_narrow(ui.ctx()) { + ui.set_max_width(ui.available_width()); + } + + NoteView::new( + self.note_context, + &preview_note, + self.note_options.clone(), + self.jobs, + ) + .actionbar(false) + .options_button(false) + .show(ui); + }); + ui.separator(); + } + } + let current_zoom = ui.ctx().zoom_factor(); ui.horizontal(|ui| { - ui.label(small_richtext( - self.i18n, + ui.label(richtext_small(tr!( + self.note_context.i18n, "Zoom Level:", "Label for zoom level, Appearance settings section", - )); + ))); let min_reached = current_zoom <= MIN_ZOOM; let max_reached = current_zoom >= MAX_ZOOM; @@ -250,11 +325,11 @@ impl<'a> SettingsView<'a> { }; if ui - .button(small_richtext( - self.i18n, + .button(richtext_small(tr!( + self.note_context.i18n, "Reset", "Label for reset zoom level, Appearance settings section", - )) + ))) .clicked() { action = Some(SettingsAction::SetZoomFactor(RESET_ZOOM)); @@ -262,18 +337,19 @@ impl<'a> SettingsView<'a> { }); ui.horizontal(|ui| { - ui.label(small_richtext( - self.i18n, + ui.label(richtext_small(tr!( + self.note_context.i18n, "Language:", "Label for language, Appearance settings section", - )); + ))); // ComboBox::from_label("") .selected_text(self.get_selected_language_name()) .show_ui(ui, |ui| { - for lang in self.i18n.get_available_locales() { + for lang in self.note_context.i18n.get_available_locales() { let name = self + .note_context .i18n .get_locale_native_name(lang) .map(|s| s.to_owned()) @@ -289,21 +365,21 @@ impl<'a> SettingsView<'a> { }); ui.horizontal(|ui| { - ui.label(small_richtext( - self.i18n, + ui.label(richtext_small(tr!( + self.note_context.i18n, "Theme:", "Label for theme, Appearance settings section", - )); + ))); if ui .selectable_value( &mut self.settings.theme, ThemePreference::Light, - small_richtext( - self.i18n, - THEME_LIGHT.into(), + richtext_small(tr!( + self.note_context.i18n, + THEME_LIGHT, "Label for Theme Light, Appearance settings section", - ), + )), ) .clicked() { @@ -314,11 +390,11 @@ impl<'a> SettingsView<'a> { .selectable_value( &mut self.settings.theme, ThemePreference::Dark, - small_richtext( - self.i18n, - THEME_DARK.into(), + richtext_small(tr!( + self.note_context.i18n, + THEME_DARK, "Label for Theme Dark, Appearance settings section", - ), + )), ) .clicked() { @@ -333,18 +409,28 @@ impl<'a> SettingsView<'a> { pub fn storage_section(&mut self, ui: &mut egui::Ui) -> Option { let id = ui.id(); let mut action: Option = None; - let title = tr!(self.i18n, "Storage", "Label for storage settings section"); + let title = tr!( + self.note_context.i18n, + "Storage", + "Label for storage settings section" + ); settings_group(ui, title, |ui| { ui.horizontal_wrapped(|ui| { - let static_imgs_size = self.img_cache.static_imgs.cache_size.lock().unwrap(); + let static_imgs_size = self + .note_context + .img_cache + .static_imgs + .cache_size + .lock() + .unwrap(); - let gifs_size = self.img_cache.gifs.cache_size.lock().unwrap(); + let gifs_size = self.note_context.img_cache.gifs.cache_size.lock().unwrap(); ui.label( RichText::new(format!( "{} {}", tr!( - self.i18n, + self.note_context.i18n, "Image cache size:", "Label for Image cache size, Storage settings section" ), @@ -361,22 +447,22 @@ impl<'a> SettingsView<'a> { if !notedeck::ui::is_compiled_as_mobile() && ui - .button(small_richtext( - self.i18n, + .button(richtext_small(tr!( + self.note_context.i18n, "View folder", "Label for view folder button, Storage settings section", - )) + ))) .clicked() { action = Some(SettingsAction::OpenCacheFolder); } let clearcache_resp = ui.button( - small_richtext( - self.i18n, + richtext_small(tr!( + self.note_context.i18n, "Clear cache", "Label for clear cache button, Storage settings section", - ) + )) .color(Color32::LIGHT_RED), ); @@ -389,7 +475,7 @@ impl<'a> SettingsView<'a> { let mut confirm_pressed = false; clearcache_resp.show_tooltip_ui(|ui| { let confirm_resp = ui.button(tr!( - self.i18n, + self.note_context.i18n, "Confirm", "Label for confirm clear cache, Storage settings section" )); @@ -400,7 +486,7 @@ impl<'a> SettingsView<'a> { if confirm_resp.clicked() || ui .button(tr!( - self.i18n, + self.note_context.i18n, "Cancel", "Label for cancel clear cache, Storage settings section" )) @@ -425,19 +511,23 @@ impl<'a> SettingsView<'a> { fn other_options_section(&mut self, ui: &mut egui::Ui) -> Option { let mut action = None; - let title = tr!(self.i18n, "Others", "Label for others settings section"); + let title = tr!( + self.note_context.i18n, + "Others", + "Label for others settings section" + ); settings_group(ui, title, |ui| { ui.horizontal(|ui| { - ui.label(small_richtext( - self.i18n, + ui.label(richtext_small(tr!( + self.note_context.i18n, "Sort replies newest first", "Label for Sort replies newest first, others settings section", - )); + ))); if ui .toggle_value( &mut self.settings.show_replies_newest_first, - RichText::new(tr!(self.i18n, "ON", "ON")) + RichText::new(tr!(self.note_context.i18n, "ON", "ON")) .text_style(NotedeckTextStyle::Small.text_style()), ) .changed() @@ -449,11 +539,11 @@ impl<'a> SettingsView<'a> { }); ui.horizontal_wrapped(|ui| { - ui.label(small_richtext( - self.i18n, + ui.label(richtext_small(tr!( + self.note_context.i18n, "Source client", "Label for Source client, others settings section", - )); + ))); for option in [ ShowSourceClientOption::Hide, @@ -467,7 +557,7 @@ impl<'a> SettingsView<'a> { .selectable_value( &mut current, option, - RichText::new(option.label(self.i18n)) + RichText::new(option.label(self.note_context.i18n)) .text_style(NotedeckTextStyle::Small.text_style()), ) .changed() @@ -487,11 +577,11 @@ impl<'a> SettingsView<'a> { if ui .add_sized( [ui.available_width(), 30.0], - Button::new(small_richtext( - self.i18n, + Button::new(richtext_small(tr!( + self.note_context.i18n, "Configure relays", "Label for configure relays, settings section", - )), + ))), ) .clicked() { diff --git a/crates/notedeck_ui/src/mention.rs b/crates/notedeck_ui/src/mention.rs index 5c37534d..d68339c0 100644 --- a/crates/notedeck_ui/src/mention.rs +++ b/crates/notedeck_ui/src/mention.rs @@ -2,7 +2,7 @@ use crate::ProfilePreview; use egui::Sense; use enostr::Pubkey; use nostrdb::{Ndb, Transaction}; -use notedeck::{name::get_display_name, Images, NoteAction}; +use notedeck::{name::get_display_name, Images, NoteAction, NotedeckTextStyle}; pub struct Mention<'a> { ndb: &'a Ndb, @@ -75,7 +75,9 @@ fn mention_ui( get_display_name(profile.as_ref()).username_or_displayname() ); - let mut text = egui::RichText::new(name).color(link_color); + let mut text = egui::RichText::new(name) + .color(link_color) + .text_style(NotedeckTextStyle::NoteBody.text_style()); if let Some(size) = size { text = text.size(size); } diff --git a/crates/notedeck_ui/src/note/contents.rs b/crates/notedeck_ui/src/note/contents.rs index 1d7f762b..83a0e0b7 100644 --- a/crates/notedeck_ui/src/note/contents.rs +++ b/crates/notedeck_ui/src/note/contents.rs @@ -4,7 +4,7 @@ use crate::{ }; use notedeck::{JobsCache, RenderableMedia}; -use egui::{vec2, Color32, Hyperlink, RichText}; +use egui::{vec2, Color32, Hyperlink, Label, RichText}; use nostrdb::{BlockType, Mention, Note, NoteKey, Transaction}; use tracing::warn; @@ -42,6 +42,8 @@ impl<'a, 'd> NoteContents<'a, 'd> { impl egui::Widget for &mut NoteContents<'_, '_> { fn ui(self, ui: &mut egui::Ui) -> egui::Response { + ui.spacing_mut().item_spacing = vec2(0.0, 0.0); + if self.options.contains(NoteOptions::ShowNoteClientTop) { render_client(ui, self.note_context.note_cache, self.note); } @@ -200,13 +202,24 @@ pub fn render_note_contents<'a>( } _ => { - ui.colored_label(link_color, format!("@{}", &block.as_str()[..16])); + ui.colored_label( + link_color, + RichText::new(format!("@{}", &block.as_str()[..16])) + .text_style(NotedeckTextStyle::NoteBody.text_style()), + ); } }, BlockType::Hashtag => { + if block.as_str().trim().len() == 0 { + continue 'block_loop; + } let resp = ui - .colored_label(link_color, format!("#{}", block.as_str())) + .colored_label( + link_color, + RichText::new(format!("#{}", block.as_str())) + .text_style(NotedeckTextStyle::NoteBody.text_style()), + ) .on_hover_cursor(egui::CursorIcon::PointingHand); if resp.clicked() { @@ -231,8 +244,13 @@ pub fn render_note_contents<'a>( }; if hide_media || !found_supported() { + if block.as_str().trim().len() == 0 { + continue 'block_loop; + } ui.add(Hyperlink::from_label_and_url( - RichText::new(block.as_str()).color(link_color), + RichText::new(block.as_str()) + .color(link_color) + .text_style(NotedeckTextStyle::NoteBody.text_style()), block.as_str(), )); } @@ -263,18 +281,18 @@ pub fn render_note_contents<'a>( } if options.contains(NoteOptions::ScrambleText) { ui.add( - egui::Label::new( + Label::new( RichText::new(rot13(block_str)) - .text_style(NotedeckTextStyle::Body.text_style()), + .text_style(NotedeckTextStyle::NoteBody.text_style()), ) .wrap() .selectable(selectable), ); } else { ui.add( - egui::Label::new( + Label::new( RichText::new(block_str) - .text_style(NotedeckTextStyle::Body.text_style()), + .text_style(NotedeckTextStyle::NoteBody.text_style()), ) .wrap() .selectable(selectable), diff --git a/crates/notedeck_ui/src/note/mod.rs b/crates/notedeck_ui/src/note/mod.rs index 37f3e506..43914a1e 100644 --- a/crates/notedeck_ui/src/note/mod.rs +++ b/crates/notedeck_ui/src/note/mod.rs @@ -344,7 +344,12 @@ impl<'a, 'd> NoteView<'a, 'd> { 1.0, ui.visuals().noninteractive().bg_stroke.color, )) - .show(ui, |ui| self.show_impl(ui)) + .show(ui, |ui| { + if is_narrow(ui.ctx()) { + ui.set_width(ui.available_width()); + } + self.show_impl(ui) + }) .inner } else { self.show_impl(ui) From 261477339badef857716934be9d9f28705b33fd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20L=C3=B3pez=20Guevara?= Date: Wed, 30 Jul 2025 08:34:47 -0300 Subject: [PATCH 11/11] Update crates/notedeck/src/persist/settings_handler.rs Co-authored-by: Terry Yiu <963907+tyiu@users.noreply.github.com> --- crates/notedeck/src/persist/settings_handler.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/notedeck/src/persist/settings_handler.rs b/crates/notedeck/src/persist/settings_handler.rs index 94b17e82..fb82172f 100644 --- a/crates/notedeck/src/persist/settings_handler.rs +++ b/crates/notedeck/src/persist/settings_handler.rs @@ -10,7 +10,7 @@ const ZOOM_FACTOR_FILE: &str = "zoom_level.json"; const SETTINGS_FILE: &str = "settings.json"; const DEFAULT_THEME: ThemePreference = ThemePreference::Dark; -const DEFAULT_LOCALE: &str = "es-US"; +const DEFAULT_LOCALE: &str = "en-US"; const DEFAULT_ZOOM_FACTOR: f32 = 1.0; const DEFAULT_SHOW_SOURCE_CLIENT: &str = "hide"; const DEFAULT_SHOW_REPLIES_NEWEST_FIRST: bool = false;