From 8123c5375d169e0c8c7a63f2baf7776197a70fbd Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 2 Mar 2025 23:57:08 +0000 Subject: [PATCH] Regularly refresh activities and update cards in-place --- client/src/activities_view.vala | 97 +++++++++++++++++++++++++++++---- client/src/activity_card.vala | 35 ++++++++++-- client/src/card.vala | 22 ++------ client/src/iterable_box.vala | 52 ++++++++++++++++++ client/src/meson.build | 1 + 5 files changed, 176 insertions(+), 31 deletions(-) create mode 100644 client/src/iterable_box.vala diff --git a/client/src/activities_view.vala b/client/src/activities_view.vala index a65d665..d3aa359 100644 --- a/client/src/activities_view.vala +++ b/client/src/activities_view.vala @@ -1,23 +1,27 @@ namespace StudySystemClient { - public class ActivitiesView : CardArea { + public class ActivitiesView : CardArea, IRefreshable { + private const uint REFRESH_PERIOD_MS = 30000; + private Client client; + private Refresher refresher; + private bool pending_sort; public ActivitiesView(Client client) { - base(); - this.client = client; - this.map.connect(refresh); + refresher = new Refresher(this, REFRESH_PERIOD_MS); + pending_sort = false; + this.map.connect(() => { + refresher.start(); + refresh.begin(); + }); } private async void refresh() { + if (!client.connected) + return; try { var activities = yield client.list_activities(); - clear(); - foreach (var activity in activities) { - var card = new ActivityCard(activity); - card.session_logged.connect(log_session); - add(card); - } + update_cards(activities); } catch (ClientError e) { stderr.printf("Error refreshing activities: %s\n", e.message); @@ -33,5 +37,78 @@ namespace StudySystemClient { stderr.printf("Error logging session: %s\n", e.message); } } + + private void handle_pending_sort() { + if (pending_sort) { + container.sort(compare_cards); + pending_sort = false; + } + } + + private void update_cards(Array activities) { + update_existing_cards(activities); + for (uint i = 0; i < activities.length; ++i) + create_card(activities.index(i)); + if (log_in_progress()) + pending_sort = true; + else + container.sort(compare_cards); + } + + private void update_existing_cards(Array activities) { + var to_remove = new List(); + foreach (var card in container) { + if (!update_existing_card(card, activities)) + to_remove.append(card); + } + foreach (var card in to_remove) + container.remove(card); + } + + private bool update_existing_card(ActivityCard card, + Array activities) { + var activity_index = find_activity(card, activities); + if (activity_index == null) + return false; + var priority = activities.index(activity_index).priority; + card.update_priority(priority); + activities._remove_index_fast(activity_index); + return true; + } + + private uint? find_activity(ActivityCard card, + Array activities) { + for (uint i = 0; i < activities.length; ++i) { + if (activities.index(i).subject == card.activity.subject + && activities.index(i).type == card.activity.type) + return i; + } + return null; + } + + private void create_card(Activity activity) { + var card = new ActivityCard(activity); + card.session_logged.connect(log_session); + card.log_closed.connect(handle_pending_sort); + container.append(card); + } + + private bool log_in_progress() { + foreach (var card in container) { + if (card.logging) + return true; + } + return false; + } + + private static int compare_cards(ActivityCard card1, + ActivityCard card2) { + if (card1.activity.priority < card2.activity.priority) + return -1; + else if (card1.activity.priority > card2.activity.priority) + return 1; + else + return 0; + } } } diff --git a/client/src/activity_card.vala b/client/src/activity_card.vala index c7c3d63..f80624c 100644 --- a/client/src/activity_card.vala +++ b/client/src/activity_card.vala @@ -2,10 +2,19 @@ namespace StudySystemClient { public class ActivityCard : Card { public signal void session_logged(string subject, ActivityType type, int minutes); + public signal void log_closed(); + + public Activity activity { get; private set; } + public bool logging { get; private set; } + + private Gtk.Label priority_label; public ActivityCard(Activity activity) { base(); + this.activity = activity; + logging = false; + var subject = new Gtk.Label(activity.subject); subject.halign = Gtk.Align.START; subject.add_css_class("activity-subject"); @@ -14,13 +23,14 @@ namespace StudySystemClient { type.add_css_class("activity-type"); var separator = new Gtk.Label("ยท"); separator.add_css_class("activity-priority"); - var priority = new Gtk.Label("%0.2f".printf(activity.priority)); - priority.add_css_class("activity-priority"); + + priority_label = new Gtk.Label(priority_text(activity.priority)); + priority_label.add_css_class("activity-priority"); var details = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6); details.append(type); details.append(separator); - details.append(priority); + details.append(priority_label); var text = new Gtk.Box(Gtk.Orientation.VERTICAL, 6); text.hexpand = true; @@ -42,10 +52,27 @@ namespace StudySystemClient { var log_session_popover = new LogSessionPopover(); log_session_popover.set_parent(button); - button.clicked.connect(() => log_session_popover.popup()); + log_session_popover.closed.connect(() => { + logging = false; + log_closed(); + }); log_session_popover.session_logged.connect((minutes) => { session_logged(activity.subject, activity.type, minutes); }); + + button.clicked.connect(() => { + logging = true; + log_session_popover.popup(); + }); + } + + public void update_priority(double new_priority) { + activity = { activity.subject, activity.type, new_priority }; + priority_label.set_text(priority_text(new_priority)); + } + + private static string priority_text(double priority) { + return "%0.2f".printf(priority); } } diff --git a/client/src/card.vala b/client/src/card.vala index 9d21d1d..c90396b 100644 --- a/client/src/card.vala +++ b/client/src/card.vala @@ -6,35 +6,23 @@ namespace StudySystemClient { } } - public class CardArea : Gtk.Box { - private Gtk.FlowBox flow_box; + public class CardArea : Gtk.Box { + protected IterableBox container; public CardArea() { hexpand = vexpand = true; margin_top = margin_bottom = margin_start = margin_end = 0; - flow_box = new Gtk.FlowBox(); - flow_box.homogeneous = true; - flow_box.min_children_per_line = 1; - flow_box.max_children_per_line = 1; - flow_box.selection_mode = Gtk.SelectionMode.NONE; - flow_box.valign = Gtk.Align.START; + container = new IterableBox(Gtk.Orientation.VERTICAL, 6); + container.valign = Gtk.Align.START; var scrolled_window = new Gtk.ScrolledWindow(); scrolled_window.hscrollbar_policy = Gtk.PolicyType.NEVER; scrolled_window.hexpand = scrolled_window.vexpand = true; scrolled_window.add_css_class("card-container"); - scrolled_window.set_child(flow_box); + scrolled_window.set_child(container); append(scrolled_window); } - - public void clear() { - flow_box.remove_all(); - } - - public void add(Card card) { - flow_box.append(card); - } } } diff --git a/client/src/iterable_box.vala b/client/src/iterable_box.vala new file mode 100644 index 0000000..387a4a9 --- /dev/null +++ b/client/src/iterable_box.vala @@ -0,0 +1,52 @@ +namespace StudySystemClient { + public class IterableBox : Gtk.Box { + private List elements; + + public IterableBox(Gtk.Orientation orientation, int spacing) { + this.orientation = orientation; + this.spacing = spacing; + elements = new List(); + } + + public new void append(T element) { + elements.append(element); + base.append(element as Gtk.Widget); + } + + public new void remove(T element) { + elements.remove(element); + base.remove(element as Gtk.Widget); + } + + public Iterator iterator() { + return new Iterator(elements); + } + + public void sort(CompareDataFunc comparison) { + elements.sort_with_data(comparison); + foreach (var element in elements) + reorder_child_after(element as Gtk.Widget, null); + } + + public class Iterator { + private unowned List head; + private unowned T value; + + public Iterator(List elements) { + head = elements; + } + + public bool next() { + if (head.is_empty()) + return false; + value = head.data; + head = head.next; + return true; + } + + public unowned T get() { + return value; + } + } + } +} diff --git a/client/src/meson.build b/client/src/meson.build index c4d5227..53f5aee 100644 --- a/client/src/meson.build +++ b/client/src/meson.build @@ -17,6 +17,7 @@ lib = library( 'client.vala', 'connection.vala', 'der.vala', + 'iterable_box.vala', 'main_window.vala', 'periodic.vala', 'refresher.vala',