diff options
author | Ricky Kresslein <rk@lakoliu.com> | 2022-05-20 12:26:53 +0300 |
---|---|---|
committer | Ricky Kresslein <rk@lakoliu.com> | 2022-05-20 12:26:53 +0300 |
commit | 3942e6cab7a0883220d78068dc47ddf81760a90e (patch) | |
tree | d8ea83980739befb888ca827d6a9952fc4e9d478 /src | |
parent | 1dd125d02f5d66643e39fe2da636d036ed733904 (diff) | |
download | Furtherance-3942e6cab7a0883220d78068dc47ddf81760a90e.tar.zst |
Autosave (Issue #45)
Diffstat (limited to 'src')
-rwxr-xr-x | src/database.rs | 15 | ||||
-rwxr-xr-x | src/gtk/preferences_window.ui | 28 | ||||
-rwxr-xr-x | src/ui/preferences_window.rs | 17 | ||||
-rwxr-xr-x | src/ui/window.rs | 468 |
4 files changed, 350 insertions, 178 deletions
diff --git a/src/database.rs b/src/database.rs index 5412896..69b56dc 100755 --- a/src/database.rs +++ b/src/database.rs @@ -81,6 +81,21 @@ pub fn db_write(task_name: &str, Ok(()) } +pub fn write_autosave(task_name: &str, + start_time: &str, + stop_time: &str, + tags: &str) -> Result<()> { + // Write data into database + let conn = Connection::open(get_directory())?; + + conn.execute( + "INSERT INTO tasks (task_name, start_time, stop_time, tags) values (?1, ?2, ?3, ?4)", + &[&task_name, &start_time, &stop_time, &tags], + )?; + + Ok(()) +} + pub fn retrieve() -> Result<Vec<Task>, rusqlite::Error> { // Retrieve all tasks from the database let conn = Connection::open(get_directory())?; diff --git a/src/gtk/preferences_window.ui b/src/gtk/preferences_window.ui index cda3534..be331b7 100755 --- a/src/gtk/preferences_window.ui +++ b/src/gtk/preferences_window.ui @@ -176,6 +176,34 @@ </child> </object> </child> + <child> + <object class="AdwExpanderRow" id="autosave_expander"> + <property name="title" translatable="yes">_Autosave</property> + <property name="subtitle" translatable="yes">Prevent losing tracked time due to improper shutdown</property> + <property name="show_enable_switch">True</property> + <property name="use_underline">True</property> + <child> + <object class="AdwActionRow"> + <property name="title" translatable="yes">Autosave every X _minutes</property> + <property name="use_underline">True</property> + <child> + <object class="GtkSpinButton" id="autosave_spin"> + <property name="valign">center</property> + <property name="adjustment"> + <object class="GtkAdjustment"> + <property name="upper">60</property> + <property name="lower">1</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + </property> + <property name="numeric">True</property> + </object> + </child> + </object> + </child> + </object> + </child> </object> </child> </object> diff --git a/src/ui/preferences_window.rs b/src/ui/preferences_window.rs index 6436026..d034a9f 100755 --- a/src/ui/preferences_window.rs +++ b/src/ui/preferences_window.rs @@ -64,6 +64,11 @@ mod imp { pub pomodoro_expander: TemplateChild<adw::ExpanderRow>, #[template_child] pub pomodoro_spin: TemplateChild<gtk::SpinButton>, + + #[template_child] + pub autosave_expander: TemplateChild<adw::ExpanderRow>, + #[template_child] + pub autosave_spin: TemplateChild<gtk::SpinButton>, } #[glib::object_subclass] @@ -194,6 +199,18 @@ impl FurPreferencesWindow { "value" ); + settings_manager::bind_property( + "autosave", + &*imp.autosave_expander, + "enable-expansion" + ); + + settings_manager::bind_property( + "autosave-time", + &*imp.autosave_spin, + "value" + ); + imp.dark_theme_switch.connect_active_notify(move |_|{ let app = FurtheranceApplication::default(); app.update_light_dark(); diff --git a/src/ui/window.rs b/src/ui/window.rs index fa722b7..9d32e14 100755 --- a/src/ui/window.rs +++ b/src/ui/window.rs @@ -27,6 +27,10 @@ use std::cell::RefCell; use chrono::{DateTime, Local, NaiveDateTime, ParseError, Duration as ChronDur, offset::TimeZone}; use dbus::blocking::Connection; use itertools::Itertools; +use std::fs::{File, create_dir_all, remove_file}; +use std::io::{self, BufWriter, Write, BufReader, BufRead}; +use directories::ProjectDirs; +use std::path::PathBuf; use crate::ui::FurHistoryBox; use crate::FurtheranceApplication; @@ -134,22 +138,8 @@ impl FurtheranceWindow { *imp.subtract_idle.lock().unwrap() = false; } - let task_input_text = imp.task_input.text(); - let mut split_tags: Vec<&str> = task_input_text.trim().split("#").collect(); - // Remove task name from tags list - let task_name = *split_tags.first().unwrap(); - split_tags.remove(0); - // Trim whitespace around each tag - split_tags = split_tags.iter().map(|x| x.trim()).collect(); - // Don't allow empty tags - split_tags.retain(|&x| !x.trim().is_empty()); - // Handle duplicate tags before they are ever saved - split_tags = split_tags.into_iter().unique().collect(); - // Lowercase tags - let lower_tags: Vec<String> = split_tags.iter().map(|x| x.to_lowercase()).collect(); - let tag_list = lower_tags.join(" #"); - - let _ = database::db_write(task_name.trim(), start_time, stop_time, tag_list); + let (task_name, tag_list) = self.split_tags_and_task(); + let _ = database::db_write(&task_name, start_time, stop_time, tag_list); imp.task_input.set_text(""); imp.history_box.create_tasks_page(); self.reset_idle(); @@ -184,6 +174,10 @@ impl FurtheranceWindow { imp.start_button.add_css_class("suggested-action"); self.refresh_timer(); imp.task_input.grab_focus(); + + if settings_manager::get_bool("autosave") { + self.check_for_autosave(); + } } fn setup_signals(&self) { @@ -207,8 +201,9 @@ impl FurtheranceWindow { let imp2 = imp::FurtheranceWindow::from_instance(&this); if !*imp2.running.lock().unwrap() { if settings_manager::get_bool("pomodoro") && !*imp2.pomodoro_continue.lock().unwrap() { + let pomodoro_time = settings_manager::get_int("pomodoro-time"); let mut secs: i32 = 0; - let mut mins: i32 = settings_manager::get_int("pomodoro-time"); + let mut mins: i32 = pomodoro_time; let mut hrs: i32 = mins / 60; mins = mins % 60; @@ -232,6 +227,13 @@ impl FurtheranceWindow { let watch_text: &str = &format!("{:02}:{:02}:{:02}", hrs, mins, secs).to_string(); this_clone.set_watch_time(watch_text); } + if settings_manager::get_bool("autosave") { + let autosave_mins = settings_manager::get_int("autosave-time"); + let total_elapsed = (pomodoro_time * 60) - (hrs * 3600) - (mins * 60) - secs; + if total_elapsed % (autosave_mins * 60) == 0 { + this_clone.write_autosave(timer_start); + } + } if hrs == 0 && mins == 0 && secs == 0 { let timer_stop = Local::now(); *imp3.running.lock().unwrap() = false; @@ -262,6 +264,7 @@ impl FurtheranceWindow { *imp2.running.lock().unwrap() = true; imp2.task_input.set_sensitive(false); + let autosave_start = *start_time.borrow(); let duration = Duration::new(1,0); timeout_add_local(duration, clone!(@strong this as this_clone => move || { let imp3 = imp::FurtheranceWindow::from_instance(&this_clone); @@ -277,6 +280,14 @@ impl FurtheranceWindow { } let watch_text: &str = &format!("{:02}:{:02}:{:02}", hrs, mins, secs).to_string(); this_clone.set_watch_time(watch_text); + + if settings_manager::get_bool("autosave") { + let autosave_mins = settings_manager::get_int("autosave-time") as u32; + let total_elapsed = (hrs * 3600) + (mins * 60) + secs; + if total_elapsed % (autosave_mins * 60) == 0 { + this_clone.write_autosave(autosave_start); + } + } } Continue(*imp3.running.lock().unwrap()) })); @@ -289,184 +300,184 @@ impl FurtheranceWindow { this.refresh_timer(); imp2.task_input.set_sensitive(true); this.save_task(*start_time.borrow(), *stop_time.borrow()); + FurtheranceWindow::delete_autosave(); } })); imp.add_task.connect_clicked(clone!(@weak self as this => move |_| { let dialog = gtk::MessageDialog::new( - Some(&this), - gtk::DialogFlags::MODAL, - gtk::MessageType::Question, - gtk::ButtonsType::None, - &format!("<span size='x-large' weight='bold'>{}</span>", &gettext("New Task")), - ); - dialog.set_use_markup(true); - dialog.add_buttons(&[ - (&gettext("Cancel"), gtk::ResponseType::Cancel), - (&gettext("Add"), gtk::ResponseType::Ok) - ]); - - let message_area = dialog.message_area().downcast::<gtk::Box>().unwrap(); - let vert_box = gtk::Box::new(gtk::Orientation::Vertical, 5); - let task_name_edit = gtk::Entry::new(); - task_name_edit.set_placeholder_text(Some(&gettext("Task Name"))); - let task_tags_edit = gtk::Entry::new(); - let tags_placeholder = format!("#{}", &gettext("Tags")); - task_tags_edit.set_placeholder_text(Some(&tags_placeholder)); - - let labels_box = gtk::Box::new(gtk::Orientation::Horizontal, 5); - labels_box.set_homogeneous(true); - let start_label = gtk::Label::new(Some(&gettext("Start"))); - start_label.add_css_class("title-4"); - let stop_label = gtk::Label::new(Some(&gettext("Stop"))); - stop_label.add_css_class("title-4"); - let times_box = gtk::Box::new(gtk::Orientation::Horizontal, 5); - times_box.set_homogeneous(true); - - let stop_time = Local::now(); - let start_time = stop_time - ChronDur::seconds(1); - - let mut start_time_w_year = start_time.format("%h %d %Y %H:%M:%S").to_string(); - if !settings_manager::get_bool("show-seconds") { - start_time_w_year = start_time.format("%h %d %Y %H:%M").to_string(); - } - let mut stop_time_w_year = stop_time.format("%h %d %Y %H:%M:%S").to_string(); - if !settings_manager::get_bool("show-seconds") { - stop_time_w_year = stop_time.format("%h %d %Y %H:%M").to_string(); - } - let start_time_edit = gtk::Entry::new(); - start_time_edit.set_text(&start_time_w_year); - let stop_time_edit = gtk::Entry::new(); - stop_time_edit.set_text(&stop_time_w_year); - - let instructions = gtk::Label::new(Some( - &gettext("*Use the format MMM DD YYYY HH:MM:SS"))); - if !settings_manager::get_bool("show-seconds") { - instructions.set_text(&gettext("*Use the format MMM DD YYYY HH:MM")); - } - instructions.set_visible(false); - instructions.add_css_class("error_message"); - - let time_error = gtk::Label::new(Some( - &gettext("*Start time cannot be later than stop time."))); - time_error.set_visible(false); - time_error.add_css_class("error_message"); - - let future_error = gtk::Label::new(Some( - &gettext("*Time cannot be in the future."))); - future_error.set_visible(false); - future_error.add_css_class("error_message"); - - let name_error = gtk::Label::new(Some( - &gettext("*Task name cannot be blank."))); - name_error.set_visible(false); - name_error.add_css_class("error_message"); - - vert_box.append(&task_name_edit); - vert_box.append(&task_tags_edit); - labels_box.append(&start_label); - labels_box.append(&stop_label); - times_box.append(&start_time_edit); - times_box.append(&stop_time_edit); - vert_box.append(&labels_box); - vert_box.append(×_box); - vert_box.append(&instructions); - vert_box.append(&time_error); - vert_box.append(&future_error); - vert_box.append(&name_error); - message_area.append(&vert_box); - - dialog.connect_response(clone!(@strong dialog => move |_ , resp| { - if resp == gtk::ResponseType::Ok { - instructions.set_visible(false); - time_error.set_visible(false); - future_error.set_visible(false); - name_error.set_visible(false); - let mut do_not_close = false; - let mut new_start_time_local = Local::now(); - let mut new_stop_time_local = Local::now(); - - // Task Name - if task_name_edit.text().trim().is_empty() { - name_error.set_visible(true); - do_not_close = true; - } + Some(&this), + gtk::DialogFlags::MODAL, + gtk::MessageType::Question, + gtk::ButtonsType::None, + &format!("<span size='x-large' weight='bold'>{}</span>", &gettext("New Task")), + ); + dialog.set_use_markup(true); + dialog.add_buttons(&[ + (&gettext("Cancel"), gtk::ResponseType::Cancel), + (&gettext("Add"), gtk::ResponseType::Ok) + ]); + + let message_area = dialog.message_area().downcast::<gtk::Box>().unwrap(); + let vert_box = gtk::Box::new(gtk::Orientation::Vertical, 5); + let task_name_edit = gtk::Entry::new(); + task_name_edit.set_placeholder_text(Some(&gettext("Task Name"))); + let task_tags_edit = gtk::Entry::new(); + let tags_placeholder = format!("#{}", &gettext("Tags")); + task_tags_edit.set_placeholder_text(Some(&tags_placeholder)); + + let labels_box = gtk::Box::new(gtk::Orientation::Horizontal, 5); + labels_box.set_homogeneous(true); + let start_label = gtk::Label::new(Some(&gettext("Start"))); + start_label.add_css_class("title-4"); + let stop_label = gtk::Label::new(Some(&gettext("Stop"))); + stop_label.add_css_class("title-4"); + let times_box = gtk::Box::new(gtk::Orientation::Horizontal, 5); + times_box.set_homogeneous(true); + + let stop_time = Local::now(); + let start_time = stop_time - ChronDur::seconds(1); + + let mut start_time_w_year = start_time.format("%h %d %Y %H:%M:%S").to_string(); + if !settings_manager::get_bool("show-seconds") { + start_time_w_year = start_time.format("%h %d %Y %H:%M").to_string(); + } + let mut stop_time_w_year = stop_time.format("%h %d %Y %H:%M:%S").to_string(); + if !settings_manager::get_bool("show-seconds") { + stop_time_w_year = stop_time.format("%h %d %Y %H:%M").to_string(); + } + let start_time_edit = gtk::Entry::new(); + start_time_edit.set_text(&start_time_w_year); + let stop_time_edit = gtk::Entry::new(); + stop_time_edit.set_text(&stop_time_w_year); + + let instructions = gtk::Label::new(Some( + &gettext("*Use the format MMM DD YYYY HH:MM:SS"))); + if !settings_manager::get_bool("show-seconds") { + instructions.set_text(&gettext("*Use the format MMM DD YYYY HH:MM")); + } + instructions.set_visible(false); + instructions.add_css_class("error_message"); + + let time_error = gtk::Label::new(Some( + &gettext("*Start time cannot be later than stop time."))); + time_error.set_visible(false); + time_error.add_css_class("error_message"); + + let future_error = gtk::Label::new(Some( + &gettext("*Time cannot be in the future."))); + future_error.set_visible(false); + future_error.add_css_class("error_message"); + + let name_error = gtk::Label::new(Some( + &gettext("*Task name cannot be blank."))); + name_error.set_visible(false); + name_error.add_css_class("error_message"); + + vert_box.append(&task_name_edit); + vert_box.append(&task_tags_edit); + labels_box.append(&start_label); + labels_box.append(&stop_label); + times_box.append(&start_time_edit); + times_box.append(&stop_time_edit); + vert_box.append(&labels_box); + vert_box.append(×_box); + vert_box.append(&instructions); + vert_box.append(&time_error); + vert_box.append(&future_error); + vert_box.append(&name_error); + message_area.append(&vert_box); + + dialog.connect_response(clone!(@strong dialog => move |_ , resp| { + if resp == gtk::ResponseType::Ok { + instructions.set_visible(false); + time_error.set_visible(false); + future_error.set_visible(false); + name_error.set_visible(false); + let mut do_not_close = false; + let mut new_start_time_local = Local::now(); + let mut new_stop_time_local = Local::now(); + + // Task Name + if task_name_edit.text().trim().is_empty() { + name_error.set_visible(true); + do_not_close = true; + } - // Start Time - let new_start_time_str = start_time_edit.text(); - let new_start_time: Result<NaiveDateTime, ParseError>; - if settings_manager::get_bool("show-seconds") { - new_start_time = NaiveDateTime::parse_from_str( + // Start Time + let new_start_time_str = start_time_edit.text(); + let new_start_time: Result<NaiveDateTime, ParseError>; + if settings_manager::get_bool("show-seconds") { + new_start_time = NaiveDateTime::parse_from_str( + &new_start_time_str, + "%h %d %Y %H:%M:%S"); + } else { + new_start_time = NaiveDateTime::parse_from_str( &new_start_time_str, - "%h %d %Y %H:%M:%S"); - } else { - new_start_time = NaiveDateTime::parse_from_str( - &new_start_time_str, - "%h %d %Y %H:%M"); - } - if let Err(_) = new_start_time { - instructions.set_visible(true); + "%h %d %Y %H:%M"); + } + if let Err(_) = new_start_time { + instructions.set_visible(true); + do_not_close = true; + } else { + new_start_time_local = Local.from_local_datetime(&new_start_time.unwrap()).unwrap(); + if (Local::now() - new_start_time_local).num_seconds() < 0 { + future_error.set_visible(true); do_not_close = true; - } else { - new_start_time_local = Local.from_local_datetime(&new_start_time.unwrap()).unwrap(); - if (Local::now() - new_start_time_local).num_seconds() < 0 { - future_error.set_visible(true); - do_not_close = true; - } } + } - // Stop Time - let new_stop_time_str = stop_time_edit.text(); - let new_stop_time: Result<NaiveDateTime, ParseError>; - if settings_manager::get_bool("show-seconds") { - new_stop_time = NaiveDateTime::parse_from_str( + // Stop Time + let new_stop_time_str = stop_time_edit.text(); + let new_stop_time: Result<NaiveDateTime, ParseError>; + if settings_manager::get_bool("show-seconds") { + new_stop_time = NaiveDateTime::parse_from_str( + &new_stop_time_str, + "%h %d %Y %H:%M:%S"); + } else { + new_stop_time = NaiveDateTime::parse_from_str( &new_stop_time_str, - "%h %d %Y %H:%M:%S"); - } else { - new_stop_time = NaiveDateTime::parse_from_str( - &new_stop_time_str, - "%h %d %Y %H:%M"); - } - if let Err(_) = new_stop_time { - instructions.set_visible(true); + "%h %d %Y %H:%M"); + } + if let Err(_) = new_stop_time { + instructions.set_visible(true); + do_not_close = true; + } else { + new_stop_time_local = Local.from_local_datetime(&new_stop_time.unwrap()).unwrap(); + if (Local::now() - new_stop_time_local).num_seconds() < 0 { + future_error.set_visible(true); do_not_close = true; - } else { - new_stop_time_local = Local.from_local_datetime(&new_stop_time.unwrap()).unwrap(); - if (Local::now() - new_stop_time_local).num_seconds() < 0 { - future_error.set_visible(true); - do_not_close = true; - } - } - - // Tags - let mut new_tag_list = "".to_string(); - if !task_tags_edit.text().trim().is_empty() { - let new_tags = task_tags_edit.text(); - let mut split_tags: Vec<&str> = new_tags.trim().split("#").collect(); - split_tags = split_tags.iter().map(|x| x.trim()).collect(); - // Don't allow empty tags - split_tags.retain(|&x| !x.trim().is_empty()); - // Handle duplicate tags before they are saved - split_tags = split_tags.into_iter().unique().collect(); - // Lowercase tags - let lower_tags: Vec<String> = split_tags.iter().map(|x| x.to_lowercase()).collect(); - new_tag_list = lower_tags.join(" #"); } + } - if !do_not_close { - let _ = database::db_write(task_name_edit.text().trim(), - new_start_time_local, - new_stop_time_local, - new_tag_list); - this.reset_history_box(); - dialog.close(); - } + // Tags + let mut new_tag_list = "".to_string(); + if !task_tags_edit.text().trim().is_empty() { + let new_tags = task_tags_edit.text(); + let mut split_tags: Vec<&str> = new_tags.trim().split("#").collect(); + split_tags = split_tags.iter().map(|x| x.trim()).collect(); + // Don't allow empty tags + split_tags.retain(|&x| !x.trim().is_empty()); + // Handle duplicate tags before they are saved + split_tags = split_tags.into_iter().unique().collect(); + // Lowercase tags + let lower_tags: Vec<String> = split_tags.iter().map(|x| x.to_lowercase()).collect(); + new_tag_list = lower_tags.join(" #"); + } - } else if resp == gtk::ResponseType::Cancel { + if !do_not_close { + let _ = database::db_write(task_name_edit.text().trim(), + new_start_time_local, + new_stop_time_local, + new_tag_list); + this.reset_history_box(); dialog.close(); } - }), - ); + + } else if resp == gtk::ResponseType::Cancel { + dialog.close(); + } + })); dialog.show(); })); @@ -611,6 +622,107 @@ impl FurtheranceWindow { dialog.show(); } + fn write_autosave(&self, auto_start_time: DateTime<Local>) { + let auto_stop_time = Local::now().to_rfc3339(); + let auto_start_time = auto_start_time.to_rfc3339(); + let (task_name, tag_list) = self.split_tags_and_task(); + + let path = FurtheranceWindow::get_autosave_path(); + let file = File::create(path).expect("Couldn't create autosave file"); + let mut file = BufWriter::new(file); + + writeln!(file, "{}", task_name).expect("Unable to write autosave"); + writeln!(file, "{}", auto_start_time).expect("Unable to write autosave"); + writeln!(file, "{}", auto_stop_time).expect("Unable to write autosave"); + write!(file, "{}", tag_list).expect("Unable to write autosave"); + } + + fn delete_autosave() { + let path = FurtheranceWindow::get_autosave_path(); + if path.exists() { + remove_file(path).expect("Could not delete autosave"); + } + } + + fn get_autosave_path() -> PathBuf { + let mut path = PathBuf::new(); + if let Some(proj_dirs) = ProjectDirs::from("com", "lakoliu", "Furtherance") { + path = PathBuf::from(proj_dirs.data_dir()); + create_dir_all(path.clone()).expect("Unable to create autosave directory"); + path.extend(&["furtherance_autosave.txt"]); + } + path + } + + fn split_tags_and_task(&self) -> (String, String) { + let imp = imp::FurtheranceWindow::from_instance(self); + let task_input_text = imp.task_input.text(); + let mut split_tags: Vec<&str> = task_input_text.trim().split("#").collect(); + // Remove task name from tags list + let task_name = *split_tags.first().unwrap(); + split_tags.remove(0); + // Trim whitespace around each tag + split_tags = split_tags.iter().map(|x| x.trim()).collect(); + // Don't allow empty tags + split_tags.retain(|&x| !x.trim().is_empty()); + // Handle duplicate tags before they are ever saved + split_tags = split_tags.into_iter().unique().collect(); + // Lowercase tags + let lower_tags: Vec<String> = split_tags.iter().map(|x| x.to_lowercase()).collect(); + let tag_list = lower_tags.join(" #"); + (task_name.trim().to_string(), tag_list) + } + + fn check_for_autosave(&self) { + let path = FurtheranceWindow::get_autosave_path(); + if path.exists() { + let autosave = FurtheranceWindow::read_autosave().unwrap(); + + database::write_autosave(&autosave[0], &autosave[1], &autosave[2], &autosave[3]) + .expect("Could not write autosave"); + + let dialog = gtk::MessageDialog::new( + Some(self), + gtk::DialogFlags::MODAL, + gtk::MessageType::Info, + gtk::ButtonsType::Ok, + &gettext("Autosave Restored"), + ); + dialog.set_secondary_text(Some( + &gettext("Furtherance shut down improperly. An autosave was restored.") + )); + + dialog.connect_response(clone!( + @weak self as this, + @strong dialog => move |_, resp| { + if resp == gtk::ResponseType::Ok { + this.reset_history_box(); + dialog.close(); + } + })); + + dialog.show(); + FurtheranceWindow::delete_autosave(); + } + } + + fn read_autosave() -> io::Result<Vec<String>> { + let path = FurtheranceWindow::get_autosave_path(); + let file = File::open(path)?; + let reader = BufReader::new(file); + let mut vars = Vec::new(); + + for line in reader.lines() { + vars.push(line?); + } + // Add empty string if there are no tags + if vars.len() == 3 { + vars.push("".to_string()); + } + + Ok(vars) + } + pub fn reset_idle(&self) { let imp = imp::FurtheranceWindow::from_instance(self); *imp.stored_idle.lock().unwrap() = 0; |