From 25f6164f3467b5eeda99ee6d8dd191fa687615ed Mon Sep 17 00:00:00 2001 From: Alain Date: Thu, 20 Jun 2024 13:28:06 -0500 Subject: [PATCH] feat: #1232 --- core/Enum.vala | 102 ++++++++++-------- core/Objects/DueDate.vala | 12 --- core/Objects/Item.vala | 96 +++++++++++++---- core/Objects/Reminder.vala | 61 ++++++++--- core/Services/Database.vala | 31 +++++- core/Services/Settings.vala | 6 +- core/Services/Todoist.vala | 32 +++++- core/Util/Datetime.vala | 11 ++ core/Util/Util.vala | 61 +++++++++++ core/Widgets/DateTimePicker/TimePicker.vala | 2 +- .../ReminderPicker/ReminderButton.vala | 8 +- .../ReminderPicker/ReminderPicker.vala | 38 +++---- core/Widgets/ReminderPicker/ReminderRow.vala | 16 +-- data/io.github.alainm23.planify.gschema.xml | 22 ++++ quick-add/MainWindow.vala | 19 +++- .../Preferences/PreferencesWindow.vala | 32 ++++++ src/Dialogs/QuickAdd.vala | 27 ++--- src/Layouts/ItemBoard.vala | 8 +- src/Layouts/ItemRow.vala | 31 +----- src/Layouts/ItemSidebarView.vala | 10 +- src/MainWindow.vala | 7 +- src/Services/Notification.vala | 18 ++-- 22 files changed, 448 insertions(+), 202 deletions(-) diff --git a/core/Enum.vala b/core/Enum.vala index d3341877e..7469d27bc 100644 --- a/core/Enum.vala +++ b/core/Enum.vala @@ -221,49 +221,49 @@ public enum RecurrencyType { public string to_friendly_string (int? interval = null) { switch (this) { - case NONE: - return _("Don't Repeat"); - case MINUTELY: - if (interval == null || interval == 0) { - return _("Every minute"); - } else { - return GLib.ngettext (_("Every minute"), _("Every %d minutes"), interval).printf (interval); - } - case HOURLY: - if (interval == null || interval == 0) { - return _("Every hour"); - } else { - return GLib.ngettext (_("Every hour"), _("Every %d hours"), interval).printf (interval); - } - case EVERY_DAY: - if (interval == null || interval == 0) { - return _("Every day"); - } else { - return GLib.ngettext (_("Every day"), _("Every %d days"), interval).printf (interval); - } - case EVERY_WEEK: - if (interval == null || interval == 0) { - return _("Every week"); - } else { - return GLib.ngettext (_("Every week"), _("Every %d weeks"), interval).printf (interval); - } - - case EVERY_MONTH: - if (interval == null || interval == 0) { - return _("Every month"); - } else { - return GLib.ngettext (_("Every month"), _("Every %d months"), interval).printf (interval); - } - - case EVERY_YEAR: - if (interval == null || interval == 0) { - return _("Every year"); - } else { - return GLib.ngettext (_("Every year"), _("Every %d years"), interval).printf (interval); - } + case NONE: + return _("Don't Repeat"); + case MINUTELY: + if (interval == null || interval == 0) { + return _("Every minute"); + } else { + return GLib.ngettext (_("Every minute"), _("Every %d minutes"), interval).printf (interval); + } + case HOURLY: + if (interval == null || interval == 0) { + return _("Every hour"); + } else { + return GLib.ngettext (_("Every hour"), _("Every %d hours"), interval).printf (interval); + } + case EVERY_DAY: + if (interval == null || interval == 0) { + return _("Every day"); + } else { + return GLib.ngettext (_("Every day"), _("Every %d days"), interval).printf (interval); + } + case EVERY_WEEK: + if (interval == null || interval == 0) { + return _("Every week"); + } else { + return GLib.ngettext (_("Every week"), _("Every %d weeks"), interval).printf (interval); + } + + case EVERY_MONTH: + if (interval == null || interval == 0) { + return _("Every month"); + } else { + return GLib.ngettext (_("Every month"), _("Every %d months"), interval).printf (interval); + } + + case EVERY_YEAR: + if (interval == null || interval == 0) { + return _("Every year"); + } else { + return GLib.ngettext (_("Every year"), _("Every %d years"), interval).printf (interval); + } - default: - assert_not_reached (); + default: + assert_not_reached (); } } } @@ -362,3 +362,21 @@ public enum FilterItemType { } } } + +public enum ReminderType { + ABSOLUTE, + RELATIVE; + + public string to_string () { + switch (this) { + case ABSOLUTE: + return "absolute"; + + case RELATIVE: + return "relative"; + + default: + assert_not_reached (); + } + } +} diff --git a/core/Objects/DueDate.vala b/core/Objects/DueDate.vala index fefe6649d..47e1cec0f 100644 --- a/core/Objects/DueDate.vala +++ b/core/Objects/DueDate.vala @@ -105,17 +105,6 @@ public class Objects.DueDate : GLib.Object { is_recurring = object.get_boolean_member ("is_recurring"); Utils.Datetime.parse_todoist_recurrency (this, object); } - // if (object.has_member ("recurrency_type")) { - // recurrency_type = (RecurrencyType) int.parse (object.get_string_member ("recurrency_type")); - // } - - // if (object.has_member ("recurrency_interval")) { - // recurrency_interval = int.parse (object.get_string_member ("recurrency_interval")); - // } - - // if (object.has_member ("recurrency_weeks")) { - // recurrency_weeks = object.get_string_member ("recurrency_weeks"); - // } } public void update_from_json (Json.Object object) { @@ -221,7 +210,6 @@ public class Objects.DueDate : GLib.Object { new_due.recurrency_count = recurrency_count; new_due.recurrency_end = recurrency_end; new_due.recurrence_supported = recurrence_supported; - return new_due; } } diff --git a/core/Objects/Item.vala b/core/Objects/Item.vala index b73ae8b96..a4cedf080 100644 --- a/core/Objects/Item.vala +++ b/core/Objects/Item.vala @@ -134,6 +134,16 @@ public class Objects.Item : Objects.BaseObject { } } + public bool has_time { + get { + if (due.datetime == null) { + return false; + } + + return Utils.Datetime.has_time (due.datetime); + } + } + GLib.DateTime _completed_date; public GLib.DateTime completed_date { get { @@ -773,7 +783,7 @@ public class Objects.Item : Objects.BaseObject { reminder_added (reminder); } - add_reminder (reminder); + _add_reminder (reminder); } return return_value; } @@ -783,7 +793,7 @@ public class Objects.Item : Objects.BaseObject { Objects.Reminder? return_value = null; lock (_reminders) { foreach (var _reminder in _reminders) { - if (reminder.due.datetime.compare (_reminder.due.datetime) == 0) { + if (reminder.datetime.compare (_reminder.datetime) == 0) { return_value = _reminder; break; } @@ -792,7 +802,7 @@ public class Objects.Item : Objects.BaseObject { return return_value; } - private void add_reminder (Objects.Reminder reminder) { + private void _add_reminder (Objects.Reminder reminder) { _reminders.add (reminder); } @@ -1303,24 +1313,6 @@ public class Objects.Item : Objects.BaseObject { return new_item; } - // public void duplicate () { - // var new_item = generate_copy (); - // new_item.content = "[%s] %s".printf (_("Duplicate"), content); - - // if (project.backend_type == BackendType.TODOIST) { - // Services.Todoist.get_default ().add.begin (new_item, (obj, res) => { - // HttpResponse response = Services.Todoist.get_default ().add.end (res); - // if (response.status) { - // new_item.id = response.data; - // insert_duplicate (new_item); - // } - // }); - // } else { - // new_item.id = Util.get_default ().generate_id (new_item); - // insert_duplicate (new_item); - // } - // } - public Objects.Item duplicate () { var new_item = new Objects.Item (); new_item.content = content; @@ -1329,7 +1321,6 @@ public class Objects.Item : Objects.BaseObject { new_item.pinned = pinned; new_item.priority = priority; new_item.labels = labels; - return new_item; } @@ -1553,4 +1544,65 @@ public class Objects.Item : Objects.BaseObject { return text; } + + public void update_due (GLib.DateTime? datetime) { + due.date = datetime == null ? "" : Utils.Datetime.get_todoist_datetime_format (datetime); + + if (Services.Settings.get_default ().get_boolean ("automatic-reminders-enabled") && has_time) { + remove_all_relative_reminders (); + + var reminder = new Objects.Reminder (); + reminder.mm_offset = Util.get_reminders_mm_offset (); + reminder.reminder_type = ReminderType.RELATIVE; + add_reminder (reminder); + } + + if (due.date == "") { + due.reset (); + remove_all_relative_reminders (); + } + + if (!has_time) { + remove_all_relative_reminders (); + } + + update_async (""); + } + + public void add_reminder (Objects.Reminder reminder) { + reminder.item_id = id; + + if (project.backend_type == BackendType.TODOIST) { + Services.Todoist.get_default ().add.begin (reminder, (obj, res) => { + HttpResponse response = Services.Todoist.get_default ().add.end (res); + loading = false; + + if (response.status) { + reminder.id = response.data; + } else { + reminder.id = Util.get_default ().generate_id (reminder); + } + + add_reminder_if_not_exists (reminder); + }); + } else { + reminder.id = Util.get_default ().generate_id (reminder); + add_reminder_if_not_exists (reminder); + } + } + + public void add_reminder_events (Objects.Reminder reminder) { + Services.Database.get_default ().reminder_added (reminder); + Services.Database.get_default ().reminders.add (reminder); + reminder.item.reminder_added (reminder); + _add_reminder (reminder); + } + + private void remove_all_relative_reminders () { + foreach (Objects.Reminder reminder in reminders) { + if (reminder.reminder_type == ReminderType.RELATIVE) { + reminder.delete (); + } + } + } } diff --git a/core/Objects/Reminder.vala b/core/Objects/Reminder.vala index 8619f8865..cd65e9561 100644 --- a/core/Objects/Reminder.vala +++ b/core/Objects/Reminder.vala @@ -26,6 +26,7 @@ public class Objects.Reminder : Objects.BaseObject { public Objects.DueDate due { get; set; default = new Objects.DueDate (); } public int mm_offset { get; set; default = 0; } public int is_deleted { get; set; default = 0; } + public ReminderType reminder_type { get; set; default = ReminderType.ABSOLUTE; } Objects.Item? _item; public Objects.Item item { @@ -39,19 +40,45 @@ public class Objects.Reminder : Objects.BaseObject { } } - bool _loading = false; - public bool loading { - set { - _loading = value; - loading_changed (_loading); + GLib.DateTime _datetime; + public GLib.DateTime datetime { + get { + if (reminder_type == ReminderType.ABSOLUTE) { + _datetime = due.datetime; + } else { + _datetime = item.due.datetime.add_minutes (mm_offset * -1); + } + + return _datetime; } + } + + string _relative_text; + public string relative_text { get { - return _loading; + _relative_text = ""; + + if (reminder_type == ReminderType.ABSOLUTE) { + _relative_text = Utils.Datetime.get_relative_date_from_date (due.datetime); + } else { + _relative_text = Util.get_reminders_mm_offset_text (mm_offset); + } + + return _relative_text; } } - public signal void loading_changed (bool value); + public Reminder.from_json (Json.Node node) { + id = node.get_object ().get_string_member ("id"); + item_id = node.get_object ().get_string_member ("item_id"); + reminder_type = node.get_object ().get_string_member ("type") == "absolute" ? ReminderType.ABSOLUTE : ReminderType.RELATIVE; + mm_offset = (int32) node.get_object ().get_int_member ("item_id"); + + if (reminder_type == ReminderType.ABSOLUTE) { + due.update_from_todoist_json (node.get_object ().get_object_member ("due")); + } + } construct { deleted.connect (() => { @@ -94,13 +121,19 @@ public class Objects.Reminder : Objects.BaseObject { builder.set_member_name ("item_id"); builder.add_string_value (item_id); - builder.set_member_name ("due"); - builder.begin_object (); - - builder.set_member_name ("date"); - builder.add_string_value (due.date); - - builder.end_object (); + builder.set_member_name ("type"); + builder.add_string_value (reminder_type.to_string ()); + + builder.set_member_name ("minute_offset"); + builder.add_int_value (mm_offset); + + if (reminder_type == ReminderType.ABSOLUTE) { + builder.set_member_name ("due"); + builder.begin_object (); + builder.set_member_name ("date"); + builder.add_string_value (due.date); + builder.end_object (); + } builder.end_object (); builder.end_object (); diff --git a/core/Services/Database.vala b/core/Services/Database.vala index 199f796fc..49868dd57 100644 --- a/core/Services/Database.vala +++ b/core/Services/Database.vala @@ -1294,7 +1294,6 @@ public class Services.Database : GLib.Object { if (stmt.step () == Sqlite.DONE) { add_item (item, insert); - // add_item_event (Objects.ObjectEvent.for_add_item (item)); } else { warning ("Error: %d: %s", db.errcode (), db.errmsg ()); } @@ -1382,6 +1381,24 @@ public class Services.Database : GLib.Object { return returned; } + public Gee.ArrayList get_reminders_by_item_id (string id) { + Gee.ArrayList return_value = new Gee.ArrayList (); + Sqlite.Statement stmt; + + sql = """ + SELECT id, item_id, type, due, mm_offset FROM Reminders WHERE item_id=$item_id; + """; + + db.prepare_v2 (sql, sql.length, out stmt); + set_parameter_str (stmt, "$item_id", id); + + while (stmt.step () == Sqlite.ROW) { + return_value.add (_fill_reminder (stmt)); + } + stmt.reset (); + return return_value; + } + public Gee.ArrayList get_item_by_baseobject (Objects.BaseObject object) { Gee.ArrayList return_value = new Gee.ArrayList (); lock (_items) { @@ -1987,14 +2004,16 @@ public class Services.Database : GLib.Object { string sql; sql = """ - INSERT OR IGNORE INTO Reminders (id, item_id, due) - VALUES ($id, $item_id, $due); + INSERT OR IGNORE INTO Reminders (id, item_id, type, due, mm_offset) + VALUES ($id, $item_id, $type, $due, $mm_offset); """; db.prepare_v2 (sql, sql.length, out stmt); set_parameter_str (stmt, "$id", reminder.id); set_parameter_str (stmt, "$item_id", reminder.item_id); + set_parameter_str (stmt, "$type", reminder.reminder_type.to_string ()); set_parameter_str (stmt, "$due", reminder.due.to_string ()); + set_parameter_int (stmt, "$mm_offset", reminder.mm_offset); if (stmt.step () != Sqlite.DONE) { warning ("Error: %d: %s", db.errcode (), db.errmsg ()); @@ -2012,7 +2031,7 @@ public class Services.Database : GLib.Object { Sqlite.Statement stmt; sql = """ - SELECT id, item_id, due FROM Reminders; + SELECT id, item_id, type, due, mm_offset FROM Reminders; """; db.prepare_v2 (sql, sql.length, out stmt); @@ -2028,7 +2047,9 @@ public class Services.Database : GLib.Object { Objects.Reminder return_value = new Objects.Reminder (); return_value.id = stmt.column_text (0); return_value.item_id = stmt.column_text (1); - return_value.due.update_from_json (get_due_parameter (stmt.column_text (2))); + return_value.reminder_type = stmt.column_text (2) == "absolute" ? ReminderType.ABSOLUTE : ReminderType.RELATIVE; + return_value.due.update_from_json (get_due_parameter (stmt.column_text (3))); + return_value.mm_offset = stmt.column_int (4); return return_value; } diff --git a/core/Services/Settings.vala b/core/Services/Settings.vala index 7142925f5..def4141c6 100644 --- a/core/Services/Settings.vala +++ b/core/Services/Settings.vala @@ -48,4 +48,8 @@ public class Services.Settings : GLib.Object { var value = Services.Settings.get_default ().settings.get_enum ("new-tasks-position"); return value == 0 ? NewTaskPosition.TOP : NewTaskPosition.BOTTOM; } -} + + public bool get_boolean (string key) { + return settings.get_boolean (key); + } + } diff --git a/core/Services/Todoist.vala b/core/Services/Todoist.vala index 217428938..73f3e52af 100644 --- a/core/Services/Todoist.vala +++ b/core/Services/Todoist.vala @@ -28,6 +28,7 @@ public class Services.Todoist : GLib.Object { private const string SECTIONS_COLLECTION = "sections"; private const string ITEMS_COLLECTION = "items"; private const string LABELS_COLLECTION = "labels"; + private const string REMINDERS_COLLECTION = "reminders"; private static Todoist? _instance; public static Todoist get_default () { @@ -214,6 +215,16 @@ public class Services.Todoist : GLib.Object { add_item_if_not_exists (_node); } + // Create Reminders + unowned Json.Array reminders = parser.get_root ().get_object ().get_array_member (REMINDERS_COLLECTION); + foreach (unowned Json.Node _node in reminders.get_elements ()) { + Objects.Reminder reminder = new Objects.Reminder.from_json (_node); + Objects.Item? item = Services.Database.get_default ().get_item (reminder.item_id); + if (item != null) { + item.add_reminder_if_not_exists (reminder); + } + } + first_sync_progress (0.85); // Download Profile Image @@ -364,6 +375,25 @@ public class Services.Todoist : GLib.Object { } } + // Reminders + unowned Json.Array reminders = parser.get_root ().get_object ().get_array_member (REMINDERS_COLLECTION); + foreach (unowned Json.Node _node in reminders.get_elements ()) { + Objects.Reminder? reminder = Services.Database.get_default ().get_reminder (_node.get_object ().get_string_member ("id")); + + if (reminder != null) { + if (_node.get_object ().get_boolean_member ("is_deleted")) { + Services.Database.get_default ().delete_reminder (reminder); + } + } else { + reminder = new Objects.Reminder.from_json (_node); + + Objects.Item? item = Services.Database.get_default ().get_item (reminder.item_id); + if (item != null) { + item.add_reminder_if_not_exists (reminder); + } + } + } + Services.Settings.get_default ().settings.set_string ("todoist-last-sync", new GLib.DateTime.now_local ().to_string ()); } } catch (Error e) { @@ -774,7 +804,7 @@ public class Services.Todoist : GLib.Object { string uuid = Util.get_default ().generate_string (); string id; string json = object.get_add_json (temp_id, uuid); - + var message = new Soup.Message ("POST", TODOIST_SYNC_URL); message.request_headers.append ( "Authorization", diff --git a/core/Util/Datetime.vala b/core/Util/Datetime.vala index ac0ae14bf..0b90cfb98 100644 --- a/core/Util/Datetime.vala +++ b/core/Util/Datetime.vala @@ -576,4 +576,15 @@ public class Utils.Datetime { return " (" + get_relative_date_from_date (item.due.datetime) + ") "; } + + public static GLib.DateTime get_datetime_no_seconds (GLib.DateTime date, GLib.DateTime? time = null) { + return new DateTime.local ( + date.get_year (), + date.get_month (), + date.get_day_of_month (), + time == null ? date.get_hour () : time.get_hour (), + time == null ? date.get_minute () : time.get_minute (), + 0 + ); + } } diff --git a/core/Util/Util.vala b/core/Util/Util.vala index 7e10ca1d7..9b61d8705 100644 --- a/core/Util/Util.vala +++ b/core/Util/Util.vala @@ -1173,6 +1173,67 @@ We hope you’ll enjoy using Planify!"""); return false; } + + public static int get_reminders_mm_offset () { + int value = Services.Settings.get_default ().settings.get_enum ("automatic-reminders"); + int return_value = 0; + + switch (value) { + case 0: + return_value = 0; + break; + case 1: + return_value = 10; + break; + case 2: + return_value = 30; + break; + case 3: + return_value = 45; + break; + case 4: + return_value = 60; + break; + case 5: + return_value = 120; + break; + case 6: + return_value = 180; + break; + } + + return return_value; + } + + public static string get_reminders_mm_offset_text (int value) { + string return_value = ""; + + switch (value) { + case 0: + return_value = _("At due time"); + break; + case 10: + return_value = _("10 minutes before"); + break; + case 30: + return_value = _("30 minutes before"); + break; + case 45: + return_value = _("45 minutes before"); + break; + case 60: + return_value = _("1 hour before"); + break; + case 120: + return_value = _("2 hours before"); + break; + case 180: + return_value = _("3 hours before"); + break; + } + + return return_value; + } } public class RegexMarkdown { diff --git a/core/Widgets/DateTimePicker/TimePicker.vala b/core/Widgets/DateTimePicker/TimePicker.vala index bbb42c2b8..bcdffedb9 100644 --- a/core/Widgets/DateTimePicker/TimePicker.vala +++ b/core/Widgets/DateTimePicker/TimePicker.vala @@ -33,7 +33,7 @@ public class Widgets.DateTimePicker.TimePicker : Adw.Bin { if (_time == null) { time = new GLib.DateTime.now_local (); } - + return _time; } diff --git a/core/Widgets/ReminderPicker/ReminderButton.vala b/core/Widgets/ReminderPicker/ReminderButton.vala index d6169c459..65c57e947 100644 --- a/core/Widgets/ReminderPicker/ReminderButton.vala +++ b/core/Widgets/ReminderPicker/ReminderButton.vala @@ -138,6 +138,8 @@ public class Widgets.ReminderPicker.ReminderButton : Adw.Bin { if (reminders.size > 0) { build_value_label (reminders); } + + indicator_revealer.reveal_child = reminders.size > 0; } public void add_reminder (Objects.Reminder reminder, Gee.ArrayList reminders) { @@ -146,6 +148,8 @@ public class Widgets.ReminderPicker.ReminderButton : Adw.Bin { if (reminders.size > 0) { build_value_label (reminders); } + + indicator_revealer.reveal_child = reminders.size > 0; } public void delete_reminder (Objects.Reminder reminder, Gee.ArrayList reminders) { @@ -157,12 +161,14 @@ public class Widgets.ReminderPicker.ReminderButton : Adw.Bin { if (reminders.size > 0) { build_value_label (reminders); } + + indicator_revealer.reveal_child = reminders.size > 0; } private void build_value_label (Gee.ArrayList reminders) { value_label.label = ""; for (int index = 0; index < reminders.size; index++) { - var date = Utils.Datetime.get_relative_date_from_date (reminders[index].due.datetime); + string date = reminders[index].relative_text; if (index < reminders.size - 1) { value_label.label += date + ", "; diff --git a/core/Widgets/ReminderPicker/ReminderPicker.vala b/core/Widgets/ReminderPicker/ReminderPicker.vala index 9c7537606..9c4a18601 100644 --- a/core/Widgets/ReminderPicker/ReminderPicker.vala +++ b/core/Widgets/ReminderPicker/ReminderPicker.vala @@ -36,7 +36,7 @@ public class Widgets.ReminderPicker._ReminderPicker : Gtk.Popover { Object ( is_creating: is_creating, position: Gtk.PositionType.BOTTOM, - width_request: 250 + width_request: 275 ); } @@ -106,33 +106,25 @@ public class Widgets.ReminderPicker._ReminderPicker : Gtk.Popover { } var reminder = new Objects.Reminder (); - reminder.due.date = Utils.Datetime.get_todoist_datetime_format (get_datetime_picker ()); + reminder.due.date = Utils.Datetime.get_todoist_datetime_format ( + Utils.Datetime.get_datetime_no_seconds (calendar.date, time_picker.time) + ); reminder_added (reminder); main_stack.visible_child_name = "listbox"; submit_button.is_loading = false; } - private GLib.DateTime get_datetime_picker () { - return new DateTime.local ( - calendar.date.get_year (), - calendar.date.get_month (), - calendar.date.get_day_of_month (), - time_picker.time.get_hour (), - time_picker.time.get_minute (), - time_picker.time.get_seconds () - ); - } - private Gtk.Widget get_picker () { + var back_item = new Widgets.ContextMenu.MenuItem (_("Back"), "go-previous-symbolic"); + calendar = new Widgets.Calendar.Calendar () { vexpand = true, hexpand = true }; var calendar_grid = new Adw.Bin () { - child = calendar, - css_classes = { "card" } + child = calendar }; var time_icon = new Gtk.Image.from_icon_name ("alarm-symbolic") { @@ -150,9 +142,7 @@ public class Widgets.ReminderPicker._ReminderPicker : Gtk.Popover { }; var time_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) { - hexpand = true, - margin_top = 6, - css_classes = { "card" } + hexpand = true }; time_box.append (time_icon); @@ -168,12 +158,24 @@ public class Widgets.ReminderPicker._ReminderPicker : Gtk.Popover { }; var main_grid = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + main_grid.append (back_item); + main_grid.append (new Widgets.ContextMenu.MenuSeparator ()); main_grid.append (calendar_grid); + main_grid.append (new Gtk.Separator (Gtk.Orientation.VERTICAL) { + margin_start = 9, + margin_end = 6, + margin_top = 6, + margin_bottom = 6 + }); main_grid.append (time_box); main_grid.append (submit_button); submit_button.clicked.connect (insert_reminder); + back_item.clicked.connect (() => { + main_stack.visible_child_name = "listbox"; + }); + return main_grid; } diff --git a/core/Widgets/ReminderPicker/ReminderRow.vala b/core/Widgets/ReminderPicker/ReminderRow.vala index 5db6ae0be..3efc49fae 100644 --- a/core/Widgets/ReminderPicker/ReminderRow.vala +++ b/core/Widgets/ReminderPicker/ReminderRow.vala @@ -24,12 +24,6 @@ public class Widgets.ReminderPicker.ReminderRow : Gtk.ListBoxRow { private Gtk.Revealer main_revealer; - public bool is_creating { - get { - return reminder.id == ""; - } - } - public signal void activated (); public signal void deleted (); @@ -49,8 +43,8 @@ public class Widgets.ReminderPicker.ReminderRow : Gtk.ListBoxRow { construct { add_css_class ("row"); - - var reminder_label = new Gtk.Label (is_creating ? _("Add Reminder") : Utils.Datetime.get_relative_date_from_date (reminder.due.datetime)); + + var reminder_label = new Gtk.Label (reminder.relative_text); var remove_button = new Widgets.LoadingButton.with_icon ("cross-large-circle-filled-symbolic") { hexpand = true, @@ -68,7 +62,7 @@ public class Widgets.ReminderPicker.ReminderRow : Gtk.ListBoxRow { margin_end = 3 }; - reminder_box.append (new Gtk.Image.from_icon_name (is_creating ? "plus-large-symbolic" : "alarm-symbolic")); + reminder_box.append (new Gtk.Image.from_icon_name ("alarm-symbolic")); reminder_box.append (reminder_label); reminder_box.append (remove_button); @@ -88,8 +82,8 @@ public class Widgets.ReminderPicker.ReminderRow : Gtk.ListBoxRow { deleted (); }); - reminder.loading_changed.connect ((value) => { - remove_button.is_loading = value; + reminder.loading_change.connect (() => { + remove_button.is_loading = reminder.loading; }); } diff --git a/data/io.github.alainm23.planify.gschema.xml b/data/io.github.alainm23.planify.gschema.xml index bb496e588..591debfc1 100644 --- a/data/io.github.alainm23.planify.gschema.xml +++ b/data/io.github.alainm23.planify.gschema.xml @@ -68,6 +68,16 @@ + + + + + + + + + + "None" @@ -422,5 +432,17 @@ CalDAV backend type CalDAV backend type + + + true + Automatic Reminders Enabled. + Automatic Reminders Enabled. + + + + "At due time" + Automatic Reminders + Automatic Reminders + diff --git a/quick-add/MainWindow.vala b/quick-add/MainWindow.vala index c7bbffa39..331a7a613 100644 --- a/quick-add/MainWindow.vala +++ b/quick-add/MainWindow.vala @@ -28,11 +28,26 @@ public class MainWindow : Adw.ApplicationWindow { quick_add_widget.add_item_db.connect ((add_item_db)); } - private void add_item_db (Objects.Item item) { + private void add_item_db (Objects.Item item, Gee.ArrayList reminders) { if (Services.Database.get_default ().insert_item (item)) { + if (reminders.size > 0) { + quick_add_widget.is_loading = true; + + foreach (Objects.Reminder reminder in reminders) { + item.add_reminder (reminder); + } + } + + if (Services.Settings.get_default ().get_boolean ("automatic-reminders-enabled") && item.has_time) { + var reminder = new Objects.Reminder (); + reminder.mm_offset = Util.get_reminders_mm_offset (); + reminder.reminder_type = ReminderType.RELATIVE; + item.add_reminder (reminder); + } + send_interface_id (item.id); quick_add_widget.added_successfully (); - } + } } private void send_interface_id (string id) { diff --git a/src/Dialogs/Preferences/PreferencesWindow.vala b/src/Dialogs/Preferences/PreferencesWindow.vala index 55dadb82a..763885d27 100644 --- a/src/Dialogs/Preferences/PreferencesWindow.vala +++ b/src/Dialogs/Preferences/PreferencesWindow.vala @@ -601,8 +601,36 @@ public class Dialogs.Preferences.PreferencesWindow : Adw.PreferencesDialog { group.add (attention_at_one); + var reminders_group = new Adw.PreferencesGroup () { + title = _("Reminders") + }; + + var automatic_reminders = new Adw.SwitchRow (); + automatic_reminders.title = _("Enabled"); + Services.Settings.get_default ().settings.bind ("automatic-reminders-enabled", automatic_reminders, "active", GLib.SettingsBindFlags.DEFAULT); + + var reminders_model = new Gtk.StringList (null); + reminders_model.append (_("At due time")); + reminders_model.append (_("10 minutes before")); + reminders_model.append (_("30 minutes before")); + reminders_model.append (_("45 minutes before")); + reminders_model.append (_("1 hour before")); + reminders_model.append (_("2 hours before")); + reminders_model.append (_("3 hours before")); + + var reminders_comborow = new Adw.ComboRow (); + reminders_comborow.title = _("Automatic reminders"); + reminders_comborow.subtitle = _("When enabled, a reminder before the task’s due time will be added by default."); + reminders_comborow.model = reminders_model; + reminders_comborow.selected = Services.Settings.get_default ().settings.get_enum ("automatic-reminders"); + Services.Settings.get_default ().settings.bind ("automatic-reminders-enabled", reminders_comborow, "sensitive", GLib.SettingsBindFlags.DEFAULT); + + reminders_group.add (automatic_reminders); + reminders_group.add (reminders_comborow); + var content_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 12); content_box.append (group); + content_box.append (reminders_group); var content_clamp = new Adw.Clamp () { maximum_size = 600, @@ -643,6 +671,10 @@ public class Dialogs.Preferences.PreferencesWindow : Adw.PreferencesDialog { Services.Settings.get_default ().settings.set_boolean ("underline-completed-tasks", underline_completed_switch.active); }); + reminders_comborow.notify["selected"].connect (() => { + Services.Settings.get_default ().settings.set_enum ("automatic-reminders", (int) reminders_comborow.selected); + }); + settings_header.back_activated.connect (() => { pop_subpage (); }); diff --git a/src/Dialogs/QuickAdd.vala b/src/Dialogs/QuickAdd.vala index e5ca37a72..ae97af29f 100644 --- a/src/Dialogs/QuickAdd.vala +++ b/src/Dialogs/QuickAdd.vala @@ -52,27 +52,16 @@ public class Dialogs.QuickAdd : Adw.Dialog { quick_add_widget.is_loading = true; foreach (Objects.Reminder reminder in reminders) { - reminder.item_id = item.id; - - if (item.project.backend_type == BackendType.TODOIST) { - Services.Todoist.get_default ().add.begin (reminder, (obj, res) => { - HttpResponse response = Services.Todoist.get_default ().add.end (res); - item.loading = false; - - if (response.status) { - reminder.id = response.data; - } else { - reminder.id = Util.get_default ().generate_id (reminder); - } - - item.add_reminder_if_not_exists (reminder); - }); - } else { - reminder.id = Util.get_default ().generate_id (reminder); - item.add_reminder_if_not_exists (reminder); - } + item.add_reminder (reminder); } } + + if (Services.Settings.get_default ().get_boolean ("automatic-reminders-enabled") && item.has_time) { + var reminder = new Objects.Reminder (); + reminder.mm_offset = Util.get_reminders_mm_offset (); + reminder.reminder_type = ReminderType.RELATIVE; + item.add_reminder (reminder); + } Services.EventBus.get_default ().update_section_sort_func (item.project_id, item.section_id, false); quick_add_widget.added_successfully (); diff --git a/src/Layouts/ItemBoard.vala b/src/Layouts/ItemBoard.vala index 63257128d..c56c2ab02 100644 --- a/src/Layouts/ItemBoard.vala +++ b/src/Layouts/ItemBoard.vala @@ -1035,13 +1035,7 @@ public class Layouts.ItemBoard : Layouts.ItemBase { } public void update_due (GLib.DateTime? datetime) { - item.due.date = datetime == null ? "" : Utils.Datetime.get_todoist_datetime_format (datetime); - - if (item.due.date == "") { - item.due.reset (); - } - - item.update_async (""); + item.update_due (datetime); } private void selected_toggled (bool active) { diff --git a/src/Layouts/ItemRow.vala b/src/Layouts/ItemRow.vala index 4760819aa..74809fd70 100644 --- a/src/Layouts/ItemRow.vala +++ b/src/Layouts/ItemRow.vala @@ -762,26 +762,7 @@ public class Layouts.ItemRow : Layouts.ItemBase { }); reminder_button.reminder_added.connect ((reminder) => { - reminder.item_id = item.id; - - if (item.project.backend_type == BackendType.TODOIST) { - item.loading = true; - Services.Todoist.get_default ().add.begin (reminder, (obj, res) => { - HttpResponse response = Services.Todoist.get_default ().add.end (res); - item.loading = false; - - if (response.status) { - reminder.id = response.data; - } else { - reminder.id = Util.get_default ().generate_id (reminder); - } - - item.add_reminder_if_not_exists (reminder); - }); - } else { - reminder.id = Util.get_default ().generate_id (reminder); - item.add_reminder_if_not_exists (reminder); - } + item.add_reminder (reminder); }); item.reminder_added.connect ((reminder) => { @@ -1193,7 +1174,7 @@ public class Layouts.ItemRow : Layouts.ItemBase { duplicate_item.clicked.connect (() => { popover.popdown (); - Util.get_default ().duplicate_item.begin (item, item.section_id, item.parent_id); + Util.get_default ().duplicate_item.begin (item, item.project_id, item.section_id, item.parent_id); }); move_item.clicked.connect (() => { @@ -1426,13 +1407,7 @@ public class Layouts.ItemRow : Layouts.ItemBase { } public void update_due (GLib.DateTime? datetime) { - item.due.date = datetime == null ? "" : Utils.Datetime.get_todoist_datetime_format (datetime); - - if (item.due.date == "") { - item.due.reset (); - } - - item.update_async (""); + item.update_due (datetime); } private void update_next_recurrency () { diff --git a/src/Layouts/ItemSidebarView.vala b/src/Layouts/ItemSidebarView.vala index e5c1c6275..7e069b934 100644 --- a/src/Layouts/ItemSidebarView.vala +++ b/src/Layouts/ItemSidebarView.vala @@ -397,13 +397,7 @@ public class Layouts.ItemSidebarView : Adw.Bin { return; } - item.due.date = datetime == null ? "" : Utils.Datetime.get_todoist_datetime_format (datetime); - - if (item.due.date == "") { - item.due.reset (); - } - - item.update_async (""); + item.update_due (datetime); } public void update_labels (Gee.HashMap new_labels) { @@ -494,7 +488,7 @@ public class Layouts.ItemSidebarView : Adw.Bin { duplicate_item.clicked.connect (() => { popover.popdown (); - Util.get_default ().duplicate_item.begin (item, item.section_id, item.parent_id); + Util.get_default ().duplicate_item.begin (item, item.project_id, item.section_id, item.parent_id); }); move_item.clicked.connect (() => { diff --git a/src/MainWindow.vala b/src/MainWindow.vala index e30e05975..085483c07 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -57,8 +57,13 @@ public class MainWindow : Adw.ApplicationWindow { action_manager = new Services.ActionManager (app, this); Services.DBusServer.get_default ().item_added.connect ((id) => { - var item = Services.Database.get_default ().get_item_by_id (id); + Objects.Item item = Services.Database.get_default ().get_item_by_id (id); + Gee.ArrayList reminders = Services.Database.get_default ().get_reminders_by_item_id (id); + Services.Database.get_default ().add_item (item); + foreach (Objects.Reminder reminder in reminders) { + item.add_reminder_events (reminder); + } }); var settings_popover = build_menu_app (); diff --git a/src/Services/Notification.vala b/src/Services/Notification.vala index b8fe32be4..cd39bf45d 100644 --- a/src/Services/Notification.vala +++ b/src/Services/Notification.vala @@ -51,21 +51,21 @@ public class Services.Notification : GLib.Object { }); Services.Database.get_default ().reminder_deleted.connect ((reminder) => { - if (reminders.has_key (reminder.id_string)) { - reminders.unset (reminder.id_string); + if (reminders.has_key (reminder.id)) { + reminders.unset (reminder.id); } }); } private void reminder_added (Objects.Reminder reminder) { - if (reminder.due.datetime.compare (new GLib.DateTime.now_local ()) <= 0) { + if (reminder.datetime.compare (new GLib.DateTime.now_local ()) <= 0) { GLib.Notification notification = build_notification (reminder); Planify.instance.send_notification (reminder.id, notification); Services.Database.get_default ().delete_reminder (reminder); - } else if (Utils.Datetime.is_same_day (reminder.due.datetime, new GLib.DateTime.now_local ())) { - var interval = (uint) time_until_now (reminder.due.datetime); - var uid = "%u-%u".printf (interval, GLib.Random.next_int ()); - reminders.set (reminder.id_string, uid); + } else if (Utils.Datetime.is_same_day (reminder.datetime, new GLib.DateTime.now_local ())) { + uint interval = (uint) time_until_now (reminder.datetime); + string uid = "%u-%u".printf (interval, GLib.Random.next_int ()); + reminders.set (reminder.id, uid); Timeout.add_seconds (interval, () => { queue_reminder_notification (reminder, uid); @@ -79,7 +79,7 @@ public class Services.Notification : GLib.Object { return dt.difference (now) / TimeSpan.SECOND; } - public void queue_reminder_notification (Objects.Reminder reminder, string uid) { + private void queue_reminder_notification (Objects.Reminder reminder, string uid) { if (reminders.values.contains (uid) == false) { return; } @@ -89,7 +89,7 @@ public class Services.Notification : GLib.Object { Services.Database.get_default ().delete_reminder (reminder); } - public GLib.Notification build_notification (Objects.Reminder reminder) { + private GLib.Notification build_notification (Objects.Reminder reminder) { var notification = new GLib.Notification (reminder.item.project.name); notification.set_body (reminder.item.content); notification.set_icon (new ThemedIcon ("io.github.alainm23.planify"));