diff options
-rwxr-xr-x | Cargo.lock | 16 | ||||
-rwxr-xr-x | Cargo.toml | 1 | ||||
-rwxr-xr-x | data/com.lakoliu.Furtherance.gschema.xml | 3 | ||||
-rwxr-xr-x | src/application.rs | 3 | ||||
-rwxr-xr-x | src/database.rs | 37 | ||||
-rwxr-xr-x | src/gtk/preferences_window.ui | 27 | ||||
-rwxr-xr-x | src/gtk/task_row.ui | 34 | ||||
-rwxr-xr-x | src/gtk/window.ui | 2 | ||||
-rwxr-xr-x | src/ui/preferences_window.rs | 13 | ||||
-rwxr-xr-x | src/ui/task_details.rs | 39 | ||||
-rwxr-xr-x | src/ui/task_row.rs | 23 | ||||
-rwxr-xr-x | src/ui/tasks_group.rs | 6 | ||||
-rwxr-xr-x | src/ui/window.rs | 36 |
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.")); } } } |