From b6bd6061f35d9413093e070e655d26d504ebc338 Mon Sep 17 00:00:00 2001 From: Alain Date: Wed, 10 Jul 2024 04:35:19 -0500 Subject: [PATCH] feat #622 --- core/Enum.vala | 103 +++++++++ core/Objects/ObjectEvent.vala | 142 +++++++------ core/Services/Database.vala | 198 +++++++++++++++--- core/Util/Datetime.vala | 12 +- data/io.github.alainm23.planify.gresource.xml | 3 + .../icons/rotation-edit-symbolic.svg | 46 ++++ src/Dialogs/ItemChangeHistory.vala | 196 +++++++++++++++++ src/Layouts/ItemRow.vala | 25 +-- src/Layouts/ItemSidebarView.vala | 25 +-- src/Widgets/ItemChangeHistoryRow.vala | 193 +++++++++++++++++ src/meson.build | 3 +- 11 files changed, 812 insertions(+), 134 deletions(-) create mode 100644 data/resources/icons/rotation-edit-symbolic.svg create mode 100644 src/Dialogs/ItemChangeHistory.vala create mode 100644 src/Widgets/ItemChangeHistoryRow.vala diff --git a/core/Enum.vala b/core/Enum.vala index d094fdf30..7ab10c95d 100644 --- a/core/Enum.vala +++ b/core/Enum.vala @@ -437,3 +437,106 @@ public enum ItemType { } } } + +public enum ObjectEventType { + INSERT, + UPDATE; + + public static ObjectEventType parse (string value) { + switch (value) { + case "insert": + return ObjectEventType.INSERT; + + case "update": + return ObjectEventType.UPDATE; + + default: + assert_not_reached (); + } + } + + public string to_string () { + switch (this) { + case INSERT: + return "insert"; + + case UPDATE: + return "update"; + + default: + assert_not_reached (); + } + } + + public string get_label () { + switch (this) { + case INSERT: + return _("Task Created"); + + case UPDATE: + return _("Task Updated"); + + default: + assert_not_reached (); + } + } +} + +public enum ObjectEventKeyType { + CONTENT, + DESCRIPTION, + DUE, + PRIORITY, + LABELS, + PINNED; + + public static ObjectEventKeyType parse (string value) { + switch (value) { + case "content": + return ObjectEventKeyType.CONTENT; + + case "description": + return ObjectEventKeyType.DESCRIPTION; + + case "due": + return ObjectEventKeyType.DUE; + + case "priority": + return ObjectEventKeyType.PRIORITY; + + case "labels": + return ObjectEventKeyType.LABELS; + + case "pinned": + return ObjectEventKeyType.PINNED; + + default: + assert_not_reached (); + } + } + + public string get_label () { + switch (this) { + case ObjectEventKeyType.CONTENT: + return _("Content"); + + case ObjectEventKeyType.DESCRIPTION: + return _("Description"); + + case ObjectEventKeyType.DUE: + return _("Scheduled"); + + case ObjectEventKeyType.PRIORITY: + return _("Priority"); + + case ObjectEventKeyType.LABELS: + return _("Labels"); + + case ObjectEventKeyType.PINNED: + return _("Pin"); + + default: + assert_not_reached (); + } + } +} \ No newline at end of file diff --git a/core/Objects/ObjectEvent.vala b/core/Objects/ObjectEvent.vala index 7b9457650..52d166185 100644 --- a/core/Objects/ObjectEvent.vala +++ b/core/Objects/ObjectEvent.vala @@ -21,89 +21,99 @@ public class Objects.ObjectEvent : GLib.Object { public int64 id { get; set; default = 0; } + public ObjectEventType event_type { get; set; } public string event_date { get; set; default = ""; } - public string event_type { get; set; default = ""; } - public string extra_data { get; set; default = ""; } public string object_id { get; set; default = ""; } public string object_type { get; set; default = ""; } + public ObjectEventKeyType object_key { get; set; } + public string object_old_value { get; set; default = ""; } + public string object_new_value { get; set; default = ""; } public string parent_item_id { get; set; default = ""; } public string parent_project_id { get; set; default = ""; } - public static Objects.ObjectEvent for_add_item (Objects.Item item) { - Objects.ObjectEvent return_value = new Objects.ObjectEvent (); - - return_value.event_date = new GLib.DateTime.now_local ().to_string (); - return_value.event_type = "added"; - return_value.object_type = "item"; - return_value.object_id = item.id; - return_value.parent_project_id = item.project_id; - return_value.extra_data = generate_extradata ("content", item.content, item.project.name, item.project.color); - - return return_value; + public string icon_name { + get { + if (event_type == ObjectEventType.INSERT) { + return "plus-large-symbolic"; + } else if (event_type == ObjectEventType.UPDATE) { + if (object_key == ObjectEventKeyType.CONTENT) { + return "edit-symbolic"; + } else if (object_key == ObjectEventKeyType.DESCRIPTION) { + return "paper-symbolic"; + } else if (object_key == ObjectEventKeyType.DUE) { + return "month-symbolic"; + } else if (object_key == ObjectEventKeyType.PRIORITY) { + return "flag-outline-thick-symbolic"; + } else if (object_key == ObjectEventKeyType.LABELS) { + return "tag-outline-symbolic"; + } else if (object_key == ObjectEventKeyType.PINNED) { + return "pin-symbolic"; + } + } + + return "plus-large-symbolic"; + } } - public static Objects.ObjectEvent for_update_item (Objects.Item item, string key) { - Objects.ObjectEvent return_value = new Objects.ObjectEvent (); - - return_value.event_date = new GLib.DateTime.now_local ().to_string (); - return_value.event_type = "updated"; - return_value.object_type = "item"; - return_value.object_id = item.id; - return_value.parent_project_id = item.project_id; - return_value.extra_data = generate_extradata ("content", item.content, item.project.name, item.project.color); - - return return_value; + GLib.DateTime _datetime; + public GLib.DateTime datetime { + get { + _datetime = Utils.Datetime.get_date_from_string (event_date); + return _datetime; + } } - public static string generate_extradata (string key, string value, string parent_project_name, string parent_project_color) { - var builder = new Json.Builder (); - - builder.begin_object (); - - builder.set_member_name ("client"); - builder.add_string_value ("Planify"); - - builder.set_member_name (key); - builder.add_string_value (value); - - builder.set_member_name ("parent_project_color"); - builder.add_string_value (parent_project_color); - - builder.set_member_name ("parent_project_name"); - builder.add_string_value (parent_project_name); - - builder.end_object (); - - Json.Generator generator = new Json.Generator (); - Json.Node root = builder.get_root (); - generator.set_root (root); - - return generator.to_data (null); + GLib.DateTime _date; + public GLib.DateTime date { + get { + _date = Utils.Datetime.format_date (Utils.Datetime.get_date_from_string (event_date)); + return _date; + } } - public static string _generate_extradata (Objects.Item item, string key) { - var builder = new Json.Builder (); - - builder.begin_object (); - - builder.set_member_name ("client"); - builder.add_string_value ("Planify"); + string _time; + public string time { + get { + if (Utils.Datetime.is_clock_format_12h ()) { + _time = datetime.format (Granite.DateTime.get_default_time_format (true)); + } else { + _time = datetime.format (Granite.DateTime.get_default_time_format (false)); + } + + return _time; + } + } - builder.set_member_name ("content"); - builder.add_string_value (item.content); + public Objects.DueDate? get_due_value (string value) { + Json.Parser parser = new Json.Parser (); + + try { + parser.load_from_data (value, value.length); + } catch (Error e) { + warning (e.message); + return null; + } - builder.set_member_name ("parent_project_color"); - builder.add_string_value (item.project.color); + var due = new Objects.DueDate (); + due.update_from_json (parser.get_root ().get_object ()); - builder.set_member_name ("parent_project_name"); - builder.add_string_value (item.project.name); + return due; + } - builder.end_object (); + public string get_labels_value (string value) { + string return_value = ""; + Gee.ArrayList labels = Services.Database.get_default ().get_labels_by_item_labels (value); - Json.Generator generator = new Json.Generator (); - Json.Node root = builder.get_root (); - generator.set_root (root); + if (labels.size > 0) { + for (int index = 0; index < labels.size; index++) { + if (index < labels.size - 1) { + return_value += labels[index].name + ", "; + } else { + return_value += labels[index].name; + } + } + } - return generator.to_data (null); + return return_value; } } \ No newline at end of file diff --git a/core/Services/Database.vala b/core/Services/Database.vala index e359e142f..ec033512f 100644 --- a/core/Services/Database.vala +++ b/core/Services/Database.vala @@ -168,8 +168,10 @@ public class Services.Database : GLib.Object { public void init_database () { db_path = Environment.get_user_data_dir () + "/io.github.alainm23.planify/database.db"; - + Sqlite.Database.open (db_path, out db); + create_tables (); + create_triggers (); patch_database (); opened (); } @@ -233,8 +235,6 @@ public class Services.Database : GLib.Object { } private void create_tables () { - Sqlite.Database.open (db_path, out db); - sql = """ CREATE TABLE IF NOT EXISTS Labels ( id TEXT PRIMARY KEY, @@ -396,13 +396,15 @@ public class Services.Database : GLib.Object { } sql = """ - CREATE TABLE IF NOT EXISTS ObjectEvents ( + CREATE TABLE IF NOT EXISTS OEvents ( id INTEGER PRIMARY KEY AUTOINCREMENT, - event_date TEXT, event_type TEXT, - extra_data TEXT, + event_date DATETIME DEFAULT (datetime('now','localtime')), object_id TEXT, object_type TEXT, + object_key TEXT, + object_old_value TEXT, + object_new_value TEXT, parent_item_id TEXT, parent_project_id TEXT ); @@ -419,6 +421,125 @@ public class Services.Database : GLib.Object { } } + private void create_triggers () { + sql = """ + CREATE TRIGGER IF NOT EXISTS after_insert_item + AFTER INSERT ON Items + BEGIN + INSERT OR IGNORE INTO OEvents (event_type, object_id, + object_type, object_key, object_old_value, object_new_value, parent_project_id) + VALUES ("insert", NEW.id, "item", "content", NEW.content, + NEW.content, NEW.project_id); + END; + """; + + if (db.exec (sql, null, out errormsg) != Sqlite.OK) { + warning (errormsg); + } + + sql = """ + CREATE TRIGGER IF NOT EXISTS after_update_content_item + AFTER UPDATE ON Items + FOR EACH ROW + WHEN NEW.content != OLD.content + BEGIN + INSERT OR IGNORE INTO OEvents (event_type, object_id, + object_type, object_key, object_old_value, object_new_value, parent_project_id) + VALUES ("update", NEW.id, "item", "content", OLD.content, + NEW.content, NEW.project_id); + END; + """; + + if (db.exec (sql, null, out errormsg) != Sqlite.OK) { + warning (errormsg); + } + + sql = """ + CREATE TRIGGER IF NOT EXISTS after_update_description_item + AFTER UPDATE ON Items + FOR EACH ROW + WHEN NEW.description != OLD.description + BEGIN + INSERT OR IGNORE INTO OEvents (event_type, object_id, + object_type, object_key, object_old_value, object_new_value, parent_project_id) + VALUES ("update", NEW.id, "item", "description", OLD.description, + NEW.description, NEW.project_id); + END; + """; + + if (db.exec (sql, null, out errormsg) != Sqlite.OK) { + warning (errormsg); + } + + sql = """ + CREATE TRIGGER IF NOT EXISTS after_update_due_item + AFTER UPDATE ON Items + FOR EACH ROW + WHEN NEW.due != OLD.due + BEGIN + INSERT OR IGNORE INTO OEvents (event_type, object_id, + object_type, object_key, object_old_value, object_new_value, parent_project_id) + VALUES ("update", NEW.id, "item", "due", OLD.due, + NEW.due, NEW.project_id); + END; + """; + + if (db.exec (sql, null, out errormsg) != Sqlite.OK) { + warning (errormsg); + } + + sql = """ + CREATE TRIGGER IF NOT EXISTS after_update_priority_item + AFTER UPDATE ON Items + FOR EACH ROW + WHEN NEW.priority != OLD.priority + BEGIN + INSERT OR IGNORE INTO OEvents (event_type, object_id, + object_type, object_key, object_old_value, object_new_value, parent_project_id) + VALUES ("update", NEW.id, "item", "priority", OLD.priority, + NEW.priority, NEW.project_id); + END; + """; + + if (db.exec (sql, null, out errormsg) != Sqlite.OK) { + warning (errormsg); + } + + sql = """ + CREATE TRIGGER IF NOT EXISTS after_update_labels_item + AFTER UPDATE ON Items + FOR EACH ROW + WHEN NEW.labels != OLD.labels + BEGIN + INSERT OR IGNORE INTO OEvents (event_type, object_id, + object_type, object_key, object_old_value, object_new_value, parent_project_id) + VALUES ("update", NEW.id, "item", "labels", OLD.labels, + NEW.labels, NEW.project_id); + END; + """; + + if (db.exec (sql, null, out errormsg) != Sqlite.OK) { + warning (errormsg); + } + + sql = """ + CREATE TRIGGER IF NOT EXISTS after_update_pinned_item + AFTER UPDATE ON Items + FOR EACH ROW + WHEN NEW.pinned != OLD.pinned + BEGIN + INSERT OR IGNORE INTO OEvents (event_type, object_id, + object_type, object_key, object_old_value, object_new_value, parent_project_id) + VALUES ("update", NEW.id, "item", "pinned", OLD.pinned, + NEW.pinned, NEW.project_id); + END; + """; + + if (db.exec (sql, null, out errormsg) != Sqlite.OK) { + warning (errormsg); + } + } + public void clear_database () { string db_path = Environment.get_user_data_dir () + "/io.github.alainm23.planify/database.db"; File db_file = File.new_for_path (db_path); @@ -1873,29 +1994,6 @@ public class Services.Database : GLib.Object { } } - private void add_item_event (Objects.ObjectEvent object_event) { - Sqlite.Statement stmt; - - sql = """ - INSERT OR IGNORE INTO ObjectEvents (event_date, event_type, extra_data, object_id, object_type, parent_project_id) - VALUES ($event_date, $event_type, $extra_data, $object_id, $object_type, $parent_project_id); - """; - - db.prepare_v2 (sql, sql.length, out stmt); - set_parameter_str (stmt, "$event_date", object_event.event_date); - set_parameter_str (stmt, "$event_type", object_event.event_type); - set_parameter_str (stmt, "$extra_data", object_event.extra_data); - set_parameter_str (stmt, "$object_id", object_event.object_id); - set_parameter_str (stmt, "$object_type", object_event.object_type); - set_parameter_str (stmt, "$parent_project_id", object_event.parent_project_id); - - if (stmt.step () != Sqlite.DONE) { - warning ("Error: %d: %s", db.errcode (), db.errmsg ()); - } - - stmt.reset (); - } - /* Quick Find */ @@ -2505,6 +2603,48 @@ public class Services.Database : GLib.Object { stmt.reset (); } + /* + * ObjectsEvent + */ + + public Gee.ArrayList get_events_by_item (string id, int start_week, int end_week) { + Gee.ArrayList return_value = new Gee.ArrayList (); + + Sqlite.Statement stmt; + + sql = """ + SELECT * FROM OEvents + WHERE object_id = $object_id + AND (event_date >= DATETIME('now', '-%d days') + AND event_date <= DATETIME('now', '-%d days')) + ORDER BY event_date DESC + """.printf (end_week, start_week); + + db.prepare_v2 (sql, sql.length, out stmt); + set_parameter_str (stmt, "$object_id", id); + + while (stmt.step () == Sqlite.ROW) { + return_value.add (_fill_object_event (stmt)); + } + stmt.reset (); + return return_value; + } + + public Objects.ObjectEvent _fill_object_event (Sqlite.Statement stmt) { + Objects.ObjectEvent return_value = new Objects.ObjectEvent (); + return_value.id = stmt.column_int64 (0); + return_value.event_type = ObjectEventType.parse (stmt.column_text (1)); + return_value.event_date = stmt.column_text (2); + return_value.object_id = stmt.column_text (3); + return_value.object_type = stmt.column_text (4); + return_value.object_key = ObjectEventKeyType.parse (stmt.column_text (5)); + return_value.object_old_value = stmt.column_text (6); + return_value.object_new_value = stmt.column_text (7); + return_value.parent_item_id = stmt.column_text (8); + return_value.parent_project_id = stmt.column_text (9); + return return_value; + } + // PARAMETER REGION private void set_parameter_int (Sqlite.Statement? stmt, string par, int val) { int par_position = stmt.bind_parameter_index (par); diff --git a/core/Util/Datetime.vala b/core/Util/Datetime.vala index 0b90cfb98..3cc2c255a 100644 --- a/core/Util/Datetime.vala +++ b/core/Util/Datetime.vala @@ -57,7 +57,11 @@ public class Utils.Datetime { return datetime; } - public static string get_relative_date_from_date (GLib.DateTime datetime) { + public static string get_relative_date_from_date (GLib.DateTime? datetime) { + if (datetime == null) { + return ""; + } + string returned = ""; if (is_today (datetime)) { @@ -437,7 +441,11 @@ public class Utils.Datetime { ); } - public static string get_default_date_format_from_date (GLib.DateTime date) { + public static string get_default_date_format_from_date (GLib.DateTime? date) { + if (date == null) { + return ""; + } + var format = date.format (Granite.DateTime.get_default_date_format ( false, true, diff --git a/data/io.github.alainm23.planify.gresource.xml b/data/io.github.alainm23.planify.gresource.xml index da8f8b374..15a9ebc76 100644 --- a/data/io.github.alainm23.planify.gresource.xml +++ b/data/io.github.alainm23.planify.gresource.xml @@ -91,6 +91,7 @@ resources/icons/eye-open-negative-filled-symbolic.svg resources/icons/size-vertically-symbolic.svg resources/icons/info-outline-symbolic.svg + resources/icons/rotation-edit-symbolic.svg @@ -156,6 +157,7 @@ resources/icons/eye-open-negative-filled-symbolic.svg resources/icons/size-vertically-symbolic.svg resources/icons/info-outline-symbolic.svg + resources/icons/rotation-edit-symbolic.svg @@ -221,5 +223,6 @@ resources/icons/eye-open-negative-filled-symbolic.svg resources/icons/size-vertically-symbolic.svg resources/icons/info-outline-symbolic.svg + resources/icons/rotation-edit-symbolic.svg diff --git a/data/resources/icons/rotation-edit-symbolic.svg b/data/resources/icons/rotation-edit-symbolic.svg new file mode 100644 index 000000000..f10d09633 --- /dev/null +++ b/data/resources/icons/rotation-edit-symbolic.svg @@ -0,0 +1,46 @@ + + + + + + + + + diff --git a/src/Dialogs/ItemChangeHistory.vala b/src/Dialogs/ItemChangeHistory.vala new file mode 100644 index 000000000..98cb631d0 --- /dev/null +++ b/src/Dialogs/ItemChangeHistory.vala @@ -0,0 +1,196 @@ +/* +* Copyright © 2023 Alain M. (https://github.com/alainm23/planify) +* +* 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, write to the +* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +* Boston, MA 02110-1301 USA +* +* Authored by: Alain M. +*/ + +public class Dialogs.ItemChangeHistory : Adw.Dialog { + public Objects.Item item { get; construct; } + + private Gtk.ListBox listbox; + private Gtk.Button load_button; + private int start_week = 0; + private int end_week = 7; + + public ItemChangeHistory (Objects.Item item) { + Object ( + item: item, + title: _("Change History"), + content_width: 450, + content_height: 500 + ); + } + + construct { + var headerbar = new Adw.HeaderBar (); + headerbar.add_css_class ("flat"); + + var create_update_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12) { + margin_start = 6, + margin_end = 6, + valign = START, + }; + create_update_box.append (build_card ("plus-large-symbolic", _("Added at"), Utils.Datetime.get_relative_date_from_date (item.added_datetime))); + + string updated_date = "(" + _("Not available") + ")"; + if (item.updated_at != "") { + updated_date = Utils.Datetime.get_relative_date_from_date (item.updated_datetime); + } + create_update_box.append (build_card ("update-symbolic", _("Updated at"), updated_date)); + + listbox = new Gtk.ListBox () { + hexpand = true, + valign = START, + css_classes = { "listbox-background" } + }; + listbox.set_header_func (header_completed_function); + listbox.set_placeholder (new Gtk.Label (_("Your change history will be displayed here once you start making changes.")) { + css_classes = { "dim-label" }, + margin_top = 24, + margin_start = 24, + margin_end = 24, + wrap = true, + justify = Gtk.Justification.CENTER + }); + + load_button = new Gtk.Button () { + css_classes = { "flat" }, + vexpand = true, + valign = Gtk.Align.END + }; + + var v_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 6) { + hexpand = true, + valign = Gtk.Align.START + }; + + v_box.append (create_update_box); + v_box.append (listbox); + + var v2_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 6) { + hexpand = true, + vexpand = true, + margin_start = 12, + margin_end = 12, + margin_bottom = 12, + margin_top = 12 + }; + + v2_box.append (v_box); + v2_box.append (load_button); + + var listbox_scrolled = new Gtk.ScrolledWindow () { + hscrollbar_policy = Gtk.PolicyType.NEVER, + hexpand = true, + vexpand = true, + child = v2_box + }; + + var toolbar_view = new Adw.ToolbarView (); + toolbar_view.add_top_bar (headerbar); + toolbar_view.content = listbox_scrolled; + + child = toolbar_view; + fetch_data (); + + load_button.clicked.connect (() => { + start_week = end_week; + end_week = end_week + 7; + fetch_data (); + }); + } + + private void fetch_data () { + foreach (Objects.ObjectEvent object_event in Services.Database.get_default ().get_events_by_item (item.id, start_week, end_week)) { + listbox.append (new Widgets.ItemChangeHistoryRow (object_event)); + } + + listbox.invalidate_headers (); + load_button.label = _("Load more history from %d weeks ago…".printf ((end_week / 7) + 1)); + } + + private void header_completed_function (Gtk.ListBoxRow lbrow, Gtk.ListBoxRow? lbbefore) { + var row = (Widgets.ItemChangeHistoryRow) lbrow; + if (row.object_event.event_date == "") { + return; + } + + if (lbbefore != null) { + var before = (Widgets.ItemChangeHistoryRow) lbbefore; + if (before.object_event.date.compare (row.object_event.date) == 0) { + return; + } + } + + row.set_header (get_header_box (Utils.Datetime.get_relative_date_from_date (row.object_event.date))); + } + + private Gtk.Widget get_header_box (string title) { + var header_label = new Gtk.Label (title) { + css_classes = { "heading" }, + halign = START + }; + + var header_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 6) { + margin_start = 12, + margin_top = 6, + margin_bottom = 6 + }; + + header_box.append (header_label); + + return header_box; + } + + private Gtk.Widget build_card (string icon_name, string header, string value) { + var image = new Gtk.Image.from_icon_name (icon_name); + + var header_label = new Gtk.Label (header) { + halign = START, + css_classes = { "title-4", "caption", "font-bold" } + }; + + var value_label = new Gtk.Label (value) { + xalign = 0, + use_markup = true, + halign = START, + ellipsize = Pango.EllipsizeMode.END, + css_classes = { "caption" } + }; + + var card_content = new Gtk.Grid () { + column_spacing = 12, + vexpand = true, + hexpand = true, + margin_start = 12, + margin_end = 6, + margin_top = 6, + margin_bottom = 6, + }; + card_content.attach (image, 0, 0, 1, 2); + card_content.attach (header_label, 1, 0, 1, 1); + card_content.attach (value_label, 1, 1, 1, 1); + + var card = new Adw.Bin () { + child = card_content, + css_classes = { "card" } + }; + + return card; + } +} diff --git a/src/Layouts/ItemRow.vala b/src/Layouts/ItemRow.vala index 156389015..308181494 100644 --- a/src/Layouts/ItemRow.vala +++ b/src/Layouts/ItemRow.vala @@ -1109,18 +1109,6 @@ public class Layouts.ItemRow : Layouts.ItemBase { }); } - private string get_updated_info () { - string added_at = _("Added at"); - string updated_at = _("Updated at"); - string added_date = Utils.Datetime.get_relative_date_from_date (item.added_datetime); - string updated_date = "(" + _("Not available") + ")"; - if (item.updated_at != "") { - updated_date = Utils.Datetime.get_relative_date_from_date (item.updated_datetime); - } - - return "%s: %s\n%s: %s".printf (added_at, added_date, updated_at, updated_date); - } - private Gtk.Popover build_button_context_menu () { var back_item = new Widgets.ContextMenu.MenuItem (_("Back"), "go-previous-symbolic"); @@ -1136,8 +1124,7 @@ public class Layouts.ItemRow : Layouts.ItemBase { var delete_item = new Widgets.ContextMenu.MenuItem (_("Delete Task"), "user-trash-symbolic"); delete_item.add_css_class ("menu-item-danger"); - var more_information_item = new Widgets.ContextMenu.MenuItem ("", null); - more_information_item.add_css_class ("caption"); + var more_information_item = new Widgets.ContextMenu.MenuItem (_("Change History"), "rotation-edit-symbolic"); var popover = new Gtk.Popover () { has_arrow = false, @@ -1218,15 +1205,17 @@ public class Layouts.ItemRow : Layouts.ItemBase { menu_stack.set_visible_child_name ("menu"); }); - popover.show.connect (() => { - more_information_item.title = get_updated_info (); - }); - delete_item.activate_item.connect (() => { popover.popdown (); delete_request (); }); + more_information_item.activate_item.connect (() => { + popover.popdown (); + var dialog = new Dialogs.ItemChangeHistory (item); + dialog.present (Planify._instance.main_window); + }); + return popover; } diff --git a/src/Layouts/ItemSidebarView.vala b/src/Layouts/ItemSidebarView.vala index 51e39a018..e322efdf3 100644 --- a/src/Layouts/ItemSidebarView.vala +++ b/src/Layouts/ItemSidebarView.vala @@ -463,8 +463,7 @@ public class Layouts.ItemSidebarView : Adw.Bin { var delete_item = new Widgets.ContextMenu.MenuItem (_("Delete Task"), "user-trash-symbolic"); delete_item.add_css_class ("menu-item-danger"); - var more_information_item = new Widgets.ContextMenu.MenuItem ("", null); - more_information_item.add_css_class ("caption"); + var more_information_item = new Widgets.ContextMenu.MenuItem (_("Change History"), "rotation-edit-symbolic"); var popover = new Gtk.Popover () { has_arrow = false, @@ -552,15 +551,17 @@ public class Layouts.ItemSidebarView : Adw.Bin { menu_stack.set_visible_child_name ("menu"); }); - popover.show.connect (() => { - more_information_item.title = get_updated_info (); - }); - delete_item.activate_item.connect (() => { popover.popdown (); delete_request (); }); + more_information_item.activate_item.connect (() => { + popover.popdown (); + var dialog = new Dialogs.ItemChangeHistory (item); + dialog.present (Planify._instance.main_window); + }); + return popover; } @@ -669,18 +670,6 @@ public class Layouts.ItemSidebarView : Adw.Bin { } } - private string get_updated_info () { - string added_at = _("Added at"); - string updated_at = _("Updated at"); - string added_date = Utils.Datetime.get_relative_date_from_date (item.added_datetime); - string updated_date = "(" + _("Not available") + ")"; - if (item.updated_at != "") { - updated_date = Utils.Datetime.get_relative_date_from_date (item.updated_datetime); - } - - return "%s: %s\n%s: %s".printf (added_at, added_date, updated_at, updated_date); - } - public void delete_request (bool undo = true) { var dialog = new Adw.AlertDialog ( _("Are you sure you want to delete?"), diff --git a/src/Widgets/ItemChangeHistoryRow.vala b/src/Widgets/ItemChangeHistoryRow.vala new file mode 100644 index 000000000..f438db09c --- /dev/null +++ b/src/Widgets/ItemChangeHistoryRow.vala @@ -0,0 +1,193 @@ +/* +* Copyright © 2023 Alain M. (https://github.com/alainm23/planify) +* +* 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, write to the +* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +* Boston, MA 02110-1301 USA +* +* Authored by: Alain M. +*/ + +public class Widgets.ItemChangeHistoryRow : Gtk.ListBoxRow { + public Objects.ObjectEvent object_event { get; construct; } + + private Gtk.Revealer main_revealer; + + public ItemChangeHistoryRow (Objects.ObjectEvent object_event) { + Object ( + object_event: object_event + ); + } + + construct { + add_css_class ("no-selectable"); + + var object_event_icon = new Gtk.Image.from_icon_name (object_event.icon_name) { + pixel_size = 16, + valign = START + }; + + string _type_string = "%s: %s".printf (object_event.event_type.get_label (), object_event.object_key.get_label ()); + var type_string = new Gtk.Label (_type_string) { + css_classes = { "font-bold" }, + halign = Gtk.Align.START, + margin_bottom = 3 + }; + + var datetime_string = new Gtk.Label (object_event.time) { + css_classes = { "dim-label", "small-label" }, + halign = Gtk.Align.END, + hexpand = true + }; + + var header_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) { + hexpand = true + }; + header_box.append (type_string); + header_box.append (datetime_string); + + var old_value_header = new Gtk.Label (_("Previous") + ": ") { + css_classes = { "font-bold" } + }; + + var old_value_label = new Gtk.Label (null) { + css_classes = { "dim-label" }, + selectable = true, + ellipsize = Pango.EllipsizeMode.END + }; + + var old_value_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) { + halign = Gtk.Align.START + }; + old_value_box.append (old_value_header); + old_value_box.append (old_value_label); + + var old_value_revealer = new Gtk.Revealer () { + transition_type = Gtk.RevealerTransitionType.SLIDE_DOWN, + child = old_value_box + }; + + var new_value_header = new Gtk.Label (_("New") + ": ") { + css_classes = { "font-bold" } + }; + + var new_value_label = new Gtk.Label (null) { + css_classes = { "dim-label", "caption" }, + selectable = true, + wrap = true + }; + + var new_value_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) { + halign = Gtk.Align.START + }; + // new_value_box.append (new_value_header); + new_value_box.append (new_value_label); + + var new_value_revealer = new Gtk.Revealer () { + transition_type = Gtk.RevealerTransitionType.SLIDE_DOWN, + child = new_value_box + }; + + var detail_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0) { + valign = Gtk.Align.START + }; + + detail_box.append (header_box); + // detail_box.append (old_value_revealer); + detail_box.append (new_value_revealer); + + var content_box = new Gtk.Grid () { + column_spacing = 9, + margin_top = 12, + margin_bottom = 12, + margin_start = 12, + margin_end = 12 + }; + + content_box.attach (object_event_icon, 0, 0, 1, 2); + content_box.attach (detail_box, 1, 1, 1, 1); + + var card = new Adw.Bin () { + child = content_box, + css_classes = { "card" }, + margin_top = 3, + margin_bottom = 3, + margin_start = 3, + margin_end = 3 + }; + + main_revealer = new Gtk.Revealer () { + transition_type = Gtk.RevealerTransitionType.SLIDE_DOWN, + child = card + }; + + child = main_revealer; + + string _old_value_label = ""; + string _new_value_label = ""; + + if (object_event.event_type == ObjectEventType.INSERT) { + _new_value_label = object_event.object_new_value; + } else if (object_event.event_type == ObjectEventType.UPDATE) { + if (object_event.object_key == ObjectEventKeyType.DESCRIPTION) { + _old_value_label = object_event.object_old_value; + _new_value_label = object_event.object_new_value; + } else if (object_event.object_key == ObjectEventKeyType.DUE) { + _old_value_label = Utils.Datetime.get_relative_date_from_date ( + object_event.get_due_value (object_event.object_old_value).datetime + ); + _new_value_label = Utils.Datetime.get_relative_date_from_date ( + object_event.get_due_value (object_event.object_new_value).datetime + ); + } else if (object_event.object_key == ObjectEventKeyType.PRIORITY) { + if (object_event.object_old_value != "") { + _old_value_label = Util.get_default ().get_priority_title (int.parse (object_event.object_old_value)); + } + + if (object_event.object_new_value != "") { + _new_value_label = Util.get_default ().get_priority_title (int.parse (object_event.object_new_value)); + } + } else if (object_event.object_key == ObjectEventKeyType.LABELS) { + _old_value_label = object_event.get_labels_value (object_event.object_old_value); + _new_value_label = object_event.get_labels_value (object_event.object_new_value); + } else if (object_event.object_key == ObjectEventKeyType.PINNED) { + _old_value_label = object_event.object_old_value == "1" ? _("Pin: Active") : _("Pin: Inactive"); + _new_value_label = object_event.object_new_value == "1" ? _("Pin: Active") : _("Pin: Inactive"); + } + } + + if (_old_value_label.length > 0) { + old_value_revealer.reveal_child = true; + old_value_label.label = _old_value_label; + } + + if (_new_value_label.length > 0) { + new_value_revealer.reveal_child = true; + new_value_label.label = _new_value_label; + } + + Timeout.add (main_revealer.transition_duration, () => { + main_revealer.reveal_child = true; + return GLib.Source.REMOVE; + }); + } + + public void hide_destroy () { + main_revealer.reveal_child = false; + Timeout.add (main_revealer.transition_duration, () => { + ((Gtk.ListBox) parent).remove (this); + return GLib.Source.REMOVE; + }); + } +} \ No newline at end of file diff --git a/src/meson.build b/src/meson.build index 09de63b14..12d230a4d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -54,6 +54,7 @@ sources = files( 'Widgets/ReorderChild.vala', 'Widgets/FilterFlowBox.vala', 'Widgets/Attachments.vala', + 'Widgets/ItemChangeHistoryRow.vala', 'Views/Project/Project.vala', 'Views/Project/List.vala', @@ -79,10 +80,10 @@ sources = files( 'Dialogs/ProjectPicker/ProjectPickerRow.vala', 'Dialogs/ProjectPicker/SectionPickerRow.vala', 'Dialogs/ManageProjects.vala', - 'Dialogs/DatePicker.vala', 'Dialogs/LabelPicker.vala', 'Dialogs/RepeatConfig.vala', + 'Dialogs/ItemChangeHistory.vala', 'Dialogs/QuickFind/QuickFind.vala', 'Dialogs/QuickFind/QuickFindItem.vala',