From 8064e9ef49dc31e88ce5ee0ebfe3a702e6cb8dcf Mon Sep 17 00:00:00 2001 From: LeSnake04 Date: Wed, 26 Apr 2023 12:37:41 +0200 Subject: [PATCH 01/20] initial implementation of recovery feature --- .gitignore | 4 +- rnote-ui/data/app.gschema.xml.in | 8 ++ rnote-ui/data/ui/mainheader.ui | 6 ++ rnote-ui/data/ui/settingspanel.ui | 32 ++++++++ rnote-ui/src/appwindow/imp.rs | 67 ++++++++++++++++ rnote-ui/src/appwindow/mod.rs | 14 ++++ rnote-ui/src/canvas/imexport.rs | 13 +++- rnote-ui/src/canvas/mod.rs | 123 ++++++++++++++++++++++++++---- rnote-ui/src/mainheader.rs | 6 ++ rnote-ui/src/settingspanel/mod.rs | 23 ++++++ 10 files changed, 280 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 89905f5d9b..2bb82d0b3f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,6 @@ *.code-workspace .idea/ .toggletasks.json -*.flatpak \ No newline at end of file +*.flatpak +justfile +rust-toolchain \ No newline at end of file diff --git a/rnote-ui/data/app.gschema.xml.in b/rnote-ui/data/app.gschema.xml.in index 5d15b53bf7..751c0ee412 100644 --- a/rnote-ui/data/app.gschema.xml.in +++ b/rnote-ui/data/app.gschema.xml.in @@ -125,6 +125,14 @@ 120 the sec interval for the autosave + + true + true when document recovery is enabled + + + 80 + the sec interval for the recovery saves + false Whether the canvas scrollbars are shown diff --git a/rnote-ui/data/ui/mainheader.ui b/rnote-ui/data/ui/mainheader.ui index c2df687a4e..1e4e8e910e 100644 --- a/rnote-ui/data/ui/mainheader.ui +++ b/rnote-ui/data/ui/mainheader.ui @@ -22,6 +22,12 @@ + + + + false + + main_titlebox diff --git a/rnote-ui/data/ui/settingspanel.ui b/rnote-ui/data/ui/settingspanel.ui index f25e27771f..38eb57b69e 100644 --- a/rnote-ui/data/ui/settingspanel.ui +++ b/rnote-ui/data/ui/settingspanel.ui @@ -71,6 +71,38 @@ + + + Recovery + Enable or disable document recovery + + + center + + + + + + + Recoevry Interval (secs) + Set the recovery save interval in seconds + + + 1 + 9999 + 5 + 120 + + + general_recovery_interval_secs_adj + horizontal + false + center + 0 + + + + Format Border Color diff --git a/rnote-ui/src/appwindow/imp.rs b/rnote-ui/src/appwindow/imp.rs index 3e11dbc9d4..ebea3c8454 100644 --- a/rnote-ui/src/appwindow/imp.rs +++ b/rnote-ui/src/appwindow/imp.rs @@ -24,10 +24,13 @@ pub(crate) struct RnAppWindow { pub(crate) filechoosernative: Rc>>, pub(crate) drawing_pad_controller: RefCell>, pub(crate) autosave_source_id: RefCell>, + pub(crate) recovery_source_id: RefCell>, pub(crate) periodic_configsave_source_id: RefCell>, pub(crate) autosave: Cell, pub(crate) autosave_interval_secs: Cell, + pub(crate) recovery: Cell, + pub(crate) recovery_interval_secs: Cell, pub(crate) righthanded: Cell, pub(crate) block_pinch_zoom: Cell, pub(crate) touch_drawing: Cell, @@ -69,10 +72,13 @@ impl Default for RnAppWindow { filechoosernative: Rc::new(RefCell::new(None)), drawing_pad_controller: RefCell::new(None), autosave_source_id: RefCell::new(None), + recovery_source_id: RefCell::new(None), periodic_configsave_source_id: RefCell::new(None), autosave: Cell::new(true), autosave_interval_secs: Cell::new(super::RnAppWindow::AUTOSAVE_INTERVAL_DEFAULT), + recovery: Cell::new(true), + recovery_interval_secs: Cell::new(super::RnAppWindow::RECOVERY_INTERVAL_DEFAULT), righthanded: Cell::new(true), block_pinch_zoom: Cell::new(false), touch_drawing: Cell::new(false), @@ -147,6 +153,14 @@ impl ObjectImpl for RnAppWindow { .maximum(u32::MAX) .default_value(super::RnAppWindow::AUTOSAVE_INTERVAL_DEFAULT) .build(), + glib::ParamSpecBoolean::builder("recovery") + .default_value(true) + .build(), + glib::ParamSpecUInt::builder("recovery-interval-secs") + .minimum(5) + .maximum(u32::MAX) + .default_value(super::RnAppWindow::RECOVERY_INTERVAL_DEFAULT) + .build(), glib::ParamSpecBoolean::builder("righthanded") .default_value(false) .build(), @@ -165,6 +179,8 @@ impl ObjectImpl for RnAppWindow { match pspec.name() { "autosave" => self.autosave.get().to_value(), "autosave-interval-secs" => self.autosave_interval_secs.get().to_value(), + "recovery" => self.recovery.get().to_value(), + "recovery-interval-secs" => self.recovery_interval_secs.get().to_value(), "righthanded" => self.righthanded.get().to_value(), "block-pinch-zoom" => self.block_pinch_zoom.get().to_value(), "touch-drawing" => self.touch_drawing.get().to_value(), @@ -199,6 +215,31 @@ impl ObjectImpl for RnAppWindow { self.update_autosave_handler(); } } + "recovery" => { + let recovery = value + .get::() + .expect("The value needs to be of type `bool`"); + + self.recovery.replace(recovery); + + if recovery { + self.update_recovery_handler(); + } else if let Some(recovery_source_id) = self.recovery_source_id.borrow_mut().take() + { + recovery_source_id.remove(); + } + } + "recovery-interval-secs" => { + let recovery_interval_secs = value + .get::() + .expect("The value needs to be of type `u32`"); + + self.recovery_interval_secs.replace(recovery_interval_secs); + + if self.autosave.get() { + self.update_autosave_handler(); + } + } "righthanded" => { let righthanded = value .get::() @@ -267,7 +308,33 @@ impl RnAppWindow { } )); } + glib::source::Continue(true) + }))) { + removed_id.remove(); + } + } + fn update_recovery_handler(&self) { + let obj = self.obj(); + + if let Some(removed_id) = self.recovery_source_id.borrow_mut().replace(glib::source::timeout_add_seconds_local(self.recovery_interval_secs.get(), + clone!(@weak obj as appwindow => @default-return glib::source::Continue(false), move || { + let canvas = appwindow.active_tab().canvas(); + + glib::MainContext::default().spawn_local(clone!(@weak canvas, @weak appwindow => async move { + let tmp_file = canvas.get_or_generate_tmp_file(); + appwindow.overlays().start_pulsing_progressbar(); + canvas.set_recovery_in_progress(true); + canvas.imp().output_file_cache.replace(canvas.imp().output_file.take()); + if let Err(e) = canvas.save_document_to_file(&tmp_file).await { + log::error!("saving document failed, Error: `{e:?}`"); + appwindow.overlays().dispatch_toast_error(&gettext("Saving document failed")); + } + canvas.imp().output_file.replace(canvas.imp().output_file_cache.take()); + canvas.set_recovery_in_progress(false); + appwindow.overlays().finish_progressbar(); + } + )); glib::source::Continue(true) }))) { removed_id.remove(); diff --git a/rnote-ui/src/appwindow/mod.rs b/rnote-ui/src/appwindow/mod.rs index 714c488df4..334aa39d7c 100644 --- a/rnote-ui/src/appwindow/mod.rs +++ b/rnote-ui/src/appwindow/mod.rs @@ -30,6 +30,7 @@ glib::wrapper! { impl RnAppWindow { const AUTOSAVE_INTERVAL_DEFAULT: u32 = 30; + const RECOVERY_INTERVAL_DEFAULT: u32 = 20; const PERIODIC_CONFIGSAVE_INTERVAL: u32 = 10; const FLAP_FOLDED_RESIZE_MARGIN: u32 = 64; @@ -47,6 +48,16 @@ impl RnAppWindow { self.set_property("autosave", autosave.to_value()); } + #[allow(unused)] + pub(crate) fn recovery(&self) -> bool { + self.property::("recovery") + } + + #[allow(unused)] + pub(crate) fn set_recovery(&self, recovery: bool) { + self.set_property("recovery", recovery.to_value()); + } + #[allow(unused)] pub(crate) fn autosave_interval_secs(&self) -> u32 { self.property::("autosave-interval-secs") @@ -395,6 +406,9 @@ impl RnAppWindow { self.mainheader() .main_title_unsaved_indicator() .set_visible(canvas.unsaved_changes()); + self.mainheader() + .main_title_unsaved_recovery_indicator() + .set_visible(canvas.unsaved_changes_recovery()); if canvas.unsaved_changes() { self.mainheader() .main_title() diff --git a/rnote-ui/src/canvas/imexport.rs b/rnote-ui/src/canvas/imexport.rs index a9310167ba..128a4a85f9 100644 --- a/rnote-ui/src/canvas/imexport.rs +++ b/rnote-ui/src/canvas/imexport.rs @@ -228,6 +228,7 @@ impl RnCanvas { /// Returns Ok(true) if saved successfully, Ok(false) when a save is already in progress and no file operatiosn were executed, /// Err(e) when saving failed in any way. pub(crate) async fn save_document_to_file(&self, file: &gio::File) -> anyhow::Result { + let recovery_in_progress = || self.recovery_in_progress(); // skip saving when it is already in progress if self.save_in_progress() { log::debug!("saving file already in progress"); @@ -283,6 +284,9 @@ impl RnCanvas { if let Err(e) = res { self.set_save_in_progress(false); + if recovery_in_progress() { + self.set_recovery_in_progress(false) + } // If the file operations failed in any way, we make sure to clear the expect_write flag // because we can't know for sure if the output_file monitor will be able to. @@ -290,8 +294,15 @@ impl RnCanvas { return Err(e); } - self.set_unsaved_changes(false); + if self.recovery_in_progress() { + self.set_unsaved_changes_recovery(false); + } else { + self.set_unsaved_changes(false); + } self.set_save_in_progress(false); + if recovery_in_progress() { + self.set_recovery_in_progress(false) + } Ok(true) } diff --git a/rnote-ui/src/canvas/mod.rs b/rnote-ui/src/canvas/mod.rs index 0417ebc138..5f7f026a81 100644 --- a/rnote-ui/src/canvas/mod.rs +++ b/rnote-ui/src/canvas/mod.rs @@ -72,6 +72,9 @@ mod imp { pub(crate) engine: Rc>, + pub(crate) recovery_in_progress: Cell, + pub(crate) recovery_file: RefCell>, + pub(crate) output_file_cache: RefCell>, pub(crate) output_file: RefCell>, pub(crate) output_file_monitor: RefCell>, pub(crate) output_file_monitor_changed_handler: RefCell>, @@ -79,6 +82,7 @@ mod imp { pub(crate) output_file_expect_write: Cell, pub(crate) save_in_progress: Cell, pub(crate) unsaved_changes: Cell, + pub(crate) unsaved_changes_recovery: Cell, pub(crate) empty: Cell, pub(crate) touch_drawing: Cell, } @@ -150,6 +154,9 @@ mod imp { engine: Rc::new(RefCell::new(engine)), + recovery_in_progress: Cell::new(false), + recovery_file: RefCell::new(None), + output_file_cache: RefCell::new(None), output_file: RefCell::new(None), output_file_monitor: RefCell::new(None), output_file_monitor_changed_handler: RefCell::new(None), @@ -157,6 +164,7 @@ mod imp { output_file_expect_write: Cell::new(false), save_in_progress: Cell::new(false), unsaved_changes: Cell::new(false), + unsaved_changes_recovery: Cell::new(false), empty: Cell::new(true), touch_drawing: Cell::new(false), } @@ -232,6 +240,9 @@ mod imp { glib::ParamSpecBoolean::builder("unsaved-changes") .default_value(false) .build(), + glib::ParamSpecBoolean::builder("unsaved-changes-recovery") + .default_value(false) + .build(), glib::ParamSpecBoolean::builder("empty") .default_value(true) .build(), @@ -258,6 +269,7 @@ mod imp { match pspec.name() { "output-file" => self.output_file.borrow().to_value(), "unsaved-changes" => self.unsaved_changes.get().to_value(), + "unsaved-changes-recovery" => self.unsaved_changes.get().to_value(), "empty" => self.empty.get().to_value(), "hadjustment" => self.hadjustment.borrow().to_value(), "vadjustment" => self.vadjustment.borrow().to_value(), @@ -285,6 +297,12 @@ mod imp { value.get().expect("The value needs to be of type `bool`"); self.unsaved_changes.replace(unsaved_changes); } + "unsaved-changes-recovery" => { + let unsaved_changes_recovery: bool = + value.get().expect("The value needs to be of type `bool`"); + self.unsaved_changes_recovery + .replace(unsaved_changes_recovery); + } "empty" => { let empty: bool = value.get().expect("The value needs to be of type `bool`"); self.empty.replace(empty); @@ -531,6 +549,49 @@ impl RnCanvas { self.set_property("drawing-cursor", drawing_cursor.to_value()); } + #[allow(unused)] + pub(crate) fn tmp_file(&self) -> Option { + self.imp().recovery_file.borrow().clone() + } + + #[allow(unused)] + pub(crate) fn set_tmp_file(&self, tmp_file: Option) { + self.imp().recovery_file.replace(tmp_file); + } + + #[allow(unused)] + pub(crate) fn output_file_cache(&self) -> Option { + self.imp().recovery_file.borrow().clone() + } + + #[allow(unused)] + pub(crate) fn set_output_file_cache(&self) -> Option { + self.imp().recovery_file.borrow().clone() + } + + pub(crate) fn get_or_generate_tmp_file(&self) -> gio::File { + // if !recovery { + // return None; + // } + if self.imp().recovery_file.borrow().is_none() { + let mut path = std::path::PathBuf::from("/tmp/rnote-recovery/"); + let time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + if !path.exists() { + std::fs::create_dir(&path).expect("Failed to create tmp dir"); + } + let name = format!("draft-{time}.rnote"); + path.push(name); + self.imp() + .recovery_file + .replace(Some(gio::File::for_path(path))); + } + self.imp().recovery_file.borrow().as_ref().unwrap().clone() + } + #[allow(unused)] pub(crate) fn output_file(&self) -> Option { self.property::>("output-file") @@ -556,6 +617,16 @@ impl RnCanvas { self.imp().save_in_progress.set(save_in_progress); } + #[allow(unused)] + pub(crate) fn recovery_in_progress(&self) -> bool { + self.imp().recovery_in_progress.get() + } + + #[allow(unused)] + pub(crate) fn set_recovery_in_progress(&self, save_in_progress: bool) { + self.imp().recovery_in_progress.set(save_in_progress); + } + #[allow(unused)] pub(crate) fn set_output_file(&self, output_file: Option) { self.set_property("output-file", output_file.to_value()); @@ -569,10 +640,28 @@ impl RnCanvas { #[allow(unused)] pub(crate) fn set_unsaved_changes(&self, unsaved_changes: bool) { if self.imp().unsaved_changes.get() != unsaved_changes { + if unsaved_changes { + self.set_unsaved_changes_recovery(true); + } self.set_property("unsaved-changes", unsaved_changes.to_value()); } } + #[allow(unused)] + pub(crate) fn unsaved_changes_recovery(&self) -> bool { + self.property::("unsaved-changes-recovery") + } + + #[allow(unused)] + pub(crate) fn set_unsaved_changes_recovery(&self, unsaved_changes_recovery: bool) { + if self.imp().unsaved_changes_recovery.get() != unsaved_changes_recovery { + self.set_property( + "unsaved-changes-recovery", + unsaved_changes_recovery.to_value(), + ); + } + } + #[allow(unused)] pub(crate) fn empty(&self) -> bool { self.property::("empty") @@ -691,26 +780,32 @@ impl RnCanvas { /// /// When there is no output-file, falls back to the "New document" string pub(crate) fn doc_title_display(&self) -> String { - self.output_file() - .map(|f| { - f.basename() - .and_then(|t| Some(t.file_stem()?.to_string_lossy().to_string())) - .unwrap_or_else(|| gettext("- invalid file name -")) - }) - .unwrap_or_else(|| OUTPUT_FILE_NEW_TITLE.to_string()) + match self.recovery_in_progress() { + true => self.output_file_cache(), + false => self.output_file(), + } + .map(|f| { + f.basename() + .and_then(|t| Some(t.file_stem()?.to_string_lossy().to_string())) + .unwrap_or_else(|| gettext("- invalid file name -")) + }) + .unwrap_or_else(|| OUTPUT_FILE_NEW_TITLE.to_string()) } /// The document folder path for display. To get the actual path, use output-file /// /// When there is no output-file, falls back to the "Draft" string pub(crate) fn doc_folderpath_display(&self) -> String { - self.output_file() - .map(|f| { - f.parent() - .and_then(|p| Some(p.path()?.display().to_string())) - .unwrap_or_else(|| gettext("- invalid folder path -")) - }) - .unwrap_or_else(|| OUTPUT_FILE_NEW_SUBTITLE.to_string()) + match self.recovery_in_progress() { + true => self.output_file_cache(), + false => self.output_file(), + } + .map(|f| { + f.parent() + .and_then(|p| Some(p.path()?.display().to_string())) + .unwrap_or_else(|| gettext("- invalid folder path -")) + }) + .unwrap_or_else(|| OUTPUT_FILE_NEW_SUBTITLE.to_string()) } pub(crate) fn create_output_file_monitor(&self, file: &gio::File, appwindow: &RnAppWindow) { diff --git a/rnote-ui/src/mainheader.rs b/rnote-ui/src/mainheader.rs index cc60df6094..116c9734b2 100644 --- a/rnote-ui/src/mainheader.rs +++ b/rnote-ui/src/mainheader.rs @@ -16,6 +16,8 @@ mod imp { #[template_child] pub(crate) main_title_unsaved_indicator: TemplateChild - - - Format Border Color - Set the format border color - - - horizontal - 6 - false - false - center - - - - - - - Show Scrollbars @@ -567,4 +549,4 @@ on a drawing pad - + \ No newline at end of file From 89a6d42a43c1ac00d32868660bb0aef622ebf868 Mon Sep 17 00:00:00 2001 From: LeSnake04 Date: Mon, 5 Jun 2023 12:10:58 +0200 Subject: [PATCH 06/20] add recovery metadata --- Cargo.lock | 10 +++++ rnote-fileformats/src/lib.rs | 2 + rnote-fileformats/src/recovery_metadata.rs | 43 ++++++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 rnote-fileformats/src/recovery_metadata.rs diff --git a/Cargo.lock b/Cargo.lock index 8559938e4c..f7bc0d55ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1010,6 +1010,15 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-sys" version = "0.4.1" @@ -3448,6 +3457,7 @@ dependencies = [ "anyhow", "cairo-rs", "directories", + "dirs", "fs_extra", "futures", "gettext-rs", diff --git a/rnote-fileformats/src/lib.rs b/rnote-fileformats/src/lib.rs index cb5745364c..38e3daf803 100644 --- a/rnote-fileformats/src/lib.rs +++ b/rnote-fileformats/src/lib.rs @@ -15,6 +15,8 @@ //! - Xournal++ - `.xopp` // Modules +/// The Metadata of recovery saves +pub mod recovery_metadata; /// The Rnote `.rnote` file format. pub mod rnoteformat; /// The Xournal++ `.xopp` file format. diff --git a/rnote-fileformats/src/recovery_metadata.rs b/rnote-fileformats/src/recovery_metadata.rs new file mode 100644 index 0000000000..1772ee2267 --- /dev/null +++ b/rnote-fileformats/src/recovery_metadata.rs @@ -0,0 +1,43 @@ +// Imports +use serde::{Deserialize, Serialize}; +use std::{cell::Cell, path::PathBuf}; + +#[derive(Debug, Serialize, Deserialize)] +/// Metadata of a revovery save +pub struct RecoveryMetadata { + last_changed: Cell, + rnote_path: PathBuf, + #[serde(skip_serializing)] + metdata_path: PathBuf, +} + +impl RecoveryMetadata { + /// Create new Cargo metadata + pub fn new(metadata_path: impl Into, rnote_path: impl Into) -> Self { + let out = Self { + last_changed: Cell::new(0), + rnote_path: rnote_path.into(), + metdata_path: metadata_path.into(), + }; + out.update_last_changed(); + out + } + /// Save recovery data + pub fn save(&self) { + std::fs::write( + &self.metdata_path, + serde_json::to_string(self).expect("Failed to parse recovery format"), + ) + .expect("Failed to write file") + } + + /// Replace last_changed with the current unix time + pub fn update_last_changed(&self) { + self.last_changed.replace( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("Failed to get unix time") + .as_secs(), + ); + } +} From f05f743f614530bf666c48a55b2fc86e7ef42614 Mon Sep 17 00:00:00 2001 From: LeSnake04 Date: Mon, 5 Jun 2023 12:11:24 +0200 Subject: [PATCH 07/20] remove dublicate code --- rnote-ui/src/settingspanel/mod.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/rnote-ui/src/settingspanel/mod.rs b/rnote-ui/src/settingspanel/mod.rs index 3e5c8e9f79..2514f11398 100644 --- a/rnote-ui/src/settingspanel/mod.rs +++ b/rnote-ui/src/settingspanel/mod.rs @@ -514,15 +514,6 @@ impl RnSettingsPanel { .sync_create() .build(); - imp.general_autosave_interval_secs_spinbutton - .get() - .bind_property("value", appwindow, "autosave-interval-secs") - .transform_to(|_, val: f64| Some((val.round() as u32).to_value())) - .transform_from(|_, val: u32| Some(f64::from(val).to_value())) - .sync_create() - .bidirectional() - .build(); - imp.general_recovery_interval_secs_spinbutton .get() .bind_property("value", appwindow, "recovery-interval-secs") From 32a9fd6e4a4743e99a7d5eae858ae9979664bdb4 Mon Sep 17 00:00:00 2001 From: LeSnake04 Date: Mon, 5 Jun 2023 12:19:16 +0200 Subject: [PATCH 08/20] add dirs dependency --- rnote-ui/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/rnote-ui/Cargo.toml b/rnote-ui/Cargo.toml index 792eff1745..470ebb6c45 100644 --- a/rnote-ui/Cargo.toml +++ b/rnote-ui/Cargo.toml @@ -50,3 +50,4 @@ fs_extra = "1" same-file = "1" regex = "1.7" directories = "5" +dirs = "5.0.1" From 852bddb55be69e9ebdabd89ace31194b87fe08e9 Mon Sep 17 00:00:00 2001 From: LeSnake04 Date: Mon, 5 Jun 2023 12:21:43 +0200 Subject: [PATCH 09/20] remove output_file_cache --- rnote-ui/src/appwindow/imp.rs | 8 ++-- rnote-ui/src/canvas/imexport.rs | 2 +- rnote-ui/src/canvas/mod.rs | 85 ++++++++++++++++----------------- 3 files changed, 46 insertions(+), 49 deletions(-) diff --git a/rnote-ui/src/appwindow/imp.rs b/rnote-ui/src/appwindow/imp.rs index c36842dea6..08e615c410 100644 --- a/rnote-ui/src/appwindow/imp.rs +++ b/rnote-ui/src/appwindow/imp.rs @@ -238,8 +238,8 @@ impl ObjectImpl for RnAppWindow { self.recovery_interval_secs.replace(recovery_interval_secs); - if self.autosave.get() { - self.update_autosave_handler(); + if self.recovery.get() { + self.update_recovery_handler(); } } "righthanded" => { @@ -335,12 +335,12 @@ impl RnAppWindow { let tmp_file = canvas.get_or_generate_tmp_file(); appwindow.overlays().start_pulsing_progressbar(); canvas.set_recovery_in_progress(true); - canvas.imp().output_file_cache.replace(canvas.imp().output_file.take()); + // canvas.imp().output_file_cache.replace(canvas.imp().output_file.take()); if let Err(e) = canvas.save_document_to_file(&tmp_file).await { log::error!("saving document failed, Error: `{e:?}`"); appwindow.overlays().dispatch_toast_error(&gettext("Saving document failed")); } - canvas.imp().output_file.replace(canvas.imp().output_file_cache.take()); + // canvas.imp().output_file.replace(canvas.imp().output_file_cache.take()); canvas.set_recovery_in_progress(false); appwindow.overlays().finish_progressbar(); } diff --git a/rnote-ui/src/canvas/imexport.rs b/rnote-ui/src/canvas/imexport.rs index 2267ebab22..2e9001e8be 100644 --- a/rnote-ui/src/canvas/imexport.rs +++ b/rnote-ui/src/canvas/imexport.rs @@ -288,7 +288,7 @@ impl RnCanvas { // this **must** come before actually saving the file to disk, // else the event might not be caught by the monitor for new or changed files - if !skip_set_output_file { + if !skip_set_output_file && !self.recovery_in_progress() { self.set_output_file(Some(file.to_owned())); } diff --git a/rnote-ui/src/canvas/mod.rs b/rnote-ui/src/canvas/mod.rs index ac91a471ac..bff4596d17 100644 --- a/rnote-ui/src/canvas/mod.rs +++ b/rnote-ui/src/canvas/mod.rs @@ -47,6 +47,7 @@ pub(crate) struct Handlers { } mod imp { + use super::*; #[derive(Debug)] @@ -72,7 +73,9 @@ mod imp { pub(crate) recovery_in_progress: Cell, pub(crate) recovery_file: RefCell>, - pub(crate) output_file_cache: RefCell>, + // pub(crate) recovery_file_monitor: RefCell>, + pub(crate) recovery_file_metadata: RefCell>, + // pub(crate) output_file_cache: RefCell>, pub(crate) output_file: RefCell>, pub(crate) output_file_monitor: RefCell>, pub(crate) output_file_monitor_changed_handler: RefCell>, @@ -167,7 +170,9 @@ mod imp { recovery_in_progress: Cell::new(false), recovery_file: RefCell::new(None), - output_file_cache: RefCell::new(None), + // recovery_file_monitor: RefCell::new(None), + recovery_file_metadata: RefCell::new(None), + // output_file_cache: RefCell::new(None), output_file: RefCell::new(None), output_file_monitor: RefCell::new(None), output_file_monitor_changed_handler: RefCell::new(None), @@ -589,35 +594,33 @@ impl RnCanvas { self.imp().recovery_file.replace(tmp_file); } - #[allow(unused)] - pub(crate) fn output_file_cache(&self) -> Option { - self.imp().recovery_file.borrow().clone() - } - - #[allow(unused)] - pub(crate) fn set_output_file_cache(&self) -> Option { - self.imp().recovery_file.borrow().clone() - } - pub(crate) fn get_or_generate_tmp_file(&self) -> gio::File { - // if !recovery { - // return None; - // } if self.imp().recovery_file.borrow().is_none() { - let mut path = std::path::PathBuf::from("/tmp/rnote-recovery/"); + let imp = self.imp(); + let mut rnote_path = dirs::data_dir().expect("Failed to get data dir"); + rnote_path.push("rnote"); + rnote_path.push("recovery"); + if !rnote_path.exists() { + std::fs::create_dir_all(&rnote_path).expect("Failed to create directory") + }; let time = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .unwrap() + .expect("Failed to get unix time") .as_secs(); - if !path.exists() { - std::fs::create_dir(&path).expect("Failed to create tmp dir"); + if !rnote_path.exists() { + std::fs::create_dir(&rnote_path).expect("Failed to create tmp dir"); } - let name = format!("draft-{time}.rnote"); - path.push(name); + let name = format!("{time}.rnote"); + rnote_path.push(name); + let mut metadata_path = rnote_path.clone(); self.imp() .recovery_file - .replace(Some(gio::File::for_path(path))); + .replace(Some(gio::File::for_path(&rnote_path))); + + metadata_path.set_extension("json"); + let metadata = RecoveryMetadata::new(metadata_path, rnote_path); + imp.recovery_file_metadata.replace(Some(metadata)); } self.imp().recovery_file.borrow().as_ref().unwrap().clone() } @@ -653,8 +656,8 @@ impl RnCanvas { } #[allow(unused)] - pub(crate) fn set_recovery_in_progress(&self, save_in_progress: bool) { - self.imp().recovery_in_progress.set(save_in_progress); + pub(crate) fn set_recovery_in_progress(&self, recovery_in_progress: bool) { + self.imp().recovery_in_progress.set(recovery_in_progress); } #[allow(unused)] @@ -828,32 +831,26 @@ impl RnCanvas { /// /// When there is no output-file, falls back to the "New document" string pub(crate) fn doc_title_display(&self) -> String { - match self.recovery_in_progress() { - true => self.output_file_cache(), - false => self.output_file(), - } - .map(|f| { - f.basename() - .and_then(|t| Some(t.file_stem()?.to_string_lossy().to_string())) - .unwrap_or_else(|| gettext("- invalid file name -")) - }) - .unwrap_or_else(|| OUTPUT_FILE_NEW_TITLE.to_string()) + self.output_file() + .map(|f| { + f.basename() + .and_then(|t| Some(t.file_stem()?.to_string_lossy().to_string())) + .unwrap_or_else(|| gettext("- invalid file name -")) + }) + .unwrap_or_else(|| OUTPUT_FILE_NEW_TITLE.to_string()) } /// The document folder path for display. To get the actual path, use output-file /// /// When there is no output-file, falls back to the "Draft" string pub(crate) fn doc_folderpath_display(&self) -> String { - match self.recovery_in_progress() { - true => self.output_file_cache(), - false => self.output_file(), - } - .map(|f| { - f.parent() - .and_then(|p| Some(p.path()?.display().to_string())) - .unwrap_or_else(|| gettext("- invalid folder path -")) - }) - .unwrap_or_else(|| OUTPUT_FILE_NEW_SUBTITLE.to_string()) + self.output_file() + .map(|f| { + f.parent() + .and_then(|p| Some(p.path()?.display().to_string())) + .unwrap_or_else(|| gettext("- invalid folder path -")) + }) + .unwrap_or_else(|| OUTPUT_FILE_NEW_SUBTITLE.to_string()) } pub(crate) fn create_output_file_monitor(&self, file: &gio::File, appwindow: &RnAppWindow) { From 57c3a6752c9d1d7b46f7258bc64df0d5f06e428c Mon Sep 17 00:00:00 2001 From: LeSnake04 Date: Mon, 5 Jun 2023 12:22:13 +0200 Subject: [PATCH 10/20] import RecoveryMetadata --- rnote-ui/src/canvas/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rnote-ui/src/canvas/mod.rs b/rnote-ui/src/canvas/mod.rs index bff4596d17..5a918fbc10 100644 --- a/rnote-ui/src/canvas/mod.rs +++ b/rnote-ui/src/canvas/mod.rs @@ -5,6 +5,7 @@ mod input; // Re-exports pub(crate) use canvaslayout::RnCanvasLayout; +use rnote_fileformats::recovery_metadata::RecoveryMetadata; // Imports use crate::{config, RnAppWindow}; @@ -47,6 +48,7 @@ pub(crate) struct Handlers { } mod imp { + use rnote_fileformats::recovery_metadata::RecoveryMetadata; use super::*; From 0dfe244cf9afc96863bf6d319ca44976ad121379 Mon Sep 17 00:00:00 2001 From: LeSnake04 Date: Mon, 5 Jun 2023 12:26:53 +0200 Subject: [PATCH 11/20] skip serializing and deserializing --- rnote-fileformats/src/recovery_metadata.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rnote-fileformats/src/recovery_metadata.rs b/rnote-fileformats/src/recovery_metadata.rs index 1772ee2267..e5575206f6 100644 --- a/rnote-fileformats/src/recovery_metadata.rs +++ b/rnote-fileformats/src/recovery_metadata.rs @@ -7,7 +7,7 @@ use std::{cell::Cell, path::PathBuf}; pub struct RecoveryMetadata { last_changed: Cell, rnote_path: PathBuf, - #[serde(skip_serializing)] + #[serde(skip)] metdata_path: PathBuf, } From 2a9b7ea2c5b7e4d6d75afae0dc5145421647295b Mon Sep 17 00:00:00 2001 From: LeSnake04 Date: Wed, 7 Jun 2023 08:45:52 +0200 Subject: [PATCH 12/20] update last changed metadata when saving --- .gitignore | 1 - rnote-ui/src/appwindow/imp.rs | 2 -- rnote-ui/src/canvas/imexport.rs | 1 + rnote-ui/src/canvas/mod.rs | 5 +++++ 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 2bb82d0b3f..5a63817819 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,3 @@ .toggletasks.json *.flatpak justfile -rust-toolchain \ No newline at end of file diff --git a/rnote-ui/src/appwindow/imp.rs b/rnote-ui/src/appwindow/imp.rs index 08e615c410..7ffeeb8345 100644 --- a/rnote-ui/src/appwindow/imp.rs +++ b/rnote-ui/src/appwindow/imp.rs @@ -335,12 +335,10 @@ impl RnAppWindow { let tmp_file = canvas.get_or_generate_tmp_file(); appwindow.overlays().start_pulsing_progressbar(); canvas.set_recovery_in_progress(true); - // canvas.imp().output_file_cache.replace(canvas.imp().output_file.take()); if let Err(e) = canvas.save_document_to_file(&tmp_file).await { log::error!("saving document failed, Error: `{e:?}`"); appwindow.overlays().dispatch_toast_error(&gettext("Saving document failed")); } - // canvas.imp().output_file.replace(canvas.imp().output_file_cache.take()); canvas.set_recovery_in_progress(false); appwindow.overlays().finish_progressbar(); } diff --git a/rnote-ui/src/canvas/imexport.rs b/rnote-ui/src/canvas/imexport.rs index 2e9001e8be..d20ad48441 100644 --- a/rnote-ui/src/canvas/imexport.rs +++ b/rnote-ui/src/canvas/imexport.rs @@ -311,6 +311,7 @@ impl RnCanvas { self.set_output_file_expect_write(false); return Err(e); } + self.update_recovery_file_metadata_last_changed(); if self.recovery_in_progress() { self.set_unsaved_changes_recovery(false); diff --git a/rnote-ui/src/canvas/mod.rs b/rnote-ui/src/canvas/mod.rs index 5a918fbc10..5aa6a7c8bb 100644 --- a/rnote-ui/src/canvas/mod.rs +++ b/rnote-ui/src/canvas/mod.rs @@ -1303,4 +1303,9 @@ impl RnCanvas { self.engine().borrow_mut().background_regenerate_pattern(); self.queue_draw(); } + pub(crate) fn update_recovery_file_metadata_last_changed(&self) { + if let Some(m) = self.imp().recovery_file_metadata.borrow().as_ref() { + m.update_last_changed() + } + } } From 6c6bfeed31918b1287c613ef2a1b6e8c86946bc0 Mon Sep 17 00:00:00 2001 From: LeSnake04 Date: Wed, 7 Jun 2023 12:38:37 +0200 Subject: [PATCH 13/20] switch to directories --- Cargo.lock | 10 ---------- rnote-ui/Cargo.toml | 1 - rnote-ui/src/canvas/mod.rs | 6 ++++-- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f7bc0d55ca..8559938e4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1010,15 +1010,6 @@ dependencies = [ "dirs-sys", ] -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys", -] - [[package]] name = "dirs-sys" version = "0.4.1" @@ -3457,7 +3448,6 @@ dependencies = [ "anyhow", "cairo-rs", "directories", - "dirs", "fs_extra", "futures", "gettext-rs", diff --git a/rnote-ui/Cargo.toml b/rnote-ui/Cargo.toml index 470ebb6c45..792eff1745 100644 --- a/rnote-ui/Cargo.toml +++ b/rnote-ui/Cargo.toml @@ -50,4 +50,3 @@ fs_extra = "1" same-file = "1" regex = "1.7" directories = "5" -dirs = "5.0.1" diff --git a/rnote-ui/src/canvas/mod.rs b/rnote-ui/src/canvas/mod.rs index 5aa6a7c8bb..0baf67407c 100644 --- a/rnote-ui/src/canvas/mod.rs +++ b/rnote-ui/src/canvas/mod.rs @@ -599,8 +599,10 @@ impl RnCanvas { pub(crate) fn get_or_generate_tmp_file(&self) -> gio::File { if self.imp().recovery_file.borrow().is_none() { let imp = self.imp(); - let mut rnote_path = dirs::data_dir().expect("Failed to get data dir"); - rnote_path.push("rnote"); + let mut rnote_path = directories::ProjectDirs::from("com.gthub", "flxt", "rnote") + .expect("Failed to get ProjectDirs") + .data_dir() + .to_path_buf(); rnote_path.push("recovery"); if !rnote_path.exists() { std::fs::create_dir_all(&rnote_path).expect("Failed to create directory") From 08237b1210b268e98ae677a0e7289048a79cbd9a Mon Sep 17 00:00:00 2001 From: LeSnake04 Date: Wed, 7 Jun 2023 12:39:17 +0200 Subject: [PATCH 14/20] move recovery indicator --- rnote-ui/data/ui/mainheader.ui | 6 ------ rnote-ui/data/ui/settingspanel.ui | 4 ++++ rnote-ui/src/appwindow/mod.rs | 5 +++-- rnote-ui/src/mainheader.rs | 6 ------ rnote-ui/src/settingspanel/mod.rs | 10 ++++++++-- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/rnote-ui/data/ui/mainheader.ui b/rnote-ui/data/ui/mainheader.ui index 98eb50ea95..c38d9569ac 100644 --- a/rnote-ui/data/ui/mainheader.ui +++ b/rnote-ui/data/ui/mainheader.ui @@ -22,12 +22,6 @@ - - - - false - - main_titlebox diff --git a/rnote-ui/data/ui/settingspanel.ui b/rnote-ui/data/ui/settingspanel.ui index aa1547d349..fc1d8a61b7 100644 --- a/rnote-ui/data/ui/settingspanel.ui +++ b/rnote-ui/data/ui/settingspanel.ui @@ -76,6 +76,10 @@ Recovery Enable or disable document recovery + + + false + center diff --git a/rnote-ui/src/appwindow/mod.rs b/rnote-ui/src/appwindow/mod.rs index 2eb00d8c6e..40fc6f4982 100644 --- a/rnote-ui/src/appwindow/mod.rs +++ b/rnote-ui/src/appwindow/mod.rs @@ -429,8 +429,9 @@ impl RnAppWindow { self.mainheader() .main_title_unsaved_indicator() .set_visible(canvas.unsaved_changes()); - self.mainheader() - .main_title_unsaved_recovery_indicator() + // This is not in the title bar, but I will keep the logic here to keep it close to the other unsaved indicator + self.settings_panel() + .general_recovery_unsaved_indicator() .set_visible(canvas.unsaved_changes_recovery()); if canvas.unsaved_changes() { self.mainheader() diff --git a/rnote-ui/src/mainheader.rs b/rnote-ui/src/mainheader.rs index 1d7162503a..edff00de14 100644 --- a/rnote-ui/src/mainheader.rs +++ b/rnote-ui/src/mainheader.rs @@ -17,8 +17,6 @@ mod imp { #[template_child] pub(crate) main_title_unsaved_indicator: TemplateChild + + + + 800 + 600 + true + false + fill + fill + + + vertical + 24 + 12 + 12 + 12 + 12 + + + + + + + + + + \ No newline at end of file diff --git a/rnote-ui/src/appwindow/imp.rs b/rnote-ui/src/appwindow/imp.rs index 9b21fed245..f86491d823 100644 --- a/rnote-ui/src/appwindow/imp.rs +++ b/rnote-ui/src/appwindow/imp.rs @@ -339,6 +339,7 @@ impl RnAppWindow { let canvas = appwindow.active_tab().canvas(); glib::MainContext::default().spawn_local(clone!(@weak canvas, @weak appwindow => async move { + if !canvas.unsaved_changes_recovery() {return;} let tmp_file = canvas.get_or_generate_tmp_file(); appwindow.overlays().start_pulsing_progressbar(); canvas.set_recovery_in_progress(true); diff --git a/rnote-ui/src/appwindow/mod.rs b/rnote-ui/src/appwindow/mod.rs index 4116c3f0e1..f9d8c6b57b 100644 --- a/rnote-ui/src/appwindow/mod.rs +++ b/rnote-ui/src/appwindow/mod.rs @@ -54,6 +54,15 @@ impl RnAppWindow { pub(crate) fn set_recovery(&self, recovery: bool) { self.set_property("recovery", recovery.to_value()); } + #[allow(unused)] + pub(crate) fn recovery_interval_secs(&self) -> u32 { + self.property::("recovery-interval-secs") + } + + #[allow(unused)] + pub(crate) fn set_recovery_interval_secs(&self, autosave_interval_secs: u32) { + self.set_property("recovery-interval-secs", autosave_interval_secs.to_value()); + } #[allow(unused)] pub(crate) fn autosave_interval_secs(&self) -> u32 { @@ -187,6 +196,9 @@ impl RnAppWindow { self.overlays().undo_button().set_sensitive(false); self.overlays().redo_button().set_sensitive(false); self.refresh_ui_from_engine(&self.active_tab()); + glib::MainContext::default().spawn_local(clone!(@weak self as appwindow => async move { + dialogs::dialog_recover_documents(&appwindow).await; + })); } /// Called to close the window diff --git a/rnote-ui/src/dialogs/mod.rs b/rnote-ui/src/dialogs/mod.rs index 139e49915d..da01368834 100644 --- a/rnote-ui/src/dialogs/mod.rs +++ b/rnote-ui/src/dialogs/mod.rs @@ -4,6 +4,7 @@ // Modules pub(crate) mod export; pub(crate) mod import; +mod recovery; // Imports use crate::appwindow::RnAppWindow; @@ -19,6 +20,9 @@ use gtk4::{ Label, MenuButton, ResponseType, ShortcutsWindow, StringList, }; +// Re-exports +pub(crate) use recovery::dialog_recover_documents; + // About Dialog pub(crate) fn dialog_about(appwindow: &RnAppWindow) { let app_icon_name = if config::PROFILE == "devel" { @@ -502,10 +506,6 @@ pub(crate) async fn dialog_edit_selected_workspace(appwindow: &RnAppWindow) { } } -pub(crate) async fn dialog_recover_documents() { - todo!() -} - const WORKSPACELISTENTRY_ICONS_LIST: &[&str] = &[ "workspacelistentryicon-bandaid-symbolic", "workspacelistentryicon-bank-symbolic",