~tristan957/harvest-almanac

47403459db2d1c0f0482d01a69af698616ba3519 — Tristan Partin 1 year, 9 months ago 8ceaa01
meta: dirty commit :/

This commit features:

* Context support
* Reactive UI
* GString cleanup
* Singleton refactor
M data/ui/hal-window.ui => data/ui/hal-window.ui +2 -2
@@ 273,12 273,12 @@
                    <property name="orientation">vertical</property>
                    <property name="spacing">3</property>
                    <child>
                      <object class="GtkLabel">
                      <object class="GtkLabel" id="username">
                        <property name="visible">True</property>
                        <property name="can_focus">False</property>
                        <property name="valign">end</property>
                        <property name="vexpand">True</property>
                        <property name="label" translatable="yes">Tristan Partin</property>
                        <property name="label" translatable="yes"></property>
                        <property name="xalign">0</property>
                      </object>
                      <packing>

M harvest-almanac/hal-application.c => harvest-almanac/hal-application.c +11 -12
@@ 19,6 19,8 @@

#define HAL_MAX_CONNS_PER_HOST 4

extern HalContext *CONTEXT;
extern HarvestApiClient *API_CLIENT;
extern HalTimeEntry *CURRENTLY_RUNNING_TIME_ENTRY;

struct _HalApplication


@@ 66,14 68,13 @@ construct_client(HalApplication *self, const char *access_token, const char *acc

	g_autoptr(SoupLogger) logger = soup_logger_new(logger_level, -1);

	g_autoptr(GString) user_agent = g_string_new(NULL);
	g_string_printf(user_agent, "Harvest Almanac (%s)", contact_email);
	g_autofree char *user_agent = g_strdup_printf("Harvest Almanac (%s)", contact_email);

	g_autoptr(SoupSession) session = soup_session_new_with_options(SOUP_SESSION_MAX_CONNS,
		max_connections, SOUP_SESSION_USER_AGENT, user_agent->str, SOUP_SESSION_ADD_FEATURE_BY_TYPE,
		max_connections, SOUP_SESSION_USER_AGENT, user_agent, SOUP_SESSION_ADD_FEATURE_BY_TYPE,
		SOUP_TYPE_CONTENT_SNIFFER, SOUP_SESSION_ADD_FEATURE, SOUP_SESSION_FEATURE(logger), NULL);

	self->client = harvest_api_client_new(session, access_token, account_id);
	harvest_api_client_initialize(session, access_token, account_id);

	harvest_user_get_me_async(validate_user, self);
}


