]> uap-core.de Git - note.git/commitdiff
implement new_note
authorOlaf Wintermann <olaf.wintermann@gmail.com>
Sun, 31 May 2026 11:54:35 +0000 (13:54 +0200)
committerOlaf Wintermann <olaf.wintermann@gmail.com>
Sun, 31 May 2026 11:54:35 +0000 (13:54 +0200)
application/src/backend.rs
application/src/main.rs
application/src/note.rs
application/src/notebook.rs
application/src/window.rs
entity/src/note.rs
ui-rs/src/ui/event.rs

index 809228b93f769d9126985a3c7e733dc215ea4f4a..52f54799f9bafb53cf0c512f9826557217294dca 100644 (file)
@@ -49,10 +49,10 @@ pub enum BroadcastMessage {
     NotesUpdate(NotesUpdate),
 }
 
-#[derive(Clone)]
+#[derive(Clone, PartialEq, Eq)]
 pub enum NoteId {
     Id(i32),
-    TmpId(String),
+    TmpId(u64),
 }
 
 impl Default for NoteId {
@@ -362,10 +362,11 @@ impl BackendHandle {
             match result {
                 Ok(note) => {
                     let note_id = note.note_id;
-                    if let Some(content) = content {
+                    if let Some(mut content) = content {
                         let result2 = if content.id.is_set() {
                             content.update(&backend.db).await
                         } else {
+                            content.note_id = Set(note_id);
                             content.insert(&backend.db).await
                         };
 
index 6612cdcab31534d4cc2442fcf317aa92f6658b6e..563e6b33ee7eb4c31101d15d89b50f13feaf8ea9 100644 (file)
@@ -36,7 +36,7 @@ use std::env;
 use entity::collection::{create_notebook_hierarchy, Node};
 use ui_rs::{ui};
 use ui_rs::ui::*;
-use crate::backend::{Backend, BackendHandle, BroadcastMessage};
+use crate::backend::{Backend, BackendHandle};
 use crate::window::*;
 
 fn main() {
index dd84d8984b78f8ef9b60c89714d6ba07a99ae06b..87e4430922257b4c680fe35d3354d2746ed0a415 100644 (file)
@@ -1,4 +1,5 @@
 use std::rc::Rc;
+use std::sync::atomic::{AtomicU64, Ordering};
 use sea_orm::prelude::DateTimeWithTimeZone;
 use sea_orm::sea_query::prelude::Utc;
 use sea_orm::{NotSet, Set};
@@ -8,12 +9,15 @@ use ui_rs::ui::*;
 use crate::backend::{BackendHandle, BroadcastMessage, NoteId, NoteUpdate, NotesUpdate};
 use crate::window::NoteTypeTabView;
 
+static TMP_ID: AtomicU64 = AtomicU64::new(1);
+
 #[derive(UiModel)]
 pub struct Note {
     pub backend: Rc<BackendHandle>,
 
+    pub id: NoteId,
+
     pub collection_id: i32,
-    pub note_id: i32,
     pub content_id: i32,
 
     pub kind: NoteType,
@@ -32,12 +36,12 @@ pub struct Note {
 
 #[ui_actions]
 impl Note {
-    pub fn new(collection_id: i32, backend: Rc<BackendHandle>) -> Self {
+    pub fn new(id: NoteId, collection_id: i32, backend: Rc<BackendHandle>) -> Self {
         Note {
             backend: backend,
             collection_id: collection_id,
 
-            note_id: 0,
+            id: id,
             content_id: 0,
             kind: NoteType::PlainTextNote,
             created: Default::default(),
@@ -51,7 +55,7 @@ impl Note {
     }
 
     pub fn init_from_model(&mut self, model: &entity::note::Model) {
-        self.note_id = model.note_id;
+        self.id = NoteId::Id(model.note_id);
 
         let tab = match model.kind {
             NoteType::PlainTextNote => NoteTypeTabView::TextArea,
@@ -59,6 +63,10 @@ impl Note {
         };
         self.note_type.set(tab as i64);
     }
+
+    pub fn init_new(&mut self) {
+        self.note_type.set(NoteTypeTabView::TextArea as i64);
+    }
     
     pub fn init_content(&mut self, content: &entity::notecontent::Model) {
         self.content_id = content.id;
@@ -67,15 +75,19 @@ impl Note {
     }
 
     pub fn load_content(&mut self, doc: &UiDoc<Note>) {
+        let NoteId::Id(note_id) = self.id else {
+            return;
+        };
+
         let proxy = doc.doc_proxy();
-        self.backend.get_note_content(self.note_id, move|result|{
-            proxy.call_mainthread(|doc, note|{
+        self.backend.get_note_content(note_id, move|result|{
+            proxy.call_mainthread(move |_doc, note|{
                 match result {
                     Ok(content) => {
                         if let Some(content) = content {
                             note.init_content(&content);
                         } else {
-                            println!("note {}: no content", note.note_id);
+                            println!("note {}: no content", note_id);
                         }
                     },
                     Err(e) => {
@@ -95,7 +107,7 @@ impl Note {
 
                 if notify {
                     let update = NoteUpdate {
-                        note_id: NoteId::Id(self.note_id),
+                        note_id: self.id.clone(),
                         title: Some(title.to_string()),
                         ..Default::default()
                     };
@@ -117,7 +129,10 @@ impl Note {
             None => "Note".to_string()
         };
 
-        let note_id = if self.note_id == 0 { NotSet } else { Set(self.note_id) };
+        let (note_id, created) = match &self.id {
+            NoteId::Id(id) => (Set(*id), NotSet),
+            NoteId::TmpId(t) => (NotSet, Set(Utc::now().into()))
+        };
 
         let note = entity::note::ActiveModel {
             note_id: note_id.clone(),
@@ -125,7 +140,7 @@ impl Note {
             kind: Set(self.kind.clone()),
             title: Set(title),
             lastmodified: Set(Utc::now().into()),
-            created: if self.note_id == 0 { Set(Utc::now().into()) } else { NotSet }
+            created
         };
 
         let notecontent = entity::notecontent::ActiveModel {
@@ -136,7 +151,8 @@ impl Note {
 
         backend.save_note(note, Some(notecontent), |result|{
             match result {
-                Ok(_model) => {
+                Ok((note_id, content_id)) => {
+                    // TODO: how to save the note_id, content_id in self?
                     println!("Note saved");
                 },
                 Err(error) => {
@@ -191,4 +207,8 @@ fn generate_title(s: &str) -> Option<(&str, usize)> {
     }
 
     None
+}
+
+pub fn new_note_id() -> NoteId {
+    NoteId::TmpId(TMP_ID.fetch_add(1, Ordering::Relaxed))
 }
\ No newline at end of file
index a96138ddbe18862410a1d30b437a76aaa661f5d9..d8e0320ac12518931bcc6e222dac53e54c64ae9f 100644 (file)
@@ -3,7 +3,8 @@ use ui_rs::{action, ui_actions, UiModel};
 use ui_rs::ui::*;
 
 use entity::note::{Model as Note};
-use crate::backend::{BackendHandle, BroadcastMessage, NoteId};
+use crate::backend::{BackendHandle, BroadcastMessage, NoteId, NoteUpdate};
+use crate::note::new_note_id;
 
 /// UI model for a notebook
 #[derive(UiModel)]
@@ -14,6 +15,7 @@ pub struct Notebook {
     pub collection_id: i32,
 
     pub selected_note: Option<UiDoc<crate::note::Note>>,
+    pub selected_index: Option<usize>,
 
     #[bind("notes")]
     pub notes: UiList<NoteItem>
@@ -28,6 +30,7 @@ impl Notebook {
             broadcast_rx: backend.btx.subscribe(),
             collection_id: id,
             selected_note: None,
+            selected_index: None,
             notes: Default::default()
         }
     }
@@ -37,7 +40,7 @@ impl Notebook {
         notes_list.clear();
 
         for note in notes {
-            notes_list.push(NoteItem::new(note));
+            notes_list.push(NoteItem::from_model(note));
         }
 
         self.notes.update();
@@ -66,18 +69,25 @@ impl Notebook {
     #[action]
     pub fn note_selected(&mut self, event: &ActionEvent) {
         if let EventType::ListSelection(s) = event.event_type {
-            self.detach_current_note();
-            self.selected_note = None;
+            self.selected_index = s.selected_index();
+            if self.selected_index.is_some() {
+                self.detach_current_note();
+                self.selected_note = None;
+            }
             if let Some(note) = s.selected_element_mut(self.notes.data_mut()) {
                 let doc = if let Some(doc) = &note.model {
                     doc.clone()
                 } else {
                     // Create the new note
-                    let note_data = crate::note::Note::new(self.collection_id, self.backend.clone());
+                    let note_data = crate::note::Note::new(note.id.clone(), self.collection_id, self.backend.clone());
                     // Create the note document object
                     UiDoc::new2(note_data, |n,d| {
-                        n.init_from_model(&note.data);
-                        n.load_content(d);
+                        if note.is_new() {
+                            n.init_new();
+                        } else {
+                            n.init_from_model(&note.data);
+                            n.load_content(d);
+                        }
                     })
                 };
                 note.model = Some(doc.clone());
@@ -90,29 +100,68 @@ impl Notebook {
         }
     }
 
+    #[action]
+    pub fn new_note(&mut self, _event: &ActionEvent) {
+        println!("new note");
+        
+        let item = NoteItem::new(new_note_id());
+        self.notes.data_mut().insert(0, item);
+        self.notes.update();
+        self.notes.select_with_event(0, true);
+    }
+
+    pub fn update_note(&mut self, update: &NoteUpdate) {
+        // Find the note in the list (or add it if it doesn't exist yet)
+        // Start with the currently selected note
+        let mut update_row: Option<usize> = None;
+        if let Some(index) = self.selected_index {
+            if let Some(item) = self.notes.data_mut().get_mut(index) {
+                if item.id == update.note_id {
+                    item.update(&update);
+                    update_row = Some(index);
+                }
+            }
+        }
+
+        if update_row.is_none() {
+            for (index, item) in self.notes.data_mut().iter_mut().enumerate() {
+                if item.id == update.note_id {
+                    item.update(&update);
+                    update_row = Some(index);
+                    break;
+                }
+            }
+        }
+
+        if let Some(index) = update_row {
+            self.notes.update_row(index);
+        }
+    }
+
     #[action]
     pub fn message(&mut self, _event: &ActionEvent) {
         while let Ok(msg) = self.broadcast_rx.try_recv() {
             match msg {
                 BroadcastMessage::NotesUpdate(update) => {
-                    let mut update_rows: Vec<usize> = Vec::new();
-                    for u in &update.note_updates {
-                        if let NoteId::Id(id) = u.note_id {
-                            let note_data = self.notes.data_mut();
-                            for (i, note) in note_data.iter_mut().enumerate() {
-                                if note.data.note_id == id {
-                                    // update note
-                                    if let Some(title) = &u.title {
-                                        note.data.title = title.clone();
+                    if update.note_updates.len() == 1 {
+                        self.update_note(&update.note_updates[0]);
+                    } else if update.note_updates.len() > 1 {
+                        let mut update_rows: Vec<usize> = Vec::new();
+                        for u in &update.note_updates {
+                            if let NoteId::Id(id) = u.note_id {
+                                let note_data = self.notes.data_mut();
+                                for (i, note) in note_data.iter_mut().enumerate() {
+                                    if note.data.note_id == id {
+                                        note.update(u);
+                                        update_rows.push(i);
                                     }
-                                    update_rows.push(i);
                                 }
                             }
                         }
-                    }
 
-                    for i in update_rows {
-                        self.notes.update_row(i);
+                        for i in update_rows {
+                            self.notes.update_row(i);
+                        }
                     }
                 },
                 _ => {
@@ -145,11 +194,32 @@ pub struct NoteItem {
 }
 
 impl NoteItem {
-    pub fn new(data: entity::note::Model) -> Self {
+    pub fn from_model(data: entity::note::Model) -> Self {
         NoteItem {
             id: NoteId::Id(data.note_id),
             data: data,
             model: None
         }
     }
+
+    pub fn new(id: NoteId) -> Self {
+        let mut item = NoteItem {
+            id: id,
+            data: entity::note::Model::new(),
+            model: None
+        };
+        item.data.title = "New Note".to_owned();
+        item
+    }
+
+    pub fn is_new(&self) -> bool {
+        return matches!(self.id, NoteId::TmpId(_))
+    }
+
+    pub fn update(&mut self, update: &NoteUpdate) {
+        // update note
+        if let Some(title) = &update.title {
+            self.data.title = title.clone();
+        }
+    }
 }
\ No newline at end of file
index fa0c9d3006a0687eea2e1170be8c6881c8ef9690..855527cb280419921e3ffbd0179107cc2d1264c3 100644 (file)
@@ -30,7 +30,6 @@ use ui_rs::{action, ui_actions, UiModel};
 use ui_rs::ui::*;
 use crate::App;
 use entity::collection::{Model as Collection, Node};
-use entity::note::Model as Note;
 use crate::backend::{BackendHandle, BroadcastMessage};
 use crate::newnotebook::new_notebook_dialog;
 use crate::notebook::{notelist_getvalue, NoteItem, Notebook};
@@ -67,12 +66,65 @@ impl MainWindow {
 
     #[action]
     pub fn new_notebook(&mut self, event: &ActionEvent) {
-        new_notebook_dialog(event.obj, &self.groups, self.backend.clone());
+        if let Some(obj) = event.obj {
+            new_notebook_dialog(obj, &self.groups, self.backend.clone());
+        }
     }
 
     #[action]
     pub fn new_note(&mut self, _event: &ActionEvent) {
+        // The new_note action is handled by Notebook and MainWindow
+        // When a notebook is attached, the notebook receives this action, but in case
+        // no notebook is selected, the action is handled in the main window
+
+        if self.selected_notebook.is_some() {
+            println!("Error: MainWindow received new_note action but selected_notebook is not None");
+            return;
+        }
+
+        // Select the first notebook
+        // TODO: maybe select a configured default notebook
+        if let Some(group) = self.notebooks.get(0) {
+            if let Some(notebook) = group.data().get(0) {
+                self.select_notebook(notebook.collection_id);
+                // self.selected_notebook has a value after select_notebook()
+                if let Some(doc) = &self.selected_notebook {
+                    // Now that a notebook is selected, we can open the new note UI there
+                    doc.ctx.call_action("new_note");
+                }
+            }
+        }
+    }
+
+    pub fn select_notebook(&mut self, collection_id: i32) {
+        let Some(mut obj) = self.obj.get_object() else {
+            return;
+        };
+
+        if let Some(current) = &self.selected_notebook {
+            obj.ctx.detach(current);
+        }
+
+        let notebook = Notebook::new(collection_id, &self.backend);
+        let doc = UiDoc::new2(notebook, |notebook, doc| {
+            notebook.doc_ref = doc.doc_ref();
+        });
+        let proxy = doc.doc_proxy();
+        self.backend.get_notes(collection_id, |result|{
+            proxy.call_mainthread(|_, nb| {
+                match result {
+                    Ok(notes) => {
+                        nb.set_notes(notes);
+                    },
+                    Err(err) => {
+                        eprintln!("get_notes failed: {}", err);
+                    }
+                }
+            });
+        });
 
+        obj.ctx.attach(&doc);
+        self.selected_notebook = Some(doc);
     }
 
     #[action]
@@ -133,30 +185,7 @@ pub fn create_window(app: &App, ctx: &AppContext<MainWindow>) -> UiObject<MainWi
                 b.onactivate(|e|{
                     if let EventType::SubList(sl) = &e.event_type {
                         if let Some(nb) = sl.get_from(&e.data.notebooks) {
-                            if let Some(current) = &e.data.selected_notebook {
-                                e.obj.ctx.detach(current);
-                            }
-
-                            let notebook = Notebook::new(nb.collection_id, &e.data.backend);
-                            let doc = UiDoc::new2(notebook, |notebook, doc| {
-                                notebook.doc_ref = doc.doc_ref();
-                            });
-                            let proxy = doc.doc_proxy();
-                            e.data.backend.get_notes(nb.collection_id, |result|{
-                                proxy.call_mainthread(|_, nb| {
-                                    match result {
-                                        Ok(notes) => {
-                                            nb.set_notes(notes);
-                                        },
-                                        Err(err) => {
-                                            eprintln!("get_notes failed: {}", err);
-                                        }
-                                    }
-                                });
-                            });
-
-                            e.obj.ctx.attach(&doc);
-                            e.data.selected_notebook = Some(doc);
+                            e.data.select_notebook(nb.collection_id);
                         }
                     }
                 });
@@ -202,7 +231,7 @@ pub fn create_window(app: &App, ctx: &AppContext<MainWindow>) -> UiObject<MainWi
     window.show();
     window.onclose(|event| {
         if let Some(doc) = &event.data.selected_notebook {
-            doc.doc_proxy().call_mainthread(|doc, notebook| {
+            doc.doc_proxy().call_mainthread(|_doc, notebook| {
                 notebook.detach_current_note();
             });
         }
index cd71986ec60d1b7151ee6802a0336c22dfd2f15d..c7d51ca9c3ad6871081f95dc711471ae3d121443 100644 (file)
@@ -28,3 +28,16 @@ pub enum NoteType {
 }
 
 impl ActiveModelBehavior for ActiveModel {}
+
+impl Model {
+    pub fn new() -> Self {
+        Model {
+            note_id: 0,
+            collection_id: 0,
+            kind: Default::default(),
+            title: Default::default(),
+            lastmodified: Default::default(),
+            created: Default::default()
+        }
+    }
+}
index cbdcd72f9c9d0a8502f8feb9dbd117b4e6544c1c..884baf44fa05721ac659835a80f6b9e42686a938 100644 (file)
@@ -43,7 +43,7 @@ pub struct Event<'a, T> {
 }
 
 pub struct ActionEvent<'a> {
-    pub obj: &'a UiObject<NoAppData>,
+    pub obj: Option<&'a UiObject<NoAppData>>,
     pub event_type: EventType<'a>,
     pub intval: i32,
     pub set: bool
@@ -268,10 +268,14 @@ pub extern "C" fn action_event_wrapper<T>(e: *const ffi::UiEvent, data: *mut c_v
         let event_type = get_event_type(&mut event_data);
 
         let obj_ptr = ui_event_get_obj(e);
-        let obj: UiObject<NoAppData> = UiObject::from_ptr(obj_ptr);
+        let obj = if obj_ptr.is_null() {
+            None
+        } else {
+            Some(&UiObject::from_ptr(obj_ptr))
+        };
 
         let event = ActionEvent {
-            obj: &obj,
+            obj: obj,
             event_type: event_type,
             intval: ev_int,
             set: ev_set