Compare commits
21 Commits
09f0648138
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c8f2dbaadc | |||
| e81a47d83f | |||
| 5697cf0652 | |||
| b2f5288c4b | |||
| 8123c5375d | |||
| 3e99cc293a | |||
| e275fa01ed | |||
| 487427cf0a | |||
| 5912302043 | |||
| 7c26a9278f | |||
| 8a7032309f | |||
| 708343c37f | |||
| 10a7fe5c82 | |||
| c2d81778a8 | |||
| 94df48db7b | |||
| 76aca12fec | |||
| 5a6b535beb | |||
| 019bdf9ce6 | |||
| 17629f1db7 | |||
| 0f0bd37cc8 | |||
| bf876336f2 |
@@ -1,134 +1,140 @@
|
|||||||
namespace StudySystemClient {
|
namespace StudySystemClient {
|
||||||
public class ActivitiesView : Gtk.Box {
|
public class ActivitiesView : ListView<ActivityCard> {
|
||||||
|
private const uint REFRESH_PERIOD_MS = 30000;
|
||||||
|
|
||||||
private Client client;
|
private Client client;
|
||||||
|
private Updater updater;
|
||||||
|
private Refresher refresher;
|
||||||
|
private bool pending_sort;
|
||||||
|
|
||||||
public ActivitiesView(Client client) {
|
public ActivitiesView(Client client) {
|
||||||
margin_top = margin_bottom = margin_start = margin_end = 0;
|
base();
|
||||||
|
add_css_class("card-container");
|
||||||
this.client = client;
|
this.client = client;
|
||||||
|
updater = new Updater(this, client);
|
||||||
|
refresher = new Refresher(updater, REFRESH_PERIOD_MS);
|
||||||
|
pending_sort = false;
|
||||||
|
this.map.connect(() => {
|
||||||
|
updater.refresh.begin();
|
||||||
|
refresher.start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var scrolled_window = new Gtk.ScrolledWindow();
|
internal IterableBox.Iterator<Type, ActivityCard> iterator() {
|
||||||
scrolled_window.hscrollbar_policy = Gtk.PolicyType.NEVER;
|
return container.iterator();
|
||||||
scrolled_window.vexpand = true;
|
}
|
||||||
|
|
||||||
var card_container = new Gtk.FlowBox();
|
internal void new_card(Activity activity) {
|
||||||
card_container.homogeneous = true;
|
var card = new ActivityCard(activity);
|
||||||
card_container.min_children_per_line = 1;
|
card.session_logged.connect(log_session);
|
||||||
card_container.max_children_per_line = 1;
|
card.log_closed.connect(handle_pending_sort);
|
||||||
card_container.selection_mode = Gtk.SelectionMode.NONE;
|
container.append(card);
|
||||||
card_container.valign = Gtk.Align.START;
|
}
|
||||||
scrolled_window.add_css_class("card-container");
|
|
||||||
|
|
||||||
var activities = new Activity[] {
|
internal void remove_card(ActivityCard card) {
|
||||||
{ "Linguistics", ActivityType.EXERCISES },
|
container.remove(card);
|
||||||
{ "Cybernetics", ActivityType.EXERCISES },
|
}
|
||||||
{ "Linguistics", ActivityType.READING },
|
|
||||||
{ "Physics", ActivityType.READING },
|
|
||||||
{ "Cybernetics", ActivityType.READING },
|
|
||||||
{ "Physics", ActivityType.EXERCISES },
|
|
||||||
};
|
|
||||||
foreach (var activity in activities) {
|
|
||||||
var card = new ActivityCard(activity);
|
|
||||||
card.session_logged.connect(log_session);
|
|
||||||
card_container.append(card);
|
|
||||||
}
|
|
||||||
|
|
||||||
scrolled_window.set_child(card_container);
|
internal void sort() {
|
||||||
this.append(scrolled_window);
|
if (log_in_progress())
|
||||||
|
pending_sort = true;
|
||||||
|
else
|
||||||
|
container.sort(compare_cards);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void log_session(string subject, ActivityType type,
|
private async void log_session(string subject, ActivityType type,
|
||||||
int minutes) {
|
int minutes) {
|
||||||
try {
|
try {
|
||||||
yield client.log_session(subject, type, minutes);
|
yield client.log_session(subject, type, minutes);
|
||||||
stderr.printf("Successfully logged session\n");
|
yield updater.refresh();
|
||||||
} catch (ClientError e) {
|
} catch (ClientError e) {
|
||||||
stderr.printf("Error logging session: %s\n", e.message);
|
stderr.printf("Error logging session: %s\n", e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private class ActivityCard : Gtk.Frame {
|
private void handle_pending_sort() {
|
||||||
public signal void session_logged(string subject, ActivityType type,
|
if (pending_sort) {
|
||||||
int minutes);
|
container.sort(compare_cards);
|
||||||
|
pending_sort = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public ActivityCard(Activity activity) {
|
private bool log_in_progress() {
|
||||||
add_css_class("card");
|
foreach (var card in container) {
|
||||||
|
if (card.logging)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var content = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 12);
|
private static int compare_cards(ActivityCard card1,
|
||||||
|
ActivityCard card2) {
|
||||||
var text = new Gtk.Box(Gtk.Orientation.VERTICAL, 6);
|
if (card1.activity.priority < card2.activity.priority)
|
||||||
text.hexpand = true;
|
return -1;
|
||||||
|
else if (card1.activity.priority > card2.activity.priority)
|
||||||
var subject = new Gtk.Label(activity.subject);
|
return 1;
|
||||||
subject.halign = Gtk.Align.START;
|
else
|
||||||
subject.add_css_class("activity-subject");
|
return 0;
|
||||||
text.append(subject);
|
|
||||||
|
|
||||||
var type = new Gtk.Label(activity.type.to_string());
|
|
||||||
type.halign = Gtk.Align.START;
|
|
||||||
text.append(type);
|
|
||||||
|
|
||||||
content.append(text);
|
|
||||||
|
|
||||||
var button
|
|
||||||
= new Gtk.Button.from_icon_name("appointment-new-symbolic");
|
|
||||||
button.vexpand = false;
|
|
||||||
button.valign = Gtk.Align.CENTER;
|
|
||||||
button.set_tooltip_text("Log session");
|
|
||||||
button.add_css_class("log-session-button");
|
|
||||||
content.append(button);
|
|
||||||
|
|
||||||
set_child(content);
|
|
||||||
|
|
||||||
var log_session_popover = new LogSessionPopover();
|
|
||||||
log_session_popover.set_parent(button);
|
|
||||||
button.clicked.connect(() => log_session_popover.popup());
|
|
||||||
log_session_popover.session_logged.connect((minutes) => {
|
|
||||||
session_logged(activity.subject, activity.type, minutes);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class LogSessionPopover : Gtk.Popover {
|
private class Updater : IRefreshable {
|
||||||
public signal void session_logged(int minutes);
|
private weak ActivitiesView target;
|
||||||
|
private Client client;
|
||||||
|
|
||||||
private const int DEFAULT_LENGTH = 30;
|
public Updater(ActivitiesView target, Client client) {
|
||||||
|
this.target = target;
|
||||||
private Gtk.SpinButton input;
|
this.client = client;
|
||||||
|
|
||||||
public LogSessionPopover() {
|
|
||||||
var content = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6);
|
|
||||||
|
|
||||||
var label = new Gtk.Label("Minutes");
|
|
||||||
label.halign = Gtk.Align.START;
|
|
||||||
content.append(label);
|
|
||||||
|
|
||||||
var adjustment
|
|
||||||
= new Gtk.Adjustment(DEFAULT_LENGTH, 10, 480, 10, 10, 0);
|
|
||||||
input = new Gtk.SpinButton(adjustment, 1, 0);
|
|
||||||
input.numeric = true;
|
|
||||||
content.append(input);
|
|
||||||
|
|
||||||
var button = new Gtk.Button.from_icon_name("emblem-ok-symbolic");
|
|
||||||
button.halign = Gtk.Align.END;
|
|
||||||
button.set_tooltip_text("Submit");
|
|
||||||
button.add_css_class("suggested-action");
|
|
||||||
button.clicked.connect(submit);
|
|
||||||
content.append(button);
|
|
||||||
|
|
||||||
set_child(content);
|
|
||||||
|
|
||||||
closed.connect(reset);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void submit() {
|
public async void refresh() {
|
||||||
session_logged((int)input.value);
|
if (!client.connected || target == null)
|
||||||
reset();
|
return;
|
||||||
popdown();
|
try {
|
||||||
|
var activities = yield client.list_activities();
|
||||||
|
apply_update(activities);
|
||||||
|
target.sort();
|
||||||
|
} catch (ClientError e) {
|
||||||
|
stderr.printf("Error refreshing activities: %s\n",
|
||||||
|
e.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reset() {
|
private void apply_update(Array<Activity> activities) {
|
||||||
input.value = DEFAULT_LENGTH;
|
update_existing(activities);
|
||||||
|
for (uint i = 0; i < activities.length; ++i)
|
||||||
|
target.new_card(activities.index(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update_existing(Array<Activity> activities) {
|
||||||
|
var to_remove = new List<ActivityCard>();
|
||||||
|
foreach (var card in target) {
|
||||||
|
if (!update_card(card, activities))
|
||||||
|
to_remove.append(card);
|
||||||
|
}
|
||||||
|
foreach (var card in to_remove)
|
||||||
|
target.remove_card(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool update_card(ActivityCard card,
|
||||||
|
Array<Activity> 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 static uint? find_activity(ActivityCard card,
|
||||||
|
Array<Activity> 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ namespace StudySystemClient {
|
|||||||
|
|
||||||
public enum ActivityType {
|
public enum ActivityType {
|
||||||
READING = 0,
|
READING = 0,
|
||||||
EXERCISES = 1;
|
EXERCISES = 1,
|
||||||
|
|
||||||
|
COUNT;
|
||||||
|
|
||||||
public string to_string() {
|
public string to_string() {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
|
|||||||
122
client/src/activity_card.vala
Normal file
122
client/src/activity_card.vala
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
namespace StudySystemClient {
|
||||||
|
public class ActivityCard : Gtk.Frame {
|
||||||
|
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) {
|
||||||
|
hexpand = true;
|
||||||
|
add_css_class("card");
|
||||||
|
|
||||||
|
this.activity = activity;
|
||||||
|
logging = false;
|
||||||
|
|
||||||
|
var subject = new Gtk.Label(activity.subject);
|
||||||
|
subject.halign = Gtk.Align.START;
|
||||||
|
subject.add_css_class("activity-subject");
|
||||||
|
|
||||||
|
var type = new Gtk.Label(activity.type.to_string());
|
||||||
|
type.add_css_class("activity-type");
|
||||||
|
var separator = new Gtk.Label("·");
|
||||||
|
separator.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_label);
|
||||||
|
|
||||||
|
var text = new Gtk.Box(Gtk.Orientation.VERTICAL, 6);
|
||||||
|
text.hexpand = true;
|
||||||
|
text.append(subject);
|
||||||
|
text.append(details);
|
||||||
|
|
||||||
|
var content = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 12);
|
||||||
|
content.append(text);
|
||||||
|
|
||||||
|
var button
|
||||||
|
= new Gtk.Button.from_icon_name("appointment-new-symbolic");
|
||||||
|
button.vexpand = false;
|
||||||
|
button.valign = Gtk.Align.CENTER;
|
||||||
|
button.set_tooltip_text("Log session");
|
||||||
|
button.add_css_class("log-session-button");
|
||||||
|
content.append(button);
|
||||||
|
|
||||||
|
set_child(content);
|
||||||
|
|
||||||
|
var log_session_popover = new LogSessionPopover();
|
||||||
|
log_session_popover.set_parent(button);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LogSessionPopover : Gtk.Popover {
|
||||||
|
public signal void session_logged(int minutes);
|
||||||
|
|
||||||
|
private const int DEFAULT_LENGTH = 30;
|
||||||
|
|
||||||
|
private Gtk.SpinButton input;
|
||||||
|
|
||||||
|
public LogSessionPopover() {
|
||||||
|
var content = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6);
|
||||||
|
|
||||||
|
var label = new Gtk.Label("Minutes");
|
||||||
|
label.halign = Gtk.Align.START;
|
||||||
|
content.append(label);
|
||||||
|
|
||||||
|
var adjustment
|
||||||
|
= new Gtk.Adjustment(DEFAULT_LENGTH, 10, 480, 10, 10, 0);
|
||||||
|
input = new Gtk.SpinButton(adjustment, 1, 0);
|
||||||
|
input.numeric = true;
|
||||||
|
content.append(input);
|
||||||
|
|
||||||
|
var button = new Gtk.Button.from_icon_name("emblem-ok-symbolic");
|
||||||
|
button.halign = Gtk.Align.END;
|
||||||
|
button.set_tooltip_text("Submit");
|
||||||
|
button.add_css_class("suggested-action");
|
||||||
|
button.clicked.connect(submit);
|
||||||
|
content.append(button);
|
||||||
|
|
||||||
|
set_child(content);
|
||||||
|
|
||||||
|
closed.connect(reset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void submit() {
|
||||||
|
session_logged((int)input.value);
|
||||||
|
reset();
|
||||||
|
popdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reset() {
|
||||||
|
input.value = DEFAULT_LENGTH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,10 +5,16 @@ namespace StudySystemClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public class Client {
|
public class Client {
|
||||||
|
public signal void connection_status(bool connected);
|
||||||
|
|
||||||
|
public bool connected { get { return connection.connected; } }
|
||||||
|
|
||||||
private Connection connection;
|
private Connection connection;
|
||||||
|
|
||||||
public Client(string cert_dir) throws Error {
|
public Client(string cert_dir) throws Error {
|
||||||
connection = new Connection(cert_dir);
|
connection = new Connection(cert_dir, (connected) => {
|
||||||
|
connection_status(connected);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void ping() throws ClientError {
|
public async void ping() throws ClientError {
|
||||||
|
|||||||
@@ -1,17 +1,27 @@
|
|||||||
namespace StudySystemClient {
|
namespace StudySystemClient {
|
||||||
public class Connection {
|
public class Connection {
|
||||||
|
public delegate void StatusCallback(bool connected);
|
||||||
|
|
||||||
|
public bool connected { get; private set; }
|
||||||
|
|
||||||
|
private StatusCallback status_callback;
|
||||||
private SessionManager session_manager;
|
private SessionManager session_manager;
|
||||||
private TransactionManager transaction_manager;
|
private TransactionManager transaction_manager;
|
||||||
private Worker worker;
|
private Worker worker;
|
||||||
|
|
||||||
public Connection(string cert_dir) throws Error {
|
public Connection(string cert_dir,
|
||||||
|
owned StatusCallback status_callback)
|
||||||
|
throws Error {
|
||||||
var loopback = new InetAddress.loopback(SocketFamily.IPV6);
|
var loopback = new InetAddress.loopback(SocketFamily.IPV6);
|
||||||
var session_factory
|
var session_factory
|
||||||
= new SessionFactory(loopback, 12888, cert_dir);
|
= new SessionFactory(loopback, 12888, cert_dir);
|
||||||
|
this.status_callback = (owned) status_callback;
|
||||||
session_manager = new SessionManager(
|
session_manager = new SessionManager(
|
||||||
session_factory, (msg) => receive(msg));
|
session_factory, (msg) => receive(msg),
|
||||||
|
(connected) => update_status(connected));
|
||||||
transaction_manager = new TransactionManager();
|
transaction_manager = new TransactionManager();
|
||||||
worker = new Worker(session_manager);
|
worker = new Worker(session_manager);
|
||||||
|
connected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Response.Body? send(Request.Body body) {
|
public async Response.Body? send(Request.Body body) {
|
||||||
@@ -35,6 +45,16 @@ namespace StudySystemClient {
|
|||||||
return false;
|
return false;
|
||||||
}, GLib.Priority.DEFAULT_IDLE);
|
}, GLib.Priority.DEFAULT_IDLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void update_status(bool connected) {
|
||||||
|
Idle.add(() => {
|
||||||
|
if (connected != this.connected) {
|
||||||
|
this.connected = connected;
|
||||||
|
status_callback(connected);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}, GLib.Priority.DEFAULT_IDLE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Continuation {
|
private class Continuation {
|
||||||
@@ -84,29 +104,19 @@ namespace StudySystemClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Worker {
|
private class Worker : Periodic {
|
||||||
private uint TASK_PERIOD_MS = 10;
|
private const uint TASK_PERIOD_MS = 10;
|
||||||
|
|
||||||
private SessionManager session_manager;
|
private SessionManager session_manager;
|
||||||
private bool exit;
|
|
||||||
private Thread<void> thread;
|
|
||||||
|
|
||||||
public Worker(SessionManager session_manager) {
|
public Worker(SessionManager session_manager) {
|
||||||
|
base(TASK_PERIOD_MS);
|
||||||
this.session_manager = session_manager;
|
this.session_manager = session_manager;
|
||||||
exit = false;
|
start();
|
||||||
thread = new Thread<void>("connection_worker", body);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
~Worker() {
|
protected override void task() {
|
||||||
exit = true;
|
session_manager.task();
|
||||||
thread.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void body() {
|
|
||||||
while (!exit) {
|
|
||||||
session_manager.task();
|
|
||||||
Thread.usleep(1000 * TASK_PERIOD_MS);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
client/src/iterable_box.vala
Normal file
52
client/src/iterable_box.vala
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
namespace StudySystemClient {
|
||||||
|
public class IterableBox<T> : Gtk.Box {
|
||||||
|
private List<T> elements;
|
||||||
|
|
||||||
|
public IterableBox(Gtk.Orientation orientation, int spacing) {
|
||||||
|
this.orientation = orientation;
|
||||||
|
this.spacing = spacing;
|
||||||
|
elements = new List<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Type, T> iterator() {
|
||||||
|
return new Iterator<Type, T>(elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sort(CompareDataFunc<T> comparison) {
|
||||||
|
elements.sort_with_data(comparison);
|
||||||
|
foreach (var element in elements)
|
||||||
|
reorder_child_after(element as Gtk.Widget, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Iterator<Type, T> {
|
||||||
|
private unowned List<T> head;
|
||||||
|
private unowned T value;
|
||||||
|
|
||||||
|
public Iterator(List<T> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
client/src/list_view.vala
Normal file
20
client/src/list_view.vala
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
namespace StudySystemClient {
|
||||||
|
public class ListView<T> : Gtk.Box {
|
||||||
|
protected IterableBox<T> container;
|
||||||
|
|
||||||
|
public ListView() {
|
||||||
|
hexpand = vexpand = true;
|
||||||
|
margin_top = margin_bottom = margin_start = margin_end = 0;
|
||||||
|
|
||||||
|
container = new IterableBox<T>(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.set_child(container);
|
||||||
|
|
||||||
|
append(scrolled_window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,8 +12,38 @@ namespace StudySystemClient {
|
|||||||
header_bar.title_widget = title;
|
header_bar.title_widget = title;
|
||||||
set_titlebar(header_bar);
|
set_titlebar(header_bar);
|
||||||
|
|
||||||
|
var connection_indicator = new ConnectionIndicator(client);
|
||||||
var activities_view = new ActivitiesView(client);
|
var activities_view = new ActivitiesView(client);
|
||||||
set_child(activities_view);
|
|
||||||
|
var content = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
|
||||||
|
content.append(connection_indicator);
|
||||||
|
content.append(activities_view);
|
||||||
|
|
||||||
|
set_child(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ConnectionIndicator : Gtk.Box {
|
||||||
|
public ConnectionIndicator(Client client) {
|
||||||
|
var icon
|
||||||
|
= new Gtk.Image.from_icon_name("network-offline-symbolic");
|
||||||
|
var label = new Gtk.Label("Disconnected");
|
||||||
|
|
||||||
|
var content = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 8);
|
||||||
|
content.margin_top = content.margin_bottom
|
||||||
|
= content.margin_start = content.margin_end = 12;
|
||||||
|
content.hexpand = true;
|
||||||
|
content.halign = Gtk.Align.CENTER;
|
||||||
|
content.append(icon);
|
||||||
|
content.append(label);
|
||||||
|
|
||||||
|
var revealer = new Gtk.Revealer();
|
||||||
|
revealer.set_child(content);
|
||||||
|
append(revealer);
|
||||||
|
|
||||||
|
client.connection_status.connect((connected) => {
|
||||||
|
revealer.reveal_child = !connected;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,17 @@ configure_file(
|
|||||||
lib = library(
|
lib = library(
|
||||||
'study-system-client',
|
'study-system-client',
|
||||||
sources: files(
|
sources: files(
|
||||||
'activity.vala',
|
|
||||||
'activities_view.vala',
|
'activities_view.vala',
|
||||||
|
'activity.vala',
|
||||||
|
'activity_card.vala',
|
||||||
'client.vala',
|
'client.vala',
|
||||||
'connection.vala',
|
'connection.vala',
|
||||||
'der.vala',
|
'der.vala',
|
||||||
|
'iterable_box.vala',
|
||||||
|
'list_view.vala',
|
||||||
'main_window.vala',
|
'main_window.vala',
|
||||||
|
'periodic.vala',
|
||||||
|
'refresher.vala',
|
||||||
'request.vala',
|
'request.vala',
|
||||||
'response.vala',
|
'response.vala',
|
||||||
'session_manager.vala',
|
'session_manager.vala',
|
||||||
|
|||||||
35
client/src/periodic.vala
Normal file
35
client/src/periodic.vala
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
namespace StudySystemClient {
|
||||||
|
public abstract class Periodic {
|
||||||
|
private uint period_ms;
|
||||||
|
private bool exit;
|
||||||
|
private Thread<void> thread;
|
||||||
|
|
||||||
|
protected Periodic(uint period_ms) {
|
||||||
|
this.period_ms = period_ms;
|
||||||
|
exit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
~Periodic() {
|
||||||
|
if (!exit) {
|
||||||
|
exit = true;
|
||||||
|
thread.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void task();
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
if (exit) {
|
||||||
|
exit = false;
|
||||||
|
thread = new Thread<void>("Periodic task", body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void body() {
|
||||||
|
while (!exit) {
|
||||||
|
task();
|
||||||
|
Thread.usleep(1000 * period_ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
client/src/refresher.vala
Normal file
23
client/src/refresher.vala
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
namespace StudySystemClient {
|
||||||
|
public interface IRefreshable {
|
||||||
|
public abstract async void refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Refresher : Periodic {
|
||||||
|
private weak IRefreshable target;
|
||||||
|
|
||||||
|
public Refresher(IRefreshable target, uint period_ms) {
|
||||||
|
base(period_ms);
|
||||||
|
this.target = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void task() {
|
||||||
|
if (target != null) {
|
||||||
|
Idle.add(() => {
|
||||||
|
target.refresh.begin();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@ namespace StudySystemClient.Response {
|
|||||||
protected enum Tag {
|
protected enum Tag {
|
||||||
ERROR = 0,
|
ERROR = 0,
|
||||||
ACK = 1,
|
ACK = 1,
|
||||||
PRIORITIZED_ACTIVITIES = 2,
|
ACTIVITIES = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static Body from_datum(Der.Datum datum) throws DecodeError {
|
internal static Body from_datum(Der.Datum datum) throws DecodeError {
|
||||||
@@ -54,9 +54,8 @@ namespace StudySystemClient.Response {
|
|||||||
return new Error.from_datum(choice.value);
|
return new Error.from_datum(choice.value);
|
||||||
case Tag.ACK:
|
case Tag.ACK:
|
||||||
return new Ack.from_datum(choice.value);
|
return new Ack.from_datum(choice.value);
|
||||||
case Tag.PRIORITIZED_ACTIVITIES:
|
case Tag.ACTIVITIES:
|
||||||
throw new DecodeError.NOT_IMPLEMENTED(
|
return new Activities.from_datum(choice.value);
|
||||||
"PrioritizedActivities not yet implemented");
|
|
||||||
default:
|
default:
|
||||||
throw new DecodeError.INVALID_BODY(
|
throw new DecodeError.INVALID_BODY(
|
||||||
"Invalid ResponseBody tag");
|
"Invalid ResponseBody tag");
|
||||||
@@ -68,7 +67,9 @@ namespace StudySystemClient.Response {
|
|||||||
public enum Value {
|
public enum Value {
|
||||||
INVALID_REQUEST = 0,
|
INVALID_REQUEST = 0,
|
||||||
INVALID_ARGUMENTS = 1,
|
INVALID_ARGUMENTS = 1,
|
||||||
SERVER_ERROR = 2;
|
SERVER_ERROR = 2,
|
||||||
|
|
||||||
|
COUNT;
|
||||||
|
|
||||||
public string to_string() {
|
public string to_string() {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
@@ -92,9 +93,9 @@ namespace StudySystemClient.Response {
|
|||||||
throw new DecodeError.INVALID_BODY(
|
throw new DecodeError.INVALID_BODY(
|
||||||
"Error was not an ENUMERATED");
|
"Error was not an ENUMERATED");
|
||||||
}
|
}
|
||||||
if (enumerated.value < 0 || enumerated.value > 2) {
|
if (enumerated.value < 0 || enumerated.value >= Value.COUNT) {
|
||||||
throw new DecodeError.INVALID_BODY(
|
throw new DecodeError.INVALID_BODY(
|
||||||
"Error type was not in range 0..2");
|
"Error type was not in range 0..$(Value.COUNT)");
|
||||||
}
|
}
|
||||||
value = (Value)enumerated.value;
|
value = (Value)enumerated.value;
|
||||||
}
|
}
|
||||||
@@ -163,7 +164,7 @@ namespace StudySystemClient.Response {
|
|||||||
string name, Der.Datum datum) throws DecodeError {
|
string name, Der.Datum datum) throws DecodeError {
|
||||||
if (datum is Der.Enumerated) {
|
if (datum is Der.Enumerated) {
|
||||||
var value = datum.value;
|
var value = datum.value;
|
||||||
if (0 <= value <= 1)
|
if (0 <= value < ActivityType.COUNT)
|
||||||
return (ActivityType)value;
|
return (ActivityType)value;
|
||||||
throw new DecodeError.INVALID_BODY(
|
throw new DecodeError.INVALID_BODY(
|
||||||
"Invalid value for ActivityType: %lld", value);
|
"Invalid value for ActivityType: %lld", value);
|
||||||
|
|||||||
@@ -3,19 +3,23 @@ namespace StudySystemClient {
|
|||||||
public delegate void ReceiveCallback(owned uint8[] msg);
|
public delegate void ReceiveCallback(owned uint8[] msg);
|
||||||
|
|
||||||
private const uint INIT_RECONNECT_WAIT_MS = 500;
|
private const uint INIT_RECONNECT_WAIT_MS = 500;
|
||||||
private const uint MAX_RECONNECT_WAIT_MS = 60000;
|
private const uint MAX_RECONNECT_WAIT_MS = 30000;
|
||||||
private const double RECONNECT_BACKOFF = 1.6;
|
private const double RECONNECT_BACKOFF = 1.6;
|
||||||
|
|
||||||
private SessionFactory session_factory;
|
private SessionFactory session_factory;
|
||||||
private ReceiveCallback receive_callback;
|
private ReceiveCallback receive_callback;
|
||||||
|
private Connection.StatusCallback status_callback;
|
||||||
private Session? session;
|
private Session? session;
|
||||||
private AsyncQueue<OutgoingMessage> queue;
|
private AsyncQueue<OutgoingMessage> queue;
|
||||||
private uint reconnect_wait_ms;
|
private uint reconnect_wait_ms;
|
||||||
|
|
||||||
public SessionManager(SessionFactory session_factory,
|
public SessionManager(
|
||||||
owned ReceiveCallback receive_callback) {
|
SessionFactory session_factory,
|
||||||
|
owned ReceiveCallback receive_callback,
|
||||||
|
owned Connection.StatusCallback status_callback) {
|
||||||
this.session_factory = session_factory;
|
this.session_factory = session_factory;
|
||||||
this.receive_callback = (owned) receive_callback;
|
this.receive_callback = (owned) receive_callback;
|
||||||
|
this.status_callback = (owned) status_callback;
|
||||||
this.session = null;
|
this.session = null;
|
||||||
queue = new AsyncQueue<OutgoingMessage>();
|
queue = new AsyncQueue<OutgoingMessage>();
|
||||||
reconnect_wait_ms = INIT_RECONNECT_WAIT_MS;
|
reconnect_wait_ms = INIT_RECONNECT_WAIT_MS;
|
||||||
@@ -40,6 +44,7 @@ namespace StudySystemClient {
|
|||||||
if (msg.should_retry())
|
if (msg.should_retry())
|
||||||
queue.push(msg);
|
queue.push(msg);
|
||||||
session = null;
|
session = null;
|
||||||
|
status_callback(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void try_start_session() {
|
private void try_start_session() {
|
||||||
@@ -47,7 +52,9 @@ namespace StudySystemClient {
|
|||||||
session = session_factory.start_session();
|
session = session_factory.start_session();
|
||||||
session.received.connect((msg) => receive_callback(msg));
|
session.received.connect((msg) => receive_callback(msg));
|
||||||
reconnect_wait_ms = INIT_RECONNECT_WAIT_MS;
|
reconnect_wait_ms = INIT_RECONNECT_WAIT_MS;
|
||||||
|
status_callback(true);
|
||||||
} catch (Error _) {
|
} catch (Error _) {
|
||||||
|
status_callback(false);
|
||||||
Thread.usleep(1000 * reconnect_wait_ms);
|
Thread.usleep(1000 * reconnect_wait_ms);
|
||||||
update_reconnect_wait();
|
update_reconnect_wait();
|
||||||
}
|
}
|
||||||
@@ -65,7 +72,7 @@ namespace StudySystemClient {
|
|||||||
public class SessionFactory {
|
public class SessionFactory {
|
||||||
private const string CA_FILENAME = "/ca.pem";
|
private const string CA_FILENAME = "/ca.pem";
|
||||||
private const string CERT_FILENAME = "/client.pem";
|
private const string CERT_FILENAME = "/client.pem";
|
||||||
private const uint TIMEOUT_S = 5;
|
private const uint TIMEOUT_S = 2;
|
||||||
|
|
||||||
private InetSocketAddress host;
|
private InetSocketAddress host;
|
||||||
private TlsCertificate cert;
|
private TlsCertificate cert;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
.card-container {
|
.card-container {
|
||||||
background-color: mix(@theme_base_color, @theme_bg_color, 0.7);
|
background-color: color-mix(in oklab, @theme_base_color,
|
||||||
|
@theme_bg_color 60%);
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
border: 1px solid alpha(@theme_fg_color, 0.2);
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
|
||||||
box-shadow: 0 1px 2px alpha(black, 0.15);
|
|
||||||
background-color: @theme_bg_color;
|
background-color: @theme_bg_color;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,10 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.activity-priority {
|
||||||
|
color: color-mix(in oklab, @theme_fg_color 60%, @theme_bg_color);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The visual center (i.e. the center of the clock) of the
|
* The visual center (i.e. the center of the clock) of the
|
||||||
* "appointment-new-symbolic" icon is slightly displaced from the
|
* "appointment-new-symbolic" icon is slightly displaced from the
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ start_link(Socket) ->
|
|||||||
gen_server:start_link(?MODULE, Socket, []).
|
gen_server:start_link(?MODULE, Socket, []).
|
||||||
|
|
||||||
init(Socket) ->
|
init(Socket) ->
|
||||||
|
ok = ssl:controlling_process(Socket, self()),
|
||||||
ok = ssl:setopts(Socket, [{active, true}]),
|
ok = ssl:setopts(Socket, [{active, true}]),
|
||||||
process_flag(trap_exit, true),
|
process_flag(trap_exit, true),
|
||||||
{ok, #{socket => Socket, transactions => #{}}}.
|
{ok, #{socket => Socket, transactions => #{}}}.
|
||||||
@@ -38,9 +39,7 @@ handle_info({'EXIT', Pid, Reason},
|
|||||||
TransactionId = maps:get(Pid, Transactions),
|
TransactionId = maps:get(Pid, Transactions),
|
||||||
Response = case Reason of
|
Response = case Reason of
|
||||||
{response, Value} -> Value;
|
{response, Value} -> Value;
|
||||||
_ ->
|
_ -> {error, serverError}
|
||||||
io:format("Error handling request: ~p~n", [Reason]),
|
|
||||||
{error, serverError}
|
|
||||||
end,
|
end,
|
||||||
send(Socket, TransactionId, Response),
|
send(Socket, TransactionId, Response),
|
||||||
{noreply, State#{transactions := maps:remove(Pid, Transactions)}};
|
{noreply, State#{transactions := maps:remove(Pid, Transactions)}};
|
||||||
@@ -59,19 +58,24 @@ code_change(_OldVsn, State, _Extra) ->
|
|||||||
|
|
||||||
handle_request(Request) ->
|
handle_request(Request) ->
|
||||||
timer:kill_after(500),
|
timer:kill_after(500),
|
||||||
exit(map_request(Request)).
|
exit({response, map_request(Request)}).
|
||||||
|
|
||||||
map_request({ping, 'NULL'}) ->
|
map_request({ping, 'NULL'}) ->
|
||||||
{response, {ack, 'NULL'}};
|
{ack, 'NULL'};
|
||||||
|
map_request({listActivities, 'NULL'}) ->
|
||||||
|
{activities, Activities} = subject_router:get_activities(),
|
||||||
|
{activities,
|
||||||
|
[{'Activity', Subject, Type, round(Priority * 100)}
|
||||||
|
|| {Subject, Type, Priority} <- Activities]};
|
||||||
map_request({logSession, {'Session', Subject, Type, Timestamp, Minutes}}) ->
|
map_request({logSession, {'Session', Subject, Type, Timestamp, Minutes}}) ->
|
||||||
Session = {unicode:characters_to_list(Subject),
|
Session = {unicode:characters_to_list(Subject),
|
||||||
Type, Timestamp, Minutes},
|
Type, Timestamp, Minutes},
|
||||||
case subject_router:log_session(Session) of
|
case subject_router:log_session(Session) of
|
||||||
ok -> {response, {ack, 'NULL'}};
|
ok -> {ack, 'NULL'};
|
||||||
{error, invalid_subject} -> {response, {error, invalidArguments}}
|
{error, _Error} -> {error, invalidArguments}
|
||||||
end;
|
end;
|
||||||
map_request(_) ->
|
map_request(_) ->
|
||||||
{response, {error, invalidArguments}}.
|
{error, invalidArguments}.
|
||||||
|
|
||||||
send(Socket, TransactionId, Response) ->
|
send(Socket, TransactionId, Response) ->
|
||||||
{ok, Encoded} = 'StudySystemProtocol':encode(
|
{ok, Encoded} = 'StudySystemProtocol':encode(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
-module(subject_router).
|
-module(subject_router).
|
||||||
-behaviour(gen_server).
|
-behaviour(gen_server).
|
||||||
|
|
||||||
-export([start_link/0, log_session/1]).
|
-export([start_link/0, log_session/1, get_activities/0]).
|
||||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||||
terminate/2, code_change/3]).
|
terminate/2, code_change/3]).
|
||||||
|
|
||||||
@@ -14,6 +14,9 @@ start_link() ->
|
|||||||
log_session(Session) ->
|
log_session(Session) ->
|
||||||
gen_server:call(?MODULE, {log_session, Session}).
|
gen_server:call(?MODULE, {log_session, Session}).
|
||||||
|
|
||||||
|
get_activities() ->
|
||||||
|
gen_server:call(?MODULE, get_activities).
|
||||||
|
|
||||||
init([]) ->
|
init([]) ->
|
||||||
{MonitorRef, Pids} = pg:monitor(study_system_server, subject_servers),
|
{MonitorRef, Pids} = pg:monitor(study_system_server, subject_servers),
|
||||||
SubjectTable = ets:new(subject_table, [private]),
|
SubjectTable = ets:new(subject_table, [private]),
|
||||||
@@ -25,11 +28,19 @@ handle_call({log_session, {Subject, Type, Timestamp, Minutes}},
|
|||||||
_From, State = #{subject_table := SubjectTable}) ->
|
_From, State = #{subject_table := SubjectTable}) ->
|
||||||
case ets:lookup(SubjectTable, Subject) of
|
case ets:lookup(SubjectTable, Subject) of
|
||||||
[{Subject, Pid}] ->
|
[{Subject, Pid}] ->
|
||||||
Pid ! {new_session, Type, Timestamp, Minutes},
|
{reply,
|
||||||
{reply, ok, State};
|
subject_server:log_session(Pid, Type, Timestamp, Minutes),
|
||||||
|
State};
|
||||||
[] ->
|
[] ->
|
||||||
{reply, {error, invalid_subject}, State}
|
{reply, {error, invalid_subject}, State}
|
||||||
end;
|
end;
|
||||||
|
handle_call(get_activities, _From,
|
||||||
|
State = #{subject_table := SubjectTable}) ->
|
||||||
|
Pids = lists:flatten(ets:match(SubjectTable, {'_', '$1'})),
|
||||||
|
Activities = lists:flatmap(
|
||||||
|
fun(Pid) -> subject_server:get_activities(Pid) end,
|
||||||
|
Pids),
|
||||||
|
{reply, {activities, Activities}, State};
|
||||||
handle_call(_Request, _From, State) ->
|
handle_call(_Request, _From, State) ->
|
||||||
{reply, ok, State}.
|
{reply, ok, State}.
|
||||||
|
|
||||||
@@ -62,7 +73,7 @@ register_servers(SubjectTable, Pids) ->
|
|||||||
|
|
||||||
register_server(SubjectTable, Pid) ->
|
register_server(SubjectTable, Pid) ->
|
||||||
try
|
try
|
||||||
{subject, Subject} = gen_server:call(Pid, get_subject),
|
{subject, Subject} = subject_server:get_subject(Pid),
|
||||||
ets:insert(SubjectTable, {Subject, Pid})
|
ets:insert(SubjectTable, {Subject, Pid})
|
||||||
catch
|
catch
|
||||||
_:_ -> ok
|
_:_ -> ok
|
||||||
|
|||||||
@@ -4,31 +4,52 @@
|
|||||||
-module(subject_server).
|
-module(subject_server).
|
||||||
-behaviour(gen_server).
|
-behaviour(gen_server).
|
||||||
|
|
||||||
-export([start_link/1]).
|
-export([start_link/1, get_subject/1, get_activities/1, log_session/4]).
|
||||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||||
terminate/2, code_change/3]).
|
terminate/2, code_change/3]).
|
||||||
|
|
||||||
start_link(Subject) ->
|
start_link(Subject) ->
|
||||||
gen_server:start_link(?MODULE, Subject, []).
|
gen_server:start_link(?MODULE, Subject, []).
|
||||||
|
|
||||||
|
get_subject(Pid) ->
|
||||||
|
gen_server:call(Pid, get_subject).
|
||||||
|
|
||||||
|
get_activities(Pid) ->
|
||||||
|
gen_server:call(Pid, get_activities).
|
||||||
|
|
||||||
|
log_session(Pid, Type, Timestamp, Minutes) ->
|
||||||
|
gen_server:call(Pid, {log_session, {Type, Timestamp, Minutes}}).
|
||||||
|
|
||||||
init(Subject) ->
|
init(Subject) ->
|
||||||
pg:join(study_system_server, subject_servers, self()),
|
pg:join(study_system_server, subject_servers, self()),
|
||||||
{ok, #{subject => Subject}}.
|
{ok, #{subject => Subject,
|
||||||
|
priorities => #{reading => rand:uniform(),
|
||||||
|
exercises => rand:uniform()}}}.
|
||||||
|
|
||||||
handle_call(get_subject, _From, State = #{subject := Subject}) ->
|
handle_call(get_subject, _From, State = #{subject := Subject}) ->
|
||||||
{reply, {subject, Subject}, State};
|
{reply, {subject, Subject}, State};
|
||||||
|
handle_call(get_activities, _From,
|
||||||
|
State = #{subject := Subject,
|
||||||
|
priorities := #{reading := ReadingPriority,
|
||||||
|
exercises := ExercisesPriority}}) ->
|
||||||
|
Reading = {Subject, reading, ReadingPriority},
|
||||||
|
Exercises = {Subject, exercises, ExercisesPriority},
|
||||||
|
{reply, [Reading, Exercises], State};
|
||||||
|
handle_call({log_session, {Type, Timestamp, Minutes}}, _From,
|
||||||
|
State = #{subject := Subject, priorities := Priorities}) ->
|
||||||
|
case Priorities of
|
||||||
|
#{Type := Priority} ->
|
||||||
|
UpdatedPriorities = Priorities#{Type := Priority * 0.667},
|
||||||
|
{reply, ok, State#{priorities := UpdatedPriorities}};
|
||||||
|
_ ->
|
||||||
|
{reply, {error, invalid_type}, State}
|
||||||
|
end;
|
||||||
handle_call(_Request, _From, State) ->
|
handle_call(_Request, _From, State) ->
|
||||||
{reply, ok, State}.
|
{reply, ok, State}.
|
||||||
|
|
||||||
handle_cast(_Msg, State) ->
|
handle_cast(_Msg, State) ->
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info({new_session, Type, Timestamp, Minutes},
|
|
||||||
State = #{subject := Subject}) ->
|
|
||||||
io:format(
|
|
||||||
"Received new ~p session: type ~p, timestamp ~p, minutes ~p~n",
|
|
||||||
[Subject, Type, Timestamp, Minutes]),
|
|
||||||
{noreply, State};
|
|
||||||
handle_info(_Info, State) ->
|
handle_info(_Info, State) ->
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,7 @@ handle_info(accept, State = #state{socket = Socket}) ->
|
|||||||
handle_info({handshake, TlsSocket}, State) ->
|
handle_info({handshake, TlsSocket}, State) ->
|
||||||
case ssl:handshake(TlsSocket, 5000) of
|
case ssl:handshake(TlsSocket, 5000) of
|
||||||
{ok, ClientSocket} ->
|
{ok, ClientSocket} ->
|
||||||
{ok, Pid} = session_sup:start_session(ClientSocket),
|
{ok, _Pid} = session_sup:start_session(ClientSocket);
|
||||||
ok = ssl:controlling_process(ClientSocket, Pid);
|
|
||||||
{error, _Reason} ->
|
{error, _Reason} ->
|
||||||
ok
|
ok
|
||||||
end,
|
end,
|
||||||
|
|||||||
Reference in New Issue
Block a user