about summary refs log blame commit diff
path: root/src/ui/window.rs
blob: d252ef70683a0989c0ba8fd9b02e4c84e080c5a5 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17















                                                                         
                 



                                        
                     


                                                    


                                  
                            
                  



















                                                                    



                                           
                                 


















                                                                          
                                





















                                                                         
                                             








                                                              

                                                         


















                                                                                                         
                                                       










                                                                                         
                             
                                                              
 









                                                                     
                           


                                             



                                                              
                                             
                                                             
                                                
                                                                    




                                                             
                                                                                             
                  
                                                                                                   





                                                                            
                                                   


                                          
                                                         

                                                            

                                                                                              








                                                                                                              
                                                                  
                         
                                                               


                                                                         
                                                          









                                                                              
























                                                                                                     
                                                                           







                                                          
                                                                                                   


                                                          
                                                                                        













                                                                                                     

                                                                                                        




                                                     
                                                                                                  
                             
                                                             
           


                                                                               
















                                                                        
                              





                                                              
                                                

                                                              
 
                                                          
                                                              

                                                     
                                                                      
         
     









                                         
// Furtherance - Track your time without being tracked
// Copyright (C) 2022  Ricky Kresslein <rk@lakoliu.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

use adw::subclass::prelude::AdwApplicationWindowImpl;
use gettextrs::*;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, CompositeTemplate};
use glib::{clone, timeout_add_local};
use std::time::Duration;
use std::sync::Mutex;
use std::rc::Rc;
use std::cell::RefCell;
use chrono::{DateTime, Local, Duration as ChronDur};
use dbus::blocking::Connection;

use crate::ui::FurHistoryBox;
use crate::FurtheranceApplication;
use crate::database;
use crate::settings_manager;
use crate::config;

mod imp {
    use super::*;

    #[derive(Debug, Default, CompositeTemplate)]
    #[template(resource = "/com/lakoliu/Furtherance/gtk/window.ui")]
    pub struct FurtheranceWindow {
        // Template widgets
        #[template_child]
        pub header_bar: TemplateChild<gtk::HeaderBar>,
        #[template_child]
        pub watch: TemplateChild<gtk::Label>,
        #[template_child]
        pub task_input: TemplateChild<gtk::Entry>,
        #[template_child]
        pub start_button: TemplateChild<gtk::Button>,
        #[template_child]
        pub history_box: TemplateChild<FurHistoryBox>,
        #[template_child]
        pub toast_overlay: TemplateChild<adw::ToastOverlay>,

        pub stored_idle: Mutex<u64>,
        pub idle_notified: Mutex<bool>,
        pub idle_time_reached: Mutex<bool>,
        pub subtract_idle: Mutex<bool>,
        pub idle_start_time: Mutex<String>,
        pub running: Mutex<bool>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for FurtheranceWindow {
        const NAME: &'static str = "FurtheranceWindow";
        type Type = super::FurtheranceWindow;
        type ParentType = adw::ApplicationWindow;

        fn class_init(klass: &mut Self::Class) {
            FurHistoryBox::static_type();
            Self::bind_template(klass);
        }

        fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
            obj.init_template();
        }
    }

    impl ObjectImpl for FurtheranceWindow {
        fn constructed(&self, obj: &Self::Type) {
            obj.setup_widgets();
            obj.setup_signals();
            obj.setup_settings();
            self.parent_constructed(obj);
        }
    }
    impl WidgetImpl for FurtheranceWindow {}
    impl WindowImpl for FurtheranceWindow {}
    impl ApplicationWindowImpl for FurtheranceWindow {}
    impl AdwApplicationWindowImpl for FurtheranceWindow {}
}

glib::wrapper! {
    pub struct FurtheranceWindow(ObjectSubclass<imp::FurtheranceWindow>)
        @extends gtk::Widget, gtk::Window, gtk::ApplicationWindow,
        @implements gio::ActionGroup, gio::ActionMap;
}

