#![cfg_attr( all( target_os = "windows", not(debug_assertions), ), windows_subsystem = "windows" )] use std::collections::{VecDeque}; use std::fs; use std::time::SystemTime; use directories::ProjectDirs; use eframe::{CreationContext, Frame}; use egui::{Color32, Context, RichText, TextEdit, Widget, WidgetText}; use log::info; use native_dialog::MessageDialog; use simple_logger::SimpleLogger; use crate::notes::{Note, NoteMetadata}; use crate::password::{check_password, entropy, generate_new_password}; use crate::saving::Saving; mod notes; mod saving; mod password; #[derive(PartialEq)] enum CurrentMode { View, Compose, PasswordInput, } struct App { notes: Vec, update_notes_next: bool, password: Option, tested_password: bool, mode: CurrentMode, saving: Saving, password_buffer: String, title_buffer: String, text_buffer: String, } impl App { fn new(_cc: &CreationContext<'_>, notes: Vec, saving: Saving) -> Self { Self { password: None, update_notes_next: false, tested_password: false, notes, mode: CurrentMode::PasswordInput, saving, password_buffer: String::new(), title_buffer: String::new(), text_buffer: String::new(), } } } fn main() { SimpleLogger::new().init().expect("failed to initialize logger"); info!("starting the UI"); let options = eframe::NativeOptions { initial_window_size: Some(egui::vec2(640.0, 640.0)), ..Default::default() }; let project_directories = ProjectDirs::from("com", "sofiaritz", "notes") .unwrap(); let data_directory = project_directories .data_dir(); info!("data directory: {:?}", data_directory); fs::create_dir_all(data_directory).unwrap(); info!("created all intermediate directories"); let saving = Saving::new(data_directory); let notes = saving.read_notes().unwrap(); info!("successfully read initial notes"); eframe::run_native( "notes", options, Box::new(move |cc| Box::new(App::new(cc, notes, saving))) ).expect("failed to run native"); info!("shutdown"); } impl eframe::App for App { fn update(&mut self, ctx: &Context, _frame: &mut Frame) { if self.password.is_none() { self.mode = CurrentMode::PasswordInput; } if !self.tested_password && self.password.is_some() { if let Some(note) = self.notes.get(0) { if let Ok(password) = check_password(self.password.as_ref().unwrap(), note) { self.password = Some(password); } else { let _ = MessageDialog::new() .set_title("invalid password") .set_text("failed to verify the password against an existing note. please try again") .show_alert(); self.password = None; self.tested_password = false; return; } } else { self.password = Some(generate_new_password(self.password.as_ref().unwrap())) } self.tested_password = true; } if self.update_notes_next { self.notes = self.saving.read_notes().unwrap(); self.update_notes_next = false; info!("reloaded notes"); } match self.mode { CurrentMode::View => { let password = self.password.as_ref().unwrap(); egui::CentralPanel::default().show(ctx, |ui| { ui.heading("notes"); ui.horizontal(|ui| { ui.label("the simple note taking app that uses encryption by default"); ui.add_space(7.5); if ui.button("want to add one note?").clicked() { self.mode = CurrentMode::Compose; } }); ui.separator(); egui::ScrollArea::vertical().show(ui, |ui| { for note in &mut self.notes { let render_result = note.render(ui, password, |id| { let _ = &self.saving.delete_note(&id).unwrap(); info!("note with id {} was successfully deleted", id); self.update_notes_next = true; }); if let Some(new_note) = render_result { *note = new_note; } ui.add_space(10.0); } }); }); } CurrentMode::Compose => { let password = self.password.as_ref().unwrap(); egui::CentralPanel::default().show(ctx, |ui| { ui.heading("notes"); ui.horizontal(|ui| { ui.label("the simple note taking app that uses encryption by default"); ui.add_space(7.5); if ui.button("return to view mode?").clicked() { self.title_buffer = String::new(); self.text_buffer = String::new(); self.mode = CurrentMode::View; } }); ui.separator(); egui::ScrollArea::vertical().show(ui, |ui| { ui.label("title:"); ui.text_edit_singleline(&mut self.title_buffer); ui.label("text:"); ui.text_edit_multiline(&mut self.text_buffer); ui.add_space(10.0); if ui.button("add note").clicked() { let note = Note::Decrypted { id: SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_millis() .to_string(), title: self.title_buffer.clone(), metadata: NoteMetadata { date: SystemTime::now(), arbitrary: None, }, text: self.text_buffer.clone(), }; self.saving.save_note(note.clone(), password.to_string()).unwrap(); let mut vec_deque = VecDeque::from(self.notes.clone()); vec_deque.push_front(note); self.notes = Vec::from(vec_deque); self.title_buffer = String::new(); self.text_buffer = String::new(); self.mode = CurrentMode::View; } }); }); } CurrentMode::PasswordInput => { egui::CentralPanel::default().show(ctx, |ui| { ui.heading("notes"); ui.label("the simple note taking app that uses encryption by default"); ui.separator(); ui.label("you need to put your password to access the notes"); ui.label(RichText::new("after starting the app, the password will be tested against the latest note").italics()); ui.add_space(10.0); ui.horizontal(|ui| { TextEdit::singleline(&mut self.password_buffer).password(true).ui(ui); let text = if self.password_buffer.len() < 12 { WidgetText::from(format!("length: {}", self.password_buffer.len())) .color(Color32::from_rgb(240, 5, 5)) } else { WidgetText::from(format!("length: {}", self.password_buffer.len())) .color(Color32::from_rgb(144, 238, 144)) }; ui.label(text); let entropy = entropy(&self.password_buffer) .or(Some(0.0)) .unwrap(); let text = if entropy < 35_f64 { WidgetText::from(format!("entropy: {:.2}", entropy)) .color(Color32::from_rgb(240, 5, 5)) } else if entropy < 60_f64 { WidgetText::from(format!("entropy: {:.2}", entropy)) .color(Color32::from_rgb(220, 88, 42)) } else { WidgetText::from(format!("entropy: {:.2}", entropy)) .color(Color32::from_rgb(144, 238, 144)) }; ui.label(text); }); ui.add_space(7.5); if ui.button("start app").clicked() { self.password = Some(self.password_buffer.clone()); self.password_buffer = String::new(); self.mode = CurrentMode::View; } }); } } } }