commit 9ff5753bcaead14d7d992eb03ee9ff96b93562a8 parent b9e2fe5dd150eff5484b0c05fcd14ab6a3f69fd5 Author: Fernando LoĢpez Guevara <fernando.lguevara@gmail.com> Date: Tue, 29 Jul 2025 21:41:03 -0300 settings: use timed serializer, handle zoom properly, use custom text style for note body font size, added font size slider, added preview note Diffstat:
21 files changed, 376 insertions(+), 280 deletions(-)
diff --git 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<Rc<RefCell<dyn App>>>, - zoom: ZoomHandler, app_size: AppSizeHandler, unrecognized_args: BTreeSet<String>, 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<LanguageIdentifier, LanguageIdentifierError> = - 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<String> { diff --git 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 @@ -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 @@ -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 @@ -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<ThemePreference> { match serialized_theme { @@ -23,72 +28,96 @@ fn deserialize_theme(serialized_theme: &str) -> Option<ThemePreference> { } } -#[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<Settings>, current_settings: Option<Settings>, } impl SettingsHandler { - fn read_legacy_theme(&self) -> Option<ThemePreference> { + fn read_from_theme_file(&self) -> Option<ThemePreference> { 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<f32> { + match self.directory.get_file(ZOOM_FACTOR_FILE.to_string()) { + Ok(contents) => serde_json::from_str::<f32>(&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() - }); + settings.theme = theme_from_file; + migrated = true; + } else { + info!("theme.txt file not found, using default theme"); + }; - self.save(); + // 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()); - Ok(()) + settings.zoom_factor = zom_factor; + migrated = true; } else { - Err(()) + 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<Settings> = 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<S>(&mut self, locale: S) @@ -149,12 +165,12 @@ impl SettingsHandler { S: Into<String>, { 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<S>(&mut self, option: S) @@ -162,12 +178,17 @@ impl SettingsHandler { S: Into<String>, { 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<F>(&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 @@ -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<ThemePreference> { - 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 @@ -1,26 +0,0 @@ -use crate::{DataPath, DataPathType}; -use egui::Context; - -use crate::timed_serializer::TimedSerializer; - -pub struct ZoomHandler { - serializer: TimedSerializer<f32>, -} - -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<f32> { - self.serializer.get_item() - } -} diff --git 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 @@ -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<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> { +pub struct TimedSerializer<T: PartialEq + Clone + Serialize + for<'de> Deserialize<'de>> { directory: Directory, file_name: String, debouncer: Debouncer, saved_item: Option<T>, } -impl<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> TimedSerializer<T> { +impl<T: PartialEq + Clone + Serialize + for<'de> Deserialize<'de>> TimedSerializer<T> { 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<T: PartialEq + Copy + Serialize + for<'de> 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<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> TimedSerialize } pub fn get_item(&self) -> Option<T> { - 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::<T>(&file_contents) { diff --git 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<S>(text: S) -> egui::RichText +where + S: Into<String>, +{ + 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 @@ -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 @@ -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 @@ -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<String> = notedeck diff --git 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 @@ -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 @@ -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 @@ -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 @@ -1,12 +1,21 @@ -use egui::{vec2, Button, Color32, ComboBox, Frame, Margin, RichText, ScrollArea, ThemePreference}; +use egui::{ + vec2, Button, Color32, ComboBox, FontId, Frame, Margin, RichText, ScrollArea, ThemePreference, +}; +use enostr::NoteId; +use nostrdb::Transaction; use notedeck::{ - tr, Images, LanguageIdentifier, Localization, NotedeckTextStyle, Settings, SettingsHandler, + tr, + ui::{is_narrow, richtext_small}, + Images, JobsCache, LanguageIdentifier, Localization, NoteContext, NotedeckTextStyle, Settings, + SettingsHandler, DEFAULT_NOTE_BODY_FONT_SIZE, }; -use notedeck_ui::NoteOptions; +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<S>(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::<LanguageIdentifier>() { - 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<SettingsAction> { 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<SettingsAction> { let id = ui.id(); let mut action: Option<SettingsAction> = 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<SettingsAction> { 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 @@ -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 @@ -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 @@ -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)