Repository: flxzt/rnote (language: rust)

Issue:
Autosave without changes floods Nextcloud file versions
**Describe the bug**  
Rnote autosave without changes to file triggers a sync and floods the logs and file versions of the Nextcloud client / server.  
Notably autosave is done even when the file has not been changed and Rnote is out of focus (meaning no view movement either)

**To Reproduce**  
Steps to reproduce the behavior:
1. Use Nextcloud client to sync a folder
2. Open a new rnote document
3. Enable Autosave
4. Open Nextcloud client notifications from the taskbar (pictured below)

**Expected behavior**  
Autosave detects that no changes have been made so it doesn't trigger the save which then triggers Nextcloud.

**Console Output**  
N/A Cannot run Flatpak on machine in question

**Screenshots**  
![rnote-nextcloud-sync](https://github.com/flxzt/rnote/assets/77442297/64c518fe-8c8f-44f5-b64c-edde6fa5389d)

**Desktop (please complete the following information):**  
 - OS: Arch
 - App Version: Rnote v0.9.4
 - Installation Source: Archlinux Community Repo
 - Desktop Environment: KDE 5.27
 - Display Server: Wayland
 - Input Source: Touch laptop

**Additional context**  
N/A


Discussion / hints:

Fix patch:
diff --git a/crates/rnote-ui/src/appwindow/imp.rs b/crates/rnote-ui/src/appwindow/imp.rs
index d5a1f1f6e3..2ba93a7bf0 100644
--- a/crates/rnote-ui/src/appwindow/imp.rs
+++ b/crates/rnote-ui/src/appwindow/imp.rs
@@ -235,26 +235,54 @@ impl RnAppWindow {
     fn update_autosave_handler(&self) {
         let obj = self.obj();
 
-        if let Some(removed_id) = self.autosave_source_id.borrow_mut().replace(glib::source::timeout_add_seconds_local(self.autosave_interval_secs.get(),
+        if let Some(removed_id) = self.autosave_source_id.borrow_mut().replace(
+            glib::source::timeout_add_seconds_local(
+                self.autosave_interval_secs.get(),
                 clone!(@weak obj as appwindow => @default-return glib::ControlFlow::Break, move || {
-                    let canvas = appwindow.active_tab_wrapper().canvas();
-
-                    if let Some(output_file) = canvas.output_file() {
-                        glib::spawn_future_local(clone!(@weak canvas, @weak appwindow => async move {
-                            if let Err(e) = canvas.save_document_to_file(&output_file).await {
-                                canvas.set_output_file(None);
-
-                                tracing::error!("Saving document failed, Err: `{e:?}`");
-                                appwindow.overlays().dispatch_toast_error(&gettext("Saving document failed"));
-                            }
+                    // save for all tabs opened in the current window that have unsaved changes
+                    let tabs_result = appwindow.get_all_tabs(); // get all tabs in a vec
+
+                    match tabs_result {
+                        Ok(tabs) => {
+                            tabs
+                                .iter()
+                                .enumerate()
+                                .for_each(|(i,canvas_i)| {
+                                    if canvas_i.unsaved_changes() {
+                                        if let Some(output_file) = canvas_i.output_file() {
+                                            tracing::trace!(
+                                                "there are unsaved changes on the tab {:?} with a file on disk, saving",i
+                                            );
+                                            glib::spawn_future_local(
+                                                clone!(@weak canvas_i, @weak appwindow => async move {
+                                            if let Err(e) = canvas_i
+                                                .save_document_to_file(&output_file)
+                                                .await {
+                                            canvas_i.set_output_file(None);
+                                            tracing::error!("Saving document failed, Err: `{e:?}`");
+                                            appwindow
+                                                .overlays()
+                                                .dispatch_toast_error(&gettext("Saving document failed"));
+                                            }
+                                        }
+                                        ));
+                                        }
+                                    }
+                                });
                         }
-                    ));
-                }
-
+                        Err(e) => {
+                            tracing::error!("failed to open tabs, {:?}",e);
+                            appwindow
+                                .overlays()
+                                .dispatch_toast_error(&gettext("Failed to open tabs"));
+                        }
+                    }
                 glib::ControlFlow::Continue
-            }))) {
-                removed_id.remove();
-            }
+                    }),
+            ),
+        ) {
+            removed_id.remove();
+        }
     }
 
     fn setup_input(&self) {
diff --git a/crates/rnote-ui/src/appwindow/mod.rs b/crates/rnote-ui/src/appwindow/mod.rs
index d21b64531e..4ad1117ab7 100644
--- a/crates/rnote-ui/src/appwindow/mod.rs
+++ b/crates/rnote-ui/src/appwindow/mod.rs
@@ -256,6 +256,33 @@ impl RnAppWindow {
             .expect("there must always be one active tab")
     }
 
+    pub(crate) fn get_number_of_tabs(&self) -> u32 {
+        self.imp().overlays.tabview().pages().n_items()
+    }
+
+    /// returns a vector of all tabs of the current windows
+    pub(crate) fn get_all_tabs(&self) -> Result<Vec<RnCanvas>, anyhow::Error> {
+        let n_tabs = self.get_number_of_tabs();
+        let mut output = Vec::<RnCanvas>::new();
+
+        for i in 0..n_tabs {
+            let glib_obj = self.imp().overlays.tabview().pages().item(i);
+            if glib_obj.is_none() {
+                return Err(anyhow::anyhow!("could not obtain the tab {:?}", i));
+            }
+            let tab_glib = glib_obj
+                .expect("downcast failure")
+                .downcast::<adw::TabPage>();
+            let wrapper = tab_glib
+                .unwrap()
+                .child()
+                .downcast::<crate::RnCanvasWrapper>();
+            let canvas = wrapper.unwrap().canvas();
+            output.push(canvas);
+        }
+        Ok(output)
+    }
+
     /// Get the active (selected) tab page child.
     pub(crate) fn active_tab_wrapper(&self) -> RnCanvasWrapper {
         self.active_tab_page()


Test patch: