about summary refs log tree commit diff
diff options
context:
space:
mode:
authorLakoLiu <99976966+lakoliu@users.noreply.github.com>2023-10-13 09:26:09 +0200
committerGitHub <noreply@github.com>2023-10-13 09:26:09 +0200
commit03b910060da4171209dcc834f0065a0fbc038224 (patch)
tree2ec472aeebd7d12d9c9ceb43c73122e8a61f5aca
parent28a73d4af630398cfa0818bdefa57e50f76dc4f4 (diff)
parent14ae87d469555af8301356f99a11c2254edee1a4 (diff)
downloadFurtherance-03b910060da4171209dcc834f0065a0fbc038224.tar.zst
Merge pull request #118 from jlledom/autocomplete
Implement task name autocompletion
-rw-r--r--src/database.rs35
-rw-r--r--src/ui/window.rs32
2 files changed, 65 insertions, 2 deletions
diff --git a/src/database.rs b/src/database.rs
index 7050cbb..2f97d2d 100644
--- a/src/database.rs
+++ b/src/database.rs
@@ -38,6 +38,12 @@ pub struct Task {
     pub tags: String,
 }
 
+impl ToString for Task {
+    fn to_string(&self) -> String {
+        format!("{} #{}", self.task_name, self.tags)
+    }
+}
+
 #[derive(
     Debug,
     Clone,
@@ -343,6 +349,35 @@ pub fn get_list_by_id(id_list: Vec<i32>) -> Result<Vec<Task>, rusqlite::Error> {
     Ok(tasks_vec)
 }
 
+pub fn get_list_by_name_and_tags(task_name: String, tag_list: Vec<String>) -> Result<Vec<Task>, rusqlite::Error> {
+    let conn = Connection::open(get_directory())?;
+
+    let name_param = format!("%{}%", task_name);
+    let tag_list_params: Vec<String> = tag_list.iter().map(|tag| format!("%{}%", tag)).collect();
+
+    let mut sql_query = String::from("SELECT * FROM tasks WHERE lower(task_name) LIKE lower(?)");
+    tag_list_params.iter().for_each(|_| sql_query.push_str(" AND lower(tags) LIKE lower(?)"));
+    sql_query.push_str(" ORDER BY task_name");
+
+    let mut query = conn.prepare(sql_query.as_str())?;
+    query.raw_bind_parameter(1, name_param)?;
+    for (i, tag) in tag_list_params.iter().enumerate() {
+        query.raw_bind_parameter(i + 2, tag)?;
+    }
+
+    let tasks_vec = query.raw_query().mapped(|row| {
+        Ok(Task {
+            id: row.get(0)?,
+            task_name: row.get(1)?,
+            start_time: row.get(2)?,
+            stop_time: row.get(3)?,
+            tags: row.get(4)?,
+        })
+    }).map(|task_item| task_item.unwrap()).collect();
+
+    Ok(tasks_vec)
+}
+
 pub fn check_for_tasks() -> Result<String> {
     let conn = Connection::open(get_directory())?;
 
diff --git a/src/ui/window.rs b/src/ui/window.rs
index 2270711..8972ed9 100644
--- a/src/ui/window.rs
+++ b/src/ui/window.rs
@@ -115,6 +115,8 @@ glib::wrapper! {
 }
 
 impl FurtheranceWindow {
+    const MIN_PREFIX_LENGTH: i32 = 3;
+
     pub fn new(app: &Application) -> Self {
         glib::Object::builder()
             .property("application", Some(app))
@@ -182,6 +184,13 @@ impl FurtheranceWindow {
         imp.start_button.set_sensitive(false);
         imp.start_button.add_css_class("suggested-action");
         self.refresh_timer();
+
+        let task_autocomplete = gtk::EntryCompletion::new();
+        task_autocomplete.set_text_column(0);
+        task_autocomplete.set_minimum_key_length(FurtheranceWindow::MIN_PREFIX_LENGTH);
+        task_autocomplete.set_match_func(|_ac, _s, _it| { true });
+        imp.task_input.set_completion(Some(&task_autocomplete));
+
         imp.task_input.grab_focus();
 
         if settings_manager::get_bool("autosave") {
@@ -199,12 +208,19 @@ impl FurtheranceWindow {
             .connect_changed(clone!(@weak self as this => move |task_input| {
                 let imp2 = imp::FurtheranceWindow::from_obj(&this);
                 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() {
+                let mut split_tags: Vec<String> = task_input_text.split('#').map(|tag| String::from(tag.trim())).collect();
+                let task_name = split_tags.remove(0);
+                if task_name.is_empty() {
                     imp2.start_button.set_sensitive(false);
                 } else {
                     imp2.start_button.set_sensitive(true);
                 }
+
+                if task_input.text().len() >= FurtheranceWindow::MIN_PREFIX_LENGTH.try_into().unwrap() {
+                    let task_autocomplete = task_input.completion().unwrap();
+                    let model = Self::update_list_model(task_name.to_string(), split_tags).unwrap();
+                    task_autocomplete.set_model(Some(&model));
+                }
             }));
 
         imp.start_button.connect_clicked(clone!(@weak self as this => move |button| {
@@ -528,6 +544,18 @@ impl FurtheranceWindow {
         imp.task_input.set_activates_default(true);
     }
 
+    fn update_list_model(task_name: String, tag_list: Vec<String>) -> Result<gtk::ListStore, anyhow::Error> {
+        let col_types: [glib::Type; 1] = [glib::Type::STRING];
+        let mut task_list = database::get_list_by_name_and_tags(task_name, tag_list)?;
+        task_list.dedup_by(|a, b| a.task_name == b.task_name && a.tags == b.tags);
+        let store = gtk::ListStore::new(&col_types);
+
+        for task in task_list {
+            store.set(&store.append(), &[(0, &task.to_string())]);
+        }
+        Ok(store)
+    }
+
     fn get_idle_time(&self) -> Result<u64, Box<dyn std::error::Error>> {
         let c = Connection::new_session()?;