about summary refs log tree commit diff
diff options
context:
space:
mode:
authorRicky Kresslein <rk@lakoliu.com>2022-04-22 10:30:19 +0300
committerRicky Kresslein <rk@lakoliu.com>2022-04-22 10:30:19 +0300
commite3bb347a12ad929619a51b37f0ca48dcfe46b731 (patch)
tree643d2b973b003e91762af519620e34065953da64
parent22b05e7c934aee734005e7ba601968334467005c (diff)
downloadFurtherance-e3bb347a12ad929619a51b37f0ca48dcfe46b731.tar.zst
Add tags to tasks (Issue #8)
-rwxr-xr-xCargo.lock16
-rwxr-xr-xCargo.toml1
-rwxr-xr-xdata/com.lakoliu.Furtherance.gschema.xml3
-rwxr-xr-xsrc/application.rs3
-rwxr-xr-xsrc/database.rs37
-rwxr-xr-xsrc/gtk/preferences_window.ui27
-rwxr-xr-xsrc/gtk/task_row.ui34
-rwxr-xr-xsrc/gtk/window.ui2
-rwxr-xr-xsrc/ui/preferences_window.rs13
-rwxr-xr-xsrc/ui/task_details.rs39
-rwxr-xr-xsrc/ui/task_row.rs23
-rwxr-xr-xsrc/ui/tasks_group.rs6
-rwxr-xr-xsrc/ui/window.rs36
13 files changed, 201 insertions, 39 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 3e287a4..927ec1f 100755
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -182,6 +182,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "either"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
+
+[[package]]
 name = "fallible-iterator"
 version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -214,6 +220,7 @@ dependencies = [
  "gettext-rs",
  "gtk4",
  "gtk4-macros",
+ "itertools",
  "libadwaita",
  "log",
  "once_cell",
@@ -592,6 +599,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "itertools"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
+dependencies = [
+ "either",
+]
+
+[[package]]
 name = "lazy_static"
 version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 7e0c6f2..e7f6fb3 100755
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,6 +14,7 @@ dbus = "0.9.5"
 dbus-codegen = "0.10.0"
 log = "0.4"
 gtk4-macros = "=0.4.3"
+itertools = "0.10.3"
 
 [dependencies.gtk]
 package = "gtk4"
diff --git a/data/com.lakoliu.Furtherance.gschema.xml b/data/com.lakoliu.Furtherance.gschema.xml
index eb19b87..af1d9b7 100755
--- a/data/com.lakoliu.Furtherance.gschema.xml
+++ b/data/com.lakoliu.Furtherance.gschema.xml
@@ -25,5 +25,8 @@
     <key name="show-daily-sums" type="b">
       <default>true</default>
     </key>
+	  <key name="show-tags" type="b">
+      <default>true</default>
+    </key>
 	</schema>
 </schemalist>
diff --git a/src/application.rs b/src/application.rs
index 9120446..06a033b 100755
--- a/src/application.rs
+++ b/src/application.rs
@@ -60,6 +60,7 @@ mod imp {
         fn activate(&self, application: &Self::Type) {
             // Initialize the database
             let _ = database::db_init();
+            let _ = database::upgrade_old_db();
 
             // Get the current window or create one if necessary
             let window = if let Some(window) = application.active_window() {
@@ -142,7 +143,7 @@ impl FurtheranceApplication {
     }
 
     fn setup_application(&self) {
-        self.update_light_dark()
+        self.update_light_dark();
     }
 
     fn show_about(&self) {
diff --git a/src/database.rs b/src/database.rs
index 7612643..5412896 100755
--- a/src/database.rs
+++ b/src/database.rs
@@ -26,6 +26,7 @@ pub struct Task {
     pub task_name: String,
     pub start_time: String,
     pub stop_time: String,
+    pub tags: String,
 }
 
 pub fn get_directory() -> PathBuf {
@@ -45,21 +46,36 @@ pub fn db_init() -> Result<()> {
                     id integer primary key,
                     task_name text,
                     start_time timestamp,
-                    stop_time timestamp)",
+                    stop_time timestamp,
+                    tags text)",
         [],
     )?;
 
     Ok(())
 }
 
+pub fn upgrade_old_db() -> Result<()> {
+    // Update from old DB w/o tags
+    let conn = Connection::open(get_directory())?;
+
+    conn.execute(
+        "ALTER TABLE tasks ADD COLUMN tags TEXT DEFAULT ' '",
+        [],
+    )?;
+
+    Ok(())
+}
 
-pub fn db_write(task_name: &str, start_time: DateTime<Local>, stop_time: DateTime<Local>) -> Result<()> {
+pub fn db_write(task_name: &str,
+                start_time: DateTime<Local>,
+                stop_time: DateTime<Local>,
+                tags: String) -> Result<()> {
     // Write data into database
     let conn = Connection::open(get_directory())?;
 
     conn.execute(
-        "INSERT INTO tasks (task_name, start_time, stop_time) values (?1, ?2, ?3)",
-        &[&task_name.to_string(), &start_time.to_rfc3339(), &stop_time.to_rfc3339()],
+        "INSERT INTO tasks (task_name, start_time, stop_time, tags) values (?1, ?2, ?3, ?4)",
+        &[&task_name.to_string(), &start_time.to_rfc3339(), &stop_time.to_rfc3339(), &tags],
     )?;
 
     Ok(())
@@ -76,6 +92,7 @@ pub fn retrieve() -> Result<Vec<Task>, rusqlite::Error> {
             task_name: row.get(1)?,
             start_time: row.get(2)?,
             stop_time: row.get(3)?,
+            tags: row.get(4)?,
         })
     })?;
 
@@ -121,6 +138,17 @@ pub fn update_task_name(id: i32, task_name: String) -> Result<()> {
     Ok(())
 }
 
+pub fn update_tags(id: i32, tags: String) -> Result<()> {
+    let conn = Connection::open(get_directory())?;
+
+    conn.execute(
+        "UPDATE tasks SET tags = (?1) WHERE id = (?2)",
+        &[&tags, &id.to_string()]
+    )?;
+
+    Ok(())
+}
+
 pub fn get_list_by_id(id_list: Vec<i32>) -> Result<Vec<Task>, rusqlite::Error> {
     let conn = Connection::open(get_directory())?;
     let mut tasks_vec: Vec<Task> = Vec::new();
@@ -134,6 +162,7 @@ pub fn get_list_by_id(id_list: Vec<i32>) -> Result<Vec<Task>, rusqlite::Error> {
                 task_name: row.get(1)?,
                 start_time: row.get(2)?,
                 stop_time: row.get(3)?,
+                tags: row.get(4)?,
             })
         })?;
 
diff --git a/src/gtk/preferences_window.ui b/src/gtk/preferences_window.ui
index 5c6140c..e288c52 100755
--- a/src/gtk/preferences_window.ui
+++ b/src/gtk/preferences_window.ui
@@ -63,6 +63,18 @@
             <property name="title" translatable="yes">Task List</property>
             <property name="visible">True</property>
             <child>
+              <object class="AdwActionRow">
+                <property name="title" translatable="yes">_Delete confirmation</property>
+                <property name="use_underline">True</property>
+                <property name="activatable_widget">delete_confirmation_switch</property>
+                <child>
+                  <object class="GtkSwitch" id="delete_confirmation_switch">
+                    <property name="valign">center</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
               <object class="AdwExpanderRow" id="limit_tasks_expander">
                 <property name="title" translatable="yes">Limit tasks shown</property>
                 <property name="subtitle" translatable="yes">Only show X number of days in the saved tasks list</property>
@@ -93,11 +105,11 @@
             </child>
             <child>
               <object class="AdwActionRow">
-                <property name="title" translatable="yes">_Delete confirmation</property>
+                <property name="title" translatable="yes">Show daily sums</property>
                 <property name="use_underline">True</property>
-                <property name="activatable_widget">delete_confirmation_switch</property>
+                <property name="activatable_widget">show_daily_sums_switch</property>
                 <child>
-                  <object class="GtkSwitch" id="delete_confirmation_switch">
+                  <object class="GtkSwitch" id="show_daily_sums_switch">
                     <property name="valign">center</property>
                   </object>
                 </child>
@@ -116,18 +128,21 @@
                 </child>
               </object>
             </child>
+
             <child>
               <object class="AdwActionRow">
-                <property name="title" translatable="yes">Show daily sums</property>
+                <property name="title" translatable="yes">Show tags</property>
                 <property name="use_underline">True</property>
-                <property name="activatable_widget">show_daily_sums_switch</property>
+                <property name="activatable_widget">show_tags_switch</property>
+                <property name="subtitle" translatable="yes">Tags are saved, just not shown</property>
                 <child>
-                  <object class="GtkSwitch" id="show_daily_sums_switch">
+                  <object class="GtkSwitch" id="show_tags_switch">
                     <property name="valign">center</property>
                   </object>
                 </child>
               </object>
             </child>
+
           </object>
         </child>
       </object>
diff --git a/src/gtk/task_row.ui b/src/gtk/task_row.ui
index edd2899..f5de3a5 100755
--- a/src/gtk/task_row.ui
+++ b/src/gtk/task_row.ui
@@ -2,7 +2,7 @@
 <interface>
   <template class="FurTaskRow" parent="GtkListBoxRow">
     <child>
-      <object class="GtkBox">
+      <object class="GtkBox" id="row_box">
         <property name="orientation">horizontal</property>
         <property name="margin_top">10</property>
         <property name="margin_bottom">10</property>
@@ -13,14 +13,30 @@
         <property name="valign">center</property>
         <property name="homogeneous">True</property>
         <child>
-          <object class="GtkLabel" id="task_name_label">
-            <property name="halign">start</property>
-            <property name="label" translatable="yes">Task</property>
-            <property name="ellipsize">end</property>
-            <property name="single_line_mode">True</property>
-            <style>
-              <class name="heading"/>
-            </style>
+          <object class="GtkBox">
+            <property name="orientation">vertical</property>
+            <property name="spacing">3</property>
+            <child>
+              <object class="GtkLabel" id="task_name_label">
+                <property name="halign">start</property>
+                <property name="label" translatable="yes">Task</property>
+                <property name="ellipsize">end</property>
+                <property name="single_line_mode">True</property>
+                <style>
+                  <class name="heading"/>
+                </style>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="task_tags_label">
+                <property name="halign">start</property>
+                <property name="ellipsize">end</property>
+                <property name="single_line_mode">True</property>
+                <style>
+                  <class name="subtitle"/>
+                </style>
+              </object>
+            </child>
           </object>
         </child>
         <child>
diff --git a/src/gtk/window.ui b/src/gtk/window.ui
index 4da1cd3..7317b55 100755
--- a/src/gtk/window.ui
+++ b/src/gtk/window.ui
@@ -55,7 +55,7 @@
                   <property name="margin_end">8</property>
                   <child>
                     <object class="GtkEntry" id="task_input">
-                      <property name="placeholder-text" translatable="yes">Task Name</property>
+                      <property name="placeholder-text" translatable="yes">Task Name #Tags</property>
                       <property name="hexpand">True</property>
                       <property name="hexpand-set">True</property>
                     </object>
diff --git a/src/ui/preferences_window.rs b/src/ui/preferences_window.rs
index 1955d09..47a7adc 100755
--- a/src/ui/preferences_window.rs
+++ b/src/ui/preferences_window.rs
@@ -55,6 +55,8 @@ mod imp {
         pub show_seconds_switch: TemplateChild<gtk::Switch>,
         #[template_child]
         pub show_daily_sums_switch: TemplateChild<gtk::Switch>,
+        #[template_child]
+        pub show_tags_switch: TemplateChild<gtk::Switch>,
     }
 
     #[glib::object_subclass]
@@ -167,6 +169,12 @@ impl FurPreferencesWindow {
             "active"
         );
 
+        settings_manager::bind_property(
+            "show-tags",
+            &*imp.show_tags_switch,
+            "active"
+        );
+
         imp.dark_theme_switch.connect_active_notify(move |_|{
             let app = FurtheranceApplication::default();
             app.update_light_dark();
@@ -191,6 +199,11 @@ impl FurPreferencesWindow {
             let window = FurtheranceWindow::default();
             window.reset_history_box();
         });
+
+        imp.show_tags_switch.connect_active_notify(move |_|{
+            let window = FurtheranceWindow::default();
+            window.reset_history_box();
+        });
     }
 }
 