@@ 86,12 87,9 @@ hal_application_activate(GApplication *app)

	if (priv->main_window == NULL) {
		priv->main_window = hal_window_new(app);
	}

	if (self->client == NULL) {
		g_autofree const char *account_id
			= g_settings_get_string(self->settings, "harvest-account-id");
		g_autofree const char *contact_email
		g_autofree char *account_id = g_settings_get_string(self->settings, "harvest-account-id");
		g_autofree char *contact_email
			= g_settings_get_string(self->settings, "harvest-api-contact-email");

		g_autoptr(GError) err = NULL;


@@ 173,9 171,9 @@ hal_application_reconstruct_client(
	HalApplication *self = HAL_APPLICATION(data);

	const char *harvest_api_access_token = g_variant_get_string(param, NULL);
	g_autofree const char *harvest_api_contact_email
	g_autofree char *harvest_api_contact_email
		= g_settings_get_string(self->settings, "harvest-api-contact-email");
	g_autofree const char *harvest_account_id
	g_autofree char *harvest_account_id
		= g_settings_get_string(self->settings, "harvest-account-id");

	construct_client(self, harvest_api_access_token, harvest_account_id, harvest_api_contact_email);


@@ 195,8 193,9 @@ hal_application_finalize(GObject *obj)
{
	HalApplication *self = HAL_APPLICATION(obj);

	g_clear_object(&self->client);
	harvest_api_client_free();
	g_clear_object(&self->settings);
	g_clear_object(&CONTEXT);

	G_OBJECT_CLASS(hal_application_parent_class)->finalize(obj);
}

M harvest-almanac/hal-context.c => harvest-almanac/hal-context.c +19 -23
@@ 9,7 9,7 @@

#include "hal-context.h"

HalContext *instance;
HalContext *CONTEXT;

struct _HalContext
{


@@ 35,6 35,10 @@ hal_context_finalize(GObject *obj)
{
	HalContext *self = HAL_CONTEXT(obj);

	/**
	 * Using g_clear_object here due to the ownership model. CONTEXT will be the last thing to be
	 * garbage collected before the application itself.
	 */
	g_clear_object(&self->user);

	G_OBJECT_CLASS(hal_context_parent_class)->finalize(obj);


@@ 61,6 65,8 @@ hal_context_set_property(GObject *obj, guint prop_id, const GValue *val, GParamS

	switch (prop_id) {
	case PROP_USER:
		if (self->user != NULL)
			g_object_unref(self->user);
		self->user = g_value_dup_object(val);
		break;
	default:


@@ 77,9 83,9 @@ hal_context_class_init(HalContextClass *klass)
	obj_class->get_property = hal_context_get_property;
	obj_class->set_property = hal_context_set_property;

	obj_properties[PROP_USER]
		= g_param_spec_object("user", _("User"), _("Currently logged in user of the application."),
			HARVEST_TYPE_USER, G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
	obj_properties[PROP_USER] = g_param_spec_object("user", _("User"),
		_("Currently logged in user of the application."), HARVEST_TYPE_USER,
		G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties(obj_class, N_PROPS, obj_properties);
}


@@ 91,31 97,21 @@ hal_context_init(G_GNUC_UNUSED HalContext *self)
void
hal_context_initialize()
{
	instance = g_object_new(HAL_TYPE_CONTEXT, NULL);
	CONTEXT = g_object_new(HAL_TYPE_CONTEXT, NULL);
}

const HalContext *
hal_context_get()
void
hal_context_set_user(HarvestUser *user)
{
	return instance;
}
	g_return_if_fail(CONTEXT != NULL && HARVEST_IS_USER(user));

HarvestUser *
hal_context_get_user()
{
	g_return_val_if_fail(instance != NULL, NULL);
	CONTEXT->user = user;

	return instance->user;
	g_object_notify_by_pspec(G_OBJECT(CONTEXT), obj_properties[PROP_USER]);
}

void
hal_context_set_user(HarvestUser *user)
HarvestUser *
hal_context_get_user()
{
	g_return_if_fail(HARVEST_IS_USER(user) && instance != NULL);

	GValue val = G_VALUE_INIT;
	g_value_init(&val, HARVEST_TYPE_USER);
	g_value_set_object(&val, user);

	g_object_set_property(G_OBJECT(instance), "user", &val);
	return CONTEXT->user;
}

M harvest-almanac/hal-context.h => harvest-almanac/hal-context.h +1 -2
@@ 9,8 9,7 @@ G_BEGIN_DECLS
G_DECLARE_FINAL_TYPE(HalContext, hal_context, HAL, CONTEXT, GObject)

void hal_context_initialize();
const HalContext *hal_context_get() G_GNUC_CONST;
HarvestUser *hal_context_get_user() G_GNUC_CONST;
void hal_context_set_user(HarvestUser *user);
HarvestUser *hal_context_get_user() G_GNUC_CONST;

G_END_DECLS

M harvest-almanac/hal-preferences-window.c => harvest-almanac/hal-preferences-window.c +6 -6
@@ 110,7 110,7 @@ on_harvest_api_contact_email_entry_changed(GtkEditable *widget, gpointer user_da
{
	HalPreferencesWindow *self = HAL_PREFERENCES_WINDOW(user_data);

	g_autofree const char *contact_email
	g_autofree char *contact_email
		= g_settings_get_string(self->settings, "harvest-api-contact-email");

	if (!g_str_equal(gtk_entry_get_text(GTK_ENTRY(widget)), contact_email)) {


@@ 125,7 125,7 @@ on_harvest_account_id_entry_changed(GtkEditable *widget, gpointer user_data)
{
	HalPreferencesWindow *self = HAL_PREFERENCES_WINDOW(user_data);

	g_autofree const char *account_id = g_settings_get_string(self->settings, "harvest-account-id");
	g_autofree char *account_id = g_settings_get_string(self->settings, "harvest-account-id");

	if (!g_str_equal(gtk_entry_get_text(GTK_ENTRY(widget)), account_id)) {
		hal_preferences_window_set_dirty(self, TRUE);


@@ 275,8 275,8 @@ on_harvest_api_access_token_forget_button_clicked(
	HalPreferencesWindow *self		  = HAL_PREFERENCES_WINDOW(user_data);
	HalPreferencesWindowPrivate *priv = hal_preferences_window_get_instance_private(self);

	g_autofree const char *account_id = g_settings_get_string(self->settings, "harvest-account-id");
	g_autofree const char *contact_email
	g_autofree char *account_id = g_settings_get_string(self->settings, "harvest-account-id");
	g_autofree char *contact_email
		= g_settings_get_string(self->settings, "harvest-api-contact-email");

	secret_password_clear(HAL_SECRET_SCHEMA, self->cancellable, on_harvest_api_access_token_clear,


@@ 296,8 296,8 @@ hal_preferences_window_constructed(GObject *obj)
	HalPreferencesWindow *self		  = HAL_PREFERENCES_WINDOW(obj);
	HalPreferencesWindowPrivate *priv = hal_preferences_window_get_instance_private(self);

	g_autofree const char *account_id = g_settings_get_string(self->settings, "harvest-account-id");
	g_autofree const char *contact_email
	g_autofree char *account_id = g_settings_get_string(self->settings, "harvest-account-id");
	g_autofree char *contact_email
		= g_settings_get_string(self->settings, "harvest-api-contact-email");
	g_autoptr(GVariant) soup_max_connections_variant
		= g_settings_get_value(self->settings, "soup-max-connections");

M harvest-almanac/hal-time-entry.c => harvest-almanac/hal-time-entry.c +3 -4
@@ 52,10 52,9 @@ update_time_label(gpointer user_data)
		return G_SOURCE_REMOVE;
	}

	const int elapsed		 = g_timer_elapsed(self->timer, NULL);
	g_autoptr(GString) label = g_string_new(NULL);
	g_string_printf(label, "%d:%02d", elapsed / 3600, (elapsed % 3600) / 60);
	gtk_label_set_text(priv->time, label->str);
	const int elapsed	   = g_timer_elapsed(self->timer, NULL);
	g_autofree char *label = g_strdup_printf("%d:%02d", elapsed / 3600, (elapsed % 3600) / 60);
	gtk_label_set_text(priv->time, label);

	return G_SOURCE_CONTINUE;
}

M harvest-almanac/hal-window.c => harvest-almanac/hal-window.c +24 -3
@@ 6,10 6,13 @@
#include <gtk/gtk.h>
#include <handy.h>

#include "hal-context.h"
#include "hal-profile.h"
#include "hal-time-tracker.h"
#include "hal-window.h"

extern HalContext *CONTEXT;

struct _HalWindow
{
	GtkApplicationWindow parent_instance;


@@ 25,6 28,7 @@ typedef struct HalWindowPrivate
	GtkStackSidebar *stack_sidebar;
	GtkStack *stack;
	GtkButton *back_button;
	GtkLabel *username;
	HalTimeTracker *time_tracker;
	HalProfile *profile;
} HalWindowPrivate;


@@ 32,6 36,20 @@ typedef struct HalWindowPrivate
G_DEFINE_TYPE_WITH_PRIVATE(HalWindow, hal_window, GTK_TYPE_APPLICATION_WINDOW)

static void
on_context_notify_user(
	G_GNUC_UNUSED GObject *obj, G_GNUC_UNUSED GParamSpec *pspec, gpointer user_data)
{
	HalWindow *self		   = HAL_WINDOW(user_data);
	HalWindowPrivate *priv = hal_window_get_instance_private(self);
	HarvestUser *user	   = hal_context_get_user(CONTEXT);

	if (user != NULL) {
		gtk_label_set_text(priv->username, g_strconcat(harvest_user_get_first_name(user), " ",
											   harvest_user_get_last_name(user), NULL));
	}
}

static void
update_header_bar_title(HalWindow *self)
{
	HalWindowPrivate *priv = hal_window_get_instance_private(self);


@@ 119,10 137,10 @@ hal_window_finalize(GObject *obj)
static void
hal_window_class_init(HalWindowClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
	GtkWidgetClass *wid_class  = GTK_WIDGET_CLASS(klass);
	GObjectClass *obj_class	  = G_OBJECT_CLASS(klass);
	GtkWidgetClass *wid_class = GTK_WIDGET_CLASS(klass);

	object_class->finalize = hal_window_finalize;
	obj_class->finalize = hal_window_finalize;

	gtk_widget_class_set_template_from_resource(
		wid_class, "/io/partin/tristan/HarvestAlmanac/ui/hal-window.ui");


@@ 134,6 152,7 @@ hal_window_class_init(HalWindowClass *klass)
	gtk_widget_class_bind_template_child_private(wid_class, HalWindow, stack_sidebar);
	gtk_widget_class_bind_template_child_private(wid_class, HalWindow, stack);
	gtk_widget_class_bind_template_child_private(wid_class, HalWindow, back_button);
	gtk_widget_class_bind_template_child_private(wid_class, HalWindow, username);
	gtk_widget_class_bind_template_callback(wid_class, header_leaflet_notify_fold_cb);
	gtk_widget_class_bind_template_callback(wid_class, header_leaflet_notify_visible_child_cb);
	gtk_widget_class_bind_template_callback(wid_class, stack_notify_visible_child_cb);


@@ 156,6 175,8 @@ hal_window_init(HalWindow *self)
	update_header_bar_title(self);

	hdy_leaflet_set_visible_child_name(priv->content_leaflet, "content");

	g_signal_connect(CONTEXT, "notify::user", G_CALLBACK(on_context_notify_user), self);
}

HalWindow *

M harvest-glib/harvest-api-client.c => harvest-glib/harvest-api-client.c +28 -31
@@ 18,7 18,7 @@

#define HARVEST_API_URL "https://api.harvestapp.com/v2"

static HarvestApiClient *instance;
static HarvestApiClient *API_CLIENT;

struct _HarvestApiClient
{


@@ 138,27 138,24 @@ static void
harvest_api_client_init(G_GNUC_UNUSED HarvestApiClient *self)
{}

HarvestApiClient *
harvest_api_client_new(SoupSession *session, const char *access_token, const char *account_id)
void
harvest_api_client_initialize(
	SoupSession *session, const char *access_token, const char *account_id)
{
	g_return_val_if_fail(
		SOUP_IS_SESSION(session) && access_token != NULL && account_id != NULL, NULL);

	if (instance != NULL)
		g_object_unref(instance);
	g_return_if_fail(SOUP_IS_SESSION(session) && access_token != NULL && account_id != NULL);

	if (instance == NULL) {
		instance = g_object_new(HARVEST_TYPE_API_CLIENT, "session", session, "server",
			HARVEST_API_URL, "access-token", access_token, "account-id", account_id, NULL);
	}
	if (API_CLIENT != NULL)
		g_object_unref(API_CLIENT);

	return instance;
	API_CLIENT = g_object_new(HARVEST_TYPE_API_CLIENT, "session", session, "server",
		HARVEST_API_URL, "access-token", access_token, "account-id", account_id, NULL);
}

HarvestApiClient *
harvest_api_client_get_instance(void)
void
harvest_api_client_free()
{
	return instance;
	if (API_CLIENT != NULL)
		g_object_unref(API_CLIENT);
}

static void


@@ 212,11 209,11 @@ harvest_api_client_async_callback(
}

static SoupMessage *
create_message(HarvestApiClient *self, HarvestRequest *req)
create_message(HarvestRequest *req)
{
	SoupMessage *msg	   = NULL;
	g_autoptr(GString) uri = g_string_new(self->server);
	g_string_append(uri, harvest_request_get_endpoint(req));
	SoupMessage *msg = NULL;
	g_autofree char *uri
		= g_strdup_printf("%s%s", API_CLIENT->server, harvest_request_get_endpoint(req));
	gboolean response_has_body = harvest_request_get_data(req) != NULL;
	char *body				   = NULL;
	gsize len				   = 0;


@@ 228,7 225,7 @@ create_message(HarvestApiClient *self, HarvestRequest *req)

	switch (harvest_request_get_http_method(req)) {
	case HTTP_METHOD_GET:
		msg = soup_message_new("GET", uri->str);
		msg = soup_message_new("GET", uri);
		soup_message_set_request(
			msg, response_has_body ? "application/json" : NULL, SOUP_MEMORY_TAKE, body, len);
		break;


@@ 242,19 239,19 @@ create_message(HarvestApiClient *self, HarvestRequest *req)
		g_return_val_if_reached(NULL);
	}

	soup_message_headers_append(msg->request_headers, "Authorization", self->access_token);
	soup_message_headers_append(msg->request_headers, "Harvest-Account-Id", self->account_id);
	soup_message_headers_append(msg->request_headers, "Authorization", API_CLIENT->access_token);
	soup_message_headers_append(msg->request_headers, "Harvest-Account-Id", API_CLIENT->account_id);

	return msg;
}

HarvestResponse *
harvest_api_client_execute_request_sync(HarvestApiClient *self, HarvestRequest *req)
harvest_api_client_execute_request_sync(HarvestRequest *req)
{
	g_return_val_if_fail(HARVEST_IS_API_CLIENT(self) && HARVEST_IS_REQUEST(req), NULL);
	g_return_val_if_fail(HARVEST_IS_REQUEST(req), NULL);

	g_autoptr(SoupMessage) msg = create_message(self, req);
	soup_session_send_message(self->session, msg);
	g_autoptr(SoupMessage) msg = create_message(req);
	soup_session_send_message(API_CLIENT->session, msg);
	HarvestResponse *res = create_response(req, msg);
	g_signal_emit_by_name(req, "completed", res);



@@ 262,11 259,11 @@ harvest_api_client_execute_request_sync(HarvestApiClient *self, HarvestRequest *
}

void
harvest_api_client_execute_request_async(HarvestApiClient *self, HarvestRequest *req)
harvest_api_client_execute_request_async(HarvestRequest *req)
{
	g_return_if_fail(HARVEST_IS_API_CLIENT(self) && HARVEST_IS_REQUEST(req));
	g_return_if_fail(HARVEST_IS_REQUEST(req));

	SoupMessage *msg = create_message(self, req);
	SoupMessage *msg = create_message(req);

	soup_session_queue_message(self->session, msg, harvest_api_client_async_callback, req);
	soup_session_queue_message(API_CLIENT->session, msg, harvest_api_client_async_callback, req);
}

M harvest-glib/harvest-api-client.h => harvest-glib/harvest-api-client.h +5 -6
@@ 15,11 15,10 @@ G_BEGIN_DECLS
#define HARVEST_TYPE_API_CLIENT (harvest_api_client_get_type())
G_DECLARE_FINAL_TYPE(HarvestApiClient, harvest_api_client, HARVEST, API_CLIENT, GObject)

HarvestApiClient *harvest_api_client_new(SoupSession *session, const char *access_token,
	const char *account_id) G_GNUC_WARN_UNUSED_RESULT;
HarvestApiClient *harvest_api_client_get_instance(void) G_GNUC_CONST;
HarvestResponse *harvest_api_client_execute_request_sync(
	HarvestApiClient *self, HarvestRequest *req);
void harvest_api_client_execute_request_async(HarvestApiClient *self, HarvestRequest *req);
void harvest_api_client_initialize(
	SoupSession *session, const char *access_token, const char *account_id);
void harvest_api_client_free(void);
HarvestResponse *harvest_api_client_execute_request_sync(HarvestRequest *req);
void harvest_api_client_execute_request_async(HarvestRequest *req);

G_END_DECLS

M harvest-glib/harvest-late-request.c => harvest-glib/harvest-late-request.c +2 -2
@@ 166,10 166,10 @@ harvest_late_request_new(const char *first_prop_name, ...)
		g_object_new_valist(HARVEST_TYPE_LATE_REQUEST, first_prop_name, var_args));
	va_end(var_args);

	g_autoptr(GString) endpoint = g_string_new("/time_entries");
	g_autofree char *endpoint = g_strdup("/time_entries");
	g_autoptr(HarvestResponseMetadata) response_metadata
		= harvest_response_metadata_new(G_TYPE_NONE, HTTP_STATUS_OK);
	g_object_set(self, "http-method", HTTP_METHOD_GET, "endpoint", endpoint->str, "query-params",
	g_object_set(self, "http-method", HTTP_METHOD_GET, "endpoint", endpoint, "query-params",
		harvest_late_request_serialize_params(self), "response-metadata", response_metadata, NULL);

	return self;

M harvest-glib/harvest-user.c => harvest-glib/harvest-user.c +2 -6
@@ 389,22 389,18 @@ harvest_user_init(G_GNUC_UNUSED HarvestUser *self)
HarvestResponse *
harvest_user_get_me()
{
	HarvestApiClient *client = harvest_api_client_get_instance();

	HarvestUsersMeRequest *request = harvest_users_me_request_new();

	return harvest_api_client_execute_request_sync(client, HARVEST_REQUEST(request));
	return harvest_api_client_execute_request_sync(HARVEST_REQUEST(request));
}

void
harvest_user_get_me_async(HarvestCompletedCallback *callback, gpointer user_data)
{
	HarvestApiClient *client = harvest_api_client_get_instance();

	HarvestUsersMeRequest *request = harvest_users_me_request_new();
	g_signal_connect(HARVEST_REQUEST(request), "completed", G_CALLBACK(callback), user_data);

	harvest_api_client_execute_request_async(client, HARVEST_REQUEST(request));
	harvest_api_client_execute_request_async(HARVEST_REQUEST(request));
}

const char *

M harvest-glib/harvest-users-me-request.c => harvest-glib/harvest-users-me-request.c +2 -2
@@ 29,10 29,10 @@ harvest_users_me_request_init(G_GNUC_UNUSED HarvestUsersMeRequest *self)
HarvestUsersMeRequest *
harvest_users_me_request_new()
{
	g_autoptr(GString) endpoint = g_string_new("/users/me");
	g_autofree char *endpoint = g_strdup("/users/me");
	g_autoptr(HarvestResponseMetadata) response_metadata
		= harvest_response_metadata_new(HARVEST_TYPE_USER, HTTP_STATUS_OK);

	return g_object_new(HARVEST_TYPE_USERS_ME_REQUEST, "http-method", HTTP_METHOD_GET, "endpoint",
		endpoint->str, "response-metadata", response_metadata, NULL);
		endpoint, "response-metadata", response_metadata, NULL);
}