From 38976941c4322b8982eccb148cc58bc9d2c4bf91 Mon Sep 17 00:00:00 2001 From: Alain Date: Sat, 27 Jan 2024 06:39:17 -0500 Subject: [PATCH] Caldav tags support --- .gitignore | 1 + core/Objects/Item.vala | 169 +++++++++++++++++---- core/Objects/Project.vala | 8 + core/QuickAdd.vala | 8 +- core/Services/CalDAV.vala | 171 +++++++++++++++++++--- core/Util/Util.vala | 2 +- core/Widgets/ContextMenu/MenuPicker.vala | 4 +- core/Widgets/LabelPicker/LabelButton.vala | 19 ++- core/Widgets/LabelPicker/LabelPicker.vala | 26 +++- core/Widgets/LoadingButton.vala | 4 +- meson.build | 2 +- src/Dialogs/Label.vala | 19 +-- src/Layouts/ItemRow.vala | 33 +++-- src/Layouts/ItemView.vala | 3 +- src/Layouts/ProjectRow.vala | 37 +++++ src/Views/Label/Labels.vala | 44 +++++- src/Views/Project/Project.vala | 36 +++-- src/Widgets/MultiSelectToolbar.vala | 3 +- 18 files changed, 485 insertions(+), 104 deletions(-) diff --git a/.gitignore b/.gitignore index 6db483f2c..05abb2779 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ builddir/ build-aux/build-dir build-aux/flatpak_build build-aux/.flatpak-builder +build-aux/io.github.alainm23.planify.Devel.flatpak .flatpak-builder/ subprojects/gxml diff --git a/core/Objects/Item.vala b/core/Objects/Item.vala index c293a3d71..20cf621e5 100644 --- a/core/Objects/Item.vala +++ b/core/Objects/Item.vala @@ -289,6 +289,41 @@ public class Objects.Item : Objects.BaseObject { } } + public void update_from_json (Json.Node node) { + project_id = node.get_object ().get_string_member ("project_id"); + content = node.get_object ().get_string_member ("content"); + description = node.get_object ().get_string_member ("description"); + checked = node.get_object ().get_boolean_member ("checked"); + priority = (int32) node.get_object ().get_int_member ("priority"); + is_deleted = node.get_object ().get_boolean_member ("is_deleted"); + added_at = node.get_object ().get_string_member ("added_at"); + check_labels (get_labels_maps_from_json (node)); + + if (!node.get_object ().get_null_member ("section_id")) { + section_id = node.get_object ().get_string_member ("section_id"); + } else { + section_id = ""; + } + + if (!node.get_object ().get_null_member ("parent_id")) { + parent_id = node.get_object ().get_string_member ("parent_id"); + } else { + parent_id = ""; + } + + if (!node.get_object ().get_null_member ("completed_at")) { + completed_at = node.get_object ().get_string_member ("completed_at"); + } else { + completed_at = ""; + } + + if (!node.get_object ().get_null_member ("due")) { + due.update_from_json (node.get_object ().get_object_member ("due")); + } else { + due.reset (); + } + } + public Item.from_import_json (Json.Node node) { id = node.get_object ().get_string_member ("id"); content = node.get_object ().get_string_member ("content"); @@ -311,10 +346,6 @@ public class Objects.Item : Objects.BaseObject { } public Item.from_caldav_xml (GXml.DomElement element) { - update_from_caldav_xml (element); - } - - public void update_from_caldav_xml (GXml.DomElement element) { GXml.DomElement propstat = element.get_elements_by_tag_name ("d:propstat").get_element (0); GXml.DomElement prop = propstat.get_elements_by_tag_name ("d:prop").get_element (0); string data = prop.get_elements_by_tag_name ("cal:calendar-data").get_element (0).text_content; @@ -365,43 +396,87 @@ public class Objects.Item : Objects.BaseObject { completed_at = ""; } + var categories = Util.find_string_value ("CATEGORIES", data); + if (categories != "") { + labels = get_caldav_categories (categories); + } + pinned = Util.find_boolean_value ("X-PINNED", data); extra_data = Util.generate_extra_data (Util.get_task_id_from_url (element), etag, ical.as_ical_string ()); } - public void update_from_json (Json.Node node) { - project_id = node.get_object ().get_string_member ("project_id"); - content = node.get_object ().get_string_member ("content"); - description = node.get_object ().get_string_member ("description"); - checked = node.get_object ().get_boolean_member ("checked"); - priority = (int32) node.get_object ().get_int_member ("priority"); - is_deleted = node.get_object ().get_boolean_member ("is_deleted"); - added_at = node.get_object ().get_string_member ("added_at"); - check_labels (get_labels_maps_from_json (node)); + public void update_from_caldav_xml (GXml.DomElement element) { + GXml.DomElement propstat = element.get_elements_by_tag_name ("d:propstat").get_element (0); + GXml.DomElement prop = propstat.get_elements_by_tag_name ("d:prop").get_element (0); + string data = prop.get_elements_by_tag_name ("cal:calendar-data").get_element (0).text_content; + string etag = prop.get_elements_by_tag_name ("d:getetag").get_element (0).text_content; - if (!node.get_object ().get_null_member ("section_id")) { - section_id = node.get_object ().get_string_member ("section_id"); - } else { - section_id = ""; + ICal.Component ical = new ICal.Component.from_string (data); + + id = ical.get_uid (); + content = ical.get_summary (); + + if (ical.get_description () != null) { + description = ical.get_description (); } - if (!node.get_object ().get_null_member ("parent_id")) { - parent_id = node.get_object ().get_string_member ("parent_id"); - } else { - parent_id = ""; + if (Util.find_string_value ("PRIORITY", data) != "") { + int _priority = int.parse (Util.find_string_value ("PRIORITY", data)); + if (_priority <= 0) { + priority = Constants.PRIORITY_4; + } else if (_priority >= 1 && _priority <= 4) { + priority = Constants.PRIORITY_1; + } else if (_priority == 5) { + priority = Constants.PRIORITY_2; + } else if (_priority > 5 && _priority <= 9) { + priority = Constants.PRIORITY_3; + } else { + priority = Constants.PRIORITY_4; + } } - if (!node.get_object ().get_null_member ("completed_at")) { - completed_at = node.get_object ().get_string_member ("completed_at"); + if (!ical.get_due ().is_null_time ()) { + due.date = Util.ical_to_date_time_local (ical.get_due ()).to_string (); + } + + parent_id = Util.find_string_value ("RELATED-TO", data); + + if (ical.get_status () == ICal.PropertyStatus.COMPLETED) { + checked = true; + string completed = Util.find_string_value ("COMPLETED", data); + if (completed != "") { + completed_at = Util.get_default ().get_format_date ( + Util.ical_to_date_time_local (new ICal.Time.from_string (completed)) + ).to_string (); + } else { + completed_at = Util.get_default ().get_format_date (new GLib.DateTime.now_local ()).to_string (); + } } else { + checked = false; completed_at = ""; } - if (!node.get_object ().get_null_member ("due")) { - due.update_from_json (node.get_object ().get_object_member ("due")); - } else { - due.reset (); + var categories = Util.find_string_value ("CATEGORIES", data); + check_labels (get_labels_maps_from_caldav (categories)); + + pinned = Util.find_boolean_value ("X-PINNED", data); + extra_data = Util.generate_extra_data (Util.get_task_id_from_url (element), etag, ical.as_ical_string ()); + } + + private Gee.ArrayList get_caldav_categories (string categories) { + Gee.ArrayList return_value = new Gee.ArrayList (); + + // string _categories = categories.replace ("\\,", ";"); + string[] categories_list = categories.split (","); + foreach (unowned string category in categories_list) { + // string category = str.replace (";", ","); + Objects.Label label = Services.Database.get_default ().get_label_by_name (category, true, BackendType.CALDAV); + if (label != null) { + return_value.add (label); + } } + + return return_value; } public void check_labels (Gee.HashMap new_labels) { @@ -431,8 +506,22 @@ public class Objects.Item : Objects.BaseObject { Gee.HashMap return_value = new Gee.HashMap (); foreach (unowned Json.Node element in node.get_object ().get_array_member ("labels").get_elements ()) { Objects.Label label = Services.Database.get_default ().get_label_by_name (element.get_string (), true, project.backend_type); - return_value [label.id_string] = label; + return_value [label.id] = label; + } + return return_value; + } + + public Gee.HashMap get_labels_maps_from_caldav (string categories) { + Gee.HashMap return_value = new Gee.HashMap (); + + string[] categories_list = categories.split (","); + foreach (unowned string category in categories_list) { + Objects.Label label = Services.Database.get_default ().get_label_by_name (category, true, BackendType.CALDAV); + if (label != null) { + return_value [label.id] = label; + } } + return return_value; } @@ -904,6 +993,10 @@ public class Objects.Item : Objects.BaseObject { ical.set_due (new_icaltime); } + if (parent_id != "") { + ical.add_property (new ICal.Property.relatedto (parent_id)); + } + if (checked) { ical.set_status (ICal.PropertyStatus.COMPLETED); ical.add_property (new ICal.Property.percentcomplete (100)); @@ -926,7 +1019,13 @@ public class Objects.Item : Objects.BaseObject { } ical.add_property (new ICal.Property.priority (_priority)); + + if (labels.size > 0) { + ical.add_property (new ICal.Property.categories (get_labels_names (labels))); + } + stdout.printf ("%s\n", ical.as_ical_string ()); + return "%s%s%s".printf ( "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Planify App (https://github.com/alainm23/planify)\n", ical.as_ical_string (), @@ -1038,4 +1137,18 @@ public class Objects.Item : Objects.BaseObject { }); } } + + public string get_labels_names (Gee.ArrayList labels) { + string return_value = ""; + + foreach (Objects.Label label in labels) { + return_value += label.name.replace (",", "\\,") + ","; + } + + if (return_value.length > 0) { + return_value = return_value.substring (0, return_value.length - 1); + } + + return return_value; + } } diff --git a/core/Objects/Project.vala b/core/Objects/Project.vala index d9aa3c556..9e542f984 100644 --- a/core/Objects/Project.vala +++ b/core/Objects/Project.vala @@ -137,6 +137,14 @@ public class Objects.Project : Objects.BaseObject { } } + Gee.ArrayList _all_items; + public Gee.ArrayList all_items { + get { + _all_items = Services.Database.get_default ().get_items_by_project (this); + return _all_items; + } + } + Gee.ArrayList _items_checked; public Gee.ArrayList items_checked { get { diff --git a/core/QuickAdd.vala b/core/QuickAdd.vala index 5c005a168..82c31706b 100644 --- a/core/QuickAdd.vala +++ b/core/QuickAdd.vala @@ -86,7 +86,8 @@ public class Layouts.QuickAdd : Adw.Bin { pin_button = new Widgets.PinButton (item); priority_button = new Widgets.PriorityButton (); priority_button.update_from_item (item); - label_button = new Widgets.LabelPicker.LabelButton (item.project.backend_type); + label_button = new Widgets.LabelPicker.LabelButton (); + label_button.backend_type = item.project.backend_type; var action_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12) { margin_start = 3, @@ -233,6 +234,7 @@ public class Layouts.QuickAdd : Adw.Bin { project_picker_button.project_change.connect ((project) => { item.project_id = project.id; + label_button.backend_type = project.backend_type; if (Services.Settings.get_default ().settings.get_boolean ("quick-add-save-last-project")) { Services.Settings.get_default ().settings.set_string ("quick-add-project-selected", project.id); @@ -373,6 +375,7 @@ public class Layouts.QuickAdd : Adw.Bin { private void reset_item () { item = new Objects.Item (); item.project_id = Services.Settings.get_default ().settings.get_string ("inbox-project-id"); + label_button.backend_type = item.project.backend_type; } public void update_content (string content = "") { @@ -420,6 +423,7 @@ public class Layouts.QuickAdd : Adw.Bin { public void for_project (Objects.Project project) { item.project_id = project.id; project_picker_button.project = project; + label_button.backend_type = project.backend_type; } public void for_section (Objects.Section section) { @@ -428,6 +432,7 @@ public class Layouts.QuickAdd : Adw.Bin { project_picker_button.project = section.project; project_picker_button.section = section; + label_button.backend_type = section.project.backend_type; } public void for_parent (Objects.Item _item) { @@ -436,6 +441,7 @@ public class Layouts.QuickAdd : Adw.Bin { item.parent_id = _item.id; project_picker_button.project = _item.project; + label_button.backend_type = _item.project.backend_type; } public void set_index (int index) { diff --git a/core/Services/CalDAV.vala b/core/Services/CalDAV.vala index 6a9a187f7..06f8cf5e7 100644 --- a/core/Services/CalDAV.vala +++ b/core/Services/CalDAV.vala @@ -363,15 +363,53 @@ public class Services.CalDAV : GLib.Object { GXml.DomDocument doc = new GXml.Document.from_string ((string) stream.get_data ()); GXml.DomHTMLCollection response = doc.get_elements_by_tag_name ("d:response"); + + // Categories + Gee.HashMap labels_map = new Gee.HashMap (); foreach (GXml.DomElement element in response) { - add_item_if_not_exists (element, project); + setup_categories (element, labels_map); + } + + foreach (string category in labels_map.values) { + var label = new Objects.Label (); + label.id = Util.get_default ().generate_id (label); + label.name = category; + label.color = Util.get_default ().get_random_color (); + label.backend_type = BackendType.CALDAV; + Services.Database.get_default ().insert_label (label); + } + + foreach (GXml.DomElement element in response) { + var item = add_item_if_not_exists (element, project); } } catch (Error e) { debug (e.message); } } - public async void update_all_tasks_by_tasklist (Objects.Project project) { + private void setup_categories (GXml.DomElement element, Gee.HashMap labels_map) { + GXml.DomElement propstat = element.get_elements_by_tag_name ("d:propstat").get_element (0); + GXml.DomElement prop = propstat.get_elements_by_tag_name ("d:prop").get_element (0); + string data = prop.get_elements_by_tag_name ("cal:calendar-data").get_element (0).text_content; + string etag = prop.get_elements_by_tag_name ("d:getetag").get_element (0).text_content; + + ICal.Component ical = new ICal.Component.from_string (data); + + var categories = Util.find_string_value ("CATEGORIES", data); + if (categories != "") { + string _categories = categories.replace ("\\,", ";"); + string[] categories_list = _categories.split (","); + foreach (unowned string str in categories_list) { + string category = str.replace (";", ","); + + if (!labels_map.has_key (category)) { + labels_map.set (category, category); + } + } + } + } + + public async void update_all_tasks_by_tasklist (Objects.Project project, Gee.HashMap labels_map) { var server_url = Services.Settings.get_default ().settings.get_string ("caldav-server-url"); var username = Services.Settings.get_default ().settings.get_string ("caldav-username"); var credential = Services.Settings.get_default ().settings.get_string ("caldav-credential"); @@ -387,12 +425,32 @@ public class Services.CalDAV : GLib.Object { GXml.DomDocument doc = new GXml.Document.from_string ((string) stream.get_data ()); GXml.DomHTMLCollection response = doc.get_elements_by_tag_name ("d:response"); + + foreach (GXml.DomElement element in response) { + setup_categories (element, labels_map); + } + + foreach (string category in labels_map.values) { + var label = Services.Database.get_default ().get_label_by_name (category, true, BackendType.CALDAV); + if (label == null) { + label = new Objects.Label (); + label.id = Util.get_default ().generate_id (label); + label.name = category; + label.color = Util.get_default ().get_random_color (); + label.backend_type = BackendType.CALDAV; + Services.Database.get_default ().insert_label (label); + } + } + + Gee.HashMap items_map = new Gee.HashMap (); foreach (GXml.DomElement element in response) { Objects.Item? item = Services.Database.get_default ().get_item ( Util.get_task_uid (element) ); if (item != null) { + items_map.set (item.id, item); + string old_project_id = item.project_id; string old_parent_id = item.parent_id; @@ -401,11 +459,6 @@ public class Services.CalDAV : GLib.Object { Services.Database.get_default ().update_item (item); if (old_project_id != item.project_id || old_parent_id != item.parent_id) { - print ("old_project_id: %s\n".printf (old_project_id)); - print ("old_parent_id: %s\n".printf (old_parent_id)); - print ("project_id: %s\n".printf (item.project_id)); - print ("parent_id: %s\n".printf (item.parent_id)); - Services.EventBus.get_default ().item_moved (item, old_project_id, "", old_parent_id); } @@ -414,7 +467,14 @@ public class Services.CalDAV : GLib.Object { Services.Database.get_default ().checked_toggled (item, old_checked); } } else { - add_item_if_not_exists (element, project); + item = add_item_if_not_exists (element, project); + items_map.set (item.id, item); + } + } + + foreach (Objects.Item item in project.all_items) { + if (!items_map.has_key (item.id)) { + Services.Database.get_default ().delete_item (item); } } } catch (Error e) { @@ -422,24 +482,28 @@ public class Services.CalDAV : GLib.Object { } } - private void add_item_if_not_exists (GXml.DomElement element, Objects.Project project) { + private Objects.Item add_item_if_not_exists (GXml.DomElement element, Objects.Project project) { + Objects.Item return_value; + string parent_id = Util.get_related_to_uid (element); if (parent_id != "") { Objects.Item? parent_item = Services.Database.get_default ().get_item (parent_id); if (parent_item != null) { - var item = new Objects.Item.from_caldav_xml (element); - item.project_id = project.id; - parent_item.add_item_if_not_exists (item); + return_value = new Objects.Item.from_caldav_xml (element); + return_value.project_id = project.id; + parent_item.add_item_if_not_exists (return_value); } else { - var item = new Objects.Item.from_caldav_xml (element); - item.project_id = project.id; - project.add_item_if_not_exists (item); + return_value = new Objects.Item.from_caldav_xml (element); + return_value.project_id = project.id; + project.add_item_if_not_exists (return_value); } } else { - var item = new Objects.Item.from_caldav_xml (element); - item.project_id = project.id; - project.add_item_if_not_exists (item); + return_value = new Objects.Item.from_caldav_xml (element); + return_value.project_id = project.id; + project.add_item_if_not_exists (return_value); } + + return return_value; } public void sync_async () { @@ -465,6 +529,10 @@ public class Services.CalDAV : GLib.Object { GXml.DomDocument doc = new GXml.Document.from_string ((string) stream.get_data ()); GXml.DomHTMLCollection response = doc.get_elements_by_tag_name ("d:response"); + + // Categories + Gee.HashMap labels_map = new Gee.HashMap (); + foreach (GXml.DomElement element in response) { if (is_vtodo_calendar (element)) { Objects.Project? project = Services.Database.get_default ().get_project (get_id_from_url (element)); @@ -475,7 +543,7 @@ public class Services.CalDAV : GLib.Object { } else { project.update_from_xml (element); Services.Database.get_default ().update_project (project); - yield update_all_tasks_by_tasklist (project); + yield update_all_tasks_by_tasklist (project, labels_map); } } else if (is_deleted_calendar (element)) { Objects.Project? project = Services.Database.get_default ().get_project (get_id_from_url (element)); @@ -485,12 +553,38 @@ public class Services.CalDAV : GLib.Object { } } + foreach (Objects.Label label in Services.Database.get_default ().get_labels_by_backend_type (BackendType.CALDAV)) { + if (!labels_map.has_key (label.name)) { + Services.Database.get_default ().delete_label (label); + } + } + sync_finished (); } catch (Error e) { debug (e.message); } } + private async void add_project_if_not_exists (GXml.DomElement element, Gee.HashMap labels_map) { + if (is_vtodo_calendar (element)) { + Objects.Project? project = Services.Database.get_default ().get_project (get_id_from_url (element)); + if (project == null) { + project = new Objects.Project.from_caldav_xml (element); + Services.Database.get_default ().insert_project (project); + yield get_all_tasks_by_tasklist (project); + } else { + project.update_from_xml (element); + Services.Database.get_default ().update_project (project); + yield update_all_tasks_by_tasklist (project, labels_map); + } + } else if (is_deleted_calendar (element)) { + Objects.Project? project = Services.Database.get_default ().get_project (get_id_from_url (element)); + if (project != null) { + Services.Database.get_default ().delete_project (project); + } + } + } + public string get_id_from_url (GXml.DomElement element) { GXml.DomElement href = element.get_elements_by_tag_name ("d:href").get_element (0); string[] parts = href.text_content.split ("/"); @@ -566,6 +660,39 @@ public class Services.CalDAV : GLib.Object { return status; } + public async HttpResponse refresh_tasklist (Objects.Project project) { + var server_url = Services.Settings.get_default ().settings.get_string ("caldav-server-url"); + var username = Services.Settings.get_default ().settings.get_string ("caldav-username"); + var credential = Services.Settings.get_default ().settings.get_string ("caldav-credential"); + + var url = "%s/remote.php/dav/calendars/%s/%s/".printf (server_url, username, project.id); + var message = new Soup.Message ("PROPFIND", url); + message.request_headers.append ("Authorization", "Basic %s".printf (credential)); + message.set_request_body_from_bytes ("application/xml", new Bytes ((TASKLIST_REQUEST).data)); + + HttpResponse return_value = new HttpResponse (); + + try { + GLib.Bytes stream = yield session.send_and_read_async (message, GLib.Priority.HIGH, null); + + GXml.DomDocument doc = new GXml.Document.from_string ((string) stream.get_data ()); + GXml.DomHTMLCollection response = doc.get_elements_by_tag_name ("d:response"); + + // Categories + Gee.HashMap labels_map = new Gee.HashMap (); + + foreach (GXml.DomElement element in response) { + yield add_project_if_not_exists (element, labels_map); + } + + return_value.status = true; + } catch (Error e) { + debug (e.message); + } + + return return_value; + } + /* * Task */ @@ -696,9 +823,9 @@ public class Services.CalDAV : GLib.Object { } // Delete all labels; - // foreach (var label in Services.Database.get_default ().get_all_labels_by_todoist ()) { - // Services.Database.get_default ().delete_label (label); - // } + foreach (var label in Services.Database.get_default ().get_labels_by_backend_type (BackendType.CALDAV)) { + Services.Database.get_default ().delete_label (label); + } // Clear Queue // Services.Database.get_default ().clear_queue (); diff --git a/core/Util/Util.vala b/core/Util/Util.vala index 1e96e1e3f..132217f05 100644 --- a/core/Util/Util.vala +++ b/core/Util/Util.vala @@ -729,7 +729,7 @@ public class Util : GLib.Object { string returned = name; if (name.length > size) { - returned = name.substring (0, size) + "…"; + returned = name.slice (0, size) + "…"; } return returned; diff --git a/core/Widgets/ContextMenu/MenuPicker.vala b/core/Widgets/ContextMenu/MenuPicker.vala index 0d7ec2da7..0a934acec 100644 --- a/core/Widgets/ContextMenu/MenuPicker.vala +++ b/core/Widgets/ContextMenu/MenuPicker.vala @@ -94,7 +94,9 @@ public class Widgets.ContextMenu.MenuPicker : Adw.Bin { button.add_css_class (Granite.STYLE_CLASS_FLAT); button.add_css_class ("transition"); - listbox = new Gtk.ListBox (); + listbox = new Gtk.ListBox () { + css_classes = { "listbox-background" } + }; var listbox_revealer = new Gtk.Revealer () { transition_type = Gtk.RevealerTransitionType.SLIDE_DOWN, diff --git a/core/Widgets/LabelPicker/LabelButton.vala b/core/Widgets/LabelPicker/LabelButton.vala index 9ab83060a..8a5d42cf2 100644 --- a/core/Widgets/LabelPicker/LabelButton.vala +++ b/core/Widgets/LabelPicker/LabelButton.vala @@ -20,8 +20,6 @@ */ public class Widgets.LabelPicker.LabelButton : Adw.Bin { - public BackendType backend_type { get; construct; } - private Gtk.MenuButton button; private Widgets.LabelPicker.LabelPicker labels_picker; @@ -31,11 +29,22 @@ public class Widgets.LabelPicker.LabelButton : Adw.Bin { } } + BackendType _backend_type; + public BackendType backend_type { + set { + _backend_type = value; + labels_picker.backend_type = _backend_type; + } + + get { + return _backend_type; + } + } + public signal void labels_changed (Gee.HashMap labels); - public LabelButton (BackendType backend_type = BackendType.ALL) { + public LabelButton () { Object ( - backend_type: backend_type, valign: Gtk.Align.CENTER, halign: Gtk.Align.CENTER, tooltip_text: _("Add label(s)") @@ -43,7 +52,7 @@ public class Widgets.LabelPicker.LabelButton : Adw.Bin { } construct { - labels_picker = new Widgets.LabelPicker.LabelPicker (backend_type); + labels_picker = new Widgets.LabelPicker.LabelPicker (); button = new Gtk.MenuButton () { child = new Widgets.DynamicIcon.from_icon_name ("planner-tag"), diff --git a/core/Widgets/LabelPicker/LabelPicker.vala b/core/Widgets/LabelPicker/LabelPicker.vala index 90c53366f..efba97859 100644 --- a/core/Widgets/LabelPicker/LabelPicker.vala +++ b/core/Widgets/LabelPicker/LabelPicker.vala @@ -20,8 +20,6 @@ */ public class Widgets.LabelPicker.LabelPicker : Gtk.Popover { - public BackendType backend_type { get; construct; } - private Gtk.SearchEntry search_entry; private Gtk.Stack placeholder_stack; private Gtk.ListBox listbox; @@ -46,9 +44,20 @@ public class Widgets.LabelPicker.LabelPicker : Gtk.Popover { } } - public LabelPicker (BackendType backend_type = BackendType.ALL) { + BackendType _backend_type; + public BackendType backend_type { + set { + _backend_type = value; + add_all_labels (_backend_type); + } + + get { + return _backend_type; + } + } + + public LabelPicker () { Object ( - backend_type: backend_type, has_arrow: false, position: Gtk.PositionType.TOP, width_request: 275, @@ -100,7 +109,6 @@ public class Widgets.LabelPicker.LabelPicker : Gtk.Popover { toolbar_view.content = listbox_scrolled; child = toolbar_view; - add_all_labels (); var controller_key = new Gtk.EventControllerKey (); toolbar_view.add_controller (controller_key); @@ -182,7 +190,13 @@ public class Widgets.LabelPicker.LabelPicker : Gtk.Popover { } } - private void add_all_labels () { + private void add_all_labels (BackendType backend_type) { + labels_widgets_map.clear (); + + foreach (unowned Gtk.Widget child in Util.get_default ().get_children (listbox) ) { + listbox.remove (child); + } + foreach (Objects.Label label in Services.Database.get_default ().get_labels_by_backend_type (backend_type)) { add_label (label); } diff --git a/core/Widgets/LoadingButton.vala b/core/Widgets/LoadingButton.vala index 092fd6bf8..7a0cc7238 100644 --- a/core/Widgets/LoadingButton.vala +++ b/core/Widgets/LoadingButton.vala @@ -71,9 +71,9 @@ public class Widgets.LoadingButton : Gtk.Button { construct { var submit_spinner = new Gtk.Spinner () { valign = Gtk.Align.CENTER, - halign = Gtk.Align.CENTER + halign = Gtk.Align.CENTER, + spinning = true }; - submit_spinner.start (); submit_stack = new Gtk.Stack () { transition_type = Gtk.StackTransitionType.CROSSFADE, diff --git a/meson.build b/meson.build index c9e26c1ac..e06f3aca7 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,7 @@ project( 'io.github.alainm23.planify', 'vala', 'c', - version: '4.4' + version: '4.4.1' ) gnome = import('gnome') diff --git a/src/Dialogs/Label.vala b/src/Dialogs/Label.vala index 1cb5728e1..90d269cfd 100644 --- a/src/Dialogs/Label.vala +++ b/src/Dialogs/Label.vala @@ -174,20 +174,25 @@ public class Dialogs.Label : Adw.Window { if (!is_creating) { submit_button.is_loading = true; - if (label.backend_type == BackendType.TODOIST) { + if (label.backend_type == BackendType.LOCAL || label.backend_type == BackendType.CALDAV) { + Services.Database.get_default ().update_label (label); + hide_destroy (); + } else if (label.backend_type == BackendType.TODOIST) { Services.Todoist.get_default ().update.begin (label, (obj, res) => { Services.Todoist.get_default ().update.end (res); Services.Database.get_default ().update_label (label); submit_button.is_loading = false; hide_destroy (); }); - } else if (label.backend_type == BackendType.LOCAL) { - Services.Database.get_default ().update_label (label); - hide_destroy (); } } else { label.item_order = Services.Database.get_default ().get_labels_by_backend_type (label.backend_type).size; - if (label.backend_type == BackendType.TODOIST) { + + if (label.backend_type == BackendType.LOCAL || label.backend_type == BackendType.CALDAV) { + label.id = Util.get_default ().generate_id (label); + Services.Database.get_default ().insert_label (label); + hide_destroy (); + } else if (label.backend_type == BackendType.TODOIST) { submit_button.is_loading = true; Services.Todoist.get_default ().add.begin (label, (obj, res) => { HttpResponse response = Services.Todoist.get_default ().add.end (res); @@ -201,10 +206,6 @@ public class Dialogs.Label : Adw.Window { } }); - } else if (label.backend_type == BackendType.LOCAL) { - label.id = Util.get_default ().generate_id (label); - Services.Database.get_default ().insert_label (label); - hide_destroy (); } } } diff --git a/src/Layouts/ItemRow.vala b/src/Layouts/ItemRow.vala index 8997da1d4..f2f2222b4 100644 --- a/src/Layouts/ItemRow.vala +++ b/src/Layouts/ItemRow.vala @@ -359,7 +359,8 @@ public class Layouts.ItemRow : Layouts.ItemBase { schedule_button = new Widgets.ScheduleButton (); priority_button = new Widgets.PriorityButton (); - label_button = new Widgets.LabelPicker.LabelButton (item.project.backend_type); + label_button = new Widgets.LabelPicker.LabelButton (); + label_button.backend_type = item.project.backend_type; label_button.labels = item._get_labels (); pin_button = new Widgets.PinButton (item); @@ -1530,7 +1531,11 @@ public class Layouts.ItemRow : Layouts.ItemBase { picked_item.section_id = ""; picked_item.parent_id = target_item.id; - if (picked_item.project.backend_type == BackendType.TODOIST) { + if (picked_item.project.backend_type == BackendType.LOCAL) { + target_item.collapsed = true; + Services.Database.get_default ().update_item (picked_item); + Services.EventBus.get_default ().item_moved (picked_item, old_project_id, old_section_id, old_parent_id); + } else if (picked_item.project.backend_type == BackendType.TODOIST) { Services.Todoist.get_default ().move_item.begin (picked_item, "parent_id", picked_item.parent_id, (obj, res) => { if (Services.Todoist.get_default ().move_item.end (res).status) { target_item.collapsed = true; @@ -1538,10 +1543,14 @@ public class Layouts.ItemRow : Layouts.ItemBase { Services.EventBus.get_default ().item_moved (picked_item, old_project_id, old_section_id, old_parent_id); } }); - } else if (picked_item.project.backend_type == BackendType.LOCAL) { - target_item.collapsed = true; - Services.Database.get_default ().update_item (picked_item); - Services.EventBus.get_default ().item_moved (picked_item, old_project_id, old_section_id, old_parent_id); + } else if (picked_item.project.backend_type == BackendType.CALDAV) { + Services.CalDAV.get_default ().add_task.begin (picked_item, true, (obj, res) => { + if (Services.CalDAV.get_default ().add_task.end (res).status) { + target_item.collapsed = true; + Services.Database.get_default ().update_item (picked_widget.item); + Services.EventBus.get_default ().item_moved (picked_item, old_project_id, old_section_id, old_parent_id); + } + }); } return true; @@ -1621,7 +1630,9 @@ public class Layouts.ItemRow : Layouts.ItemBase { picked_widget.item.parent_id = target_widget.item.parent_id; } - if (picked_widget.item.project.backend_type == BackendType.TODOIST) { + if (picked_widget.item.project.backend_type == BackendType.LOCAL) { + Services.Database.get_default ().update_item (picked_widget.item); + } else if (picked_widget.item.project.backend_type == BackendType.TODOIST) { string move_id = picked_widget.item.project_id; string move_type = "project_id"; @@ -1640,8 +1651,12 @@ public class Layouts.ItemRow : Layouts.ItemBase { Services.Database.get_default ().update_item (picked_widget.item); } }); - } else if (picked_widget.item.project.backend_type == BackendType.LOCAL) { - Services.Database.get_default ().update_item (picked_widget.item); + } else if (picked_widget.item.project.backend_type == BackendType.CALDAV) { + Services.CalDAV.get_default ().add_task.begin (picked_widget.item, true, (obj, res) => { + if (Services.CalDAV.get_default ().add_task.end (res).status) { + Services.Database.get_default ().update_item (picked_widget.item); + } + }); } } diff --git a/src/Layouts/ItemView.vala b/src/Layouts/ItemView.vala index 118f3a092..008ca0835 100644 --- a/src/Layouts/ItemView.vala +++ b/src/Layouts/ItemView.vala @@ -114,7 +114,8 @@ public class Layouts.ItemViewContent : Adw.Bin { pin_button = new Widgets.PinButton (item); priority_button = new Widgets.PriorityButton (); priority_button.update_from_item (item); - label_button = new Widgets.LabelPicker.LabelButton (item.project.backend_type); + label_button = new Widgets.LabelPicker.LabelButton (); + label_button.backend_type = item.project.backend_type; label_button.labels = item._get_labels (); reminder_button = new Widgets.ReminderButton (item); diff --git a/src/Layouts/ProjectRow.vala b/src/Layouts/ProjectRow.vala index a746d42f9..8754c0312 100644 --- a/src/Layouts/ProjectRow.vala +++ b/src/Layouts/ProjectRow.vala @@ -36,6 +36,7 @@ public class Layouts.ProjectRow : Gtk.ListBoxRow { private Gtk.Stack progress_emoji_stack; private Gtk.Label due_label; private Gtk.Stack menu_stack; + private Gtk.Revealer loading_revealer; public Gtk.Box main_content; public Gtk.Revealer main_revealer; @@ -66,6 +67,16 @@ public class Layouts.ProjectRow : Gtk.ListBoxRow { } } + public bool is_loading { + set { + loading_revealer.reveal_child = value; + } + + get { + return loading_revealer.reveal_child; + } + } + public ProjectRow (Objects.Project project, bool show_subprojects = true, bool drag_n_drop = true) { Object ( project: project, @@ -149,6 +160,17 @@ public class Layouts.ProjectRow : Gtk.ListBoxRow { end_box.append (menu_stack); end_box.append (arrow_revealer); + var loading_spinner = new Gtk.Spinner () { + valign = Gtk.Align.CENTER, + halign = Gtk.Align.CENTER, + spinning = true + }; + + loading_revealer = new Gtk.Revealer () { + transition_type = Gtk.RevealerTransitionType.SLIDE_RIGHT, + child = loading_spinner + }; + var projectrow_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6) { margin_start = 6, margin_top = 3, @@ -159,6 +181,7 @@ public class Layouts.ProjectRow : Gtk.ListBoxRow { projectrow_box.append (icon_project); projectrow_box.append (name_label); projectrow_box.append (end_box); + projectrow_box.append (loading_revealer); handle_grid = new Adw.Bin () { css_classes = { "transition", "drop-target" }, @@ -491,6 +514,7 @@ public class Layouts.ProjectRow : Gtk.ListBoxRow { favorite_item = new Widgets.ContextMenu.MenuItem (project.is_favorite ? _("Remove From Favorites") : _("Add to Favorites"), "planner-star"); var edit_item = new Widgets.ContextMenu.MenuItem (_("Edit Project"), "planner-edit"); + var refresh_item = new Widgets.ContextMenu.MenuItem (_("Refresh"), "planner-refresh"); var delete_item = new Widgets.ContextMenu.MenuItem (_("Delete Project"), "planner-trash"); delete_item.add_css_class ("menu-item-danger"); @@ -501,6 +525,9 @@ public class Layouts.ProjectRow : Gtk.ListBoxRow { menu_box.margin_top = menu_box.margin_bottom = 3; menu_box.append (favorite_item); menu_box.append (edit_item); + if (project.backend_type == BackendType.CALDAV) { + menu_box.append (refresh_item); + } menu_box.append (new Widgets.ContextMenu.MenuSeparator ()); menu_box.append (share_markdown_item); menu_box.append (share_email_item); @@ -538,6 +565,16 @@ public class Layouts.ProjectRow : Gtk.ListBoxRow { dialog.show (); }); + refresh_item.clicked.connect (() => { + menu_popover.popdown (); + + is_loading = true; + Services.CalDAV.get_default ().refresh_tasklist.begin (project, (obj, res) => { + HttpResponse response = Services.CalDAV.get_default ().refresh_tasklist.end (res); + is_loading = false; + }); + }); + delete_item.clicked.connect (() => { menu_popover.popdown (); diff --git a/src/Views/Label/Labels.vala b/src/Views/Label/Labels.vala index f17cbd7d1..a4714b811 100644 --- a/src/Views/Label/Labels.vala +++ b/src/Views/Label/Labels.vala @@ -22,13 +22,16 @@ public class Views.Labels : Adw.Bin { private Layouts.HeaderItem labels_local_header; private Layouts.HeaderItem labels_todoist_header; + private Layouts.HeaderItem labels_caldav_header; public Gee.HashMap labels_local_map; public Gee.HashMap labels_todoist_map; + public Gee.HashMap labels_caldav_map; construct { labels_local_map = new Gee.HashMap (); labels_todoist_map = new Gee.HashMap (); + labels_caldav_map = new Gee.HashMap (); var headerbar = new Layouts.HeaderBar (); headerbar.title = _("Labels"); @@ -45,6 +48,12 @@ public class Views.Labels : Adw.Bin { labels_todoist_header.show_separator = true; labels_todoist_header.set_sort_func (sort_func); + labels_caldav_header = new Layouts.HeaderItem (_("Labels: Nextcloud")); + labels_caldav_header.reveal = Services.CalDAV.get_default ().is_logged_in (); + labels_caldav_header.card = false; + labels_caldav_header.show_separator = true; + labels_caldav_header.set_sort_func (sort_func); + var content = new Gtk.Box (Gtk.Orientation.VERTICAL, 0) { hexpand = true, vexpand = true @@ -52,6 +61,7 @@ public class Views.Labels : Adw.Bin { content.append (labels_local_header); content.append (labels_todoist_header); + content.append (labels_caldav_header); var content_clamp = new Adw.Clamp () { maximum_size = 1024, @@ -81,6 +91,7 @@ public class Views.Labels : Adw.Bin { Timeout.add (225, () => { labels_local_header.set_sort_func (null); labels_todoist_header.set_sort_func (null); + labels_caldav_header.set_sort_func (null); return GLib.Source.REMOVE; }); @@ -116,6 +127,22 @@ public class Views.Labels : Adw.Bin { dialog.show (); }); + var add_caldav_button = new Gtk.Button () { + valign = Gtk.Align.CENTER, + can_focus = false, + child = new Widgets.DynamicIcon.from_icon_name ("plus") { + valign = Gtk.Align.CENTER, + halign = Gtk.Align.CENTER, + }, + css_classes = { Granite.STYLE_CLASS_FLAT, "header-item-button" } + }; + + labels_caldav_header.add_widget_end (add_caldav_button); + add_caldav_button.clicked.connect (() => { + var dialog = new Dialogs.Label.new (BackendType.CALDAV); + dialog.show (); + }); + labels_local_header.row_activated.connect ((row) => { Services.EventBus.get_default ().pane_selected (PaneType.LABEL, ((Layouts.LabelRow) row).label.id_string); }); @@ -147,6 +174,14 @@ public class Views.Labels : Adw.Bin { Services.Todoist.get_default ().log_out.connect (() => { labels_todoist_header.reveal = Services.Todoist.get_default ().is_logged_in (); }); + + Services.CalDAV.get_default ().log_in.connect (() => { + labels_caldav_header.reveal = Services.CalDAV.get_default ().is_logged_in (); + }); + + Services.CalDAV.get_default ().log_out.connect (() => { + labels_caldav_header.reveal = Services.CalDAV.get_default ().is_logged_in (); + }); } private void add_labels () { @@ -167,13 +202,16 @@ public class Views.Labels : Adw.Bin { labels_local_map[label.id] = new Layouts.LabelRow (label); labels_local_header.add_child (labels_local_map[label.id]); } - } - - if (label.backend_type == BackendType.TODOIST) { + } else if (label.backend_type == BackendType.TODOIST) { if (!labels_todoist_map.has_key (label.id)) { labels_todoist_map[label.id] = new Layouts.LabelRow (label); labels_todoist_header.add_child (labels_todoist_map[label.id]); } + } else if (label.backend_type == BackendType.CALDAV) { + if (!labels_caldav_map.has_key (label.id)) { + labels_caldav_map[label.id] = new Layouts.LabelRow (label); + labels_caldav_header.add_child (labels_caldav_map[label.id]); + } } } } diff --git a/src/Views/Project/Project.vala b/src/Views/Project/Project.vala index 92e8ce6e5..21c94f701 100644 --- a/src/Views/Project/Project.vala +++ b/src/Views/Project/Project.vala @@ -61,12 +61,10 @@ public class Views.Project : Gtk.Grid { valign = START, }; - var view_setting_popover = build_view_setting_popover (); - var view_setting_button = new Gtk.MenuButton () { valign = Gtk.Align.CENTER, halign = Gtk.Align.CENTER, - popover = view_setting_popover, + popover = build_view_setting_popover (), child = new Widgets.DynamicIcon.from_icon_name ("planner-settings-sliders"), css_classes = { "flat" }, tooltip_text = _("View option menu") @@ -116,7 +114,7 @@ public class Views.Project : Gtk.Grid { toolbar_view.content = content_overlay; attach (toolbar_view, 0, 0); - update_project_view (project.view_style); + update_project_view (project.backend_type == BackendType.CALDAV ? ProjectViewStyle.LIST : project.view_style); check_default_view (); show (); @@ -218,7 +216,7 @@ public class Views.Project : Gtk.Grid { var schedule_item = new Widgets.ContextMenu.MenuItem (_("When?"), "planner-calendar"); var add_section_item = new Widgets.ContextMenu.MenuItem (_("Add Section"), "planner-section"); add_section_item.secondary_text = "S"; - var manage_item = new Widgets.ContextMenu.MenuItem (_("Manage Sections"), "ordered-list"); + var manage_sections = new Widgets.ContextMenu.MenuItem (_("Manage Sections"), "ordered-list"); var filter_by_tags = new Widgets.ContextMenu.MenuItem (_("Filter by Labels"), "planner-tag"); var select_item = new Widgets.ContextMenu.MenuItem (_("Select"), "unordered-list"); @@ -235,9 +233,12 @@ public class Views.Project : Gtk.Grid { menu_box.append (new Widgets.ContextMenu.MenuSeparator ()); } - menu_box.append (add_section_item); - menu_box.append (manage_item); - menu_box.append (new Widgets.ContextMenu.MenuSeparator ()); + if (project.backend_type == BackendType.LOCAL || project.backend_type == BackendType.TODOIST) { + menu_box.append (add_section_item); + menu_box.append (manage_sections); + menu_box.append (new Widgets.ContextMenu.MenuSeparator ()); + } + menu_box.append (select_item); menu_box.append (paste_item); menu_box.append (show_completed_item); @@ -296,7 +297,7 @@ public class Views.Project : Gtk.Grid { prepare_new_section (); }); - manage_item.clicked.connect (() => { + manage_sections.clicked.connect (() => { popover.popdown (); var dialog = new Dialogs.ManageSectionOrder (project); dialog.show (); @@ -390,7 +391,8 @@ public class Views.Project : Gtk.Grid { hexpand = true, homogeneous = true, margin_start = 3, - margin_end = 3 + margin_end = 3, + margin_bottom = 12 }; view_box.append (list_button); @@ -403,9 +405,7 @@ public class Views.Project : Gtk.Grid { order_by_model.add (_("Date added")); order_by_model.add (_("Priority")); - var order_by_item = new Widgets.ContextMenu.MenuPicker (_("Order by"), "ordered-list", order_by_model) { - margin_top = 12 - }; + var order_by_item = new Widgets.ContextMenu.MenuPicker (_("Order by"), "ordered-list", order_by_model); order_by_item.selected = project.sort_order; show_completed_item = new Widgets.ContextMenu.MenuItem ( @@ -415,7 +415,11 @@ public class Views.Project : Gtk.Grid { var menu_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); menu_box.margin_top = menu_box.margin_bottom = 3; - menu_box.append (view_box); + + if (project.backend_type == BackendType.LOCAL || project.backend_type == BackendType.TODOIST) { + menu_box.append (view_box); + } + menu_box.append (order_by_item); menu_box.append (new Widgets.ContextMenu.MenuSeparator ()); menu_box.append (show_completed_item); @@ -460,6 +464,10 @@ public class Views.Project : Gtk.Grid { } public void prepare_new_section () { + if (project.backend_type == BackendType.CALDAV) { + return; + } + var dialog = new Dialogs.Section.new (project); dialog.show (); } diff --git a/src/Widgets/MultiSelectToolbar.vala b/src/Widgets/MultiSelectToolbar.vala index 948b88f50..3f392cc7d 100644 --- a/src/Widgets/MultiSelectToolbar.vala +++ b/src/Widgets/MultiSelectToolbar.vala @@ -53,9 +53,10 @@ public class Widgets.MultiSelectToolbar : Adw.Bin { }; schedule_button.visible_no_date = true; - label_button = new Widgets.LabelPicker.LabelButton (project.backend_type) { + label_button = new Widgets.LabelPicker.LabelButton () { sensitive = false }; + label_button.backend_type = project.backend_type; priority_button = new Widgets.PriorityButton () { sensitive = false