~tristan957/harvest-almanac

e69f6e5676ff3344c5163ef57bf96bac0824673b — Tristan Partin 1 year, 11 months ago 61df1ac
harvest-almanac: integrated libsecret and gsettings in prep for first request
M data/io.partin.tristan.HarvestAlmanac.gschema.xml => data/io.partin.tristan.HarvestAlmanac.gschema.xml +5 -0
@@ 21,5 21,10 @@
			<summary>Harvest API Contact Email</summary>
			<description>Harvest API will contact this email if the API is misused in anyway. Must be set.</description>
		</key>
		<key name="harvest-account-id" type="s">
			<default>""</default>
			<summary>Harvest Account ID</summary>
			<description>Harvest account ID.</description>
		</key>
	</schema>
</schemalist>

M data/ui/hal-preferences-window.ui => data/ui/hal-preferences-window.ui +55 -7
@@ 36,7 36,7 @@
        </child>
        <child>
          <object class="HdyPreferencesGroup">
            <property name="description" translatable="yes">Harvest Almanac requires that an access token and contact email be set in order to properly use the Harvest API on your behalf.</property>
            <property name="description" translatable="yes">Harvest Almanac requires that an access token, account ID, and contact email be set in order to properly use the Harvest API on your behalf.</property>
            <property name="title" translatable="yes">Harvest API</property>
            <property name="visible">True</property>
            <child>


@@ 44,14 44,61 @@
                <property name="title" translatable="yes">Access Token</property>
                <property name="visible">True</property>
                <child type="action">
                  <object class="GtkEntry" id="harvest_api_access_token_entry">
                  <object class="GtkBox">
                    <property name="visible">True</property>
                    <property name="can_focus">False</property>
                    <property name="spacing">5</property>
                    <child>
                      <object class="GtkButton" id="harvest_api_access_token_forget_button">
                        <property name="label" translatable="yes">Forget</property>
                        <property name="visible">True</property>
                        <property name="can_focus">True</property>
                        <property name="receives_default">True</property>
                        <property name="valign">center</property>
                        <property name="sensitive">False</property>
                        <signal name="clicked" handler="on_harvest_api_access_token_forget_button_clicked" object="HalPreferencesWindow" swapped="no"/>
                        <style>
                            <class name="destructive-action"/>
                          </style>
                      </object>
                      <packing>
                        <property name="expand">False</property>
                        <property name="fill">True</property>
                        <property name="position">0</property>
                      </packing>
                    </child>
                    <child>
                      <object class="GtkEntry" id="harvest_api_access_token_entry">
                        <property name="visible">True</property>
                        <property name="can_focus">True</property>
                        <property name="valign">center</property>
                        <property name="visibility">False</property>
                        <property name="invisible_char">•</property>
                        <property name="input_purpose">password</property>
                        <property name="sensitive">False</property>
                        <signal name="changed" handler="on_harvest_api_access_token_entry_changed" object="HalPreferencesWindow" swapped="no"/>
                      </object>
                      <packing>
                        <property name="expand">True</property>
                        <property name="fill">True</property>
                        <property name="position">1</property>
                      </packing>
                    </child>
                  </object>
                </child>
              </object>
            </child>
            <child>
              <object class="HdyActionRow">
                <property name="title" translatable="yes">Account ID</property>
                <property name="visible">True</property>
                <child type="action">
                  <object class="GtkEntry" id="harvest_account_id_entry">
                    <property name="visible">True</property>
                    <property name="can_focus">True</property>
                    <property name="valign">center</property>
                    <property name="visibility">False</property>
                    <property name="invisible_char">•</property>
                    <property name="input_purpose">password</property>
                    <signal name="changed" handler="on_harvest_api_access_token_entry_changed" object="HalPreferencesWindow" swapped="no"/>
                    <property name="sensitive">False</property>
                    <signal name="changed" handler="on_harvest_account_id_entry_changed" object="HalPreferencesWindow" swapped="no"/>
                  </object>
                </child>
              </object>


@@ 61,10 108,11 @@
                <property name="title" translatable="yes">Contact Email</property>
                <property name="visible">True</property>
                <child type="action">
                  <object class="GtkEntry" id="harvest_api_contact_email">
                  <object class="GtkEntry" id="harvest_api_contact_email_entry">
                    <property name="visible">True</property>
                    <property name="can_focus">True</property>
                    <property name="valign">center</property>
                    <property name="sensitive">False</property>
                    <signal name="changed" handler="on_harvest_api_contact_email_entry_changed" object="HalPreferencesWindow" swapped="no"/>
                  </object>
                </child>

M harvest-almanac/hal-application.c => harvest-almanac/hal-application.c +59 -17
@@ 7,6 7,7 @@
#include <glib-object.h>
#include <gtk/gtk.h>
#include <harvest.h>
#include <libsecret/secret.h>
#include <libsoup/soup.h>

#include "hal-application.h"


@@ 33,6 34,30 @@ typedef struct HalApplicationPrivate
G_DEFINE_TYPE_WITH_PRIVATE(HalApplication, hal_application, GTK_TYPE_APPLICATION)

static void
construct_client(HalApplication *self, const char *access_token, const char *account_id,
	const char *contact_email)
{
	g_autoptr(GVariant) max_connections_v
		= g_settings_get_value(self->settings, "soup-max-connections");
	guint32 max_connections = HAL_DEFAULT_SOUP_MAX_CONNECTIONS;
	g_variant_get(max_connections_v, "u", &max_connections);
	g_autoptr(GVariant) logger_level_v = g_settings_get_value(self->settings, "soup-logger-level");
	unsigned char logger_level		   = HAL_DEFAULT_SOUP_LOGGER_LEVEL;
	g_variant_get(logger_level_v, "y", &logger_level);

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

	g_autoptr(GString) user_agent = g_string_new(NULL);
	g_string_append_printf(user_agent, "Harvest Almanac (%s)", contact_email);

	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,
		SOUP_TYPE_CONTENT_SNIFFER, SOUP_SESSION_ADD_FEATURE, SOUP_SESSION_FEATURE(logger), NULL);

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

