note-taking/src/main.rs
Sofía Aritz fab26f8c73 Show user a password-length indicator
When the password is less than 12 characters, the length is shown in red, when it is higher, it is shown in green.

Also changed the entropy colors to be more _nuanced_, now they have the following values:
[0, 35) = red
[35, 60) = orange
(60, ∞) = green

Finally closes #1.
2023-03-06 00:30:51 +01:00

264 lines
No EOL
9.4 KiB
Rust

#![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<Note>,
update_notes_next: bool,
password: Option<String>,
tested_password: bool,
mode: CurrentMode,
saving: Saving,
password_buffer: String,
title_buffer: String,
text_buffer: String,
}
impl App {
fn new(_cc: &CreationContext<'_>, notes: Vec<Note>, 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;
}
});
}
}
}
}