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:
parent
88dd8f034f
commit
b599f628de
4 changed files with 72 additions and 17 deletions
|
@ -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"
|
||||||
|
|
30
src/main.rs
30
src/main.rs
|
@ -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(),
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
|
@ -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(¬e)?)?;
|
fs::write(path, serde_json::to_string(¬e)?)?;
|
||||||
|
|
||||||
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(())
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue