From 8ee461ce5c9cdd117b0e4b9f8dfec0a388d37f29 Mon Sep 17 00:00:00 2001 From: Ricky Kresslein Date: Thu, 28 Apr 2022 19:18:15 +0300 Subject: Add Pomodoro timer --- .github/FUNDING.yml | 0 data/com.lakoliu.Furtherance.gschema.xml | 6 ++ src/application.rs | 58 +++++++--- src/gtk/preferences_window.ui | 37 ++++++- src/ui/preferences_window.rs | 29 +++++ src/ui/window.rs | 178 +++++++++++++++++++++++++------ 6 files changed, 258 insertions(+), 50 deletions(-) mode change 100644 => 100755 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml old mode 100644 new mode 100755 diff --git a/data/com.lakoliu.Furtherance.gschema.xml b/data/com.lakoliu.Furtherance.gschema.xml index af1d9b7..4a9c65b 100755 --- a/data/com.lakoliu.Furtherance.gschema.xml +++ b/data/com.lakoliu.Furtherance.gschema.xml @@ -27,6 +27,12 @@ true + + + false + + + 25 diff --git a/src/application.rs b/src/application.rs index 06a033b..cc9998b 100755 --- a/src/application.rs +++ b/src/application.rs @@ -32,7 +32,7 @@ mod imp { #[derive(Debug, Default)] pub struct FurtheranceApplication { - pub idle_dialog: Mutex, + pub pomodoro_dialog: Mutex, } #[glib::object_subclass] @@ -122,10 +122,7 @@ impl FurtheranceApplication { let window = FurtheranceWindow::default(); let imp = window.imp(); if *imp.running.lock().unwrap() && *imp.idle_time_reached.lock().unwrap() { - window.set_subtract_idle(true); - imp.start_button.emit_clicked(); - let imp_app = imp::FurtheranceApplication::from_instance(&app); - imp_app.idle_dialog.lock().unwrap().close(); + window.imp().idle_dialog.lock().unwrap().response(gtk::ResponseType::Reject); } })); self.add_action(&discard_idle_action); @@ -134,12 +131,24 @@ impl FurtheranceApplication { continue_idle_action.connect_activate(clone!(@weak self as app => move |_, _| { let window = FurtheranceWindow::default(); if *window.imp().running.lock().unwrap() { - window.reset_vars(); - let imp = imp::FurtheranceApplication::from_instance(&app); - imp.idle_dialog.lock().unwrap().close(); + window.imp().idle_dialog.lock().unwrap().response(gtk::ResponseType::Accept); } })); self.add_action(&continue_idle_action); + + let continue_pomodoro_action = gio::SimpleAction::new("continue-pomodoro-action", None); + continue_pomodoro_action.connect_activate(clone!(@weak self as app => move |_, _| { + let imp = imp::FurtheranceApplication::from_instance(&app); + imp.pomodoro_dialog.lock().unwrap().response(gtk::ResponseType::Accept); + })); + self.add_action(&continue_pomodoro_action); + + let stop_pomodoro_action = gio::SimpleAction::new("stop-pomodoro-action", None); + stop_pomodoro_action.connect_activate(clone!(@weak self as app => move |_, _| { + let imp = imp::FurtheranceApplication::from_instance(&app); + imp.pomodoro_dialog.lock().unwrap().response(gtk::ResponseType::Reject); + })); + self.add_action(&stop_pomodoro_action); } fn setup_application(&self) { @@ -233,9 +242,7 @@ impl FurtheranceApplication { } } - pub fn system_notification(&self, title: &str, subtitle: &str, dialog: gtk::MessageDialog) { - let imp = imp::FurtheranceApplication::from_instance(self); - *imp.idle_dialog.lock().unwrap() = dialog; + pub fn system_idle_notification(&self, title: &str, subtitle: &str) { let icon = Some("appointment-missed-symbolic"); let notification = gio::Notification::new(title.as_ref()); notification.set_body(Some(subtitle.as_ref())); @@ -250,9 +257,32 @@ impl FurtheranceApplication { notification.add_button(&gettext("Discard"), "app.discard-idle-action"); notification.add_button(&gettext("Continue"), "app.continue-idle-action"); - gio::Application::default() - .unwrap() - .send_notification(None, ¬ification); + notification.set_priority(gio::NotificationPriority::High); + + self.send_notification(Some("idle"), ¬ification); + } + + pub fn system_pomodoro_notification(&self, dialog: gtk::MessageDialog) { + let imp = imp::FurtheranceApplication::from_instance(self); + *imp.pomodoro_dialog.lock().unwrap() = dialog; + let icon = Some("alarm-symbolic"); + let notification = gio::Notification::new("Time's up!"); + notification.set_body(Some("Your Furtherance timer ended.")); + + if let Some(icon) = icon { + match gio::Icon::for_string(icon) { + Ok(gicon) => notification.set_icon(&gicon), + Err(err) => debug!("Unable to display notification: {:?}", err), + } + } + + notification.add_button(&gettext("Continue"), "app.continue-pomodoro-action"); + notification.add_button(&gettext("Stop"), "app.stop-pomodoro-action"); + + notification.set_priority(gio::NotificationPriority::High); + + self.withdraw_notification("idle"); + self.send_notification(Some("pomodoro"), ¬ification); } } diff --git a/src/gtk/preferences_window.ui b/src/gtk/preferences_window.ui index b3b12a8..58296bb 100755 --- a/src/gtk/preferences_window.ui +++ b/src/gtk/preferences_window.ui @@ -128,7 +128,6 @@ - Show tags @@ -142,7 +141,41 @@ - + + + + + Timer + True + + + _Pomodoro + Timer counts down instead of up. + True + True + + + Interval + Timer start time in minutes + True + + + center + + + 999 + 1 + 1 + 15 + + + True + + + + + + diff --git a/src/ui/preferences_window.rs b/src/ui/preferences_window.rs index 47a7adc..6436026 100755 --- a/src/ui/preferences_window.rs +++ b/src/ui/preferences_window.rs @@ -57,6 +57,13 @@ mod imp { pub show_daily_sums_switch: TemplateChild, #[template_child] pub show_tags_switch: TemplateChild, + + #[template_child] + pub timer_group: TemplateChild, + #[template_child] + pub pomodoro_expander: TemplateChild, + #[template_child] + pub pomodoro_spin: TemplateChild, } #[glib::object_subclass] @@ -175,6 +182,18 @@ impl FurPreferencesWindow { "active" ); + settings_manager::bind_property( + "pomodoro", + &*imp.pomodoro_expander, + "enable-expansion" + ); + + settings_manager::bind_property( + "pomodoro-time", + &*imp.pomodoro_spin, + "value" + ); + imp.dark_theme_switch.connect_active_notify(move |_|{ let app = FurtheranceApplication::default(); app.update_light_dark(); @@ -204,6 +223,16 @@ impl FurPreferencesWindow { let window = FurtheranceWindow::default(); window.reset_history_box(); }); + + imp.pomodoro_expander.connect_enable_expansion_notify(move |_|{ + let window = FurtheranceWindow::default(); + window.refresh_timer(); + }); + + imp.pomodoro_spin.connect_value_changed(move |_|{ + let window = FurtheranceWindow::default(); + window.refresh_timer(); + }); } } diff --git a/src/ui/window.rs b/src/ui/window.rs index 2d3f9d4..2c87d78 100755 --- a/src/ui/window.rs +++ b/src/ui/window.rs @@ -60,6 +60,8 @@ mod imp { pub subtract_idle: Mutex, pub idle_start_time: Mutex, pub running: Mutex, + pub pomodoro_continue: Mutex, + pub idle_dialog: Mutex, } #[glib::object_subclass] @@ -120,7 +122,7 @@ impl FurtheranceWindow { } } - fn save_task(&self, start_time: DateTime, mut stop_time: DateTime) { + pub fn save_task(&self, start_time: DateTime, mut stop_time: DateTime) { // Save the most recent task to the database and clear the task_input field let imp = imp::FurtheranceWindow::from_instance(self); @@ -149,6 +151,7 @@ impl FurtheranceWindow { let _ = database::db_write(task_name.trim(), start_time, stop_time, tag_list); imp.task_input.set_text(""); imp.history_box.create_tasks_page(); + self.reset_idle(); } pub fn reset_history_box(&self) { @@ -175,14 +178,15 @@ impl FurtheranceWindow { self.add_css_class("devel"); } + *imp.pomodoro_continue.lock().unwrap() = false; imp.start_button.set_sensitive(false); imp.start_button.add_css_class("suggested-action"); + self.refresh_timer(); imp.task_input.grab_focus(); } fn setup_signals(&self) { let imp = imp::FurtheranceWindow::from_instance(self); - // running = false *imp.running.lock().unwrap() = false; let start_time = Rc::new(RefCell::new(Local::now())); let stop_time = Rc::new(RefCell::new(Local::now())); @@ -201,37 +205,87 @@ impl FurtheranceWindow { imp.start_button.connect_clicked(clone!(@weak self as this => move |button| { let imp2 = imp::FurtheranceWindow::from_instance(&this); if !*imp2.running.lock().unwrap() { - let mut secs: u32 = 0; - let mut mins: u32 = 0; - let mut hrs: u32 = 0; - - *imp2.running.lock().unwrap() = true; - *start_time.borrow_mut() = Local::now(); - imp2.task_input.set_sensitive(false); - 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); - if *imp3.running.lock().unwrap() { - secs += 1; - if secs > 59 { - secs = 0; - mins += 1; - if mins > 59 { - mins = 0; - hrs += 1; + if settings_manager::get_bool("pomodoro") && !*imp2.pomodoro_continue.lock().unwrap() { + let mut secs: i32 = 0; + let mut mins: i32 = settings_manager::get_int("pomodoro-time"); + let mut hrs: i32 = mins / 60; + mins = mins % 60; + + *imp2.running.lock().unwrap() = true; + *start_time.borrow_mut() = Local::now(); + let timer_start = *start_time.borrow(); + imp2.task_input.set_sensitive(false); + 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); + if *imp3.running.lock().unwrap() { + secs -= 1; + if secs < 0 { + secs = 59; + mins -= 1; + if mins < 0 { + mins = 59; + hrs -= 1; + } } + let watch_text: &str = &format!("{:02}:{:02}:{:02}", hrs, mins, secs).to_string(); + this_clone.set_watch_time(watch_text); } + if hrs == 0 && mins == 0 && secs == 0 { + let timer_stop = Local::now(); + *imp3.running.lock().unwrap() = false; + this_clone.pomodoro_over(timer_start, timer_stop); + } + Continue(*imp3.running.lock().unwrap()) + })); + } else { + let mut secs: u32 = 0; + let mut mins: u32 = 0; + let mut hrs: u32 = 0; + + if *imp2.pomodoro_continue.lock().unwrap() { + let pomodoro_start_time = *start_time.borrow(); + let now_time = Local::now(); + let continue_time = now_time - pomodoro_start_time; + let continue_time = continue_time.num_seconds() as u32; + hrs = continue_time / 3600; + mins = continue_time % 3600 / 60; + secs = continue_time % 60; let watch_text: &str = &format!("{:02}:{:02}:{:02}", hrs, mins, secs).to_string(); - this_clone.set_watch_time(watch_text); + this.set_watch_time(watch_text); + + *imp2.pomodoro_continue.lock().unwrap() = false; + } else { + *start_time.borrow_mut() = Local::now(); } - Continue(*imp3.running.lock().unwrap()) - })); + + *imp2.running.lock().unwrap() = true; + imp2.task_input.set_sensitive(false); + 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); + if *imp3.running.lock().unwrap() { + secs += 1; + if secs > 59 { + secs = 0; + mins += 1; + if mins > 59 { + mins = 0; + hrs += 1; + } + } + let watch_text: &str = &format!("{:02}:{:02}:{:02}", hrs, mins, secs).to_string(); + this_clone.set_watch_time(watch_text); + } + Continue(*imp3.running.lock().unwrap()) + })); + } button.set_icon_name("media-playback-stop-symbolic"); } else { *stop_time.borrow_mut() = Local::now(); *imp2.running.lock().unwrap() = false; button.set_icon_name("media-playback-start-symbolic"); - this.set_watch_time("00:00:00"); + this.refresh_timer(); imp2.task_input.set_sensitive(true); this.save_task(*start_time.borrow(), *stop_time.borrow()); } @@ -240,7 +294,7 @@ impl FurtheranceWindow { fn setup_settings(&self) { let imp = imp::FurtheranceWindow::from_instance(self); - self.reset_vars(); + self.reset_idle(); // Enter starts timer let start = imp.start_button.clone(); @@ -267,7 +321,6 @@ impl FurtheranceWindow { Ok(val) => val, Err(_) => 1, }; - // If user was idle and has now returned... if idle_time < (settings_manager::get_int("idle-time") * 60) as u64 && *imp.idle_time_reached.lock().unwrap() @@ -309,7 +362,7 @@ impl FurtheranceWindow { gtk::DialogFlags::MODAL, gtk::MessageType::Warning, gtk::ButtonsType::None, - Some(&format!("{}", &gettext("Edit Task"))), + Some(&format!("{}", &gettext("Idle"))), ); dialog.add_buttons(&[ (&gettext("Discard"), gtk::ResponseType::Reject), @@ -317,9 +370,6 @@ impl FurtheranceWindow { ]); dialog.set_secondary_text(Some(&idle_time_msg)); - let app = FurtheranceApplication::default(); - app.system_notification(&idle_time_str, &question_str, dialog.clone()); - dialog.connect_response(clone!( @weak self as this, @strong dialog, @@ -328,16 +378,60 @@ impl FurtheranceWindow { this.set_subtract_idle(true); start_button.emit_clicked(); dialog.close(); - } else { - this.reset_vars(); + } else if resp == gtk::ResponseType::Accept { + this.reset_idle(); + dialog.close(); + } + })); + + *imp.idle_dialog.lock().unwrap() = dialog.clone(); + let app = FurtheranceApplication::default(); + app.system_idle_notification(&idle_time_str, &question_str); + + dialog.show(); + } + + fn pomodoro_over(&self, timer_start: DateTime, timer_stop: DateTime) { + let dialog = gtk::MessageDialog::with_markup( + Some(self), + gtk::DialogFlags::MODAL, + gtk::MessageType::Warning, + gtk::ButtonsType::None, + Some(&format!("{}", &gettext("Time's up!"))), + ); + dialog.add_buttons(&[ + (&gettext("Continue"), gtk::ResponseType::Accept), + (&gettext("Stop"), gtk::ResponseType::Reject) + ]); + + let app = FurtheranceApplication::default(); + app.system_pomodoro_notification(dialog.clone()); + dialog.connect_response(clone!( + @weak self as this, + @strong dialog => move |_, resp| { + let imp = imp::FurtheranceWindow::from_instance(&this); + if resp == gtk::ResponseType::Reject { + imp.start_button.set_icon_name("media-playback-start-symbolic"); + this.refresh_timer(); + imp.task_input.set_sensitive(true); + this.save_task(timer_start, timer_stop); + this.reset_idle(); + dialog.close(); + } else if resp == gtk::ResponseType::Accept { + *imp.pomodoro_continue.lock().unwrap() = true; + this.reset_idle(); + imp.start_button.emit_clicked(); dialog.close(); } })); - dialog.show() + let imp2 = imp::FurtheranceWindow::from_instance(self); + imp2.idle_dialog.lock().unwrap().close(); + + dialog.show(); } - pub fn reset_vars(&self) { + pub fn reset_idle(&self) { let imp = imp::FurtheranceWindow::from_instance(self); *imp.stored_idle.lock().unwrap() = 0; *imp.idle_notified.lock().unwrap() = false; @@ -365,6 +459,22 @@ impl FurtheranceWindow { self.display_toast(&gettext("Stop the timer to duplicate a task.")); } } + + pub fn refresh_timer (&self) { + let imp = imp::FurtheranceWindow::from_instance(self); + if settings_manager::get_bool("pomodoro") { + let mut mins = settings_manager::get_int("pomodoro-time"); + let mut hrs: i32 = 0; + if mins > 59 { + hrs = mins / 60; + mins = mins % 60; + } + let watch_text: &str = &format!("{:02}:{:02}:00", hrs, mins); + imp.watch.set_text(watch_text); + } else { + imp.watch.set_text("00:00:00"); + } + } } impl Default for FurtheranceWindow { -- cgit 1.4.1