static void
hal_application_activate(GApplication *app)
{
	HalApplication *self		= HAL_APPLICATION(app);


@@ 42,6 67,30 @@ hal_application_activate(GApplication *app)
		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_settings_get_string(self->settings, "harvest-api-contact-email");

		g_autoptr(GError) err = NULL;

		// Synchronous since window hasn't been presented yet
		gchar *access_token = secret_password_lookup_sync(HAL_SECRET_SCHEMA, NULL, &err,
			"account-id", account_id, "contact-email", contact_email, NULL);
		if (err != NULL) {
			g_error("Failed to look up Harvest API access token: %s", err->message);
		} else if (access_token == NULL) {
			g_debug("Harvest API access token is <null>");
			hal_window_hide_content(priv->main_window);
		} else {
			g_debug("Constructing Harvest API access token");
			construct_client(self, access_token, account_id, contact_email);
			hal_window_show_content(priv->main_window);
			secret_password_free(access_token);
		}
	}

	g_object_set(gtk_settings_get_default(), "gtk-application-prefer-dark-theme",
		g_settings_get_boolean(self->settings, "prefer-dark-theme"), NULL);



@@ 98,27 147,18 @@ hal_application_time_entry_stop(G_GNUC_UNUSED GSimpleAction *action, GVariant *p
}

static void
hal_application_create_client(
	G_GNUC_UNUSED GSimpleAction *action, G_GNUC_UNUSED GVariant *param, gpointer data)
hal_application_reconstruct_client(
	G_GNUC_UNUSED GSimpleAction *action, GVariant *param, gpointer data)
{
	HalApplication *self = HAL_APPLICATION(data);

	g_autofree const char *harvest_api_access_token
		= g_settings_get_string(self->settings, "harvest-api-access-token");
	const char *harvest_api_access_token = g_variant_get_string(param, NULL);
	g_autofree const char *harvest_api_contact_email
		= g_settings_get_string(self->settings, "harvest-api-contact-email");
	const unsigned int harvest_account_id
		= g_settings_get_uint(self->settings, "harvest-account-id");
	const unsigned int logger_level = g_settings_get_uint(self->settings, "soup-logger-level");

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

	SoupSession *session = soup_session_new_with_options(SOUP_SESSION_MAX_CONNS,
		g_settings_get_uint(self->settings, "soup-max-conns"), SOUP_SESSION_USER_AGENT,
		"Harvest Almanac (tristan dot partin at gmail dot com)", SOUP_SESSION_ADD_FEATURE_BY_TYPE,
		SOUP_TYPE_CONTENT_SNIFFER, SOUP_SESSION_ADD_FEATURE, SOUP_SESSION_FEATURE(logger), NULL);
	g_autofree const char *harvest_account_id
		= g_settings_get_string(self->settings, "harvest-account-id");

	self->client = harvest_api_client_new(session, harvest_api_access_token, harvest_account_id);
	construct_client(self, harvest_api_access_token, harvest_account_id, harvest_api_contact_email);
}

static void


@@ 135,6 175,7 @@ hal_application_finalize(GObject *obj)
{
	HalApplication *self = HAL_APPLICATION(obj);

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

	G_OBJECT_CLASS(hal_application_parent_class)->finalize(obj);


@@ 172,8 213,9 @@ static const GActionEntry app_entries[] = {
		.parameter_type = "t"
	},
	{
		.name	  = "check-settings",
		.activate = hal_application_create_client
		.name	  		= "reconstruct-client",
		.activate 		= hal_application_reconstruct_client,
		.parameter_type = "s"
	}
};
// clang-format on

M harvest-almanac/hal-preferences-window.c => harvest-almanac/hal-preferences-window.c +108 -17
@@ 2,6 2,8 @@

#define G_LOG_DOMAIN "HalPreferencesWindow"

#include <string.h>

#include <glib-object.h>
#include <glib/gi18n.h>
#include <gtk/gtk.h>


@@ 10,9 12,6 @@

#include "hal-preferences-window.h"

#define HAL_DEFAULT_SOUP_MAX_CONNECTIONS 4
#define HAL_DEFAULT_SOUP_LOGGER_LEVEL 0