impl FurtheranceWindow {
    pub fn new<P: glib::IsA<gtk::Application>>(application: &P) -> Self {
        glib::Object::new(&[("application", application)])
            .expect("Failed to create FurtheranceWindow")
    }

    pub fn display_toast(&self, text: &str) {
        // Display in-app notifications
        let imp = imp::FurtheranceWindow::from_instance(self);
        let toast = adw::Toast::new(text);
        imp.toast_overlay.add_toast(&toast);
    }

    fn set_watch_time(&self, text: &str) {
        // Update watch time while timer is running
        let imp = imp::FurtheranceWindow::from_instance(self);
        imp.watch.set_text(text);
        if settings_manager::get_bool("notify-of-idle") {
            self.check_user_idle();
        }
    }

    fn activate_task_input(&self, sensitive: bool) {
        // Deactivate task_input while timer is running
        let imp = imp::FurtheranceWindow::from_instance(self);
        imp.task_input.set_sensitive(sensitive);
    }

    fn get_task_text(&self) -> String {
        let imp = imp::FurtheranceWindow::from_instance(self);
        imp.task_input.text().to_string()
    }

    fn save_task(&self, start_time: DateTime<Local>, mut stop_time: DateTime<Local>) {
        // Save the most recent task to the database and clear the task_input field
        let imp = imp::FurtheranceWindow::from_instance(self);

        if *imp.subtract_idle.lock().unwrap() {
            let idle_start = DateTime::parse_from_rfc3339(&imp.idle_start_time.lock().unwrap()).unwrap();
            stop_time = idle_start.with_timezone(&Local);
            *imp.subtract_idle.lock().unwrap() = false;
        }

        let _ = database::db_write(&imp.task_input.text().trim(), start_time, stop_time);
        imp.task_input.set_text("");
        imp.history_box.create_tasks_page();
    }

    pub fn reset_history_box(&self) {
        let imp = imp::FurtheranceWindow::from_instance(self);
        imp.history_box.create_tasks_page();
    }

