Sort notes my newest, save note ID in the Note struct, add ID-based note removal

This adds a weird callback-based system to properly update the state once a note is removed.

This system isn't very nice, and it can be quite messy, but I'll improve it in the future
This commit is contained in:
Sofía Aritz 2023-02-27 16:43:33 +01:00
parent 88dd8f034f
commit b599f628de
4 changed files with 72 additions and 17 deletions

View file

@ -4,13 +4,13 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
serde = { version = "1.0", features = ["derive"] }
egui = "0.21" egui = "0.21"
eframe = "0.21" eframe = "0.21"
anyhow = "1" anyhow = "1"
log = "0.4" log = "0.4"
simple_logger = "4.0" simple_logger = "4.0"
chrono = "0.4" chrono = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
rand = "0.8" rand = "0.8"
pwbox = "0.5" pwbox = "0.5"

View file

@ -1,4 +1,10 @@
#![windows_subsystem = "windows"] #![cfg_attr(
all(
target_os = "windows",
not(debug_assertions),
),
windows_subsystem = "windows"
)]
use std::collections::{VecDeque}; use std::collections::{VecDeque};
use std::fs; use std::fs;
@ -23,6 +29,7 @@ enum CurrentMode {
struct App { struct App {
notes: Vec<Note>, notes: Vec<Note>,
update_notes_next: bool,
password: Option<String>, password: Option<String>,
tested_password: bool, tested_password: bool,
mode: CurrentMode, mode: CurrentMode,
@ -60,6 +67,7 @@ fn main() {
let app = App { let app = App {
password: None, password: None,
update_notes_next: false,
tested_password: false, tested_password: false,
notes, notes,
mode: CurrentMode::PasswordInput, mode: CurrentMode::PasswordInput,
@ -93,6 +101,12 @@ impl eframe::App for App {
} }
} }
if self.update_notes_next {
self.notes = self.saving.read_notes().unwrap();
self.update_notes_next = false;
info!("reloaded notes");
}
match self.mode { match self.mode {
CurrentMode::View => { CurrentMode::View => {
let password = self.password.as_ref().unwrap(); let password = self.password.as_ref().unwrap();
@ -109,7 +123,14 @@ impl eframe::App for App {
ui.separator(); ui.separator();
for note in &mut self.notes { for note in &mut self.notes {
if let Some(new_note) = note.render(ui, password) { 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; *note = new_note;
} }
ui.add_space(10.0); ui.add_space(10.0);
@ -141,6 +162,11 @@ impl eframe::App for App {
ui.add_space(10.0); ui.add_space(10.0);
if ui.button("add note").clicked() { if ui.button("add note").clicked() {
let note = Note::Decrypted { let note = Note::Decrypted {
id: SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_millis()
.to_string(),
title: self.title_buffer.clone(), title: self.title_buffer.clone(),
metadata: NoteMetadata { metadata: NoteMetadata {
date: SystemTime::now(), date: SystemTime::now(),

View file

@ -15,11 +15,13 @@ pub struct NoteMetadata {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Note { pub enum Note {
Encrypted { Encrypted {
id: String,
title: String, title: String,
metadata: NoteMetadata, metadata: NoteMetadata,
encrypted_text: String, encrypted_text: String,
}, },
Decrypted { Decrypted {
id: String,
title: String, title: String,
metadata: NoteMetadata, metadata: NoteMetadata,
text: String, text: String,
@ -31,7 +33,7 @@ impl Note {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
match self { match self {
Note::Decrypted { text, metadata, title } => { Note::Decrypted { id, text, metadata, title } => {
let pwbox = Sodium::build_box(&mut rng) let pwbox = Sodium::build_box(&mut rng)
.seal(password.to_string().into_bytes(), text.clone().into_bytes())?; .seal(password.to_string().into_bytes(), text.clone().into_bytes())?;
@ -40,6 +42,7 @@ impl Note {
let erased: ErasedPwBox = eraser.erase(&pwbox)?; let erased: ErasedPwBox = eraser.erase(&pwbox)?;
Ok(Self::Encrypted { Ok(Self::Encrypted {
id: id.clone(),
title: title.clone(), title: title.clone(),
metadata: metadata.clone(), metadata: metadata.clone(),
encrypted_text: serde_json::to_string(&erased)?, encrypted_text: serde_json::to_string(&erased)?,
@ -51,7 +54,7 @@ impl Note {
pub fn decrypt(&self, password: &str) -> anyhow::Result<Self> { pub fn decrypt(&self, password: &str) -> anyhow::Result<Self> {
match self { match self {
Note::Encrypted { title, metadata, encrypted_text } => { Note::Encrypted { id, title, metadata, encrypted_text } => {
let mut eraser = Eraser::new(); let mut eraser = Eraser::new();
eraser.add_suite::<Sodium>(); eraser.add_suite::<Sodium>();
let erased: ErasedPwBox = serde_json::from_str(encrypted_text)?; let erased: ErasedPwBox = serde_json::from_str(encrypted_text)?;
@ -60,6 +63,7 @@ impl Note {
let decrypted_text = String::from_utf8(decrypted_bytes)?; let decrypted_text = String::from_utf8(decrypted_bytes)?;
Ok(Self::Decrypted { Ok(Self::Decrypted {
id: id.clone(),
title: title.clone(), title: title.clone(),
metadata: metadata.clone(), metadata: metadata.clone(),
text: decrypted_text, text: decrypted_text,
@ -69,18 +73,18 @@ impl Note {
} }
} }
pub fn render(&self, ui: &mut Ui, password: &str) -> Option<Self> { pub fn render(&self, ui: &mut Ui, password: &str, cb: impl FnOnce(String)) -> Option<Self> {
let mut value = None; let mut value = None;
ui.group(|ui| { ui.group(|ui| {
match self { match self {
Note::Encrypted { title, metadata, .. } => { Note::Encrypted { id, title, metadata, .. } => {
render_title_and_metadata(ui, title, metadata); render_title_and_metadata(ui, title, metadata, false, (id.clone(), Box::new(cb)));
if ui.button("decrypt note").clicked() { if ui.button("decrypt note").clicked() {
value = Some(self.decrypt(password).unwrap()); value = Some(self.decrypt(password).unwrap());
} }
} }
Note::Decrypted { title, metadata, text } => { Note::Decrypted { id, title, metadata, text, .. } => {
render_title_and_metadata(ui, title, metadata); render_title_and_metadata(ui, title, metadata, true, (id.clone(), Box::new(cb)));
ui.label(text); ui.label(text);
} }
}; };
@ -88,15 +92,22 @@ impl Note {
value value
} }
pub fn id(&self) -> String {
match self {
Note::Encrypted { id, .. } => id.clone(),
Note::Decrypted { id, .. } => id.clone(),
}
}
} }
fn render_title_and_metadata(ui: &mut Ui, title: impl Into<String>, metadata: &NoteMetadata) { fn render_title_and_metadata(ui: &mut Ui, title: impl Into<String>, metadata: &NoteMetadata, show_delete_button: bool, delete_info: (String, Box<impl FnOnce(String)>)) {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(RichText::new(title).size(14.0)); ui.label(RichText::new(title).size(14.0));
let date: DateTime<Utc> = metadata.date.into(); let date: DateTime<Utc> = metadata.date.into();
ui.separator(); ui.separator();
ui.label(date.format("%d-%m-%y").to_string()); ui.label(date.format("%d-%m-%y %H:%M").to_string());
if let Some(arbitrary) = &metadata.arbitrary { if let Some(arbitrary) = &metadata.arbitrary {
for (k, v) in arbitrary.iter() { for (k, v) in arbitrary.iter() {
@ -104,6 +115,15 @@ fn render_title_and_metadata(ui: &mut Ui, title: impl Into<String>, metadata: &N
ui.label(RichText::new(format!("{}: {}", k, v)).monospace()); ui.label(RichText::new(format!("{}: {}", k, v)).monospace());
} }
} }
ui.separator();
if show_delete_button {
let (id, cb) = delete_info;
if ui.button("delete note").clicked() {
cb(id);
}
}
}); });
ui.separator(); ui.separator();
} }

View file

@ -1,6 +1,6 @@
use std::collections::VecDeque;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::SystemTime;
use crate::notes::Note; use crate::notes::Note;
pub struct Saving { pub struct Saving {
@ -15,27 +15,36 @@ impl Saving {
} }
pub fn read_notes(&self) -> anyhow::Result<Vec<Note>> { pub fn read_notes(&self) -> anyhow::Result<Vec<Note>> {
let mut notes = vec![]; let mut notes = VecDeque::new();
let paths = fs::read_dir(&self.path)?; let paths = fs::read_dir(&self.path)?;
for path in paths { for path in paths {
let path = path?.path(); let path = path?.path();
if path.is_file() { if path.is_file() {
notes.push(serde_json::from_str(&fs::read_to_string(path)?)?); notes.push_front(serde_json::from_str(&fs::read_to_string(path)?)?);
} }
} }
Ok(notes) Ok(notes.into())
} }
pub fn save_note(&self, note: Note, password: String) -> anyhow::Result<()> { pub fn save_note(&self, note: Note, password: String) -> anyhow::Result<()> {
let note = note.encrypt(&password)?; let note = note.encrypt(&password)?;
let mut path = self.path.clone().join("_"); let mut path = self.path.clone().join("_");
path.set_file_name(format!("{}.json", SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?.as_millis())); path.set_file_name(format!("{}.json", note.id()));
fs::write(path, serde_json::to_string(&note)?)?; fs::write(path, serde_json::to_string(&note)?)?;
Ok(()) Ok(())
} }
pub fn delete_note(&self, id: &str) -> anyhow::Result<()> {
let mut path = self.path.clone().join("_");
path.set_file_name(format!("{}.json", id));
fs::remove_file(path)?;
Ok(())
}
} }