diff --git a/src/ui/task_details.rs b/src/ui/task_details.rs
index d96a993..e462eb4 100755
--- a/src/ui/task_details.rs
+++ b/src/ui/task_details.rs
@@ -20,6 +20,7 @@ use glib::clone;
 use gtk::subclass::prelude::*;
 use gtk::{glib, prelude::*, CompositeTemplate};
 use chrono::{DateTime, NaiveDateTime, Local, ParseError, offset::TimeZone};
+use itertools::Itertools;
 
 use crate::FurtheranceApplication;
 use crate::ui::FurtheranceWindow;
@@ -172,6 +173,14 @@ impl FurTaskDetails {
                 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 task_tags_edit = gtk::Entry::new();
+                let tags_placeholder = format!("#{}", &gettext("Tags"));
+                task_tags_edit.set_placeholder_text(Some(&tags_placeholder));
+                let mut task_tags: String = task.tags.clone();
+                if !task.tags.trim().is_empty() {
+                    task_tags = format!("#{}", task.tags);
+                    task_tags_edit.set_text(&task_tags);
+                }
                 let labels_box = gtk::Box::new(gtk::Orientation::Horizontal, 5);
                 labels_box.set_homogeneous(true);
                 let start_label = gtk::Label::new(Some(&gettext("Start")));
@@ -220,6 +229,7 @@ impl FurTaskDetails {
                 delete_task_btn.set_halign(gtk::Align::End);
 
                 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);
@@ -276,9 +286,10 @@ impl FurTaskDetails {
 
                 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| {
+                        @strong task.task_name as name,
+                        @strong task.start_time as start_time,
+                        @strong task.stop_time as stop_time,
+                        @strong task.tags as tags => move |_ , resp| {
                         if resp == gtk::ResponseType::Ok {
                             instructions.set_visible(false);
                             time_error.set_visible(false);
@@ -333,7 +344,7 @@ impl FurTaskDetails {
                                             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.");
+                                                .expect("Failed to update start time.");
                                         }
                                     } else {
                                         let old_start_time = DateTime::parse_from_rfc3339(&start_time);
@@ -354,7 +365,22 @@ impl FurTaskDetails {
                             }
                             if task_name_edit.text() != name {
                                 database::update_task_name(task.id, task_name_edit.text().to_string())
-                                    .expect("Failed to update start time.");
+                                    .expect("Failed to update task name.");
+                            }
+
+                            if task_tags_edit.text() != task_tags {
+                                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();
+	                            let new_tag_list = lower_tags.join(" #");
+                                database::update_tags(task.id, new_tag_list)
+                                    .expect("Failed to update tags.");
                             }
 
                             if start_successful && !stop_successful {
@@ -367,7 +393,6 @@ impl FurTaskDetails {
                                     time_error.set_visible(true);
                                     do_not_close = true;
                                 }
-
                             }
 
                             if !do_not_close {
@@ -375,7 +400,6 @@ impl FurTaskDetails {
                                 dialog.close();
                             }
 
-
                         } else {
                             // If Cancel, close dialog and do nothing.
                             dialog.close();
@@ -383,7 +407,6 @@ impl FurTaskDetails {
                     }),
                 );
 
-
                 dialog.show();
             }));
 
diff --git a/src/ui/task_row.rs b/src/ui/task_row.rs
index fe8db68..0e46d81 100755
--- a/src/ui/task_row.rs
+++ b/src/ui/task_row.rs
@@ -35,8 +35,12 @@ mod imp {
     #[template(resource = "/com/lakoliu/Furtherance/gtk/task_row.ui")]
     pub struct FurTaskRow {
         #[template_child]
+        pub row_box: TemplateChild<gtk::Box>,
+        #[template_child]
         pub task_name_label: TemplateChild<gtk::Label>,
         #[template_child]
+        pub task_tags_label: TemplateChild<gtk::Label>,
+        #[template_child]
         pub total_time_label: TemplateChild<gtk::Label>,
 
         pub tasks: Lazy<Mutex<Vec<Task>>>,
@@ -102,16 +106,27 @@ impl FurTaskRow {
         for task in task_list.clone() {
             imp.tasks.lock().unwrap().push(task);
         }
-        let task_name_text = &task_list[0].task_name;
+
+        // Display task's name
         imp.task_name_label.set_text(&imp.tasks.lock().unwrap()[0].task_name);
 
+        // Display task's tags
+        if task_list[0].tags.trim().is_empty() || !settings_manager::get_bool("show-tags") {
+            imp.task_tags_label.hide();
+        } else {
+            let task_tags = format!("#{}", task_list[0].tags);
+            imp.task_tags_label.set_text(&task_tags);
+            imp.row_box.set_margin_top(5);
+            imp.row_box.set_margin_bottom(5);
+        }
+
         // Create right-click gesture
         let gesture = gtk::GestureClick::new();
         gesture.set_button(gtk::gdk::ffi::GDK_BUTTON_SECONDARY as u32);
-        gesture.connect_pressed(clone!(@strong task_name_text => move |gesture, _, _, _| {
+        gesture.connect_pressed(clone!(@strong task_list => move |gesture, _, _, _| {
             gesture.set_state(gtk::EventSequenceState::Claimed);
             let window = FurtheranceWindow::default();
-            window.duplicate_task(task_name_text.to_string());
+            window.duplicate_task(task_list[0].clone());
         }));
 
         self.add_controller(&gesture);
@@ -135,7 +150,7 @@ impl FurTaskRow {
         if !settings_manager::get_bool("show-seconds") {
             total_time_str = format!("{:02}:{:02}", h, m);
         }
-
+        // Display task's total time
         imp.total_time_label.set_text(&total_time_str);
     }
 
diff --git a/src/ui/tasks_group.rs b/src/ui/tasks_group.rs
index d6da39c..2453991 100755
--- a/src/ui/tasks_group.rs
+++ b/src/ui/tasks_group.rs
@@ -20,6 +20,7 @@ use gtk::{glib, prelude::*};
 
 use crate::ui::FurTaskRow;
 use crate::database;
+use crate::settings_manager;
 
 mod imp {
     use super::*;
@@ -85,7 +86,10 @@ impl FurTasksGroup {
         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 {
+                if tasks_by_name[i][0].task_name == task.task_name
+                    && ( ( settings_manager::get_bool("show-tags")
+                        && tasks_by_name[i][0].tags == task.tags ) ||
+                            !settings_manager::get_bool("show-tags") ) {
                     tasks_by_name[i].push(task.clone());
                     unique = false;
                 }
diff --git a/src/ui/window.rs b/src/ui/window.rs
index 9052e34..964cf64 100755
--- a/src/ui/window.rs
+++ b/src/ui/window.rs
@@ -26,6 +26,7 @@ use std::rc::Rc;
 use std::cell::RefCell;
 use chrono::{DateTime, Local, Duration as ChronDur};
 use dbus::blocking::Connection;
+use itertools::Itertools;
 
 use crate::ui::FurHistoryBox;
 use crate::FurtheranceApplication;
@@ -119,6 +120,7 @@ impl FurtheranceWindow {
         }
     }
 
+    // TODO remove function, just use set_sensitive
     fn activate_task_input(&self, sensitive: bool) {
         // Deactivate task_input while timer is running
         let imp = imp::FurtheranceWindow::from_instance(self);
@@ -135,7 +137,23 @@ impl FurtheranceWindow {
             *imp.subtract_idle.lock().unwrap() = false;
         }
 
-        let _ = database::db_write(&imp.task_input.text().trim(), start_time, stop_time);
+        // let task_input_text = imp.task_input.text().trim();
+        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);
         imp.task_input.set_text("");
         imp.history_box.create_tasks_page();
     }
@@ -178,7 +196,9 @@ impl FurtheranceWindow {
 
         imp.task_input.connect_changed(clone!(@weak self as this => move |task_input| {
             let imp2 = imp::FurtheranceWindow::from_instance(&this);
-            if task_input.text().trim().is_empty() {
+            let task_input_text = task_input.text();
+            let split_tags: Vec<&str> = task_input_text.trim().split("#").collect();
+            if split_tags[0].trim().is_empty() {
                 imp2.start_button.set_sensitive(false);
             } else {
                 imp2.start_button.set_sensitive(true);
@@ -337,13 +357,19 @@ impl FurtheranceWindow {
         *imp.subtract_idle.lock().unwrap() = val;
     }
 
-    pub fn duplicate_task(&self, task_name_text: String) {
+    pub fn duplicate_task(&self, task: database::Task) {
         let imp = imp::FurtheranceWindow::from_instance(self);
         if !*imp.running.lock().unwrap() {
-            imp.task_input.set_text(&task_name_text);
+            let task_text: String;
+            if task.tags.trim().is_empty() {
+                task_text = task.task_name;
+            } else {
+                task_text = format!("{} #{}", task.task_name, task.tags);
+            }
+            imp.task_input.set_text(&task_text);
             imp.start_button.emit_clicked();
         } else {
-            self.display_toast("Stop the timer to duplicate a task.");
+            self.display_toast(&gettext("Stop the timer to duplicate a task."));
         }
     }
 }