diff --git a/client/src/activities_view.vala b/client/src/activities_view.vala index 3c3a3e5..bfdd123 100644 --- a/client/src/activities_view.vala +++ b/client/src/activities_view.vala @@ -1,27 +1,6 @@ namespace StudySystemClient { - private struct Activity { - public string subject; - public ActivityType type; - } - - enum ActivityType { - EXERCISES, - READING; - - public string to_string() { - switch (this) { - case EXERCISES: - return "Exercises"; - case READING: - return "Reading"; - default: - return "Invalid activity type"; - } - } - } - public class ActivitiesView : Gtk.Box { - public ActivitiesView(Connection connection) { + public ActivitiesView(Client client) { margin_top = margin_bottom = margin_start = margin_end = 0; var scrolled_window = new Gtk.ScrolledWindow(); @@ -37,15 +16,15 @@ namespace StudySystemClient { scrolled_window.add_css_class("card-container"); var activities = new Activity[] { - { "Linguistics", ActivityType.EXERCISES }, - { "Cybernetics", ActivityType.EXERCISES }, - { "Linguistics", ActivityType.READING }, - { "Physics", ActivityType.READING }, - { "Cybernetics", ActivityType.READING }, - { "Physics", ActivityType.EXERCISES }, + { 2, "Linguistics", ActivityType.EXERCISES }, + { 1, "Cybernetics", ActivityType.EXERCISES }, + { 2, "Linguistics", ActivityType.READING }, + { 0, "Physics", ActivityType.READING }, + { 1, "Cybernetics", ActivityType.READING }, + { 0, "Physics", ActivityType.EXERCISES }, }; foreach (var activity in activities) { - var card = new ActivityCard(connection, activity); + var card = new ActivityCard(client, activity); card_container.append(card); } @@ -55,7 +34,7 @@ namespace StudySystemClient { } private class ActivityCard : Gtk.Frame { - public ActivityCard(Connection connection, Activity activity) { + public ActivityCard(Client client, Activity activity) { add_css_class("card"); var content = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 12); @@ -63,7 +42,7 @@ namespace StudySystemClient { var text = new Gtk.Box(Gtk.Orientation.VERTICAL, 6); text.hexpand = true; - var subject = new Gtk.Label(activity.subject); + var subject = new Gtk.Label(activity.subject_name); subject.halign = Gtk.Align.START; subject.add_css_class("activity-subject"); text.append(subject); @@ -84,7 +63,7 @@ namespace StudySystemClient { set_child(content); - var log_session_popover = new LogSessionPopover(connection); + var log_session_popover = new LogSessionPopover(client); log_session_popover.set_parent(button); button.clicked.connect(() => log_session_popover.popup()); } @@ -94,10 +73,10 @@ namespace StudySystemClient { private const int DEFAULT_LENGTH = 30; private Gtk.SpinButton input; - private Connection connection; + private Client client; - public LogSessionPopover(Connection connection) { - this.connection = connection; + public LogSessionPopover(Client client) { + this.client = client; var content = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6); @@ -126,8 +105,13 @@ namespace StudySystemClient { private async void submit() { reset(); popdown(); - yield connection.send(new Request.Ping()); - stderr.printf("Got ACK\n"); + + try { + yield client.ping(); + stderr.printf("Successfully pinged server\n"); + } catch (ClientError e) { + stderr.printf("Error pinging server: %s\n", e.message); + } } private void reset() { diff --git a/client/src/activity.vala b/client/src/activity.vala new file mode 100644 index 0000000..785795e --- /dev/null +++ b/client/src/activity.vala @@ -0,0 +1,24 @@ +namespace StudySystemClient { + public struct Activity { + public int subject_id; + public string subject_name; + public ActivityType type; + public double priority; + } + + public enum ActivityType { + EXERCISES, + READING; + + public string to_string() { + switch (this) { + case EXERCISES: + return "Exercises"; + case READING: + return "Reading"; + default: + return "Invalid activity type"; + } + } + } +} diff --git a/client/src/client.vala b/client/src/client.vala new file mode 100644 index 0000000..db65dfc --- /dev/null +++ b/client/src/client.vala @@ -0,0 +1,62 @@ +namespace StudySystemClient { + public errordomain ClientError { + ERROR_RESPONSE, + UNEXPECTED_RESPONSE, + } + + public class Client { + private Connection connection; + + public Client(string cert_dir) throws Error { + connection = new Connection(cert_dir); + } + + public async void ping() throws ClientError { + var response = yield connection.send(new Request.Ping()); + if (response is Response.Ack) { + return; + } else if (response is Response.Error) { + throw new ClientError.ERROR_RESPONSE( + "Error response to Ping: %s", + response.value.to_string()); + } else { + throw new ClientError.UNEXPECTED_RESPONSE( + "Unexpected response to Ping"); + } + } + + public async Array list_activities() + throws ClientError { + var request = new Request.ListActivities(); + var response = yield connection.send(request); + if (response is Response.Activities) { + return response.value; + } else if (response is Response.Error) { + throw new ClientError.ERROR_RESPONSE( + "Error response to ListActivities: %s", + response.value.to_string()); + } else { + throw new ClientError.UNEXPECTED_RESPONSE( + "Unexpected response to ListActivities"); + } + } + + public async void log_session(int subject_id, ActivityType type, + int minutes) throws ClientError { + var timestamp = new DateTime.now_utc().to_unix(); + var request = new Request.LogSession(subject_id, type, + timestamp, minutes); + var response = yield connection.send(request); + if (response is Response.Ack) { + return; + } else if (response is Response.Error) { + throw new ClientError.ERROR_RESPONSE( + "Error response to LogSession: %s", + response.value.to_string()); + } else { + throw new ClientError.UNEXPECTED_RESPONSE( + "Unexpected response to LogSession"); + } + } + } +} diff --git a/client/src/main.vala b/client/src/main.vala index 0200f9f..c0e048e 100644 --- a/client/src/main.vala +++ b/client/src/main.vala @@ -14,8 +14,8 @@ namespace StudySystemClient { css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); - var connection = new Connection(Config.CERT_DIR); - var main_window = new MainWindow(this, connection); + var client = new Client(Config.CERT_DIR); + var main_window = new MainWindow(this, client); main_window.present(); } catch (Error e) { stderr.printf("Failed to initialize: %s\n", e.message); diff --git a/client/src/main_window.vala b/client/src/main_window.vala index b78ea2f..a9ae882 100644 --- a/client/src/main_window.vala +++ b/client/src/main_window.vala @@ -1,22 +1,18 @@ namespace StudySystemClient { public class MainWindow : Gtk.ApplicationWindow { - private Connection connection; - - public MainWindow(Gtk.Application app, Connection connection) { + public MainWindow(Gtk.Application app, Client client) { Object(application: app); default_width = 360; default_height = 580; - this.connection = connection; - var header_bar = new Gtk.HeaderBar(); var title = new Gtk.Label("Study System Client"); title.add_css_class("title"); header_bar.title_widget = title; set_titlebar(header_bar); - var activities_view = new ActivitiesView(connection); + var activities_view = new ActivitiesView(client); set_child(activities_view); } } diff --git a/client/src/meson.build b/client/src/meson.build index 0c3f5ea..6463c12 100644 --- a/client/src/meson.build +++ b/client/src/meson.build @@ -10,7 +10,9 @@ configure_file( lib = library( 'study-system-client', sources: files( + 'activity.vala', 'activities_view.vala', + 'client.vala', 'connection.vala', 'der.vala', 'main_window.vala', diff --git a/client/src/request.vala b/client/src/request.vala index 023472d..793b61c 100644 --- a/client/src/request.vala +++ b/client/src/request.vala @@ -15,16 +15,40 @@ namespace StudySystemClient.Request { public abstract class Body { protected enum Tag { PING = 0, - LIST_PRIORITIZED_ACTIVITIES = 1, + LIST_ACTIVITIES = 1, LOG_SESSION = 2, } internal Der.Datum datum; + + protected Body(Tag tag, Der.Datum datum) { + this.datum = new Der.Choice(tag, datum); + } } public class Ping : Body { public Ping() { - datum = new Der.Choice(Tag.PING, new Der.Null()); + base(Tag.PING, new Der.Null()); + } + } + + public class ListActivities : Body { + public ListActivities() { + base(Tag.LIST_ACTIVITIES, new Der.Null()); + } + } + + public class LogSession : Body { + public LogSession(int subject_id, ActivityType type, + int64 timestamp, int minutes) + { + var fields = new Der.Datum[] { + new Der.Integer(subject_id), + new Der.Enumerated((int)type), + new Der.Integer(timestamp), + new Der.Integer(minutes), + }; + base(Tag.LOG_SESSION, new Der.Sequence(fields)); } } } diff --git a/client/src/response.vala b/client/src/response.vala index b4b46ce..3dfb1e9 100644 --- a/client/src/response.vala +++ b/client/src/response.vala @@ -15,9 +15,9 @@ namespace StudySystemClient.Response { throw new DecodeError.INVALID_RESPONSE( "Response was not a SEQUENCE"); } - if (sequence.value.length != 2) { + if (sequence.value.length < 2) { throw new DecodeError.INVALID_RESPONSE( - "Response sequnce contained %u fields (expected 2)", + "Too few fields in Response: %u (expected 2)", sequence.value.length); } var id_datum = sequence.value[0] as Der.Integer; @@ -68,7 +68,20 @@ namespace StudySystemClient.Response { public enum Value { INVALID_REQUEST = 0, INVALID_ARGUMENTS = 1, - SERVER_ERROR = 2, + SERVER_ERROR = 2; + + public string to_string() { + switch (this) { + case INVALID_REQUEST: + return "Invalid request"; + case INVALID_ARGUMENTS: + return "Invalid arguments"; + case SERVER_ERROR: + return "Server error"; + default: + return "Unknown error"; + } + } } public Value value { get; private set; } @@ -95,4 +108,71 @@ namespace StudySystemClient.Response { } } } + + public class Activities : Body { + public Array value { get; private set; } + + internal Activities.from_datum(Der.Datum datum) throws DecodeError { + value = new Array(); + if (datum is Der.Sequence) { + foreach (var activity_datum in datum.value) + value.append_val(activity_from_datum(activity_datum)); + } else { + throw new DecodeError.INVALID_BODY( + "Activities was not a SEQUENCE"); + } + } + + private static Activity activity_from_datum(Der.Datum datum) + throws DecodeError { + if (datum is Der.Sequence) { + var fields = datum.value; + if (fields.length < 4) { + throw new DecodeError.INVALID_BODY( + "Too few fields in Activity: %u (expected 4)", + fields.length); + } + var subject_id = get_int("Activity.subjectId", fields[0]); + var subject_name + = get_string("Activity.subjectName", fields[1]); + var activity_type + = get_activity_type("Activity.type", fields[2]); + var int_priority = get_int("Activity.priority", fields[3]); + var priority = (double)int_priority / 100.0; + return { subject_id, subject_name, activity_type, priority }; + + } else { + throw new DecodeError.INVALID_BODY( + "Activity was not a SEQUENCE"); + } + } + + private static int get_int(string name, Der.Datum datum) + throws DecodeError { + if (datum is Der.Integer) + return (int)datum.value; + throw new DecodeError.INVALID_BODY(@"$name was not an INTEGER"); + } + + private static string get_string(string name, Der.Datum datum) + throws DecodeError { + if (datum is Der.Utf8String) + return datum.value; + throw new DecodeError.INVALID_BODY( + @"$name was not a UTF8String"); + } + + private static ActivityType get_activity_type( + string name, Der.Datum datum) throws DecodeError { + if (datum is Der.Enumerated) { + var value = datum.value; + if (0 <= value <= 1) + return (ActivityType)value; + throw new DecodeError.INVALID_BODY( + "Invalid value for ActivityType: %lld", value); + } + throw new DecodeError.INVALID_BODY( + @"$name was not an ENUMERATED"); + } + } }