diff options
author | Ricky Kresslein <rk@lakoliu.com> | 2022-06-05 12:37:34 +0300 |
---|---|---|
committer | Ricky Kresslein <rk@lakoliu.com> | 2022-06-05 12:42:10 +0300 |
commit | 825a2662b1856931549fc2de1164e89b09c7f4aa (patch) | |
tree | 4fa17edbd2e366b5046f264e1aabd5220a8ba274 | |
parent | 8d85a8f7a7be6fcb3cb383d6e59f189864b81044 (diff) | |
download | Furtherance-825a2662b1856931549fc2de1164e89b09c7f4aa.tar.zst |
Reports feature (Issue #32)
-rwxr-xr-x | src/application.rs | 9 | ||||
-rwxr-xr-x | src/database.rs | 26 | ||||
-rwxr-xr-x | src/furtherance.gresource.xml | 9 | ||||
-rw-r--r-- | src/gtk/report.ui | 170 | ||||
-rwxr-xr-x | src/gtk/window.ui | 4 | ||||
-rwxr-xr-x | src/meson.build | 1 | ||||
-rwxr-xr-x | src/ui.rs | 18 | ||||
-rw-r--r-- | src/ui/report.rs | 470 | ||||
-rwxr-xr-x | src/ui/task_details.rs | 3 | ||||
-rwxr-xr-x | src/ui/tasks_group.rs | 3 |
10 files changed, 695 insertions, 18 deletions
diff --git a/src/application.rs b/src/application.rs index ce7a26f..6050af3 100755 --- a/src/application.rs +++ b/src/application.rs @@ -23,7 +23,7 @@ use log::debug; use std::sync::Mutex; use crate::config; -use crate::ui::{FurtheranceWindow, FurPreferencesWindow}; +use crate::ui::{FurtheranceWindow, FurPreferencesWindow, FurReport}; use crate::database; use crate::settings_manager; @@ -111,6 +111,13 @@ impl FurtheranceApplication { self.set_accels_for_action("app.preferences", &["<primary>comma"]); self.add_action(&preferences_action); + let report_action = gio::SimpleAction::new("report", None); + report_action.connect_activate(clone!(@weak self as app => move |_, _| { + FurReport::new().show(); + })); + self.set_accels_for_action("app.report", &["<primary>R"]); + self.add_action(&report_action); + let about_action = gio::SimpleAction::new("about", None); about_action.connect_activate(clone!(@weak self as app => move |_, _| { app.show_about(); diff --git a/src/database.rs b/src/database.rs index 69b56dc..a2485c6 100755 --- a/src/database.rs +++ b/src/database.rs @@ -120,6 +120,32 @@ pub fn retrieve() -> Result<Vec<Task>, rusqlite::Error> { } +// pub fn retrieve_date_range() -> Result<Vec<Task>, rusqlite::Error> { + // Retrieve all tasks from the database +// let conn = Connection::open(get_directory())?; + +// let mut query = conn.prepare("SELECT * FROM tasks ORDER BY start_time")?; +// let task_iter = query.query_map([], |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)?, +// }) +// })?; + +// let mut tasks_vec: Vec<Task> = Vec::new(); +// for task_item in task_iter { +// let start = DateTime::parse_from_rfc3339(&task_item.start_time).unwrap(); +// let stop = DateTime::parse_from_rfc3339(&task_item.stop_time).unwrap(); +// tasks_vec.push(task_item.unwrap()); +// } + +// Ok(tasks_vec) + +// } + pub fn update_start_time(id: i32, start_time: String) -> Result<()> { let conn = Connection::open(get_directory())?; diff --git a/src/furtherance.gresource.xml b/src/furtherance.gresource.xml index bfab2ff..bb1a9fb 100755 --- a/src/furtherance.gresource.xml +++ b/src/furtherance.gresource.xml @@ -1,13 +1,14 @@ <?xml version="1.0" encoding="UTF-8"?> <gresources> <gresource prefix="/com/lakoliu/Furtherance"> - <file>gtk/window.ui</file> <file>gtk/history_box.ui</file> + <file>gtk/preferences_window.ui</file> + <file>gtk/report.ui</file> <file>gtk/style.css</file> - <file>gtk/tasks_page.ui</file> + <file>gtk/task_details.ui</file> <file>gtk/tasks_group.ui</file> + <file>gtk/tasks_page.ui</file> <file>gtk/task_row.ui</file> - <file>gtk/task_details.ui</file> - <file>gtk/preferences_window.ui</file> + <file>gtk/window.ui</file> </gresource> </gresources> diff --git a/src/gtk/report.ui b/src/gtk/report.ui new file mode 100644 index 0000000..9d4d5c8 --- /dev/null +++ b/src/gtk/report.ui @@ -0,0 +1,170 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="FurReport" parent="AdwWindow"> + <property name="width-request">450</property> + <property name="height-request">600</property> + <property name="default-width">450</property> + <property name="default-height">600</property> + <property name="title" translatable="yes">Report</property> + <property name="modal">True</property> + <style> + <class name="report"/> + </style> + <property name="content"> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <child type="overlay"> + <object class="GtkHeaderBar"> + <style> + <class name="hidden"/> + <class name="flat-headerbar"/> + </style> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <property name="spacing">8</property> + <property name="margin-top">10</property> + <child> + <object class="GtkComboBoxText" id="range_combo"> + <property name="halign">center</property> + <items> + <item translatable="yes" id="week_item">Past week</item> + <item translatable="yes" id="month_item">This month</item> + <item translatable="yes" id="30_days_item">Past 30 days</item> + <item translatable="yes" id="six_months_item">Past 180 days</item> + <item translatable="yes" id="year_item">Past year</item> + <item translatable="yes" id="date_range_item">Date range</item> + </items> + </object> + </child> + <child> + <object class="GtkBox" id="date_range_box"> + <property name="spacing">8</property> + <property name="halign">center</property> + <property name="visible">False</property> + <child> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Start</property> + <property name="halign">start</property> + </object> + </child> + <child> + <object class="GtkEntry" id="start_date_entry"> + <property name="placeholder-text" translatable="yes">MM/DD/YYYY</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">End</property> + <property name="halign">start</property> + </object> + </child> + <child> + <object class="GtkEntry" id="end_date_entry"> + <property name="placeholder-text" translatable="yes">MM/DD/YYYY</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkLabel" id="format_error"> + <property name="label" translatable="yes">Use the format MM/DD/YYYY</property> + <property name="visible">False</property> + <style> + <class name="error_message"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="start_end_error"> + <property name="label" translatable="yes">Start date must be before end date</property> + <property name="visible">False</property> + <style> + <class name="error_message"/> + </style> + </object> + </child> + <child> + <object class="GtkBox" id="sort_by_box"> + <property name="spacing">6</property> + <property name="halign">center</property> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Sort by:</property> + </object> + </child> + <child> + <object class="GtkCheckButton" id="sort_by_task"> + <property name="label" translatable="yes">Task</property> + <property name="active">True</property> + <property name="group">sort_by_tag</property> + </object> + </child> + <child> + <object class="GtkCheckButton" id="sort_by_tag"> + <property name="label" translatable="yes">Tag</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkCheckButton" id="filter_check"> + <property name="label" translatable="yes">Filter by task or tags</property> + <property name="halign">center</property> + </object> + </child> + <child> + <object class="GtkBox" id="filter_box"> + <property name="spacing">6</property> + <property name="visible">False</property> + <property name="halign">center</property> + <child> + <object class="GtkComboBoxText" id="filter_combo"> + <items> + <item translatable="yes" id="tasks_item">Tasks</item> + <item translatable="yes" id="tags_item">Tags</item> + </items> + </object> + </child> + <child> + <object class="GtkEntry" id="filter_entry"> + <property name="placeholder-text" translatable="yes">Task, Task 2</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkButton" id="refresh_btn"> + <property name="label" translatable="yes">Refresh</property> + <property name="halign">center</property> + </object> + </child> + <child> + <object class="GtkScrolledWindow"> + <property name="vexpand">true</property> + <child> + <object class="GtkTreeView" id="results_tree"> + + </object> + </child> + </object> + </child> + + </object> + </child> + </object> + </property> + </template> +</interface> diff --git a/src/gtk/window.ui b/src/gtk/window.ui index 963922a..c403358 100755 --- a/src/gtk/window.ui +++ b/src/gtk/window.ui @@ -93,6 +93,10 @@ <attribute name="action">app.preferences</attribute> </item> <item> + <attribute name="label" translatable="yes">_Time Report</attribute> + <attribute name="action">app.report</attribute> + </item> + <item> <attribute name="label" translatable="yes">_Delete history</attribute> <attribute name="action">app.delete-history</attribute> </item> diff --git a/src/meson.build b/src/meson.build index c689689..3afd404 100755 --- a/src/meson.build +++ b/src/meson.build @@ -33,6 +33,7 @@ run_command( rust_sources = files( 'ui.rs', 'ui/preferences_window.rs', + 'ui/report.rs', 'ui/task_details.rs', 'ui/task_row.rs', 'ui/tasks_group.rs', diff --git a/src/ui.rs b/src/ui.rs index 1a3e3fc..36ba710 100755 --- a/src/ui.rs +++ b/src/ui.rs @@ -14,18 +14,20 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. -pub mod window; mod history_box; -mod tasks_page; +mod preferences_window; +mod report; +mod task_details; mod tasks_group; +mod tasks_page; mod task_row; -mod task_details; -mod preferences_window; +pub mod window; -pub use window::FurtheranceWindow; pub use history_box::FurHistoryBox; -pub use tasks_page::FurTasksPage; +pub use preferences_window::FurPreferencesWindow; +pub use report::FurReport; +pub use task_details::FurTaskDetails; pub use tasks_group::FurTasksGroup; +pub use tasks_page::FurTasksPage; pub use task_row::FurTaskRow; -pub use task_details::FurTaskDetails; -pub use preferences_window::FurPreferencesWindow; +pub use window::FurtheranceWindow; diff --git a/src/ui/report.rs b/src/ui/report.rs new file mode 100644 index 0000000..215aea0 --- /dev/null +++ b/src/ui/report.rs @@ -0,0 +1,470 @@ +// 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 gettextrs::*; +use glib::clone; +use gtk::subclass::prelude::*; +use gtk::{glib, prelude::*, CompositeTemplate}; +use chrono::{DateTime, NaiveDate, Local, Duration, Date, Datelike, offset::TimeZone}; +use itertools::Itertools; + +use crate::FurtheranceApplication; +use crate::ui::FurtheranceWindow; +use crate::database; + +mod imp { + use super::*; + use glib::subclass; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/com/lakoliu/Furtherance/gtk/report.ui")] + pub struct FurReport { + #[template_child] + pub range_combo: TemplateChild<gtk::ComboBoxText>, + #[template_child] + pub date_range_box: TemplateChild<gtk::Box>, + #[template_child] + pub start_date_entry: TemplateChild<gtk::Entry>, + #[template_child] + pub end_date_entry: TemplateChild<gtk::Entry>, + #[template_child] + pub format_error: TemplateChild<gtk::Label>, + #[template_child] + pub start_end_error: TemplateChild<gtk::Label>, + #[template_child] + pub filter_check: TemplateChild<gtk::CheckButton>, + #[template_child] + pub filter_box: TemplateChild<gtk::Box>, + #[template_child] + pub filter_combo: TemplateChild<gtk::ComboBoxText>, + #[template_child] + pub filter_entry: TemplateChild<gtk::Entry>, + #[template_child] + pub results_tree: TemplateChild<gtk::TreeView>, + #[template_child] + pub sort_by_box: TemplateChild<gtk::Box>, + #[template_child] + pub sort_by_task: TemplateChild<gtk::CheckButton>, + #[template_child] + pub sort_by_tag: TemplateChild<gtk::CheckButton>, + #[template_child] + pub refresh_btn: TemplateChild<gtk::Button>, + } + + #[glib::object_subclass] + impl ObjectSubclass for FurReport { + const NAME: &'static str = "FurReport"; + type ParentType = adw::Window; + type Type = super::FurReport; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &subclass::InitializingObject<Self>) { + obj.init_template(); + } + } + + impl ObjectImpl for FurReport { + + fn constructed(&self, obj: &Self::Type) { + obj.setup_widgets(); + self.parent_constructed(obj); + } + } + + impl WidgetImpl for FurReport {} + + impl WindowImpl for FurReport {} + + impl AdwWindowImpl for FurReport {} +} + +glib::wrapper! { + pub struct FurReport(ObjectSubclass<imp::FurReport>) + @extends gtk::Widget, gtk::Window, adw::Window; +} + +impl FurReport { + 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) { + let imp = imp::FurReport::from_instance(self); + + imp.range_combo.set_active_id(Some("week_item")); + imp.filter_combo.set_active_id(Some("tasks_item")); + + imp.range_combo.connect_changed(clone!(@weak self as this => move |combo|{ + let imp = imp::FurReport::from_instance(&this); + if combo.active_id().unwrap() != "date_range_item" { + imp.date_range_box.set_visible(false); + this.refresh_report(); + } else { + imp.date_range_box.set_visible(true); + } + })); + + imp.filter_check.connect_toggled(clone!(@weak self as this => move |_|{ + let imp = imp::FurReport::from_instance(&this); + if imp.filter_box.get_visible() { + imp.filter_box.set_visible(false); + } else { + imp.filter_box.set_visible(true); + } + })); + + imp.filter_combo.connect_changed(clone!(@weak self as this => move |combo|{ + let imp = imp::FurReport::from_instance(&this); + if combo.active_id().unwrap() == "tasks_item" { + imp.filter_entry.set_placeholder_text(Some(&gettext("Task, Task 2"))); + } else { + imp.filter_entry.set_placeholder_text(Some(&gettext("tag, tag 2"))); + } + })); + + imp.refresh_btn.connect_clicked(clone!(@weak self as this => move |_|{ + this.refresh_report(); + })); + + let renderer = gtk::CellRendererText::new(); + let task_column = gtk::TreeViewColumn::with_attributes(&gettext("Task"), &renderer, &[("text", 0)]); + task_column.set_expand(true); + task_column.set_resizable(true); + let duration_column = gtk::TreeViewColumn::with_attributes(&gettext("Duration"), &renderer, &[("text", 1)]); + duration_column.set_expand(false); + duration_column.set_resizable(true); + imp.results_tree.append_column(&task_column); + imp.results_tree.append_column(&duration_column); + imp.results_tree.set_enable_search(false); + + self.refresh_report(); + } + + fn refresh_report(&self) { + let imp = imp::FurReport::from_instance(self); + imp.format_error.set_visible(false); + imp.start_end_error.set_visible(false); + + let results_model = gtk::TreeStore::new(&[String::static_type(), String::static_type()]); + + let mut task_list = database::retrieve().unwrap(); + task_list.reverse(); + + // Get date range + let active_range = imp.range_combo.active_id().unwrap(); + let today = Local::today(); + let range_start_date: Date<Local>; + let mut range_end_date = today; + if active_range == "week_item" { + range_start_date = today - Duration::days(6); + } else if active_range == "month_item" { + let days_ago = today.day() - 1; + range_start_date = today - Duration::days(days_ago.into()); + } else if active_range == "30_days_item" { + range_start_date = today - Duration::days(30); + } else if active_range == "six_months_item" { + range_start_date = today - Duration::days(180); + } else if active_range == "year_item" { + range_start_date = today - Duration::days(365); + } else { + let input_start_date = NaiveDate::parse_from_str(&imp.start_date_entry.text(), "%m/%d/%Y"); + let input_end_date = NaiveDate::parse_from_str(&imp.end_date_entry.text(), "%m/%d/%Y"); + // Check if user entered dates properly + if let Err(_) = input_start_date { + imp.format_error.set_visible(true); + results_model.clear(); + imp.results_tree.set_model(Some(&results_model)); + return + } + if let Err(_) = input_end_date { + imp.format_error.set_visible(true); + results_model.clear(); + imp.results_tree.set_model(Some(&results_model)); + return + } + // Start date cannot be after end date + if (input_end_date.unwrap() - input_start_date.unwrap()).num_days() < 0 { + imp.start_end_error.set_visible(true); + results_model.clear(); + return + } + range_start_date = Local.from_local_date(&input_start_date.unwrap()).unwrap(); + range_end_date = Local.from_local_date(&input_end_date.unwrap()).unwrap(); + } + + let mut total_time: i64 = 0; + let mut tasks_in_range: Vec<(database::Task, i64)> = Vec::new(); + let mut user_chosen_tags: Vec<String> = Vec::new(); + let mut only_this_tag = false; + for task in task_list { + let start = DateTime::parse_from_rfc3339(&task.start_time).unwrap().with_timezone(&Local); + let stop = DateTime::parse_from_rfc3339(&task.stop_time).unwrap().with_timezone(&Local); + // Check if start time is in date range and if not remove it from task_list + let start_date = start.date(); + if start_date >= range_start_date && start_date <= range_end_date { + // Sort by only selected tasks or tags if filter is selected + if imp.filter_check.is_active() && !imp.filter_entry.text().trim().is_empty() { + if imp.filter_combo.active_id().unwrap() == "tasks_item" { + // Create a vec of tasks to match from written tasks + let chosen_tasks = imp.filter_entry.text(); + let mut split_tasks: Vec<&str> = chosen_tasks.trim().split(",").collect(); + // Trim whitespace around each task + split_tasks = split_tasks.iter().map(|x| x.trim()).collect(); + // Don't allow empty tasks + split_tasks.retain(|&x| !x.trim().is_empty()); + // Handle duplicate tasks + split_tasks = split_tasks.into_iter().unique().collect(); + // Lowercase tags + let lower_tasks: Vec<String> = split_tasks.iter().map(|x| x.to_lowercase()).collect(); + + if lower_tasks.contains(&task.task_name.to_lowercase()) { + let duration = stop - start; + let duration = duration.num_seconds(); + tasks_in_range.push((task, duration)); + total_time += duration; + } + + } else if imp.filter_combo.active_id().unwrap() == "tags_item" { + // Split user chosen tags + let chosen_tasgs = imp.filter_entry.text(); + let mut split_tags: Vec<&str> = chosen_tasgs.trim().split(",").collect(); + // 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 + split_tags = split_tags.into_iter().unique().collect(); + // Lowercase tags + user_chosen_tags = split_tags.iter().map(|x| x.to_lowercase()).collect(); + + // Split task's tags + let mut split_tags: Vec<&str> = task.tags.trim().split("#").collect(); + // Trim whitespace around each tag + split_tags = split_tags.iter().map(|x| x.trim()).collect(); + + // Only keep tasks that contain the user's chosen tags + split_tags.retain(|&x| user_chosen_tags.contains(&x.to_string())); + if !split_tags.is_empty() { + let duration = stop - start; + let duration = duration.num_seconds(); + tasks_in_range.push((task, duration)); + total_time += duration; + } + + only_this_tag = true; + } + } else { + let duration = stop - start; + let duration = duration.num_seconds(); + tasks_in_range.push((task, duration)); + total_time += duration; + } + } + } + + let all_tasks_iter:gtk::TreeIter; + if tasks_in_range.is_empty() { + all_tasks_iter = results_model.insert_with_values(None, + None, + &[ + (0, &gettext("No Results")), + (1, &"") + ]); + } else { + let total_time_str = FurReport::format_duration(total_time); + all_tasks_iter = results_model.insert_with_values(None, + None, + &[ + (0, &gettext("All Results")), + (1, &total_time_str) + ]); + } + // TODO Bold "All Results" + + if imp.sort_by_task.is_active() { + let mut tasks_by_name: Vec<Vec<(database::Task, i64)>> = Vec::new(); + for (task, task_duration) in tasks_in_range { + let mut unique = true; + for i in 0..tasks_by_name.len() { + let (tbn, _) = &tasks_by_name[i][0]; + if tbn.task_name == task.task_name { + tasks_by_name[i].push((task.clone(), task_duration)); + unique = false; + } + } + if unique { + // Add unique task to list for group name + let mut new_name_list: Vec<(database::Task, i64)> = Vec::new(); + new_name_list.push((task.clone(), task_duration)); + tasks_by_name.push(new_name_list); + } + } + + let mut sorted_tasks_by_duration: Vec<(String, i64, Vec<(String, i64)>)> = Vec::new(); + for tbn in tasks_by_name { + let mut total_duration: i64 = 0; + let mut tags_dur: Vec<(String, i64)> = Vec::new(); + let task_name = tbn[0].0.task_name.to_string(); + for tbn_tuple in tbn { + let (task, task_duration) = tbn_tuple; + total_duration += task_duration; + + let mut split_tags: Vec<&str> = task.tags.split("#").collect(); + split_tags = split_tags.iter().map(|x| x.trim()).collect(); + split_tags.retain(|&x| !x.trim().is_empty()); + if !split_tags.is_empty() { + let mut formatted_tags = split_tags.join(" #"); + formatted_tags = format!("#{}", formatted_tags); + let mut unique = true; + for i in 0..tags_dur.len() { + let (tags, dur) = &tags_dur[i]; + if tags == &formatted_tags { + let new_dur = dur + task_duration; + tags_dur[i] = (formatted_tags.clone(), new_dur); + unique = false; + } + } + if unique { + tags_dur.push((formatted_tags, task_duration)) + } + } + } + + // Sort tasks and tags in descending order by duration + tags_dur.sort_by_key(|k| k.1); + tags_dur.reverse(); + sorted_tasks_by_duration.push((task_name, total_duration, tags_dur)); + sorted_tasks_by_duration.sort_by_key(|k| k.1); + sorted_tasks_by_duration.reverse(); + } + + for stbd in sorted_tasks_by_duration { + let header_iter = results_model.append(Some(&all_tasks_iter)); + for (task, task_duration) in stbd.2 { + let _child_iter = results_model.insert_with_values( + Some(&header_iter), + None, + &[(0, &task), (1, &FurReport::format_duration(task_duration))] + ); + } + results_model.set( + &header_iter, + &[(0, &stbd.0), (1, &FurReport::format_duration(stbd.1))] + ); + } + } else if imp.sort_by_tag.is_active() { + let mut tasks_by_tag: Vec<Vec<(String, database::Task, i64)>> = Vec::new(); + for (task, task_duration) in tasks_in_range { + let mut split_tags: Vec<&str> = task.tags.split("#").collect(); + // Trim whitespace around each tag + split_tags = split_tags.iter().map(|x| x.trim()).collect(); + for tag in split_tags { + let mut unique = true; + for i in 0..tasks_by_tag.len() { + let (tbt_tag, _, _) = &tasks_by_tag[i][0]; + if tbt_tag == tag { + tasks_by_tag[i].push((tag.to_string(), task.clone(), task_duration)); + unique = false; + } + } + if unique { + // Add unique task to list for group name + let mut new_name_list: Vec<(String, database::Task, i64)> = Vec::new(); + new_name_list.push((tag.to_string(), task.clone(), task_duration)); + tasks_by_tag.push(new_name_list); + } + } + } + + let mut sorted_tasks_by_duration: Vec<(String, i64, Vec<(String, i64)>)> = Vec::new(); + for tbt in tasks_by_tag { + let mut total_duration: i64 = 0; + let mut tasks_dur: Vec<(String, i64)> = Vec::new(); + let tag_name = format!("#{}", tbt[0].0); + for tbt_tuple in tbt { + let (_, task, tag_duration) = tbt_tuple; + total_duration += tag_duration; + + let mut unique = true; + for i in 0..tasks_dur.len() { + let (td_task, dur) = &tasks_dur[i]; + if td_task == &task.task_name { + let new_dur = dur + tag_duration; + tasks_dur[i] = (task.task_name.clone(), new_dur); + unique = false; + } + } + if unique { + tasks_dur.push((task.task_name.clone(), tag_duration)) + } + } + + // Sort tags and tasks in descending order by duration + tasks_dur.sort_by_key(|k| k.1); + tasks_dur.reverse(); + sorted_tasks_by_duration.push((tag_name, total_duration, tasks_dur)); + sorted_tasks_by_duration.sort_by_key(|k| k.1); + sorted_tasks_by_duration.reverse(); + } + + for mut stbd in sorted_tasks_by_duration { + if !only_this_tag || (only_this_tag && user_chosen_tags.contains(&stbd.0[1..].to_string())) { + let header_iter = results_model.append(Some(&all_tasks_iter)); + for (task, task_duration) in stbd.2 { + let _child_iter = results_model.insert_with_values( + Some(&header_iter), + None, + &[(0, &task), (1, &FurReport::format_duration(task_duration))] + ); + } + if stbd.0 == "#" { + stbd.0 = gettext("no tags").to_string(); + } + results_model.set( + &header_iter, + &[(0, &stbd.0), (1, &FurReport::format_duration(stbd.1))] + ); + } + } + } + + imp.results_tree.set_model(Some(&results_model)); + // Automatically expand All Tasks row + let all_tasks_path = gtk::TreePath::new_first(); + imp.results_tree.expand_row(&all_tasks_path, false); + } + + fn format_duration(total_time: i64) -> String { + // Format total time to readable string + let h = total_time / 3600; + let m = total_time % 3600 / 60; + let s = total_time % 60; + format!("{:02}:{:02}:{:02}", h, m, s) + } +} + diff --git a/src/ui/task_details.rs b/src/ui/task_details.rs index 626d976..0ca40ce 100755 --- a/src/ui/task_details.rs +++ b/src/ui/task_details.rs @@ -163,7 +163,6 @@ impl FurTaskDetails { 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(&this), gtk::DialogFlags::MODAL, @@ -556,8 +555,6 @@ impl FurTaskDetails { 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(&this), diff --git a/src/ui/tasks_group.rs b/src/ui/tasks_group.rs index 2453991..bd55c36 100755 --- a/src/ui/tasks_group.rs +++ b/src/ui/tasks_group.rs @@ -81,10 +81,9 @@ impl FurTasksGroup { // 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; + let mut unique = true; for i in 0..tasks_by_name.len() { if tasks_by_name[i][0].task_name == task.task_name && ( ( settings_manager::get_bool("show-tags") |