about summary refs log tree commit diff
path: root/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/history_box.rs138
-rw-r--r--src/ui/task_details.rs441
-rw-r--r--src/ui/task_row.rs130
-rw-r--r--src/ui/tasks_group.rs112
-rw-r--r--src/ui/tasks_page.rs147
-rw-r--r--src/ui/window.rs330
6 files changed, 1298 insertions, 0 deletions
diff --git a/src/ui/history_box.rs b/src/ui/history_box.rs
new file mode 100644
index 0000000..4de6dce
--- /dev/null
+++ b/src/ui/history_box.rs
@@ -0,0 +1,138 @@
+// 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 gtk::prelude::*;
+use gtk::subclass::prelude::*;
+use gtk::{glib, CompositeTemplate};
+use glib::subclass;
+
+use crate::ui::FurTasksPage;
+use crate::FurtheranceApplication;
+use crate::database;
+
+enum View {
+    Loading,
+    Empty,
+    Tasks,
+}
+
+mod imp {
+    use super::*;
+
+    #[derive(Debug, Default, CompositeTemplate)]
+    #[template(resource = "/com/lakoliu/Furtherance/gtk/history_box.ui")]
+    pub struct FurHistoryBox {
+        // Template widgets
+        #[template_child]
+        pub stack: TemplateChild<gtk::Stack>,
+        #[template_child]
+        pub spinner: TemplateChild<gtk::Spinner>,
+        #[template_child]
+        pub welcome_page: TemplateChild<adw::StatusPage>,
+        #[template_child]
+        pub tasks_page: TemplateChild<FurTasksPage>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for FurHistoryBox {
+        const NAME: &'static str = "FurHistoryBox";
+        type Type = super::FurHistoryBox;
+        type ParentType = gtk::Box;
+
+        fn class_init(klass: &mut Self::Class) {
+            Self::bind_template(klass);
+        }
+
+        fn instance_init(obj: &subclass::InitializingObject<Self>) {
+            obj.init_template();
+        }
+
+    }
+
+    impl ObjectImpl for FurHistoryBox {
+        fn constructed(&self, obj: &Self::Type) {
+            obj.setup_widgets();
+            self.parent_constructed(obj);
+        }
+
+    }
+    impl WidgetImpl for FurHistoryBox {}
+    impl BoxImpl for FurHistoryBox {}
+}
+
+glib::wrapper! {
+    pub struct FurHistoryBox(
+        ObjectSubclass<imp::FurHistoryBox>)
+        @extends gtk::Widget, gtk::Box;
+}
+
+
+impl FurHistoryBox {
+    fn setup_widgets(&self) {
+        self.set_view(View::Loading);
+        let is_saved_task: bool = match database::check_for_tasks() {
+            Ok(_) => true,
+            Err(_) => false,
+        };
+        if is_saved_task {
+            self.set_view(View::Tasks);
+        } else {
+            self.set_view(View::Empty);
+        }
+    }
+
+    fn set_view(&self, view: View) {
+        let imp = imp::FurHistoryBox::from_instance(self);
+        let app = FurtheranceApplication::default();
+        app.delete_enabled(false);
+        imp.spinner.set_spinning(false);
+
+        let name = match view {
+            View::Loading => {
+                imp.spinner.set_spinning(true);
+                "loading"
+            }
+            View::Empty => "empty",
+            View::Tasks => {
+                app.delete_enabled(true);
+                "tasks"
+            }
+        };
+
+        imp.stack.set_visible_child_name(name);
+    }
+
+    pub fn create_tasks_page(&self) {
+        let imp = imp::FurHistoryBox::from_instance(self);
+        imp.tasks_page.clear_task_list();
+        let is_saved_task: bool = match database::check_for_tasks() {
+            Ok(_) => true,
+            Err(_) => false,
+        };
+        if is_saved_task {
+            self.set_view(View::Loading);
+            imp.tasks_page.build_task_list();
+            self.set_view(View::Tasks);
+        } else {
+            self.set_view(View::Empty);
+        }
+    }
+
+    pub fn empty_view(&self) {
+        self.set_view(View::Empty);
+    }
+
+}
diff --git a/src/ui/task_details.rs b/src/ui/task_details.rs
new file mode 100644
index 0000000..69c3494
--- /dev/null
+++ b/src/ui/task_details.rs
@@ -0,0 +1,441 @@
+// 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::*;
+use glib::clone;
+use gtk::subclass::prelude::*;
+use gtk::{glib, prelude::*, CompositeTemplate};
+use chrono::{DateTime, NaiveDateTime, Local, offset::TimeZone};
+
+use crate::FurtheranceApplication;
+use crate::ui::FurtheranceWindow;
+use crate::database;
+
+mod imp {
+    use super::*;
+    use glib::subclass;
+    use std::cell::RefCell;
+
+    #[derive(Debug, Default, CompositeTemplate)]
+    #[template(resource = "/com/lakoliu/Furtherance/gtk/task_details.ui")]
+    pub struct FurTaskDetails {
+        #[template_child]
+        pub headerbar: TemplateChild<gtk::HeaderBar>,
+        #[template_child]
+        pub dialog_title: TemplateChild<adw::WindowTitle>,
+        #[template_child]
+        pub scrolled_window: TemplateChild<gtk::ScrolledWindow>,
+
+        #[template_child]
+        pub task_name_label: TemplateChild<gtk::Label>,
+
+        #[template_child]
+        pub main_box: TemplateChild<gtk::Box>,
+
+        #[template_child]
+        pub delete_all_btn: TemplateChild<gtk::Button>,
+
+        pub all_boxes: RefCell<Vec<gtk::Box>>,
+        pub all_task_ids: RefCell<Vec<i32>>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for FurTaskDetails {
+        const NAME: &'static str = "FurTaskDetails";
+        type ParentType = adw::Window;
+        type Type = super::FurTaskDetails;
+
+        fn class_init(klass: &mut Self::Class) {
+            Self::bind_template(klass);
+        }
+
+        fn instance_init(obj: &subclass::InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for FurTaskDetails {
+
+        fn constructed(&self, obj: &Self::Type) {
+            obj.setup_signals();
+            obj.setup_delete_all();
+            self.parent_constructed(obj);
+        }
+    }
+
+    impl WidgetImpl for FurTaskDetails {}
+
+    impl WindowImpl for FurTaskDetails {}
+
+    impl AdwWindowImpl for FurTaskDetails {}
+}
+
+glib::wrapper! {
+    pub struct FurTaskDetails(ObjectSubclass<imp::FurTaskDetails>)
+        @extends gtk::Widget, gtk::Window, adw::Window;
+}
+
+impl FurTaskDetails {
+    pub fn new() -> Self {
+        let dialog: Self = glib::Object::new(&[]).unwrap();
+
+        let window = FurtheranceWindow::default();
+        dialog.set_transient_for(Some(&window));
+
+        let app = FurtheranceApplication::default();
+        app.add_window(&window);
+
+        dialog
+    }
+
+    pub fn setup_widgets(&self, mut task_group: Vec<database::Task>) {
+        let imp = imp::FurTaskDetails::from_instance(self);
+
+        imp.task_name_label.set_text(&task_group[0].task_name);
+
+        for task in task_group.clone() {
+            imp.all_task_ids.borrow_mut().push(task.id);
+        }
+
+        let task_group_len = task_group.len();
+        task_group.reverse();
+        for task in task_group {
+            let task_box = gtk::Box::new(gtk::Orientation::Horizontal, 5);
+            task_box.set_homogeneous(true);
+
+            let start_time = DateTime::parse_from_rfc3339(&task.start_time).unwrap();
+            let start_time_str = start_time.format("%H:%M:%S").to_string();
+            let start = gtk::Button::new();
+            start.set_label(&start_time_str);
+            task_box.append(&start);
+
+            let stop_time = DateTime::parse_from_rfc3339(&task.stop_time).unwrap();
+            let stop_time_str = stop_time.format("%H:%M:%S").to_string();
+            let stop = gtk::Button::new();
+            stop.set_label(&stop_time_str);
+            task_box.append(&stop);
+
+            let total_time = stop_time - start_time;
+            let total_time = total_time.num_seconds();
+            let h = total_time / 60 / 60;
+            let m = (total_time / 60) - (h * 60);
+            let s = total_time - (m * 60);
+            let total_time_str = format!("{:02}:{:02}:{:02}", h, m, s);
+            let total = gtk::Button::new();
+            total.set_label(&total_time_str);
+            total.add_css_class("inactive-button");
+            total.set_hexpand(false);
+            task_box.append(&total);
+
+            imp.main_box.append(&task_box);
+            imp.all_boxes.borrow_mut().push(task_box);
+
+            start.connect_clicked(clone!(@weak self as this => move |_|{
+                let window = FurtheranceWindow::default();
+                let dialog = gtk::MessageDialog::new(
+                    Some(&window),
+                    gtk::DialogFlags::MODAL,
+                    gtk::MessageType::Question,
+                    gtk::ButtonsType::OkCancel,
+                    "<span size='x-large' weight='bold'>Edit Task</span>",
+                );
+                dialog.set_use_markup(true);
+
+                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_text(&task.task_name);
+                let labels_box = gtk::Box::new(gtk::Orientation::Horizontal, 5);
+                labels_box.set_homogeneous(true);
+                let start_label = gtk::Label::new(Some("Start"));
+                start_label.add_css_class("title-4");
+                let stop_label = gtk::Label::new(Some("Stop"));
+                stop_label.add_css_class("title-4");
+                let times_box = gtk::Box::new(gtk::Orientation::Horizontal, 5);
+                times_box.set_homogeneous(true);
+
+                let start_time_w_year = start_time.format("%h %e %Y %H:%M:%S").to_string();
+                let stop_time_w_year = stop_time.format("%h %e %Y %H:%M:%S").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(
+                    "*Use the format MMM DD YYYY HH:MM:SS"));
+                instructions.set_visible(false);
+                instructions.add_css_class("error_message");
+
+                let time_error = gtk::Label::new(Some(
+                    "*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(
+                    "*Time cannot be in the future."));
+                future_error.set_visible(false);
+                future_error.add_css_class("error_message");
+
+                let delete_task_btn = gtk::Button::new();
+                delete_task_btn.set_icon_name("user-trash-symbolic");
+                delete_task_btn.set_tooltip_text(Some("Delete task"));
+                delete_task_btn.set_hexpand(false);
+                delete_task_btn.set_vexpand(false);
+                delete_task_btn.set_halign(gtk::Align::End);
+
+                vert_box.append(&task_name_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(&times_box);
+                vert_box.append(&instructions);
+                vert_box.append(&time_error);
+                vert_box.append(&future_error);
+                message_area.append(&delete_task_btn);
+                message_area.append(&vert_box);
+
+                delete_task_btn.connect_clicked(clone!(
+                    @strong task, @strong dialog, @weak this => move |_| {
+
+                    let delete_confirmation = gtk::MessageDialog::with_markup(
+                        Some(&window),
+                        gtk::DialogFlags::MODAL,
+                        gtk::MessageType::Question,
+                        gtk::ButtonsType::OkCancel,
+                        Some("<span size='x-large' weight='bold'>Delete task?</span>"),
+                    );
+
+                    delete_confirmation.connect_response(clone!(
+                        @strong dialog,
+                        @strong delete_confirmation => move |_, resp| {
+                        if resp == gtk::ResponseType::Ok {
+                            let _ = database::delete_by_id(task.id);
+                            if task_group_len == 1 {
+                                delete_confirmation.close();
+                                dialog.close();
+                                this.close();
+                                let window = FurtheranceWindow::default();
+                                window.reset_history_box();
+                            } else {
+                                delete_confirmation.close();
+                                this.clear_task_list();
+                                dialog.close();
+                            }
+                        } else {
+                            delete_confirmation.close();
+                        }
+                    }));
+
+                    delete_confirmation.show();
+                }));
+
+
+                dialog.connect_response(
+                    clone!(@strong dialog,
+                        @strong task.task_name as name
+                        @strong task.start_time as start_time
+                        @strong task.stop_time as stop_time => move |_ , resp| {
+                        if resp == gtk::ResponseType::Ok {
+                            instructions.set_visible(false);
+                            time_error.set_visible(false);
+                            future_error.set_visible(false);
+                            let mut start_successful = false;
+                            let mut stop_successful = false;
+                            let mut do_not_close = false;
+                            let mut new_start_time_edited: String = "".to_string();
+                            let mut new_start_time_local = Local::now();
+                            let new_stop_time_edited: String;
+                            if start_time_edit.text() != start_time_w_year {
+                                let new_start_time_str = start_time_edit.text();
+                                let new_start_time = NaiveDateTime::parse_from_str(
+                                                        &new_start_time_str,
+                                                        "%h %e %Y %H:%M:%S");
+                                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();
+                                    new_start_time_edited = new_start_time_local.to_rfc3339();
+                                    start_successful = true;
+                                }
+                            }
+                            if stop_time_edit.text() != stop_time_w_year {
+                                let new_stop_time_str = stop_time_edit.text();
+                                let new_stop_time = NaiveDateTime::parse_from_str(
+                                                        &new_stop_time_str,
+                                                        "%h %e %Y %H:%M:%S");
+                                if let Err(_) = new_stop_time {
+                                    instructions.set_visible(true);
+                                    do_not_close = true;
+                                } else {
+                                    let new_stop_time = Local.from_local_datetime(&new_stop_time.unwrap()).unwrap();
+                                    new_stop_time_edited = new_stop_time.to_rfc3339();
+                                    if start_successful {
+                                        if (new_stop_time - new_start_time_local).num_seconds() >= 0 {
+                                            database::update_stop_time(task.id, new_stop_time_edited.clone())
+                                                .expect("Failed to update stop time.");
+                                            database::update_start_time(task.id, new_start_time_edited.clone())
+                                                .expect("Failed to update stop time.");
+                                        }
+                                    } else {
+                                        let old_start_time = DateTime::parse_from_rfc3339(&start_time);
+                                        let old_start_time = old_start_time.unwrap().with_timezone(&Local);
+                                        if (Local::now() - new_stop_time).num_seconds() < 0 {
+                                            future_error.set_visible(true);
+                                            do_not_close = true;
+                                        } else if (new_stop_time - old_start_time).num_seconds() >= 0 {
+                                            database::update_stop_time(task.id, new_stop_time_edited)
+                                                .expect("Failed to update stop time.");
+                                        } else {
+                                            time_error.set_visible(true);
+                                            do_not_close = true;
+                                        }
+                                    }
+                                    stop_successful = true;
+                                }
+                            }
+                            if task_name_edit.text() != name {
+                                database::update_task_name(task.id, task_name_edit.text().to_string())
+                                    .expect("Failed to update start time.");
+                            }
+
+                            if start_successful && !stop_successful {
+                                let old_stop_time = DateTime::parse_from_rfc3339(&stop_time);
+                                let old_stop_time = old_stop_time.unwrap().with_timezone(&Local);
+                                if (old_stop_time - new_start_time_local).num_seconds() >= 0 {
+                                    database::update_start_time(task.id, new_start_time_edited)
+                                        .expect("Failed to update start time.");
+                                } else {
+                                    time_error.set_visible(true);
+                                    do_not_close = true;
+                                }
+
+                            }
+
+                            if !do_not_close {
+                                this.clear_task_list();
+                                dialog.close();
+                            }
+
+
+                        } else {
+                            // If Cancel, close dialog and do nothing.
+                            dialog.close();
+                        }
+                    }),
+                );
+
+
+                dialog.show();
+            }));
+
+            stop.connect_clicked(move |_|{
+                start.emit_clicked();
+            });
+        }
+    }
+
+    fn clear_task_list(&self) {
+        let imp = imp::FurTaskDetails::from_instance(&self);
+
+        for task_box in &*imp.all_boxes.borrow() {
+            imp.main_box.remove(task_box);
+        }
+
+        imp.all_boxes.borrow_mut().clear();
+        // Get list from database by a vec of IDs
+        let updated_list = database::get_list_by_id(imp.all_task_ids.clone().borrow().to_vec());
+        imp.all_task_ids.borrow_mut().clear();
+        let window = FurtheranceWindow::default();
+        window.reset_history_box();
+        self.setup_widgets(updated_list.unwrap());
+    }
+
+    fn setup_signals(&self) {
+        let imp = imp::FurTaskDetails::from_instance(self);
+
+        // Add headerbar to dialog when scrolled far
+        imp.scrolled_window.vadjustment().connect_value_notify(
+            clone!(@weak self as this => move |adj|{
+                let imp = imp::FurTaskDetails::from_instance(&this);
+                if adj.value() < 120.0 {
+                    imp.headerbar.add_css_class("hidden");
+                    imp.dialog_title.set_visible(false);
+                }else {
+                    imp.headerbar.remove_css_class("hidden");
+                    imp.dialog_title.set_visible(true);
+                }
+            }),
+        );
+
+        // Make dialog header smaller if the name is long
+        imp.task_name_label.connect_label_notify(|label| {
+            let large_title = !(label.text().len() > 25);
+
+            if large_title {
+                label.remove_css_class("title-2");
+                label.add_css_class("title-1");
+            } else {
+                label.remove_css_class("title-1");
+                label.add_css_class("title-2");
+            }
+        });
+    }
+
+    fn setup_delete_all(&self) {
+        let imp = imp::FurTaskDetails::from_instance(self);
+        let window = FurtheranceWindow::default();
+
+        imp.delete_all_btn.connect_clicked(clone!(@weak self as this => move |_|{
+            let dialog = gtk::MessageDialog::with_markup(
+                Some(&window),
+                gtk::DialogFlags::MODAL,
+                gtk::MessageType::Warning,
+                gtk::ButtonsType::None,
+                Some("<span size='large'>Delete All?</span>"),
+            );
+            dialog.set_secondary_text(Some("This will delete all occurrences of this task on this day."));
+            dialog.add_buttons(&[
+                ("Delete", gtk::ResponseType::Accept),
+                ("Cancel", gtk::ResponseType::Reject)
+            ]);
+            dialog.show();
+
+            dialog.connect_response(clone!(@strong dialog => move |_,resp|{
+                if resp == gtk::ResponseType::Accept {
+                    this.delete_all();
+                    dialog.close();
+                    this.close();
+                    let window = FurtheranceWindow::default();
+                    window.reset_history_box();
+                } else {
+                    dialog.close();
+                }
+            }));
+
+        }));
+
+    }
+
+    fn delete_all(&self) {
+        let imp = imp::FurTaskDetails::from_instance(self);
+        let _ = database::delete_by_ids(imp.all_task_ids.borrow().to_vec());
+    }
+
+}
+
diff --git a/src/ui/task_row.rs b/src/ui/task_row.rs
new file mode 100644
index 0000000..d17f263
--- /dev/null
+++ b/src/ui/task_row.rs
@@ -0,0 +1,130 @@
+// 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 gtk::subclass::prelude::*;
+use gtk::{glib, gio, prelude::*, CompositeTemplate};
+use chrono::DateTime;
+use std::sync::Mutex;
+use once_cell::sync::Lazy;
+use glib::clone;
+
+use crate::database::Task;
+use crate::ui::FurTaskDetails;
+
+
+mod imp {
+    use super::*;
+    use glib::subclass;
+
+    #[derive(Debug, CompositeTemplate, Default)]
+    #[template(resource = "/com/lakoliu/Furtherance/gtk/task_row.ui")]
+    pub struct FurTaskRow {
+        #[template_child]
+        pub task_name_label: TemplateChild<gtk::Label>,
+        #[template_child]
+        pub total_time_label: TemplateChild<gtk::Label>,
+        // pub tasks: Vec<database::Task>,
+        pub tasks: Lazy<Mutex<Vec<Task>>>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for FurTaskRow {
+        const NAME: &'static str = "FurTaskRow";
+        type ParentType = gtk::ListBoxRow;
+        type Type = super::FurTaskRow;
+
+        fn class_init(klass: &mut Self::Class) {
+            Self::bind_template(klass);
+        }
+
+        fn instance_init(obj: &subclass::InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for FurTaskRow {
+        fn constructed(&self, obj: &Self::Type) {
+            obj.setup_signals();
+            self.parent_constructed(obj);
+        }
+    }
+
+    impl WidgetImpl for FurTaskRow {}
+
+    impl ListBoxRowImpl for FurTaskRow {}
+}
+
+glib::wrapper! {
+    pub struct FurTaskRow(
+        ObjectSubclass<imp::FurTaskRow>)
+        @extends gtk::Widget, gtk::ListBoxRow;
+}
+
+impl FurTaskRow {
+    pub fn new() -> Self {
+        /* This should take a model, object, or vec filled with the description
+        details and fill out the labels based on those. */
+        glib::Object::new(&[]).expect("Failed to create `FurTaskRow`.")
+    }
+
+    fn setup_signals(&self) {
+        let open_details_action = gio::SimpleAction::new("open-details", None);
+
+        open_details_action.connect_activate(clone!(@strong self as this => move |_, _| {
+            let dialog = FurTaskDetails::new();
+            dialog.setup_widgets(this.get_tasks());
+            dialog.show();
+        }));
+
+        let actions = gio::SimpleActionGroup::new();
+        self.insert_action_group("task-row", Some(&actions));
+        actions.add_action(&open_details_action);
+    }
+
+    pub fn set_row_labels(&self, task_list: Vec<Task>) {
+        let imp = imp::FurTaskRow::from_instance(&self);
+        for task in task_list.clone() {
+            imp.tasks.lock().unwrap().push(task);
+        }
+        imp.task_name_label.set_text(&imp.tasks.lock().unwrap()[0].task_name);
+
+        // Add up all durations for task of said name to create total_time
+        let mut total_time: i64 = 0;
+        for task in &task_list {
+            if task.task_name == task.task_name {
+                let start_time = DateTime::parse_from_rfc3339(&task.start_time).unwrap();
+                let stop_time = DateTime::parse_from_rfc3339(&task.stop_time).unwrap();
+
+                let duration = stop_time - start_time;
+                total_time += duration.num_seconds();
+            }
+        }
+        // Format total time to readable string
+        let h = total_time / 60 / 60;
+        let m = (total_time / 60) - (h * 60);
+        let s = total_time - (m * 60);
+
+        let total_time_str = format!("{:02}:{:02}:{:02}", h, m, s);
+
+        imp.total_time_label.set_text(&total_time_str);
+    }
+
+    pub fn get_tasks(&self) -> Vec<Task> {
+        let imp = imp::FurTaskRow::from_instance(&self);
+        imp.tasks.lock().unwrap().to_vec()
+    }
+}
+
diff --git a/src/ui/tasks_group.rs b/src/ui/tasks_group.rs
new file mode 100644
index 0000000..86c20dc
--- /dev/null
+++ b/src/ui/tasks_group.rs
@@ -0,0 +1,112 @@
+// 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::*;
+use gtk::subclass::prelude::*;
+use gtk::{glib, prelude::*};
+
+use crate::ui::FurTaskRow;
+use crate::database;
+
+mod imp {
+    use super::*;
+    use glib::subclass;
+    use gtk::CompositeTemplate;
+
+    use std::cell::RefCell;
+
+    #[derive(Default, Debug, CompositeTemplate)]
+    #[template(resource = "/com/lakoliu/Furtherance/gtk/tasks_group.ui")]
+    pub struct FurTasksGroup {
+        #[template_child]
+        pub listbox_box: TemplateChild<gtk::Box>,
+        pub models: RefCell<Vec<gtk::SortListModel>>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for FurTasksGroup {
+        const NAME: &'static str = "FurTasksGroup";
+        type ParentType = adw::PreferencesGroup;
+        type Type = super::FurTasksGroup;
+
+        fn class_init(klass: &mut Self::Class) {
+            Self::bind_template(klass);
+        }
+
+        fn instance_init(obj: &subclass::InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for FurTasksGroup {}
+    impl WidgetImpl for FurTasksGroup {}
+    impl PreferencesGroupImpl for FurTasksGroup {}
+}
+
+glib::wrapper! {
+    pub struct FurTasksGroup(
+        ObjectSubclass<imp::FurTasksGroup>)
+        @extends gtk::Widget, adw::PreferencesGroup;
+
+}
+
+impl FurTasksGroup {
+    pub fn new() -> Self {
+        glib::Object::new(&[]).expect("Failed to create `FurTaskGroup`.")
+    }
+
+    pub fn add_task_model(&self, tasks: Vec<database::Task>) {
+        let imp = imp::FurTasksGroup::from_instance(&self);
+
+        let listbox = gtk::ListBox::new();
+        listbox.add_css_class("content");
+        listbox.set_selection_mode(gtk::SelectionMode::None);
+        imp.listbox_box.append(&listbox);
+
+        // Check if tasks have the same name. If they do, make one listbox row for all of them.
+        // If they don't, move on.
+        let mut tasks_by_name: Vec<Vec<database::Task>> = Vec::new();
+        let mut unique: bool;
+
+        for task in &tasks {
+            unique = true;
+            for i in 0..tasks_by_name.len() {
+                if tasks_by_name[i][0].task_name == task.task_name {
+                    tasks_by_name[i].push(task.clone());
+                    unique = false;
+                }
+            }
+            if unique {
+                // Add unique task to list for group name
+                let mut new_name_list: Vec<database::Task> = Vec::new();
+                new_name_list.push(task.clone());
+                tasks_by_name.push(new_name_list);
+            }
+        }
+
+        for same_name in tasks_by_name {
+            let listbox_row = FurTaskRow::new();
+            listbox_row.set_row_labels(same_name);
+            listbox.append(&listbox_row);
+        }
+
+        listbox.connect_row_activated(move |_, row| {
+            row.activate_action("task-row.open-details", None).unwrap();
+        });
+
+    }
+}
+
diff --git a/src/ui/tasks_page.rs b/src/ui/tasks_page.rs
new file mode 100644
index 0000000..fbbaf0c
--- /dev/null
+++ b/src/ui/tasks_page.rs
@@ -0,0 +1,147 @@
+// 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::*;
+use adw::prelude::{PreferencesPageExt, PreferencesGroupExt};
+use gtk::subclass::prelude::*;
+use gtk::{glib, prelude::*};
+use chrono::{DateTime, Local, Duration};
+
+use crate::ui::FurTasksGroup;
+use crate::database;
+
+mod imp {
+    use super::*;
+    use glib::subclass;
+    use gtk::CompositeTemplate;
+    use std::cell::RefCell;
+
+    #[derive(Default, Debug, CompositeTemplate)]
+    #[template(resource = "/com/lakoliu/Furtherance/gtk/tasks_page.ui")]
+    pub struct FurTasksPage {
+        pub all_groups: RefCell<Vec<FurTasksGroup>>,
+    }
+
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for FurTasksPage {
+        const NAME: &'static str = "FurTasksPage";
+        type ParentType = adw::PreferencesPage;
+        type Type = super::FurTasksPage;
+
+        fn class_init(klass: &mut Self::Class) {
+            Self::bind_template(klass);
+        }
+
+        fn instance_init(obj: &subclass::InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for FurTasksPage {
+        fn constructed(&self, obj: &Self::Type) {
+            obj.setup_widgets();
+            self.parent_constructed(obj);
+        }
+    }
+
+    impl WidgetImpl for FurTasksPage {}
+    impl PreferencesPageImpl for FurTasksPage {}
+}
+
+glib::wrapper! {
+    pub struct FurTasksPage(
+        ObjectSubclass<imp::FurTasksPage>)
+        @extends gtk::Widget, adw::PreferencesPage;
+}
+
+impl FurTasksPage {
+    fn setup_widgets(&self) {
+        self.build_task_list();
+    }
+
+    pub fn clear_task_list(&self) {
+        let imp = imp::FurTasksPage::from_instance(&self);
+
+        for group in &*imp.all_groups.borrow() {
+            self.remove(group);
+        }
+
+        imp.all_groups.borrow_mut().clear();
+    }
+
+    pub fn build_task_list(&self) {
+        let imp = imp::FurTasksPage::from_instance(&self);
+
+        let mut tasks_list = database::retrieve().unwrap();
+
+        // Reversing chronological order of tasks_list
+        tasks_list.reverse();
+        let mut uniq_date_list: Vec<String> = Vec::new();
+        let mut same_date_list: Vec<database::Task> = Vec::new();
+        let mut tasks_sorted_by_day: Vec<Vec<database::Task>> = Vec::new();
+
+        // Go through tasks list and look at all dates
+        let mut i: u32 = 1;
+        let len = tasks_list.len() as u32;
+        for task in tasks_list {
+            let task_clone = task.clone();
+            let date = DateTime::parse_from_rfc3339(&task.start_time).unwrap();
+            let date = date.format("%h %e").to_string();
+            if !uniq_date_list.contains(&date) {
+                // if same_date_list is empty, push "date" to it
+                // if it is not empty, push it to a vec of vecs, and then clear it
+                // and push "date" to it
+                if same_date_list.is_empty() {
+                    same_date_list.push(task_clone);
+                } else {
+                    tasks_sorted_by_day.push(same_date_list.clone());
+                    same_date_list.clear();
+                    same_date_list.push(task_clone);
+                }
+                uniq_date_list.push(date);
+            } else {
+                // otherwise push the task to the list of others with the same date
+                same_date_list.push(task_clone);
+            }
+            // If this is the last iteration, push the list of objects to sorted_by_day
+            if i == len {
+                tasks_sorted_by_day.push(same_date_list.clone());
+            }
+            i += 1;
+        }
+
+        // Create FurTasksGroups for all unique days
+        let now = Local::now();
+        let yesterday = now - Duration::days(1);
+        let yesterday = yesterday.format("%h %e").to_string();
+        let today = now.format("%h %e").to_string();
+        for i in 0..uniq_date_list.len() {
+            let group = FurTasksGroup::new();
+            if uniq_date_list[i] == today {
+                group.set_title("Today")
+            } else if uniq_date_list[i] == yesterday{
+                group.set_title("Yesterday")
+            } else {
+                group.set_title(&uniq_date_list[i])
+            }
+            self.add(&group);
+            group.add_task_model(tasks_sorted_by_day[i].clone());
+            imp.all_groups.borrow_mut().push(group);
+        }
+    }
+}
+
diff --git a/src/ui/window.rs b/src/ui/window.rs
new file mode 100644
index 0000000..965f0f3
--- /dev/null
+++ b/src/ui/window.rs
@@ -0,0 +1,330 @@
+// 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 gtk::prelude::*;
+use gtk::subclass::prelude::*;
+use gtk::{gio, glib, CompositeTemplate};
+use glib::{clone, timeout_add_local};
+use std::time::Duration;
+use std::sync::{Arc, Mutex};
+use std::rc::Rc;
+use std::cell::RefCell;
+use chrono::{DateTime, Local, Duration as ChronDur};
+use dbus::blocking::Connection;
+use once_cell::unsync::OnceCell;
+
+use crate::ui::FurHistoryBox;
+use crate::FurtheranceApplication;
+use crate::database;
+
+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 notify_of_idle: OnceCell<u64>,
+        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>,
+    }
+
+    #[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_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 inapp_notification(&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);
+        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_signals(&self) {
+        let imp = imp::FurtheranceWindow::from_instance(self);
+        let running = Arc::new(Mutex::new(false));
+        let start_time = Rc::new(RefCell::new(Local::now()));
+        let stop_time = Rc::new(RefCell::new(Local::now()));
+
+        // Development mode
+        // self.add_css_class("devel");
+
+        imp.start_button.connect_clicked(clone!(
+            @weak self as this,
+            @strong running => move |button| {
+            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("<span size='large'>No Task Name</span>"),
+                );
+                dialog.set_secondary_text(Some("Enter a task name to start the timer."));
+                dialog.show();
+
+                dialog.connect_response(clone!(@strong dialog => move |_,_|{
+                    dialog.close();
+                }));
+
+            } else {
+                if !*running.lock().unwrap() {
+                    let mut secs: u32 = 0;
+                    let mut mins: u32 = 0;
+                    let mut hrs: u32 = 0;
+
+                    *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 running as running_clone => move || {
+                        if *running_clone.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.set_watch_time(watch_text);
+                        }
+                        Continue(*running_clone.lock().unwrap())
+                    }));
+                    button.set_icon_name("media-playback-stop-symbolic");
+                } else {
+                    *stop_time.borrow_mut() = Local::now();
+                    *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);
+        imp.notify_of_idle.set(300).expect("Failed to set notify_of_idle");
+        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 < *imp.notify_of_idle.get().unwrap()
+            && *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() >= *imp.notify_of_idle.get().unwrap()
+            && !*imp.idle_time_reached.lock().unwrap() {
+
+            *imp.idle_time_reached.lock().unwrap() = true;
+            let true_idle_start_time = Local::now() -
+                ChronDur::seconds(*imp.notify_of_idle.get().unwrap() 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!(
+            "You have been idle for {:02}:{:02}:{:02}.\nWould you like to discard that time, or continue the clock?",
+            h, m, s);
+
+        let dialog = gtk::MessageDialog::with_markup(
+            Some(self),
+            gtk::DialogFlags::MODAL,
+            gtk::MessageType::Warning,
+            gtk::ButtonsType::None,
+            Some("<span size='x-large' weight='bold'>Edit Task</span>"),
+        );
+        dialog.add_buttons(&[
+            ("Discard", gtk::ResponseType::Reject),
+            ("Continue", gtk::ResponseType::Accept)
+        ]);
+        dialog.set_secondary_text(Some(&idle_time_str));
+
+        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()
+    }
+
+    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;
+    }
+
+    fn set_subtract_idle(&self, val: bool) {
+        let imp = imp::FurtheranceWindow::from_instance(self);
+        *imp.subtract_idle.lock().unwrap() = val;
+    }
+}
+
+impl Default for FurtheranceWindow {
+    fn default() -> Self {
+        FurtheranceApplication::default()
+            .active_window()
+            .unwrap()
+            .downcast()
+            .unwrap()
+    }
+}