    fn setup_widgets(&self) {
        let imp = imp::FurtheranceWindow::from_instance(self);

        // Set initial minimum height
        let is_saved_task: bool = match database::check_for_tasks() {
            Ok(_) => true,
            Err(_) => false,
        };
        if is_saved_task {
            self.set_height_request(300);
        } else {
            self.set_height_request(390);
        }

        // Development mode
        if config::PROFILE == "development" {
            self.add_css_class("devel");
        }

        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()));

        imp.start_button.connect_clicked(clone!(
            @weak self as this => move |button| {
            let imp2 = imp::FurtheranceWindow::from_instance(&this);
            if this.get_task_text().trim().is_empty() {
                let dialog = gtk::MessageDialog::with_markup(
                    Some(&this),
                    gtk::DialogFlags::MODAL,
                    gtk::MessageType::Error,
                    gtk::ButtonsType::Ok,
                    Some(&format!("<span size='large'>{}</span>", &gettext("No Task Name"))),
                );
                dialog.set_secondary_text(Some(&gettext("Enter a task name to start the timer.")));
                dialog.show();

                dialog.connect_response(clone!(@strong dialog => move |_,_|{
                    dialog.close();
                }));

            } else {
                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();
                    this.activate_task_input(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.activate_task_input(true);
                    this.save_task(*start_time.borrow(), *stop_time.borrow());
                }
            }
        }));
    }

    fn setup_settings(&self) {
        let imp = imp::FurtheranceWindow::from_instance(self);
        self.reset_vars();

        // Enter starts timer
        let start = imp.start_button.clone();
        self.set_default_widget(Some(&start));
        imp.task_input.set_activates_default(true);
    }

    fn get_idle_time(&self) -> Result<u64, Box<dyn std::error::Error>> {
        let c = Connection::new_session()?;

        let p = c.with_proxy("org.gnome.Mutter.IdleMonitor",
            "/org/gnome/Mutter/IdleMonitor/Core",
            Duration::from_millis(5000)
        );
        let (idle_time,): (u64,) = p.method_call("org.gnome.Mutter.IdleMonitor", "GetIdletime", ())?;

        Ok(idle_time / 1000)
    }

    fn check_user_idle(&self) {
        let imp = imp::FurtheranceWindow::from_instance(self);
        // Check for user idle
        let idle_time = self.get_idle_time().unwrap();

        // 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()
            && !*imp.idle_notified.lock().unwrap() {

                *imp.idle_notified.lock().unwrap() = true;
                self.resume_from_idle();
        }
        *imp.stored_idle.lock().unwrap() = idle_time;

        // If user is idle but has not returned...
        if *imp.stored_idle.lock().unwrap() >= (settings_manager::get_int("idle-time") * 60) as u64
            && !*imp.idle_time_reached.lock().unwrap() {

            *imp.idle_time_reached.lock().unwrap() = true;
            let true_idle_start_time = Local::now() -
                ChronDur::seconds((settings_manager::get_int("idle-time") * 60) as i64);
            *imp.idle_start_time.lock().unwrap() = true_idle_start_time.to_rfc3339();
        }
    }

    fn resume_from_idle(&self) {
        let imp = imp::FurtheranceWindow::from_instance(self);

        let resume_time = Local::now();
        let idle_start = DateTime::parse_from_rfc3339(&imp.idle_start_time.lock().unwrap()).unwrap();
        let idle_start = idle_start.with_timezone(&Local);
        let idle_time = resume_time - idle_start;
        let idle_time = idle_time.num_seconds();
        let h = idle_time / 60 / 60;
        let m = (idle_time / 60) - (h * 60);
        let s = idle_time - (m * 60);
        let idle_time_str = format!("{}{:02}:{:02}:{:02}", gettext("You have been idle for "), h, m, s);
        let question_str = gettext("\nWould you like to discard that time, or continue the clock?");
        let idle_time_msg = format!("{}{}", idle_time_str, question_str);

        let dialog = gtk::MessageDialog::with_markup(
            Some(self),
            gtk::DialogFlags::MODAL,
            gtk::MessageType::Warning,
            gtk::ButtonsType::None,
            Some(&format!("<span size='x-large' weight='bold'>{}</span>", &gettext("Edit Task"))),
        );
        dialog.add_buttons(&[
            (&gettext("Discard"), gtk::ResponseType::Reject),
            (&gettext("Continue"), gtk::ResponseType::Accept)
        ]);
        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,
            @strong imp.start_button as start_button => move |_, resp| {
            if resp == gtk::ResponseType::Reject {
                this.set_subtract_idle(true);
                start_button.emit_clicked();
                dialog.close();
            } else {
                this.reset_vars();
                dialog.close();
            }
        }));

        dialog.show()
    }

    pub fn reset_vars(&self) {
        let imp = imp::FurtheranceWindow::from_instance(self);
        *imp.stored_idle.lock().unwrap() = 0;
        *imp.idle_notified.lock().unwrap() = false;
        *imp.idle_time_reached.lock().unwrap() = false;
        *imp.subtract_idle.lock().unwrap() = false;
    }

    pub fn set_subtract_idle(&self, val: bool) {
        let imp = imp::FurtheranceWindow::from_instance(self);
        *imp.subtract_idle.lock().unwrap() = val;
    }

    pub fn duplicate_task(&self, task_name_text: String) {
        let imp = imp::FurtheranceWindow::from_instance(self);
        if !*imp.running.lock().unwrap() {
            imp.task_input.set_text(&task_name_text);
            imp.start_button.emit_clicked();
        } else {
            self.display_toast("Stop the timer to duplicate a task.");
        }
    }
}

impl Default for FurtheranceWindow {
    fn default() -> Self {
        FurtheranceApplication::default()
            .active_window()
            .unwrap()
            .downcast()
            .unwrap()
    }
}