Compare commits
6 Commits
09f0648138
...
76aca12fec
Author | SHA1 | Date | |
---|---|---|---|
76aca12fec | |||
5a6b535beb | |||
019bdf9ce6 | |||
17629f1db7 | |||
0f0bd37cc8 | |||
bf876336f2 |
@ -1,134 +1,107 @@
|
||||
namespace StudySystemClient {
|
||||
public class ActivitiesView : Gtk.Box {
|
||||
private Client client;
|
||||
private Gtk.FlowBox card_container;
|
||||
private RefreshingIndicator refreshing_indicator;
|
||||
|
||||
public ActivitiesView(Client client) {
|
||||
margin_top = margin_bottom = margin_start = margin_end = 0;
|
||||
Object(orientation: Gtk.Orientation.VERTICAL,
|
||||
hexpand: true,
|
||||
vexpand: true,
|
||||
margin_top: 0,
|
||||
margin_bottom: 0,
|
||||
margin_start: 0,
|
||||
margin_end: 0);
|
||||
|
||||
this.client = client;
|
||||
|
||||
var scrolled_window = new Gtk.ScrolledWindow();
|
||||
scrolled_window.hscrollbar_policy = Gtk.PolicyType.NEVER;
|
||||
scrolled_window.vexpand = true;
|
||||
|
||||
var card_container = new Gtk.FlowBox();
|
||||
card_container = new Gtk.FlowBox();
|
||||
card_container.homogeneous = true;
|
||||
card_container.min_children_per_line = 1;
|
||||
card_container.max_children_per_line = 1;
|
||||
card_container.selection_mode = Gtk.SelectionMode.NONE;
|
||||
card_container.valign = Gtk.Align.START;
|
||||
|
||||
var scrolled_window = new Gtk.ScrolledWindow();
|
||||
scrolled_window.hscrollbar_policy = Gtk.PolicyType.NEVER;
|
||||
scrolled_window.hexpand = true;
|
||||
scrolled_window.vexpand = true;
|
||||
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 },
|
||||
};
|
||||
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);
|
||||
this.append(scrolled_window);
|
||||
|
||||
var overlay = new Gtk.Overlay();
|
||||
overlay.hexpand = overlay.vexpand = true;
|
||||
overlay.set_child(scrolled_window);
|
||||
this.append(overlay);
|
||||
|
||||
refreshing_indicator = new RefreshingIndicator(overlay);
|
||||
|
||||
this.map.connect(refresh);
|
||||
}
|
||||
|
||||
private async void refresh() {
|
||||
refreshing_indicator.show();
|
||||
try {
|
||||
var activities = yield client.list_activities();
|
||||
card_container.remove_all();
|
||||
foreach (var activity in activities) {
|
||||
var card = new ActivityCard(activity);
|
||||
card.session_logged.connect(log_session);
|
||||
card_container.append(card);
|
||||
}
|
||||
} catch (ClientError e) {
|
||||
stderr.printf("Error refreshing activities: %s\n",
|
||||
e.message);
|
||||
}
|
||||
refreshing_indicator.hide();
|
||||
}
|
||||
|
||||
private async void log_session(string subject, ActivityType type,
|
||||
int minutes) {
|
||||
try {
|
||||
yield client.log_session(subject, type, minutes);
|
||||
stderr.printf("Successfully logged session\n");
|
||||
yield refresh();
|
||||
} catch (ClientError e) {
|
||||
stderr.printf("Error logging session: %s\n", e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ActivityCard : Gtk.Frame {
|
||||
public signal void session_logged(string subject, ActivityType type,
|
||||
int minutes);
|
||||
private class RefreshingIndicator {
|
||||
private Gtk.Overlay overlay;
|
||||
private Gtk.Frame frame;
|
||||
|
||||
public ActivityCard(Activity activity) {
|
||||
add_css_class("card");
|
||||
public RefreshingIndicator(Gtk.Overlay overlay) {
|
||||
this.overlay = overlay;
|
||||
|
||||
var content = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 12);
|
||||
|
||||
var text = new Gtk.Box(Gtk.Orientation.VERTICAL, 6);
|
||||
text.hexpand = true;
|
||||
|
||||
var subject = new Gtk.Label(activity.subject);
|
||||
subject.halign = Gtk.Align.START;
|
||||
subject.add_css_class("activity-subject");
|
||||
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 {
|
||||
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");
|
||||
var label = new Gtk.Label("Refreshing");
|
||||
label.halign = Gtk.Align.START;
|
||||
|
||||
var spinner = new Gtk.Spinner();
|
||||
spinner.start();
|
||||
|
||||
var content = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6);
|
||||
content.margin_top = content.margin_bottom
|
||||
= content.margin_start = content.margin_end = 12;
|
||||
content.append(label);
|
||||
content.append(spinner);
|
||||
|
||||
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);
|
||||
frame = new Gtk.Frame(null);
|
||||
frame.halign = Gtk.Align.CENTER;
|
||||
frame.valign = Gtk.Align.START;
|
||||
frame.add_css_class("osd");
|
||||
frame.add_css_class("popdown");
|
||||
frame.set_child(content);
|
||||
|
||||
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);
|
||||
overlay.add_overlay(frame);
|
||||
}
|
||||
|
||||
private void submit() {
|
||||
session_logged((int)input.value);
|
||||
reset();
|
||||
popdown();
|
||||
public void show() {
|
||||
frame.add_css_class("visible");
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
input.value = DEFAULT_LENGTH;
|
||||
public void hide() {
|
||||
frame.remove_css_class("visible");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
94
client/src/activity_card.vala
Normal file
94
client/src/activity_card.vala
Normal file
@ -0,0 +1,94 @@
|
||||
namespace StudySystemClient {
|
||||
public class ActivityCard : Gtk.Frame {
|
||||
public signal void session_logged(string subject, ActivityType type,
|
||||
int minutes);
|
||||
|
||||
public ActivityCard(Activity activity) {
|
||||
add_css_class("card");
|
||||
|
||||
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");
|
||||
var priority = new Gtk.Label("%0.2f".printf(activity.priority));
|
||||
priority.add_css_class("activity-priority");
|
||||
|
||||
var details = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6);
|
||||
details.append(type);
|
||||
details.append(separator);
|
||||
details.append(priority);
|
||||
|
||||
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);
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -10,8 +10,9 @@ configure_file(
|
||||
lib = library(
|
||||
'study-system-client',
|
||||
sources: files(
|
||||
'activity.vala',
|
||||
'activities_view.vala',
|
||||
'activity.vala',
|
||||
'activity_card.vala',
|
||||
'client.vala',
|
||||
'connection.vala',
|
||||
'der.vala',
|
||||
|
@ -40,7 +40,7 @@ namespace StudySystemClient.Response {
|
||||
protected enum Tag {
|
||||
ERROR = 0,
|
||||
ACK = 1,
|
||||
PRIORITIZED_ACTIVITIES = 2,
|
||||
ACTIVITIES = 2,
|
||||
}
|
||||
|
||||
internal static Body from_datum(Der.Datum datum) throws DecodeError {
|
||||
@ -54,9 +54,8 @@ namespace StudySystemClient.Response {
|
||||
return new Error.from_datum(choice.value);
|
||||
case Tag.ACK:
|
||||
return new Ack.from_datum(choice.value);
|
||||
case Tag.PRIORITIZED_ACTIVITIES:
|
||||
throw new DecodeError.NOT_IMPLEMENTED(
|
||||
"PrioritizedActivities not yet implemented");
|
||||
case Tag.ACTIVITIES:
|
||||
return new Activities.from_datum(choice.value);
|
||||
default:
|
||||
throw new DecodeError.INVALID_BODY(
|
||||
"Invalid ResponseBody tag");
|
||||
|
@ -65,7 +65,7 @@ namespace StudySystemClient {
|
||||
public class SessionFactory {
|
||||
private const string CA_FILENAME = "/ca.pem";
|
||||
private const string CERT_FILENAME = "/client.pem";
|
||||
private const uint TIMEOUT_S = 5;
|
||||
private const uint TIMEOUT_S = 1;
|
||||
|
||||
private InetSocketAddress host;
|
||||
private TlsCertificate cert;
|
||||
|
@ -14,6 +14,10 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.activity-priority {
|
||||
color: alpha(@theme_fg_color, 0.6);
|
||||
}
|
||||
|
||||
/*
|
||||
* The visual center (i.e. the center of the clock) of the
|
||||
* "appointment-new-symbolic" icon is slightly displaced from the
|
||||
@ -23,3 +27,20 @@
|
||||
margin-top: -2px;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Couldn't find a built-in way to get the common OSD overlay style of
|
||||
* rounding the bottom corners but not the top ones, so doing this
|
||||
* myself with this CSS rule.
|
||||
*/
|
||||
overlay > frame.osd {
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
.popdown {
|
||||
transition: transform 200ms ease-in-out;
|
||||
transform: translateY(-100px);
|
||||
}
|
||||
.popdown.visible {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
@ -63,12 +63,18 @@ handle_request(Request) ->
|
||||
|
||||
map_request({ping, 'NULL'}) ->
|
||||
{response, {ack, 'NULL'}};
|
||||
map_request({listActivities, 'NULL'}) ->
|
||||
{activities, Activities} = subject_router:get_activities(),
|
||||
SortedActivities = lists:reverse(lists:keysort(3, Activities)),
|
||||
{response, {activities,
|
||||
[{'Activity', Subject, Type, round(Priority * 100)}
|
||||
|| {Subject, Type, Priority} <- SortedActivities]}};
|
||||
map_request({logSession, {'Session', Subject, Type, Timestamp, Minutes}}) ->
|
||||
Session = {unicode:characters_to_list(Subject),
|
||||
Type, Timestamp, Minutes},
|
||||
case subject_router:log_session(Session) of
|
||||
ok -> {response, {ack, 'NULL'}};
|
||||
{error, invalid_subject} -> {response, {error, invalidArguments}}
|
||||
{error, _Error} -> {response, {error, invalidArguments}}
|
||||
end;
|
||||
map_request(_) ->
|
||||
{response, {error, invalidArguments}}.
|
||||
|
@ -4,7 +4,7 @@
|
||||
-module(subject_router).
|
||||
-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,
|
||||
terminate/2, code_change/3]).
|
||||
|
||||
@ -14,6 +14,9 @@ start_link() ->
|
||||
log_session(Session) ->
|
||||
gen_server:call(?MODULE, {log_session, Session}).
|
||||
|
||||
get_activities() ->
|
||||
gen_server:call(?MODULE, get_activities).
|
||||
|
||||
init([]) ->
|
||||
{MonitorRef, Pids} = pg:monitor(study_system_server, subject_servers),
|
||||
SubjectTable = ets:new(subject_table, [private]),
|
||||
@ -25,11 +28,19 @@ handle_call({log_session, {Subject, Type, Timestamp, Minutes}},
|
||||
_From, State = #{subject_table := SubjectTable}) ->
|
||||
case ets:lookup(SubjectTable, Subject) of
|
||||
[{Subject, Pid}] ->
|
||||
Pid ! {new_session, Type, Timestamp, Minutes},
|
||||
{reply, ok, State};
|
||||
{reply,
|
||||
subject_server:log_session(Pid, Type, Timestamp, Minutes),
|
||||
State};
|
||||
[] ->
|
||||
{reply, {error, invalid_subject}, State}
|
||||
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) ->
|
||||
{reply, ok, State}.
|
||||
|
||||
@ -62,7 +73,7 @@ register_servers(SubjectTable, Pids) ->
|
||||
|
||||
register_server(SubjectTable, Pid) ->
|
||||
try
|
||||
{subject, Subject} = gen_server:call(Pid, get_subject),
|
||||
{subject, Subject} = subject_server:get_subject(Pid),
|
||||
ets:insert(SubjectTable, {Subject, Pid})
|
||||
catch
|
||||
_:_ -> ok
|
||||
|
@ -4,31 +4,52 @@
|
||||
-module(subject_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,
|
||||
terminate/2, code_change/3]).
|
||||
|
||||
start_link(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) ->
|
||||
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}) ->
|
||||
{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) ->
|
||||
{reply, ok, State}.
|
||||
|
||||
handle_cast(_Msg, 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) ->
|
||||
{noreply, State}.
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user