commit b8207106d71ccfd4ef30738ee86d398a68863b4f
parent 5280028a82408c1ffcf3cadfaa78ef05955ee0d6
Author: Fernando LoĢpez Guevara <fernando.lguevara@gmail.com>
Date: Wed, 23 Jul 2025 23:04:49 -0300
feat(settings): persist settings to storage
Diffstat:
9 files changed, 339 insertions(+), 67 deletions(-)
diff --git a/crates/notedeck/src/app.rs b/crates/notedeck/src/app.rs
@@ -3,12 +3,11 @@ use crate::i18n::Localization;
use crate::persist::{AppSizeHandler, ZoomHandler};
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, ThemeHandler,
- UnknownIds,
+ DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, UnknownIds,
};
+use crate::{JobPool, SettingsHandler};
use egui::Margin;
use egui::ThemePreference;
use egui_winit::clipboard::Clipboard;
@@ -19,6 +18,7 @@ use std::collections::BTreeSet;
use std::path::Path;
use std::rc::Rc;
use tracing::{error, info};
+use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
pub enum AppAction {
Note(NoteAction),
@@ -40,7 +40,7 @@ pub struct Notedeck {
global_wallet: GlobalWallet,
path: DataPath,
args: Args,
- theme: ThemeHandler,
+ settings_handler: SettingsHandler,
app: Option<Rc<RefCell<dyn App>>>,
zoom: ZoomHandler,
app_size: AppSizeHandler,
@@ -159,7 +159,10 @@ impl Notedeck {
1024usize * 1024usize * 1024usize * 1024usize
};
- let theme = ThemeHandler::new(&path);
+ let mut settings_handler = SettingsHandler::new(&path);
+
+ settings_handler.load();
+
let config = Config::new().set_ingester_threads(2).set_mapsize(map_size);
let keystore = if parsed_args.use_keystore {
@@ -231,6 +234,16 @@ impl Notedeck {
// Initialize localization
let mut i18n = Localization::new();
+
+ let setting_locale: Result<LanguageIdentifier, LanguageIdentifierError> =
+ settings_handler.locale().parse();
+
+ if setting_locale.is_ok() {
+ if let Err(err) = i18n.set_locale(setting_locale.unwrap()) {
+ error!("{err}");
+ }
+ }
+
if let Some(locale) = &parsed_args.locale {
if let Err(err) = i18n.set_locale(locale.to_owned()) {
error!("{err}");
@@ -250,7 +263,7 @@ impl Notedeck {
global_wallet,
path: path.clone(),
args: parsed_args,
- theme,
+ settings_handler,
app: None,
zoom,
app_size,
@@ -279,7 +292,7 @@ impl Notedeck {
global_wallet: &mut self.global_wallet,
path: &self.path,
args: &self.args,
- theme: &mut self.theme,
+ settings_handler: &mut self.settings_handler,
clipboard: &mut self.clipboard,
zaps: &mut self.zaps,
frame_history: &mut self.frame_history,
@@ -297,7 +310,7 @@ impl Notedeck {
}
pub fn theme(&self) -> ThemePreference {
- self.theme.load()
+ self.settings_handler.theme()
}
pub fn unrecognized_args(&self) -> &BTreeSet<String> {
diff --git a/crates/notedeck/src/context.rs b/crates/notedeck/src/context.rs
@@ -1,6 +1,6 @@
use crate::{
account::accounts::Accounts, frame_history::FrameHistory, i18n::Localization,
- wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, ThemeHandler,
+ wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, SettingsHandler,
UnknownIds,
};
use egui_winit::clipboard::Clipboard;
@@ -20,7 +20,7 @@ pub struct AppContext<'a> {
pub global_wallet: &'a mut GlobalWallet,
pub path: &'a DataPath,
pub args: &'a Args,
- pub theme: &'a mut ThemeHandler,
+ pub settings_handler: &'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/persist/mod.rs b/crates/notedeck/src/persist/mod.rs
@@ -1,9 +1,11 @@
mod app_size;
+mod settings_handler;
mod theme_handler;
mod token_handler;
mod zoom;
pub use app_size::AppSizeHandler;
+pub use settings_handler::SettingsHandler;
pub use theme_handler::ThemeHandler;
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
@@ -0,0 +1,208 @@
+use crate::{
+ storage::{self, delete_file},
+ DataPath, DataPathType, Directory,
+};
+use egui::ThemePreference;
+use serde::{Deserialize, Serialize};
+use tracing::{error, info};
+
+const THEME_FILE: &str = "theme.txt";
+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";
+
+fn deserialize_theme(serialized_theme: &str) -> Option<ThemePreference> {
+ match serialized_theme {
+ "dark" => Some(ThemePreference::Dark),
+ "light" => Some(ThemePreference::Light),
+ "system" => Some(ThemePreference::System),
+ _ => None,
+ }
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct Settings {
+ pub theme: ThemePreference,
+ pub locale: String,
+ pub zoom_factor: f32,
+ pub show_source_client: String,
+}
+
+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: "Hide".to_string(),
+ }
+ }
+}
+pub struct SettingsHandler {
+ directory: Directory,
+ current_settings: Option<Settings>,
+}
+
+impl SettingsHandler {
+ fn read_legacy_theme(&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<(), ()> {
+ // if theme.txt exists migrate
+ if let Some(theme_from_file) = self.read_legacy_theme() {
+ 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(())
+ } else {
+ Err(())
+ }
+ }
+
+ pub fn new(path: &DataPath) -> Self {
+ let directory = Directory::new(path.path(DataPathType::Setting));
+ let current_settings: Option<Settings> = None;
+
+ Self {
+ directory,
+ current_settings,
+ }
+ }
+
+ pub fn load(&mut self) {
+ if self.migrate_to_settings_file().is_ok() {
+ return;
+ }
+
+ match self.directory.get_file(SETTINGS_FILE.to_string()) {
+ Ok(contents_str) => {
+ // Parse JSON content
+ match serde_json::from_str::<Settings>(&contents_str) {
+ Ok(settings) => {
+ self.current_settings = Some(settings);
+ }
+ Err(_) => {
+ error!("Invalid settings format. Using defaults");
+ self.current_settings = Some(Settings::default());
+ }
+ }
+ }
+ Err(_) => {
+ error!("Could not read settings. Using defaults");
+ self.current_settings = Some(Settings::default());
+ }
+ }
+ }
+
+ 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),
+ };
+ }
+
+ fn get_settings_mut(&mut self) -> &mut Settings {
+ if self.current_settings.is_none() {
+ self.current_settings = Some(Settings::default());
+ }
+ self.current_settings.as_mut().unwrap()
+ }
+
+ pub fn set_theme(&mut self, theme: ThemePreference) {
+ self.get_settings_mut().theme = theme;
+ self.save();
+ }
+
+ pub fn set_locale<S>(&mut self, locale: S)
+ where
+ S: Into<String>,
+ {
+ self.get_settings_mut().locale = locale.into();
+ self.save();
+ }
+
+ pub fn set_zoom_factor(&mut self, zoom_factor: f32) {
+ self.get_settings_mut().zoom_factor = zoom_factor;
+ self.save();
+ }
+
+ pub fn set_show_source_client<S>(&mut self, option: S)
+ where
+ S: Into<String>,
+ {
+ self.get_settings_mut().show_source_client = option.into();
+ self.save();
+ }
+
+ pub fn update_batch<F>(&mut self, update_fn: F)
+ where
+ F: FnOnce(&mut Settings),
+ {
+ let settings = self.get_settings_mut();
+ update_fn(settings);
+ self.save();
+ }
+
+ pub fn update_settings(&mut self, new_settings: Settings) {
+ self.current_settings = Some(new_settings);
+ self.save();
+ }
+
+ pub fn theme(&self) -> ThemePreference {
+ self.current_settings
+ .as_ref()
+ .map(|s| s.theme)
+ .unwrap_or(DEFAULT_THEME)
+ }
+
+ pub fn locale(&self) -> String {
+ self.current_settings
+ .as_ref()
+ .map(|s| s.locale.clone())
+ .unwrap_or_else(|| DEFAULT_LOCALE.to_string())
+ }
+
+ pub fn zoom_factor(&self) -> f32 {
+ self.current_settings
+ .as_ref()
+ .map(|s| s.zoom_factor)
+ .unwrap_or(DEFAULT_ZOOM_FACTOR)
+ }
+
+ pub fn show_source_client(&self) -> String {
+ self.current_settings
+ .as_ref()
+ .map(|s| s.show_source_client.to_string())
+ .unwrap_or(DEFAULT_SHOW_SOURCE_CLIENT.to_string())
+ }
+
+ pub fn is_loaded(&self) -> bool {
+ self.current_settings.is_some()
+ }
+}
diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs
@@ -115,7 +115,8 @@ impl ChromePanelAction {
ui.ctx().options_mut(|o| {
o.theme_preference = *theme;
});
- ctx.theme.save(*theme);
+ ctx.settings_handler.set_theme(*theme);
+ ctx.settings_handler.save();
}
Self::Toolbar(toolbar_action) => match toolbar_action {
diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs
@@ -15,6 +15,8 @@ use crate::{
Result,
};
+use crate::ui::settings::ShowNoteClientOption;
+
use egui_extras::{Size, StripBuilder};
use enostr::{ClientMessage, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool};
use nostrdb::Transaction;
@@ -506,11 +508,14 @@ impl Damus {
);
note_options.set(
NoteOptions::ShowNoteClientTop,
- parsed_args.is_flag_set(ColumnsFlag::ShowNoteClientTop),
+ ShowNoteClientOption::Top == app_context.settings_handler.show_source_client().into()
+ || parsed_args.is_flag_set(ColumnsFlag::ShowNoteClientTop),
);
note_options.set(
NoteOptions::ShowNoteClientBottom,
- parsed_args.is_flag_set(ColumnsFlag::ShowNoteClientBottom),
+ ShowNoteClientOption::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(
diff --git 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, ShowNoteClientOptions},
+ settings::SettingsAction,
support::SupportView,
wallet::{get_default_zap_state, WalletAction, WalletState, WalletView},
AccountsView, PostReplyView, PostView, ProfileView, RelayView, SettingsView, ThreadView,
@@ -30,6 +30,8 @@ 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};
@@ -37,7 +39,6 @@ use notedeck::{
get_current_default_msats, tr, ui::is_narrow, Accounts, AppContext, NoteAction, NoteContext,
RelayAction,
};
-use notedeck_ui::NoteOptions;
use tracing::error;
/// The result of processing a nav response
@@ -486,9 +487,13 @@ fn process_render_nav_action(
.process_relay_action(ui.ctx(), ctx.pool, action);
None
}
- RenderNavAction::SettingsAction(action) => {
- action.process_settings_action(app, ctx.theme, ctx.i18n, ctx.img_cache, ui.ctx())
- }
+ RenderNavAction::SettingsAction(action) => action.process_settings_action(
+ app,
+ ctx.settings_handler,
+ ctx.i18n,
+ ctx.img_cache,
+ ui.ctx(),
+ ),
};
if let Some(action) = router_action {
@@ -583,14 +588,7 @@ fn render_nav_body(
.map(RenderNavAction::RelayAction),
Route::Settings => {
- let mut show_note_client = if app.note_options.contains(NoteOptions::ShowNoteClientTop)
- {
- ShowNoteClientOptions::Top
- } else if app.note_options.contains(NoteOptions::ShowNoteClientBottom) {
- ShowNoteClientOptions::Bottom
- } else {
- ShowNoteClientOptions::Hide
- };
+ let mut show_note_client: ShowNoteClientOption = 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
@@ -26,6 +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 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
@@ -1,21 +1,73 @@
use egui::{vec2, Button, Color32, ComboBox, Frame, Margin, RichText, ThemePreference};
-use notedeck::{tr, Images, LanguageIdentifier, Localization, NotedeckTextStyle, ThemeHandler};
+use notedeck::{tr, Images, LanguageIdentifier, Localization, NotedeckTextStyle, SettingsHandler};
use notedeck_ui::NoteOptions;
use strum::Display;
use crate::{nav::RouterAction, Damus, Route};
#[derive(Clone, Copy, PartialEq, Eq, Display)]
-pub enum ShowNoteClientOptions {
+pub enum ShowNoteClientOption {
Hide,
Top,
Bottom,
}
+impl From<ShowNoteClientOption> 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 From<NoteOptions> for ShowNoteClientOption {
+ fn from(note_options: NoteOptions) -> Self {
+ if note_options.contains(NoteOptions::ShowNoteClientTop) {
+ ShowNoteClientOption::Top
+ } else if note_options.contains(NoteOptions::ShowNoteClientBottom) {
+ ShowNoteClientOption::Bottom
+ } else {
+ ShowNoteClientOption::Hide
+ }
+ }
+}
+
+impl From<String> for ShowNoteClientOption {
+ fn from(s: String) -> Self {
+ match s.to_lowercase().as_str() {
+ "hide" => Self::Hide,
+ "top" => Self::Top,
+ "bottom" => Self::Bottom,
+ _ => Self::Hide, // default fallback
+ }
+ }
+}
+
+impl ShowNoteClientOption {
+ pub fn set_note_options(self, note_options: &mut NoteOptions) {
+ match self {
+ Self::Hide => {
+ note_options.set(NoteOptions::ShowNoteClientTop, false);
+ note_options.set(NoteOptions::ShowNoteClientBottom, false);
+ }
+ Self::Bottom => {
+ note_options.set(NoteOptions::ShowNoteClientTop, false);
+ note_options.set(NoteOptions::ShowNoteClientBottom, true);
+ }
+ Self::Top => {
+ note_options.set(NoteOptions::ShowNoteClientTop, true);
+ note_options.set(NoteOptions::ShowNoteClientBottom, false);
+ }
+ }
+ }
+}
+
pub enum SettingsAction {
- SetZoom(f32),
+ SetZoomFactor(f32),
SetTheme(ThemePreference),
- SetShowNoteClient(ShowNoteClientOptions),
+ SetShowSourceClient(ShowNoteClientOption),
SetLocale(LanguageIdentifier),
OpenRelays,
OpenCacheFolder,
@@ -26,7 +78,7 @@ impl SettingsAction {
pub fn process_settings_action<'a>(
self,
app: &mut Damus,
- theme_handler: &'a mut ThemeHandler,
+ settings_handler: &'a mut SettingsHandler,
i18n: &'a mut Localization,
img_cache: &mut Images,
ctx: &egui::Context,
@@ -37,34 +89,25 @@ impl SettingsAction {
SettingsAction::OpenRelays => {
route_action = Some(RouterAction::route_to(Route::Relays));
}
- SettingsAction::SetZoom(zoom_level) => {
- ctx.set_zoom_factor(zoom_level);
+ SettingsAction::SetZoomFactor(zoom_factor) => {
+ ctx.set_zoom_factor(zoom_factor);
+ settings_handler.set_zoom_factor(zoom_factor);
+ }
+ SettingsAction::SetShowSourceClient(option) => {
+ option.set_note_options(&mut app.note_options);
+
+ settings_handler.set_show_source_client(option);
}
- SettingsAction::SetShowNoteClient(newvalue) => match newvalue {
- ShowNoteClientOptions::Hide => {
- app.note_options.set(NoteOptions::ShowNoteClientTop, false);
- app.note_options
- .set(NoteOptions::ShowNoteClientBottom, false);
- }
- ShowNoteClientOptions::Bottom => {
- app.note_options.set(NoteOptions::ShowNoteClientTop, false);
- app.note_options
- .set(NoteOptions::ShowNoteClientBottom, true);
- }
- ShowNoteClientOptions::Top => {
- app.note_options.set(NoteOptions::ShowNoteClientTop, true);
- app.note_options
- .set(NoteOptions::ShowNoteClientBottom, false);
- }
- },
SettingsAction::SetTheme(theme) => {
ctx.options_mut(|o| {
o.theme_preference = theme;
});
- theme_handler.save(theme);
+ settings_handler.set_theme(theme);
}
SettingsAction::SetLocale(language) => {
- _ = i18n.set_locale(language);
+ if i18n.set_locale(language.clone()).is_ok() {
+ settings_handler.set_locale(language.to_string());
+ }
}
SettingsAction::OpenCacheFolder => {
use opener;
@@ -74,6 +117,7 @@ impl SettingsAction {
let _ = img_cache.clear_folder_contents();
}
}
+ settings_handler.save();
route_action
}
}
@@ -81,7 +125,7 @@ impl SettingsAction {
pub struct SettingsView<'a> {
theme: &'a mut String,
selected_language: &'a mut String,
- show_note_client: &'a mut ShowNoteClientOptions,
+ show_note_client: &'a mut ShowNoteClientOption,
i18n: &'a mut Localization,
img_cache: &'a mut Images,
}
@@ -91,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 ShowNoteClientOptions,
+ show_note_client: &'a mut ShowNoteClientOption,
i18n: &'a mut Localization,
) -> Self {
Self {
@@ -115,20 +159,20 @@ impl<'a> SettingsView<'a> {
}
}
- /// Get the localized label for ShowNoteClientOptions
- fn get_show_note_client_label(&mut self, option: ShowNoteClientOptions) -> String {
+ /// Get the localized label for ShowNoteClientOption
+ fn get_show_note_client_label(&mut self, option: ShowNoteClientOption) -> String {
match option {
- ShowNoteClientOptions::Hide => tr!(
+ ShowNoteClientOption::Hide => tr!(
self.i18n,
"Hide",
"Option in settings section to hide the source client label in note display"
),
- ShowNoteClientOptions::Top => tr!(
+ ShowNoteClientOption::Top => tr!(
self.i18n,
"Top",
"Option in settings section to show the source client label at the top of the note"
),
- ShowNoteClientOptions::Bottom => tr!(
+ ShowNoteClientOption::Bottom => tr!(
self.i18n,
"Bottom",
"Option in settings section to show the source client label at the bottom of the note"
@@ -179,7 +223,7 @@ impl<'a> SettingsView<'a> {
.clicked()
{
let new_zoom = (current_zoom - 0.1).max(0.1);
- action = Some(SettingsAction::SetZoom(new_zoom));
+ action = Some(SettingsAction::SetZoomFactor(new_zoom));
};
ui.label(
@@ -195,7 +239,7 @@ impl<'a> SettingsView<'a> {
.clicked()
{
let new_zoom = (current_zoom + 0.1).min(10.0);
- action = Some(SettingsAction::SetZoom(new_zoom));
+ action = Some(SettingsAction::SetZoomFactor(new_zoom));
};
if ui
@@ -209,7 +253,7 @@ impl<'a> SettingsView<'a> {
)
.clicked()
{
- action = Some(SettingsAction::SetZoom(1.0));
+ action = Some(SettingsAction::SetZoomFactor(1.0));
}
});
@@ -336,7 +380,7 @@ impl<'a> SettingsView<'a> {
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"))
+ 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);
}
@@ -419,9 +463,9 @@ impl<'a> SettingsView<'a> {
);
for option in [
- ShowNoteClientOptions::Hide,
- ShowNoteClientOptions::Top,
- ShowNoteClientOptions::Bottom,
+ ShowNoteClientOption::Hide,
+ ShowNoteClientOption::Top,
+ ShowNoteClientOption::Bottom,
] {
let label = self.get_show_note_client_label(option);
@@ -434,7 +478,7 @@ impl<'a> SettingsView<'a> {
)
.changed()
{
- action = Some(SettingsAction::SetShowNoteClient(option));
+ action = Some(SettingsAction::SetShowSourceClient(option));
}
}
});