struct _HalPreferencesWindow
{
	HdyPreferencesWindow parent_instance;


@@ 28,11 27,13 @@ struct _HalPreferencesWindow
typedef struct HalPreferencesWindowPrivate
{
	GtkEntry *harvest_api_access_token_entry;
	GtkEntry *harvest_api_contact_email;
	GtkEntry *harvest_account_id_entry;
	GtkEntry *harvest_api_contact_email_entry;
	GtkSwitch *prefer_dark_theme_switch;
	GtkSpinButton *soup_max_connections_spin;
	GtkComboBoxText *soup_logger_level_combo;
	GtkButton *save_button;
	GtkButton *harvest_api_access_token_forget_button;
} HalPreferencesWindowPrivate;

G_DEFINE_TYPE_WITH_PRIVATE(


@@ 56,7 57,11 @@ hal_get_secret_schema(void)
	static const SecretSchema hal_schema = {
		.name  = "io.partin.tristan.HarvestAlmanac",
		.flags = SECRET_SCHEMA_NONE,
		.attributes = {}
		.attributes = {
			{ "contact-email", SECRET_SCHEMA_ATTRIBUTE_STRING },
			{ "account-id", SECRET_SCHEMA_ATTRIBUTE_STRING },
			{ "NULL", 0 },
		}
	};
	// clang-format on
#pragma GCC diagnostic pop


@@ 116,6 121,20 @@ on_harvest_api_contact_email_entry_changed(GtkEditable *widget, gpointer user_da
}

static void
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");

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

static void
on_soup_logger_level_combo_changed(GtkComboBox *widget, gpointer user_data)
{
	HalPreferencesWindow *self = HAL_PREFERENCES_WINDOW(user_data);


@@ 148,10 167,11 @@ on_soup_max_connections_spin_value_changed(GtkSpinButton *widget, gpointer user_
}

static void
on_harvest_api_access_token_stored(
on_harvest_api_access_token_store(
	G_GNUC_UNUSED GObject *source, GAsyncResult *result, gpointer user_data)
{
	HalPreferencesWindow *self = HAL_PREFERENCES_WINDOW(user_data);
	HalPreferencesWindow *self		  = HAL_PREFERENCES_WINDOW(user_data);
	HalPreferencesWindowPrivate *priv = hal_preferences_window_get_instance_private(self);

	g_autoptr(GError) err = NULL;



@@ 162,6 182,17 @@ on_harvest_api_access_token_stored(
		hal_preferences_window_set_dirty(self, TRUE);
	} else {
		hal_preferences_window_set_dirty(self, FALSE);
		gtk_widget_set_sensitive(GTK_WIDGET(priv->harvest_api_access_token_forget_button), TRUE);
		gtk_widget_set_sensitive(GTK_WIDGET(priv->harvest_api_access_token_entry), FALSE);
		gtk_widget_set_sensitive(GTK_WIDGET(priv->harvest_account_id_entry), FALSE);
		gtk_widget_set_sensitive(GTK_WIDGET(priv->harvest_api_contact_email_entry), FALSE);

		GApplication *app = g_application_get_default();
		GActionMap *map	  = G_ACTION_MAP(app);
		GAction *action	  = g_action_map_lookup_action(map, "reconstruct-client");
		GVariant *data
			= g_variant_new_string(gtk_entry_get_text(priv->harvest_api_access_token_entry));
		g_action_activate(action, data);
	}
}



@@ 178,21 209,50 @@ on_harvest_api_access_token_lookup(
	if (err != NULL) {
		g_log(G_LOG_DOMAIN, G_LOG_LEVEL_ERROR, "Failed to lookup Harvest API access token: %s",
			access_token);
	} else if (access_token == NULL) {
	} else if (access_token == NULL || strlen(access_token) == 0) {
		self->cached_access_token = NULL;
		gtk_widget_set_sensitive(GTK_WIDGET(priv->harvest_api_access_token_forget_button), FALSE);
		gtk_widget_set_sensitive(GTK_WIDGET(priv->harvest_api_access_token_entry), TRUE);
		gtk_widget_set_sensitive(GTK_WIDGET(priv->harvest_account_id_entry), TRUE);
		gtk_widget_set_sensitive(GTK_WIDGET(priv->harvest_api_contact_email_entry), TRUE);
	} else {
		self->cached_access_token = g_strdup(access_token);
		gtk_entry_set_text(priv->harvest_api_access_token_entry, access_token);
		gtk_widget_set_sensitive(GTK_WIDGET(priv->harvest_api_access_token_forget_button), TRUE);
		secret_password_free(access_token);
	}
}

static void
on_harvest_api_access_token_clear(
	G_GNUC_UNUSED GObject *source, GAsyncResult *result, gpointer user_data)
{
	HalPreferencesWindow *self		  = HAL_PREFERENCES_WINDOW(user_data);
	HalPreferencesWindowPrivate *priv = hal_preferences_window_get_instance_private(self);

	g_autoptr(GError) err = NULL;

	G_GNUC_UNUSED const gboolean cleared = secret_password_clear_finish(result, &err);
	if (err != NULL) {
		g_log(G_LOG_DOMAIN, G_LOG_LEVEL_ERROR, "Failed to clear Harvest API access token: %s",
			err->message);
	} else {
		self->cached_access_token = NULL;
		gtk_widget_set_sensitive(GTK_WIDGET(priv->harvest_api_access_token_forget_button), FALSE);
		gtk_widget_set_sensitive(GTK_WIDGET(priv->harvest_api_access_token_entry), TRUE);
		gtk_widget_set_sensitive(GTK_WIDGET(priv->harvest_account_id_entry), TRUE);
		gtk_widget_set_sensitive(GTK_WIDGET(priv->harvest_api_contact_email_entry), TRUE);
	}
}

static void
on_save_button_clicked(G_GNUC_UNUSED GtkButton *widget, gpointer user_data)
{
	HalPreferencesWindow *self		  = HAL_PREFERENCES_WINDOW(user_data);
	HalPreferencesWindowPrivate *priv = hal_preferences_window_get_instance_private(self);

	const char *account_id		= gtk_entry_get_text(priv->harvest_account_id_entry);
	const char *contact_email	= gtk_entry_get_text(priv->harvest_api_contact_email_entry);
	GVariant *soup_logger_level = g_variant_new(
		"y", gtk_combo_box_get_active(GTK_COMBO_BOX(priv->soup_logger_level_combo)));
	GVariant *soup_max_connections


@@ 200,15 260,37 @@ on_save_button_clicked(G_GNUC_UNUSED GtkButton *widget, gpointer user_data)

	// Update the cache
	self->cached_access_token = g_strdup(gtk_entry_get_text(priv->harvest_api_access_token_entry));
	secret_password_store(hal_get_secret_schema(), SECRET_COLLECTION_DEFAULT,
		"Harvest API Access Token", self->cached_access_token, self->cancellable,
		on_harvest_api_access_token_stored, self, NULL);
	g_settings_set_boolean(
		self->settings, "prefer-dark-theme", gtk_switch_get_active(priv->prefer_dark_theme_switch));
	g_settings_set_string(self->settings, "harvest-api-contact-email",
		gtk_entry_get_text(priv->harvest_api_contact_email));
	g_settings_set_string(self->settings, "harvest-account-id", account_id);
	g_settings_set_string(self->settings, "harvest-api-contact-email", contact_email);
	g_settings_set_value(self->settings, "soup-logger-level", soup_logger_level);
	g_settings_set_value(self->settings, "soup-max-connections", soup_max_connections);
	secret_password_store(HAL_SECRET_SCHEMA, SECRET_COLLECTION_DEFAULT, "Harvest API Access Token",
		self->cached_access_token, self->cancellable, on_harvest_api_access_token_store, self,
		"account-id", account_id, "contact-email", contact_email, NULL);
}

static void
on_harvest_api_access_token_forget_button_clicked(
	G_GNUC_UNUSED GtkButton *widget, gpointer user_data)
{
	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_settings_get_string(self->settings, "harvest-api-contact-email");

	secret_password_clear(HAL_SECRET_SCHEMA, self->cancellable, on_harvest_api_access_token_clear,
		self, "account-id", account_id, "contact-email", contact_email, NULL);

	g_settings_set_string(self->settings, "harvest-account-id", "");
	g_settings_set_string(self->settings, "harvest-api-contact-email", "");

	gtk_entry_set_text(priv->harvest_api_access_token_entry, "");
	gtk_entry_set_text(priv->harvest_account_id_entry, "");
	gtk_entry_set_text(priv->harvest_api_contact_email_entry, "");
}

static void


@@ 217,6 299,7 @@ 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_settings_get_string(self->settings, "harvest-api-contact-email");
	g_autoptr(GVariant) soup_max_connections_variant


@@ 228,11 311,12 @@ hal_preferences_window_constructed(GObject *obj)
	unsigned char soup_logger_level = HAL_DEFAULT_SOUP_LOGGER_LEVEL;
	g_variant_get(soup_logger_level_variant, "y", &soup_logger_level);

	secret_password_lookup(
		hal_get_secret_schema(), self->cancellable, on_harvest_api_access_token_lookup, self, NULL);
	secret_password_lookup(HAL_SECRET_SCHEMA, self->cancellable, on_harvest_api_access_token_lookup,
		self, "account-id", account_id, "contact-email", contact_email, NULL);
	gtk_switch_set_active(priv->prefer_dark_theme_switch,
		g_settings_get_boolean(self->settings, "prefer-dark-theme"));
	gtk_entry_set_text(priv->harvest_api_contact_email, contact_email);
	gtk_entry_set_text(priv->harvest_account_id_entry, account_id);
	gtk_entry_set_text(priv->harvest_api_contact_email_entry, contact_email);
	gtk_spin_button_set_value(priv->soup_max_connections_spin, (gdouble) soup_max_connections);
	gtk_combo_box_set_active(
		GTK_COMBO_BOX(priv->soup_logger_level_combo), (gint) soup_logger_level);


@@ 290,7 374,11 @@ hal_preferences_window_class_init(HalPreferencesWindowClass *klass)
	gtk_widget_class_bind_template_child_private(
		wid_class, HalPreferencesWindow, harvest_api_access_token_entry);
	gtk_widget_class_bind_template_child_private(
		wid_class, HalPreferencesWindow, harvest_api_contact_email);
		wid_class, HalPreferencesWindow, harvest_api_access_token_forget_button);
	gtk_widget_class_bind_template_child_private(
		wid_class, HalPreferencesWindow, harvest_account_id_entry);
	gtk_widget_class_bind_template_child_private(
		wid_class, HalPreferencesWindow, harvest_api_contact_email_entry);
	gtk_widget_class_bind_template_child_private(
		wid_class, HalPreferencesWindow, prefer_dark_theme_switch);
	gtk_widget_class_bind_template_child_private(


@@ 301,9 389,12 @@ hal_preferences_window_class_init(HalPreferencesWindowClass *klass)
	gtk_widget_class_bind_template_callback(wid_class, on_save_button_clicked);
	gtk_widget_class_bind_template_callback(wid_class, on_prefer_dark_theme_switch_notify_active);
	gtk_widget_class_bind_template_callback(wid_class, on_harvest_api_access_token_entry_changed);
	gtk_widget_class_bind_template_callback(wid_class, on_harvest_account_id_entry_changed);
	gtk_widget_class_bind_template_callback(wid_class, on_harvest_api_contact_email_entry_changed);
	gtk_widget_class_bind_template_callback(wid_class, on_soup_logger_level_combo_changed);
	gtk_widget_class_bind_template_callback(wid_class, on_soup_max_connections_spin_value_changed);
	gtk_widget_class_bind_template_callback(
		wid_class, on_harvest_api_access_token_forget_button_clicked);
}

static void

M harvest-almanac/hal-preferences-window.h => harvest-almanac/hal-preferences-window.h +7 -4
@@ 5,15 5,18 @@
#include <handy.h>
#include <libsecret/secret.h>

G_BEGIN_DECLS

#define HAL_SECRET_SCHEMA (hal_get_secret_schema())

#define HAL_DEFAULT_SOUP_MAX_CONNECTIONS 4
#define HAL_DEFAULT_SOUP_LOGGER_LEVEL 0

#define HAL_TYPE_PREFERENCES_WINDOW (hal_preferences_window_get_type())
G_DECLARE_FINAL_TYPE(
	HalPreferencesWindow, hal_preferences_window, HAL, PREFERENCES_WINDOW, HdyPreferencesWindow)

G_BEGIN_DECLS

const SecretSchema *hal_get_secret_schema(void) G_GNUC_CONST;

HalPreferencesWindow *hal_preferences_window_new(GSettings *settings) G_GNUC_WARN_UNUSED_RESULT;
const SecretSchema *hal_get_secret_schema(void) G_GNUC_CONST;

G_END_DECLS

M harvest-almanac/hal-window.c => harvest-almanac/hal-window.c +5 -39
@@ 34,11 34,9 @@ typedef struct HalWindowPrivate

G_DEFINE_TYPE_WITH_PRIVATE(HalWindow, hal_window, GTK_TYPE_APPLICATION_WINDOW)

static void
hal_window_show_content(
	G_GNUC_UNUSED GSimpleAction *action, G_GNUC_UNUSED GVariant *param, gpointer data)
void
hal_window_show_content(HalWindow *self)
{
	HalWindow *self		   = HAL_WINDOW(data);
	HalWindowPrivate *priv = hal_window_get_instance_private(self);

	/**


@@ 49,11 47,9 @@ hal_window_show_content(
	gtk_stack_set_visible_child_name(GTK_STACK(priv->profile), "profile");
}

static void
hal_window_hide_content(
	G_GNUC_UNUSED GSimpleAction *action, G_GNUC_UNUSED GVariant *param, gpointer data)
void
hal_window_hide_content(HalWindow *self)
{
	HalWindow *self		   = HAL_WINDOW(data);
	HalWindowPrivate *priv = hal_window_get_instance_private(self);

	gtk_stack_set_visible_child_name(priv->function_stack, "profile");


@@ 122,30 118,14 @@ hal_window_class_init(HalWindowClass *klass)
	gtk_widget_class_bind_template_child_private(wid_class, HalWindow, title_label);
}

// clang-format off
static const GActionEntry win_entries[] = {
	{
		.name 	  = "show-content",
		.activate = hal_window_show_content
	},
	{
		.name 	  = "hide-content",
		.activate = hal_window_hide_content
	}
};
// clang-format on

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

	g_action_map_add_action_entries(
		G_ACTION_MAP(self), win_entries, G_N_ELEMENTS(win_entries), self);

	gtk_widget_init_template(GTK_WIDGET(self));

	priv->profile	  = hal_profile_new();
	priv->profile	   = hal_profile_new();
	priv->time_tracker = hal_time_tracker_new();

	gtk_stack_add_titled(priv->function_stack, GTK_WIDGET(priv->profile), "profile", "Profile");


@@ 157,20 137,6 @@ hal_window_init(HalWindow *self)
		"icon-name", "document-open-recent-symbolic", NULL);

	g_autoptr(GSettings) settings = g_settings_new("io.partin.tristan.HarvestAlmanac");
	const char *harvest_api_access_token
		= g_settings_get_string(settings, "harvest-api-access-token");
	const char *harvest_api_contact_email
		= g_settings_get_string(settings, "harvest-api-contact-email");

	GActionMap *map = G_ACTION_MAP(self);
	if ((harvest_api_access_token != NULL && strlen(harvest_api_access_token) != 0)
		&& (harvest_api_contact_email != NULL || strlen(harvest_api_contact_email) != 0)) {
		GAction *show_content = g_action_map_lookup_action(map, "show-content");
		g_action_activate(show_content, NULL);
	} else {
		GAction *show_content = g_action_map_lookup_action(map, "hide-content");
		g_action_activate(show_content, NULL);
	}

	self->user_validated = FALSE;


M harvest-almanac/hal-window.h => harvest-almanac/hal-window.h +2 -0
@@ 9,5 9,7 @@ G_DECLARE_FINAL_TYPE(HalWindow, hal_window, HAL, WINDOW, GtkApplicationWindow)
G_BEGIN_DECLS

HalWindow *hal_window_new(GApplication *app) G_GNUC_WARN_UNUSED_RESULT;
void hal_window_hide_content(HalWindow *self);
void hal_window_show_content(HalWindow *self);

G_END_DECLS

M harvest-almanac/main.c => harvest-almanac/main.c +4 -4
@@ 19,16 19,16 @@ main(int argc, char *argv[])
	bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
	textdomain(GETTEXT_PACKAGE);

	g_autoptr(HalApplication) app = hal_application_new("io.partin.tristan.HarvestAlmanac");

	g_set_application_name(_("Harvest Almanac"));

	const gboolean success = hdy_init(&argc, &argv);
	if (!success) {
		g_log(G_LOG_DOMAIN, G_LOG_LEVEL_ERROR, "Unable to initialize libhandy");
		return 1;
	}

	g_autoptr(HalApplication) app = hal_application_new("io.partin.tristan.HarvestAlmanac");

	g_set_application_name(_("Harvest Almanac"));

	const int status = g_application_run(G_APPLICATION(app), argc, argv);

	return status;

M harvest-glib/harvest-api-client.c => harvest-glib/harvest-api-client.c +34 -32
@@ 1,4 1,5 @@
#include "config.h"

#define G_LOG_DOMAIN "HarvestApiClient"

#include <limits.h>


@@ 20,14 21,6 @@

static HarvestApiClient *instance;

typedef struct HarvestAsyncCallbackData
{
	GType body_type;
	HttpStatusCode expected_status;
	HarvestAsyncCallback callback;
	gpointer user_data;
} HarvestAsyncCallbackData;

struct _HarvestApiClient
{
	GObject parent_instance;


@@ 98,7 91,7 @@ harvest_api_client_set_property(GObject *obj, guint prop_id, const GValue *val, 
		break;
	case PROP_ACCOUNT_ID:
		g_free(self->account_id);
		self->account_id = g_strdup_printf("%u", g_value_get_uint(val));
		self->account_id = g_value_dup_string(val);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);


@@ 138,8 131,8 @@ harvest_api_client_class_init(HarvestApiClientClass *klass)
		_("Developer access token for the Harvest API."), NULL,
		G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
	obj_properties[PROP_ACCOUNT_ID]
		= g_param_spec_uint("account-id", _("Account ID"), _("Harvest account ID to use."), 1,
			UINT_MAX, 1, G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
		= g_param_spec_string("account-id", _("Account ID"), _("Harvest account ID to use."), NULL,
			G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties(obj_class, N_PROPS, obj_properties);
}


@@ 149,12 142,13 @@ harvest_api_client_init(G_GNUC_UNUSED HarvestApiClient *self)
{}

HarvestApiClient *
harvest_api_client_new(SoupSession *session, const char *access_token, unsigned int account_id)
harvest_api_client_new(SoupSession *session, const char *access_token, const char *account_id)
{
	g_return_val_if_fail(
		!SOUP_IS_SESSION(session) || access_token == NULL || account_id == 0, NULL);
		SOUP_IS_SESSION(session) && access_token != NULL && account_id != NULL, NULL);

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


@@ 162,31 156,47 @@ harvest_api_client_new(SoupSession *session, const char *access_token, unsigned 
	return instance;
}

HarvestApiClient *
harvest_api_client_get_instance(void)
{
	return instance;
}

static void
harvest_api_client_destroy_request(HarvestRequest *req, gpointer user_data)
{
	g_object_unref(req);
	g_object_unref(HARVEST_RESPONSE(user_data));
}

static void
harvest_api_client_async_callback(
	G_GNUC_UNUSED SoupSession *Session, SoupMessage *msg, gpointer user_data)
{
	GObject *body				   = NULL;
	GError *err					   = NULL;
	HarvestAsyncCallbackData *data = user_data;
	HarvestRequest *req				  = HARVEST_REQUEST(user_data);
	HarvestResponseMetadata *metadata = harvest_request_get_response_metadata(req);
	const GType body_type			  = harvest_response_metadata_get_body_type(metadata);

	GObject *body = NULL;
	GError *err	  = NULL;

	if (data->body_type != G_TYPE_NONE) {
	if (body_type != G_TYPE_NONE) {
		g_autoptr(SoupBuffer) buf = soup_message_body_flatten(msg->response_body);
		g_autoptr(GBytes) bytes	  = soup_buffer_get_as_bytes(buf);

		gsize size				= 0;
		gconstpointer body_data = g_bytes_get_data(bytes, &size);
		body					= json_gobject_from_data(data->body_type, body_data, size, &err);
		body					= json_gobject_from_data(body_type, body_data, size, &err);
	}

	HarvestResponse *response = harvest_response_new(body, msg->status_code, err);

	data->callback(response);
	g_signal_connect_after(
		req, "completed", G_CALLBACK(harvest_api_client_destroy_request), response);
	g_signal_emit_by_name(req, "completed", response);
}

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



@@ 196,6 206,7 @@ harvest_api_client_execute_request_async(
	gboolean response_has_body = harvest_request_get_data(req) != NULL;
	char *body				   = NULL;
	gsize len				   = 0;

	if (response_has_body) {
		body = json_gobject_to_data(harvest_request_get_data(req), &len);
		len	 = strlen(body);


@@ 211,8 222,6 @@ harvest_api_client_execute_request_async(
		break;
	case HTTP_METHOD_PATCH:
		break;
	case HTTP_METHOD_PUT:
		break;
	case HTTP_METHOD_DELETE:
		break;
	default:


@@ 222,12 231,5 @@ harvest_api_client_execute_request_async(
	soup_message_headers_append(msg->request_headers, "Authorization", self->access_token);
	soup_message_headers_append(msg->request_headers, "Harvest-Account-Id", self->account_id);

	HarvestResponseMetadata *meta  = harvest_request_get_response_metadata(req);
	HarvestAsyncCallbackData *data = g_malloc(sizeof(HarvestAsyncCallbackData));
	data->body_type				   = harvest_response_metadata_get_body_type(meta);
	data->expected_status		   = harvest_response_metadata_get_expected_status(meta);
	data->callback				   = callback;
	data->user_data				   = user_data;

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

M harvest-glib/harvest-api-client.h => harvest-glib/harvest-api-client.h +3 -6
@@ 15,12 15,9 @@ 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)

typedef gboolean (*HarvestAsyncCallback)(HarvestResponse *response);

HarvestApiClient *harvest_api_client_new(SoupSession *session, const char *access_token,
	unsigned int account_id) G_GNUC_WARN_UNUSED_RESULT;
HarvestApiClient *harvest_api_client_get_instance() G_GNUC_CONST;
void harvest_api_client_execute_request_async(
	HarvestApiClient *self, HarvestRequest *req, HarvestAsyncCallback callback, gpointer user_data);
	const char *account_id) G_GNUC_WARN_UNUSED_RESULT;
HarvestApiClient *harvest_api_client_get_instance(void) G_GNUC_CONST;
void harvest_api_client_execute_request_async(HarvestApiClient *self, HarvestRequest *req);

G_END_DECLS

M harvest-glib/harvest-late-request.c => harvest-glib/harvest-late-request.c +3 -1
@@ 1,4 1,5 @@
#include "config.h"

#define G_LOG_DOMAIN "HarvestLATERequest"

#include <limits.h>


@@ 75,7 76,7 @@ harvest_late_request_finalize(GObject *obj)
	G_OBJECT_CLASS(harvest_late_request_parent_class)->finalize(obj);
}

G_GNUC_CONST G_GNUC_WARN_UNUSED_RESULT static const char *
static const char *G_GNUC_CONST G_GNUC_WARN_UNUSED_RESULT
harvest_late_request_serialize_params(HarvestLATERequest *self)
{
	HarvestLATERequestPrivate *priv = harvest_late_request_get_instance_private(self);


@@ 136,6 137,7 @@ harvest_late_request_class_init(HarvestLATERequestClass *klass)
	obj_class->get_property = harvest_late_request_get_property;
	obj_class->set_property = harvest_late_request_set_property;

	// TODO: Finish implemeting this
	obj_properties[PROP_USER_ID]   = g_param_spec_int("user_id", _("User ID"), _(""), 0, INT_MAX, 0,
		  G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
	obj_properties[PROP_CLIENT_ID] = g_param_spec_int("client_id", _("Client ID"), _(""), 0,

M harvest-glib/harvest-request.c => harvest-glib/harvest-request.c +17 -3
@@ 4,10 4,12 @@

#include <glib-object.h>
#include <glib/gi18n-lib.h>
#include <json-glib/json-glib.h>

#include "harvest-http.h"
#include "harvest-request.h"
#include "harvest-response-metadata.h"
#include "harvest-response.h"

typedef struct _HarvestRequestPrivate
{


@@ 15,12 17,20 @@ typedef struct _HarvestRequestPrivate
	char *endpoint;
	char *query_params;
	GObject *data;
	HttpStatusCode expected_status;
	HarvestResponseMetadata *response_metadata;
} HarvestRequestPrivate;

G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE(HarvestRequest, harvest_request, G_TYPE_OBJECT)

enum HarvestRequestSignals
{
	SIGNAL_QUEUED,
	SIGNAL_COMPLETED,
	N_SIGNALS,
};

static guint obj_signals[N_SIGNALS];

enum HarvestRequestProps
{
	PROP_0,


@@ 28,7 38,6 @@ enum HarvestRequestProps
	PROP_ENDPOINT,
	PROP_QUERY_PARAMS,
	PROP_DATA,
	PROP_EXPECTED_STATUS,
	PROP_RESPONSE_METADATA,
	N_PROPS,
};


@@ 120,10 129,15 @@ harvest_request_class_init(HarvestRequestClass *klass)
	obj_class->get_property = harvest_request_get_property;
	obj_class->set_property = harvest_request_set_property;

	obj_signals[SIGNAL_QUEUED]	  = g_signal_new("queued", G_TYPE_FROM_CLASS(klass),
		   G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0);
	obj_signals[SIGNAL_COMPLETED] = g_signal_new("completed", G_TYPE_FROM_CLASS(klass),
		G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, HARVEST_TYPE_RESPONSE);

	obj_properties[PROP_HTTP_METHOD]  = g_param_spec_int("http-method", _("HTTP Method"),
		 _("The HTTP method by which to send the request."), HTTP_METHOD_GET, HTTP_METHOD_DELETE,
		 HTTP_METHOD_GET, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
	obj_properties[PROP_ENDPOINT]	 = g_param_spec_string("endpoint", _("Endpoint"),
	obj_properties[PROP_ENDPOINT]	  = g_param_spec_string("endpoint", _("Endpoint"),
		_("The server endpoint to send the request to."), NULL,
		G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
	obj_properties[PROP_QUERY_PARAMS] = g_param_spec_string("query-params", _("Query Params"),

M harvest-glib/harvest-request.h => harvest-glib/harvest-request.h +5 -0
@@ 8,6 8,7 @@

#include "harvest-http.h"
#include "harvest-response-metadata.h"
#include "harvest-response.h"

G_BEGIN_DECLS



@@ 19,6 20,10 @@ struct _HarvestRequestClass
	GObjectClass parent_class;
};

typedef void (*HarvestQueuedCallback)(HarvestRequest *request, gpointer user_data);
typedef void (*HarvestCompletedCallback)(
	HarvestRequest *request, HarvestResponse *response, gpointer user_data);

HttpMethod harvest_request_get_http_method(HarvestRequest *self) G_GNUC_CONST;
const char *harvest_request_get_endpoint(HarvestRequest *self) G_GNUC_CONST;
const char *harvest_request_get_query_params(

M harvest-glib/harvest-response-metadata.c => harvest-glib/harvest-response-metadata.c +1 -1
@@ 73,7 73,7 @@ harvest_response_metadata_class_init(HarvestResponseMetadataClass *klass)

	obj_properties[PROP_EXPECTED_STATUS] = g_param_spec_int("expected-status", _("Expected Status"),
		_("The expected status code the response should come back with."), HTTP_STATUS_OK,
		HTTP_STATUS_GATEWAY_TIMEOUT, HTTP_STATUS_OK,
		HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK,
		G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
	obj_properties[PROP_BODY_TYPE]		 = g_param_spec_uint64("body-type", _("Body Type"),
		  _("The GType of the response body."), G_TYPE_NONE, __UINT64_MAX__, G_TYPE_NONE,

M harvest-glib/harvest-response.c => harvest-glib/harvest-response.c +3 -2
@@ 93,8 93,9 @@ harvest_response_class_init(HarvestResponseClass *klass)
		= g_param_spec_boxed("error", _("Error"), _("Why the request errored out."), G_TYPE_ERROR,
			G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
	obj_properties[PROP_STATUS_CODE] = g_param_spec_int("status-code", _("Status Code"),
		_("Status code the response came back with"), HTTP_STATUS_OK, HTTP_STATUS_GATEWAY_TIMEOUT,
		HTTP_STATUS_OK, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
		_("Status code the response came back with"), HTTP_STATUS_OK,
		HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK,
		G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties(obj_class, N_PROPS, obj_properties);
}

M harvest-glib/harvest-user.c => harvest-glib/harvest-user.c +11 -5
@@ 11,6 11,7 @@

#include "harvest-api-client.h"
#include "harvest-user.h"
#include "harvest-users-me-request.h"

struct _HarvestUser
{


@@ 83,7 84,7 @@ harvest_user_json_deserialize_property(JsonSerializable *serializable, const gch

		return TRUE;
	} else if (g_strcmp0(prop_name, "roles") == 0) {
		JsonArray *arr	 = json_node_get_array(prop_node);
		JsonArray *arr	   = json_node_get_array(prop_node);
		const guint length = json_array_get_length(arr);
		GPtrArray *roles   = g_ptr_array_sized_new(length);
		for (guint i = 0; i < length; i++)


@@ 338,7 339,7 @@ harvest_user_class_init(HarvestUserClass *klass)
	obj_properties[PROP_IS_CONTRACTOR] = g_param_spec_boolean("is-contractor", _("Is Contractor"),
		_("Whether the user is a contractor or an employee."), FALSE,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
	obj_properties[PROP_IS_ADMIN]	  = g_param_spec_boolean("is-admin", _("Is Admin"),
	obj_properties[PROP_IS_ADMIN]	   = g_param_spec_boolean("is-admin", _("Is Admin"),
		 _("Whether the user has admin permissions."), FALSE,
		 G_PARAM_READABLE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
	obj_properties[PROP_IS_PROJECT_MANAGER] = g_param_spec_boolean("is-project-manager",


@@ 387,8 388,13 @@ static void
harvest_user_init(G_GNUC_UNUSED HarvestUser *self)
{}

HarvestUser *
harvest_user_get_me(G_GNUC_UNUSED HarvestApiClient *client)
void
harvest_user_get_me_async(HarvestCompletedCallback *callback, gpointer user_data)
{
	return NULL;
	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));
}

M harvest-glib/harvest-user.h => harvest-glib/harvest-user.h +4 -1
@@ 6,11 6,14 @@

#include <glib-object.h>

#include "harvest-api-client.h"
#include "harvest-request.h"

G_BEGIN_DECLS

#define HARVEST_TYPE_USER (harvest_user_get_type())
G_DECLARE_FINAL_TYPE(HarvestUser, harvest_user, HARVEST, USER, GObject)

HarvestUser *harvest_user_get_me();
void harvest_user_get_me_async(HarvestCompletedCallback *callback, gpointer user_data);

G_END_DECLS

A harvest-glib/harvest-users-me-request.c => harvest-glib/harvest-users-me-request.c +38 -0
@@ 0,0 1,38 @@
#include "config.h"

#define G_LOG_DOMAIN "HarvestUsersMeRequest"

#include <glib-object.h>
#include <glib/gi18n-lib.h>

#include "harvest-http.h"
#include "harvest-request.h"
#include "harvest-response-metadata.h"
#include "harvest-user.h"
#include "harvest-users-me-request.h"

struct _HarvestUsersMeRequest
{
	HarvestRequest parent_instance;
};

G_DEFINE_TYPE(HarvestUsersMeRequest, harvest_users_me_request, HARVEST_TYPE_REQUEST)

static void
harvest_users_me_request_class_init(G_GNUC_UNUSED HarvestUsersMeRequestClass *klass)
{}

static void
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_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);
}

A harvest-glib/harvest-users-me-request.h => harvest-glib/harvest-users-me-request.h +17 -0
@@ 0,0 1,17 @@
#pragma once

#if !defined(__HARVEST_HEADER_INTERNAL__) && !defined(__HARVEST_COMPILATION__)
#	error "Only <harvest-glib/harvest.h> can be included directly."
#endif

#include <glib-object.h>

G_BEGIN_DECLS

#define HARVEST_TYPE_USERS_ME_REQUEST (harvest_users_me_request_get_type())
G_DECLARE_FINAL_TYPE(
	HarvestUsersMeRequest, harvest_users_me_request, HARVEST, HarvestUsersMe, GObject)

HarvestUsersMeRequest *harvest_users_me_request_new() G_GNUC_WARN_UNUSED_RESULT;

G_END_DECLS

M harvest-glib/harvest.h => harvest-glib/harvest.h +1 -0
@@ 27,5 27,6 @@
#include "harvest-time-entry.h"
#include "harvest-user-assignment.h"
#include "harvest-user.h"
#include "harvest-users-me-request.h"

#undef __HARVEST_HEADER_INTERNAL__

M harvest-glib/meson.build => harvest-glib/meson.build +25 -23
@@ 34,35 34,37 @@ harvest_glib_sources = [
    'harvest-time-entry.c',
    'harvest-user.c',
    'harvest-user-assignment.c',
    'harvest-users-me-request.c'
]

harvest_glib_public_headers = [
    'harvest.h',
    'harvest-api-client.c',
    'harvest-client.c',
    'harvest-common.c',
    'harvest-creator.c',
    'harvest-api-client.h',
    'harvest-client.h',
    'harvest-common.h',
    'harvest-creator.h',
    'harvest-error.h',
    'harvest-estimate.c',
    'harvest-estimate-item-category.c',
    'harvest-estimate-line-item.c',
    'harvest-estimate.h',
    'harvest-estimate-item-category.h',
    'harvest-estimate-line-item.h',
    'harvest-http.h',
    'harvest-invoice.c',
    'harvest-invoice-item-category.c',
    'harvest-invoice-line-item.c',
    'harvest-late-request.c',
    'harvest-links.c',
    'harvest-pageable.c',
    'harvest-paged.c',
    'harvest-project.c',
    'harvest-request.c',
    'harvest-response.c',
    'harvest-response-metadata.c',
    'harvest-task.c',
    'harvest-task-assignment.c',
    'harvest-time-entry.c',
    'harvest-user.c',
    'harvest-user-assignment.c',
    'harvest-invoice.h',
    'harvest-invoice-item-category.h',
    'harvest-invoice-line-item.h',
    'harvest-late-request.h',
    'harvest-links.h',
    'harvest-pageable.h',
    'harvest-paged.h',
    'harvest-project.h',
    'harvest-request.h',
    'harvest-response.h',
    'harvest-response-metadata.h',
    'harvest-task.h',
    'harvest-task-assignment.h',
    'harvest-time-entry.h',
    'harvest-user.h',
    'harvest-user-assignment.h',
    'harvest-users-me-request.h',
]

install_headers(harvest_glib_public_headers, subdir: 'harvest-glib')