summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGeorges Basile Stavracas Neto <georges.stavracas@gmail.com>2021-08-16 15:27:46 -0300
committerGeorges Basile Stavracas Neto <georges.stavracas@gmail.com>2021-08-16 18:26:21 -0300
commite689f18b1e5847a74772d5243721f035386e3565 (patch)
treecdd136efeaca8a6f4200328d60e2e44a0618c37e
parenta080d8ec0f5b782d26fb849909311454d04a8ab8 (diff)
downloadgnome-calendar-e689f18b1e5847a74772d5243721f035386e3565.tar.gz
Add file importing dialog
Add a file importing dialog, and pass through files received from command line. Fixes https://gitlab.gnome.org/GNOME/gnome-calendar/-/issues/5
-rw-r--r--po/POTFILES.in4
-rw-r--r--src/gui/gcal-application.c54
-rw-r--r--src/gui/gcal-window.c17
-rw-r--r--src/gui/gcal-window.h4
-rw-r--r--src/gui/importer/gcal-import-dialog.c569
-rw-r--r--src/gui/importer/gcal-import-dialog.h36
-rw-r--r--src/gui/importer/gcal-import-dialog.ui197
-rw-r--r--src/gui/importer/gcal-import-file-row.c388
-rw-r--r--src/gui/importer/gcal-import-file-row.h37
-rw-r--r--src/gui/importer/gcal-import-file-row.ui43
-rw-r--r--src/gui/importer/gcal-importer.c173
-rw-r--r--src/gui/importer/gcal-importer.h40
-rw-r--r--src/gui/importer/importer.gresource.xml7
-rw-r--r--src/gui/importer/meson.build13
-rw-r--r--src/gui/meson.build1
15 files changed, 1582 insertions, 1 deletions
diff --git a/po/POTFILES.in b/po/POTFILES.in
index f242f27a..4fba264b 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -34,6 +34,10 @@ src/gui/gcal-weather-settings.ui
src/gui/gcal-window.c
src/gui/gcal-window.ui
src/gui/gtk/help-overlay.ui
+src/gui/importer/gcal-import-dialog.c
+src/gui/importer/gcal-import-dialog.ui
+src/gui/importer/gcal-importer.c
+src/gui/importer/gcal-import-file-row.c
src/gui/views/gcal-month-popover.ui
src/gui/views/gcal-week-grid.c
src/gui/views/gcal-week-header.c
diff --git a/src/gui/gcal-application.c b/src/gui/gcal-application.c
index 99356732..84e38b6d 100644
--- a/src/gui/gcal-application.c
+++ b/src/gui/gcal-application.c
@@ -457,11 +457,14 @@ gcal_application_command_line (GApplication *app,
GApplicationCommandLine *command_line)
{
g_autoptr (GVariant) option = NULL;
+ g_auto (GStrv) arguments = NULL;
GcalApplication *self;
GVariantDict *options;
const gchar* date = NULL;
const gchar* uuid = NULL;
gsize length;
+ gint n_arguments;
+ gint i;
GCAL_ENTRY;
@@ -510,6 +513,37 @@ gcal_application_command_line (GApplication *app,
g_application_activate (app);
+ arguments = g_application_command_line_get_arguments (command_line, &n_arguments);
+ if (n_arguments > 1)
+ {
+ g_autoptr (GHashTable) unique_files = NULL;
+ g_autoptr (GPtrArray) files = NULL;
+ gint n_files = 0;
+
+ files = g_ptr_array_new_full (n_arguments - 1, g_object_unref);
+ unique_files = g_hash_table_new (g_file_hash, (GEqualFunc) g_file_equal);
+
+ for (i = 1; i < n_arguments; i++)
+ {
+ g_autoptr (GFile) file = NULL;
+ const gchar *arg;
+
+ arg = arguments[i];
+ file = g_application_command_line_create_file_for_arg (command_line, arg);
+
+ if (g_str_has_prefix (arg, "-") || g_hash_table_contains (unique_files, file))
+ continue;
+
+ g_hash_table_add (unique_files, file);
+ g_ptr_array_add (files, g_steal_pointer (&file));
+
+ n_files++;
+ }
+
+ if (n_files > 0)
+ g_application_open (app, (GFile **) files->pdata, n_files, "");
+ }
+
GCAL_RETURN (0);
}
@@ -575,6 +609,23 @@ gcal_application_dbus_unregister (GApplication *application,
}
static void
+gcal_application_open (GApplication *application,
+ GFile **files,
+ gint n_files,
+ const gchar *hint)
+{
+ GcalApplication *self;
+
+ GCAL_ENTRY;
+
+ self = GCAL_APPLICATION (application);
+
+ gcal_window_import_files (GCAL_WINDOW (self->window), files, n_files);
+
+ GCAL_EXIT;
+}
+
+static void
gcal_application_class_init (GcalApplicationClass *klass)
{
GObjectClass *object_class;
@@ -588,6 +639,7 @@ gcal_application_class_init (GcalApplicationClass *klass)
application_class->activate = gcal_application_activate;
application_class->startup = gcal_application_startup;
application_class->command_line = gcal_application_command_line;
+ application_class->open = gcal_application_open;
application_class->handle_local_options = gcal_application_handle_local_options;
application_class->dbus_register = gcal_application_dbus_register;
application_class->dbus_unregister = gcal_application_dbus_unregister;
@@ -631,7 +683,7 @@ gcal_application_new (void)
return g_object_new (gcal_application_get_type (),
"resource-base-path", "/org/gnome/calendar",
"application-id", APPLICATION_ID,
- "flags", G_APPLICATION_HANDLES_COMMAND_LINE,
+ "flags", G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_HANDLES_OPEN,
NULL);
}
diff --git a/src/gui/gcal-window.c b/src/gui/gcal-window.c
index 65183985..0767db65 100644
--- a/src/gui/gcal-window.c
+++ b/src/gui/gcal-window.c
@@ -38,6 +38,8 @@
#include "gcal-window.h"
#include "gcal-year-view.h"
+#include "importer/gcal-import-dialog.h"
+
#include <glib/gi18n.h>
#include <libecal/libecal.h>
@@ -116,6 +118,7 @@ struct _GcalWindow
GtkWidget *views_switcher;
GcalEventEditorDialog *event_editor;
+ GtkWidget *import_dialog;
DzlSuggestionButton *search_button;
@@ -1205,3 +1208,17 @@ gcal_window_open_event_by_uuid (GcalWindow *self,
edit_dialog_data);
}
}
+
+void
+gcal_window_import_files (GcalWindow *self,
+ GFile **files,
+ gint n_files)
+{
+ g_return_if_fail (GCAL_IS_WINDOW (self));
+
+ g_clear_pointer (&self->import_dialog, gtk_widget_destroy);
+
+ self->import_dialog = gcal_import_dialog_new_for_files (self->context, files, n_files);
+ gtk_window_set_transient_for (GTK_WINDOW (self->import_dialog), GTK_WINDOW (self));
+ gtk_window_present (GTK_WINDOW (self->import_dialog));
+}
diff --git a/src/gui/gcal-window.h b/src/gui/gcal-window.h
index 4264e4b2..efde2434 100644
--- a/src/gui/gcal-window.h
+++ b/src/gui/gcal-window.h
@@ -41,6 +41,10 @@ void gcal_window_set_search_query (GcalWindow
void gcal_window_open_event_by_uuid (GcalWindow *self,
const gchar *uuid);
+void gcal_window_import_files (GcalWindow *self,
+ GFile **files,
+ gint n_files);
+
G_END_DECLS
#endif /* __GCAL_WINDOW_H__ */
diff --git a/src/gui/importer/gcal-import-dialog.c b/src/gui/importer/gcal-import-dialog.c
new file mode 100644
index 00000000..ef37e6bf
--- /dev/null
+++ b/src/gui/importer/gcal-import-dialog.c
@@ -0,0 +1,569 @@
+/* gcal-import-dialog.c
+ *
+ * Copyright 2021 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "GcalImportDialog"
+
+#include "gcal-import-dialog.h"
+
+#include "config.h"
+#include "gcal-debug.h"
+#include "gcal-import-file-row.h"
+#include "gcal-utils.h"
+
+#include <glib/gi18n.h>
+
+struct _GcalImportDialog
+{
+ HdyWindow parent;
+
+ GtkImage *calendar_color_image;
+ GtkLabel *calendar_name_label;
+ GtkListBox *calendars_listbox;
+ GtkPopover *calendars_popover;
+ GtkWidget *cancel_button;
+ GtkListBox *files_listbox;
+ HdyHeaderBar *headerbar;
+ GtkWidget *import_button;
+ GtkSizeGroup *title_sizegroup;
+
+ GtkWidget *selected_row;
+
+ GCancellable *cancellable;
+ GcalContext *context;
+ gint n_events;
+ gint n_files;
+};
+
+
+static void on_import_row_file_loaded_cb (GcalImportFileRow *row,
+ GPtrArray *events,
+ GcalImportDialog *self);
+
+static void on_manager_calendar_added_cb (GcalManager *manager,
+ GcalCalendar *calendar,
+ GcalImportDialog *self);
+
+static void on_manager_calendar_changed_cb (GcalManager *manager,
+ GcalCalendar *calendar,
+ GcalImportDialog *self);
+
+static void on_manager_calendar_removed_cb (GcalManager *manager,
+ GcalCalendar *calendar,
+ GcalImportDialog *self);
+
+G_DEFINE_TYPE (GcalImportDialog, gcal_import_dialog, HDY_TYPE_WINDOW)
+
+enum
+{
+ PROP_0,
+ PROP_CONTEXT,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+
+/*
+ * Auxiliary methods
+ */
+
+static GtkWidget*
+create_calendar_row (GcalManager *manager,
+ GcalCalendar *calendar)
+{
+ g_autofree gchar *parent_name = NULL;
+ cairo_surface_t *surface;
+ const GdkRGBA *color;
+ GtkWidget *icon;
+ GtkWidget *row;
+
+ color = gcal_calendar_get_color (calendar);
+ surface = get_circle_surface_from_color (color, 16);
+ get_source_parent_name_color (manager,
+ gcal_calendar_get_source (calendar),
+ &parent_name,
+ NULL);
+
+ /* The icon with the source color */
+ icon = gtk_image_new_from_surface (surface);
+ gtk_style_context_add_class (gtk_widget_get_style_context (icon), "calendar-color-image");
+ gtk_widget_show (icon);
+
+ /* The row itself */
+ row = g_object_new (HDY_TYPE_ACTION_ROW,
+ "title", gcal_calendar_get_name (calendar),
+ "subtitle", parent_name,
+ "sensitive", !gcal_calendar_is_read_only (calendar),
+ "activatable", TRUE,
+ "width-request", 300,
+ NULL);
+ hdy_action_row_add_prefix (HDY_ACTION_ROW (row), icon);
+ gtk_widget_show (row);
+
+ g_object_set_data_full (G_OBJECT (row), "calendar", g_object_ref (calendar), g_object_unref);
+ g_object_set_data (G_OBJECT (row), "color-icon", icon);
+
+ g_clear_pointer (&surface, cairo_surface_destroy);
+
+ return row;
+}
+
+static GtkWidget*
+get_row_for_calendar (GcalImportDialog *self,
+ GcalCalendar *calendar)
+{
+ g_autoptr (GList) children = NULL;
+ GtkWidget *row;
+ GList *l;
+
+ row = NULL;
+ children = gtk_container_get_children (GTK_CONTAINER (self->calendars_listbox));
+
+ for (l = children; l != NULL; l = g_list_next (l))
+ {
+ GcalCalendar *row_calendar = g_object_get_data (l->data, "calendar");
+
+ if (row_calendar == calendar)
+ {
+ row = l->data;
+ break;
+ }
+ }
+
+ return row;
+}
+
+static void
+select_row (GcalImportDialog *self,
+ GtkListBoxRow *row)
+{
+ cairo_surface_t *surface;
+ const GdkRGBA *color;
+ GcalCalendar *calendar;
+
+ self->selected_row = GTK_WIDGET (row);
+
+ /* Setup the event page's source name and color */
+ calendar = g_object_get_data (G_OBJECT (row), "calendar");
+
+ gtk_label_set_label (self->calendar_name_label, gcal_calendar_get_name (calendar));
+
+ color = gcal_calendar_get_color (calendar);
+ surface = get_circle_surface_from_color (color, 16);
+ gtk_image_set_from_surface (self->calendar_color_image, surface);
+
+ g_clear_pointer (&surface, cairo_surface_destroy);
+}
+
+static void
+update_default_calendar_row (GcalImportDialog *self)
+{
+ GcalCalendar *default_calendar;
+ GcalManager *manager;
+ GtkWidget *row;
+
+ manager = gcal_context_get_manager (self->context);
+ default_calendar = gcal_manager_get_default_calendar (manager);
+
+ row = get_row_for_calendar (self, default_calendar);
+ if (row != NULL)
+ select_row (self, GTK_LIST_BOX_ROW (row));
+}
+
+static void
+setup_calendars (GcalImportDialog *self)
+{
+ g_autoptr (GList) calendars = NULL;
+ GcalManager *manager;
+ GList *l;
+
+ manager = gcal_context_get_manager (self->context);
+ calendars = gcal_manager_get_calendars (manager);
+
+ for (l = calendars; l; l = l->next)
+ on_manager_calendar_added_cb (manager, l->data, self);
+
+ update_default_calendar_row (self);
+
+ g_signal_connect_object (manager, "calendar-added", G_CALLBACK (on_manager_calendar_added_cb), self, 0);
+ g_signal_connect_object (manager, "calendar-changed", G_CALLBACK (on_manager_calendar_changed_cb), self, 0);
+ g_signal_connect_object (manager, "calendar-removed", G_CALLBACK (on_manager_calendar_removed_cb), self, 0);
+ g_signal_connect_object (manager, "notify::default-calendar", G_CALLBACK (update_default_calendar_row), self, G_CONNECT_SWAPPED);
+}
+
+static void
+setup_files (GcalImportDialog *self,
+ GFile **files,
+ gint n_files)
+{
+ gint i;
+
+ GCAL_ENTRY;
+
+ self->n_files = n_files;
+
+ for (i = 0; i < n_files; i++)
+ {
+ GtkWidget *row;
+
+ row = gcal_import_file_row_new (files[i], self->title_sizegroup);
+ g_signal_connect (row, "file-loaded", G_CALLBACK (on_import_row_file_loaded_cb), self);
+
+ if (n_files > 1)
+ gcal_import_file_row_show_filename (GCAL_IMPORT_FILE_ROW (row));
+
+ gtk_list_box_insert (self->files_listbox, row, -1);
+ }
+
+ GCAL_EXIT;
+}
+
+
+/*
+ * Callbacks
+ */
+
+static void
+on_events_created_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr (GError) error = NULL;
+ GcalImportDialog *self;
+
+ GCAL_ENTRY;
+
+ self = GCAL_IMPORT_DIALOG (user_data);
+
+ e_cal_client_create_objects_finish (E_CAL_CLIENT (source_object), result, NULL, &error);
+ if (error)
+ g_warning ("Error creating events: %s", error->message);
+
+ gtk_widget_destroy (GTK_WIDGET (self));
+
+ GCAL_EXIT;
+}
+
+static void
+on_calendars_listbox_row_activated_cb (GtkListBox *listbox,
+ GtkListBoxRow *row,
+ GcalImportDialog *self)
+{
+ GCAL_ENTRY;
+
+ select_row (self, row);
+ gtk_popover_popdown (self->calendars_popover);
+
+ GCAL_EXIT;
+}
+
+static void
+on_cancel_button_clicked_cb (GtkButton *button,
+ GcalImportDialog *self)
+{
+ gtk_widget_destroy (GTK_WIDGET (self));
+}
+
+static void
+on_import_button_clicked_cb (GtkButton *button,
+ GcalImportDialog *self)
+{
+ g_autoptr (GList) children = NULL;
+ GcalCalendar *calendar;
+ ECalClient *client;
+ GSList *slist;
+ GList *l;
+
+ GCAL_ENTRY;
+
+ calendar = g_object_get_data (G_OBJECT (self->selected_row), "calendar");
+ g_assert (self->selected_row != NULL);
+
+ slist = NULL;
+ children = gtk_container_get_children (GTK_CONTAINER (self->files_listbox));
+ for (l = children; l; l = l->next)
+ {
+ GcalImportFileRow *row = l->data;
+ GPtrArray *ical_components;
+ guint i;
+
+ ical_components = gcal_import_file_row_get_ical_components (row);
+ if (!ical_components)
+ continue;
+
+ for (i = 0; i < ical_components->len; i++)
+ slist = g_slist_prepend (slist, g_ptr_array_index (ical_components, i));
+ }
+
+ if (!slist)
+ GCAL_RETURN ();
+
+ self->cancellable = g_cancellable_new ();
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE);
+
+ client = gcal_calendar_get_client (calendar);
+ e_cal_client_create_objects (client,
+ slist,
+ E_CAL_OPERATION_FLAG_NONE,
+ self->cancellable,
+ on_events_created_cb,
+ self);
+
+ GCAL_EXIT;
+}
+
+static void
+on_import_row_file_loaded_cb (GcalImportFileRow *row,
+ GPtrArray *events,
+ GcalImportDialog *self)
+{
+ g_autofree gchar *title = NULL;
+
+ GCAL_ENTRY;
+
+ self->n_events += events ? events->len : 0;
+
+ title = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE,
+ "Import %d event",
+ "Import %d events",
+ self->n_events),
+ self->n_events);
+ hdy_header_bar_set_title (self->headerbar, title);
+
+ gtk_widget_show (GTK_WIDGET (row));
+
+ GCAL_EXIT;
+}
+
+static void
+on_manager_calendar_added_cb (GcalManager *manager,
+ GcalCalendar *calendar,
+ GcalImportDialog *self)
+{
+ if (gcal_calendar_is_read_only (calendar))
+ return;
+
+ gtk_container_add (GTK_CONTAINER (self->calendars_listbox),
+ create_calendar_row (manager, calendar));
+}
+
+static void
+on_manager_calendar_changed_cb (GcalManager *manager,
+ GcalCalendar *calendar,
+ GcalImportDialog *self)
+{
+ cairo_surface_t *surface;
+ const GdkRGBA *color;
+ GtkWidget *row, *color_icon;
+ gboolean read_only;
+
+ read_only = gcal_calendar_is_read_only (calendar);
+ row = get_row_for_calendar (self, calendar);
+
+ /* If the calendar changed from/to read-only, we add or remove it here */
+ if (read_only)
+ {
+ if (row)
+ gtk_container_remove (GTK_CONTAINER (self->calendars_listbox), row);
+ return;
+ }
+ else if (!row)
+ {
+ on_manager_calendar_added_cb (manager, calendar, self);
+ row = get_row_for_calendar (self, calendar);
+ }
+
+ hdy_preferences_row_set_title (HDY_PREFERENCES_ROW (row), gcal_calendar_get_name (calendar));
+ gtk_widget_set_sensitive (row, !read_only);
+
+ /* Setup the source color, in case it changed */
+ color = gcal_calendar_get_color (calendar);
+ surface = get_circle_surface_from_color (color, 16);
+ color_icon = g_object_get_data (G_OBJECT (row), "color-icon");
+ gtk_image_set_from_surface (GTK_IMAGE (color_icon), surface);
+
+ gtk_list_box_invalidate_sort (GTK_LIST_BOX (self->calendars_listbox));
+
+ g_clear_pointer (&surface, cairo_surface_destroy);
+}
+
+static void
+on_manager_calendar_removed_cb (GcalManager *manager,
+ GcalCalendar *calendar,
+ GcalImportDialog *self)
+{
+ GtkWidget *row;
+
+ row = get_row_for_calendar (self, calendar);
+
+ if (!row)
+ return;
+
+ gtk_container_remove (GTK_CONTAINER (self->calendars_listbox), row);
+}
+
+static void
+on_select_calendar_row_activated_cb (GtkListBox *listbox,
+ GtkListBoxRow *row,
+ GcalImportDialog *self)
+{
+ gtk_popover_popup (self->calendars_popover);
+}
+
+static gint
+sort_func (GtkListBoxRow *row1,
+ GtkListBoxRow *row2,
+ gpointer user_data)
+{
+ GcalCalendar *calendar1, *calendar2;
+ g_autofree gchar *name1 = NULL;
+ g_autofree gchar *name2 = NULL;
+
+ calendar1 = g_object_get_data (G_OBJECT (row1), "calendar");
+ calendar2 = g_object_get_data (G_OBJECT (row2), "calendar");
+
+ name1 = g_utf8_casefold (gcal_calendar_get_name (calendar1), -1);
+ name2 = g_utf8_casefold (gcal_calendar_get_name (calendar2), -1);
+
+ return g_strcmp0 (name1, name2);
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gcal_import_dialog_finalize (GObject *object)
+{
+ GcalImportDialog *self = (GcalImportDialog *)object;
+
+ g_cancellable_cancel (self->cancellable);
+ g_clear_object (&self->cancellable);
+ g_clear_object (&self->context);
+
+ G_OBJECT_CLASS (gcal_import_dialog_parent_class)->finalize (object);
+}
+
+static void
+gcal_import_dialog_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GcalImportDialog *self = GCAL_IMPORT_DIALOG (object);
+
+ switch (prop_id)
+ {
+ case PROP_CONTEXT:
+ g_value_set_object (value, self->context);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gcal_import_dialog_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GcalImportDialog *self = GCAL_IMPORT_DIALOG (object);
+
+ switch (prop_id)
+ {
+ case PROP_CONTEXT:
+ g_assert (self->context == NULL);
+ self->context = g_value_dup_object (value);
+ setup_calendars (self);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gcal_import_dialog_class_init (GcalImportDialogClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = gcal_import_dialog_finalize;
+ object_class->get_property = gcal_import_dialog_get_property;
+ object_class->set_property = gcal_import_dialog_set_property;
+
+ /**
+ * GcalEventPopover::context:
+ *
+ * The context of the import dialog.
+ */
+ properties[PROP_CONTEXT] = g_param_spec_object ("context",
+ "Context",
+ "Context",
+ GCAL_TYPE_CONTEXT,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/ui/gui/importer/gcal-import-dialog.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GcalImportDialog, calendar_color_image);
+ gtk_widget_class_bind_template_child (widget_class, GcalImportDialog, calendar_name_label);
+ gtk_widget_class_bind_template_child (widget_class, GcalImportDialog, calendars_listbox);
+ gtk_widget_class_bind_template_child (widget_class, GcalImportDialog, calendars_popover);
+ gtk_widget_class_bind_template_child (widget_class, GcalImportDialog, cancel_button);
+ gtk_widget_class_bind_template_child (widget_class, GcalImportDialog, files_listbox);
+ gtk_widget_class_bind_template_child (widget_class, GcalImportDialog, headerbar);
+ gtk_widget_class_bind_template_child (widget_class, GcalImportDialog, import_button);
+ gtk_widget_class_bind_template_child (widget_class, GcalImportDialog, title_sizegroup);
+
+ gtk_widget_class_bind_template_callback (widget_class, on_calendars_listbox_row_activated_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_cancel_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_import_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_select_calendar_row_activated_cb);
+}
+
+static void
+gcal_import_dialog_init (GcalImportDialog *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (self->calendars_listbox), sort_func, NULL, NULL);
+}
+
+GtkWidget*
+gcal_import_dialog_new_for_files (GcalContext *context,
+ GFile **files,
+ gint n_files)
+{
+ GcalImportDialog *self;
+
+ self = g_object_new (GCAL_TYPE_IMPORT_DIALOG,
+ "context", context,
+ NULL);
+
+ setup_files (self, files, n_files);
+
+ return GTK_WIDGET (self);
+}
diff --git a/src/gui/importer/gcal-import-dialog.h b/src/gui/importer/gcal-import-dialog.h
new file mode 100644
index 00000000..89d70c5a
--- /dev/null
+++ b/src/gui/importer/gcal-import-dialog.h
@@ -0,0 +1,36 @@
+/* gcal-import-dialog.h
+ *
+ * Copyright 2021 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "gcal-context.h"
+
+#include <handy.h>
+
+G_BEGIN_DECLS
+
+#define GCAL_TYPE_IMPORT_DIALOG (gcal_import_dialog_get_type())
+G_DECLARE_FINAL_TYPE (GcalImportDialog, gcal_import_dialog, GCAL, IMPORT_DIALOG, HdyWindow)
+
+GtkWidget* gcal_import_dialog_new_for_files (GcalContext *context,
+ GFile **files,
+ gint n_files);
+
+G_END_DECLS
diff --git a/src/gui/importer/gcal-import-dialog.ui b/src/gui/importer/gcal-import-dialog.ui
new file mode 100644
index 00000000..38e68ea6
--- /dev/null
+++ b/src/gui/importer/gcal-import-dialog.ui
@@ -0,0 +1,197 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GcalImportDialog" parent="HdyWindow">
+ <property name="width_request">500</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">0</property>
+ <property name="default_width">550</property>
+ <property name="default_height">500</property>
+ <property name="resizable">False</property>
+ <property name="type_hint">dialog</property>
+ <property name="modal">True</property>
+ <property name="destroy_with_parent">True</property>
+ <child>
+ <object class="HdyDeck">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+
+ <child>
+ <object class="HdyHeaderBar" id="headerbar">
+ <property name="visible">True</property>
+ <property name="title" translatable="yes">Import Files…</property>
+ <property name="show_close_button">False</property>
+
+ <!-- Cancel button -->
+ <child>
+ <object class="GtkButton" id="cancel_button">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">_Cancel</property>
+ <property name="use-underline">True</property>
+ <signal name="clicked" handler="on_cancel_button_clicked_cb" object="GcalImportDialog" swapped="no" />
+ </object>
+ </child>
+
+ <!-- Import button -->
+ <child>
+ <object class="GtkButton" id="import_button">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">_Import</property>
+ <property name="use-underline">True</property>
+ <signal name="clicked" handler="on_import_button_clicked_cb" object="GcalImportDialog" swapped="no" />
+ <style>
+ <class name="suggested-action" />
+ </style>
+ </object>
+ <packing>
+ <property name="pack-type">end</property>
+ </packing>
+ </child>
+
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="hscrollbar-policy">never</property>
+ <property name="propagate-natural-height">True</property>
+ <property name="min-content-height">400</property>
+ <property name="max-content-height">700</property>
+
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="spacing">12</property>
+ <property name="margin-top">24</property>
+ <property name="margin-bottom">24</property>
+ <property name="margin-start">36</property>
+ <property name="margin-end">36</property>
+ <property name="orientation">vertical</property>
+
+ <!-- Calendar row -->
+ <child>
+ <object class="GtkListBox">
+ <property name="visible">True</property>
+ <property name="hexpand">True</property>
+ <property name="selection-mode">none</property>
+ <signal name="row-activated" handler="on_select_calendar_row_activated_cb" object="GcalImportDialog" swapped="no" />
+ <style>
+ <class name="content" />
+ </style>
+
+ <child>
+ <object class="HdyActionRow" id="calendar_row">
+ <property name="visible">True</property>
+ <property name="activatable">True</property>
+ <property name="title" translatable="yes">C_alendar</property>
+ <property name="use-underline">True</property>
+
+ <child>
+ <object class="GtkBox" id="calendar_row_widgets_box">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-start">12</property>
+ <property name="spacing">12</property>
+
+ <!-- Color -->
+ <child>
+ <object class="GtkImage" id="calendar_color_image">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ </child>
+
+ <!-- Calendar name -->
+ <child>
+ <object class="GtkLabel" id="calendar_name_label">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="can-focus">False</property>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="pixel-size">16</property>
+ <property name="icon-name">pan-down-symbolic</property>
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <!-- Files overview -->
+ <child>
+ <object class="GtkListBox" id="files_listbox">
+ <property name="visible">True</property>
+ <property name="hexpand">True</property>
+ <property name="selection-mode">none</property>
+
+ <child type="placeholder">
+ <object class="GtkSpinner">
+ <property name="visible">True</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="active">True</property>
+ </object>
+ </child>
+
+ <style>
+ <class name="background" />
+ </style>
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+ </object>
+ </child>
+ </object>
+ </child>
+
+ </template>
+
+ <!-- Calendars popover -->
+ <object class="GtkPopover" id="calendars_popover">
+ <property name="position">bottom</property>
+ <property name="relative-to">calendar_row_widgets_box</property>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="visible">True</property>
+ <property name="hscrollbar-policy">never</property>
+ <property name="max-content-height">350</property>
+ <property name="propagate-natural-width">True</property>
+ <property name="propagate-natural-height">True</property>
+ <child>
+ <object class="GtkListBox" id="calendars_listbox">
+ <property name="visible">True</property>
+ <property name="hexpand">True</property>
+ <property name="selection-mode">none</property>
+ <signal name="row-activated" handler="on_calendars_listbox_row_activated_cb" object="GcalImportDialog" swapped="no" />
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+
+ <object class="GtkSizeGroup" id="title_sizegroup">
+ <property name="mode">horizontal</property>
+ </object>
+
+</interface>
diff --git a/src/gui/importer/gcal-import-file-row.c b/src/gui/importer/gcal-import-file-row.c
new file mode 100644
index 00000000..5970aac3
--- /dev/null
+++ b/src/gui/importer/gcal-import-file-row.c
@@ -0,0 +1,388 @@
+/* gcal-import-file-row.c
+ *
+ * Copyright 2021 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "GcalImportFileRow"
+
+#include "config.h"
+#include "gcal-import-file-row.h"
+#include "gcal-importer.h"
+#include "gcal-utils.h"
+
+#include <glib/gi18n.h>
+
+struct _GcalImportFileRow
+{
+ GtkListBoxRow parent;
+
+ GtkListBox *events_listbox;
+ GtkLabel *filename_label;
+ GtkSizeGroup *title_sizegroup;
+
+ GCancellable *cancellable;
+ GFile *file;
+ GPtrArray *ical_components;
+};
+
+static void read_calendar_finished_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data);
+
+G_DEFINE_TYPE (GcalImportFileRow, gcal_import_file_row, GTK_TYPE_LIST_BOX_ROW)
+
+enum
+{
+ PROP_0,
+ PROP_FILE,
+ N_PROPS,
+};
+
+enum
+{
+ FILE_LOADED,
+ N_SIGNALS,
+};
+
+static guint signals[N_SIGNALS] = { 0, };
+static GParamSpec *properties[N_PROPS] = { NULL, };
+
+
+/*
+ * Auxiliary methods
+ */
+
+static void
+add_grid_row (GcalImportFileRow *self,
+ GtkGrid *grid,
+ gint row,
+ const gchar *title,
+ const gchar *value)
+{
+ GtkWidget *title_label;
+ GtkWidget *value_label;
+
+ if (!value || g_utf8_strlen (value, -1) == 0)
+ return;
+
+ title_label = g_object_new (GTK_TYPE_LABEL,
+ "visible", TRUE,
+ "label", title,
+ "xalign", 1.0,
+ "yalign", 0.0,
+ "ellipsize", PANGO_ELLIPSIZE_END,
+ "max-width-chars", 40,
+ NULL);
+ gtk_style_context_add_class (gtk_widget_get_style_context (title_label), "dim-label");
+ gtk_grid_attach (grid, title_label, 0, row, 1, 1);
+
+ gtk_size_group_add_widget (self->title_sizegroup, title_label);
+
+ value_label = g_object_new (GTK_TYPE_LABEL,
+ "visible", TRUE,
+ "label", value,
+ "xalign", 0.0,
+ "selectable", TRUE,
+ "ellipsize", PANGO_ELLIPSIZE_END,
+ "max-width-chars", 40,
+ NULL);
+ gtk_grid_attach (grid, value_label, 1, row, 1, 1);
+}
+
+static void
+fill_grid_with_event_data (GcalImportFileRow *self,
+ GtkGrid *grid,
+ ICalComponent *ical_component)
+{
+ g_autofree gchar *start_string = NULL;
+ g_autofree gchar *description = NULL;
+ g_autofree gchar *end_string = NULL;
+ g_autoptr (GDateTime) start = NULL;
+ g_autoptr (GDateTime) end = NULL;
+ ICalTime *ical_start;
+ ICalTime *ical_end;
+ gint row = 0;
+
+ ical_start = i_cal_component_get_dtstart (ical_component);
+ start = gcal_date_time_from_icaltime (ical_start);
+ if (i_cal_time_is_date (ical_start))
+ start_string = g_date_time_format (start, "%x");
+ else
+ start_string = g_date_time_format (start, "%x %X");
+
+ ical_end = i_cal_component_get_dtend (ical_component);
+ if (ical_end)
+ {
+ end = gcal_date_time_from_icaltime (ical_end);
+ if (i_cal_time_is_date (ical_end))
+ end_string = g_date_time_format (end, "%x");
+ else
+ end_string = g_date_time_format (end, "%x %X");
+ }
+ else
+ {
+ end = g_date_time_add_days (start, 1);
+ if (i_cal_time_is_date (ical_start))
+ end_string = g_date_time_format (end, "%x");
+ else
+ end_string = g_date_time_format (end, "%x %X");
+ }
+
+ gcal_utils_extract_google_section (i_cal_component_get_description (ical_component),
+ &description,
+ NULL);
+
+ add_grid_row (self, grid, row++, _("Title"), i_cal_component_get_summary (ical_component));
+ add_grid_row (self, grid, row++, _("Location"), i_cal_component_get_location (ical_component));
+ add_grid_row (self, grid, row++, _("Starts"), start_string);
+ add_grid_row (self, grid, row++, _("Ends"), end_string);
+ add_grid_row (self, grid, row++, _("Description"), description);
+
+ g_clear_object (&ical_start);
+ g_clear_object (&ical_end);
+}
+
+static void
+add_events_to_listbox (GcalImportFileRow *self,
+ GPtrArray *events)
+{
+ guint i;
+
+ for (i = 0; i < events->len; i++)
+ {
+ ICalComponent *ical_component;
+ GtkWidget *grid;
+ GtkWidget *row;
+
+ ical_component = g_ptr_array_index (events, i);
+
+ row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
+ "visible", TRUE,
+ "activatable", FALSE,
+ NULL);
+
+ grid = g_object_new (GTK_TYPE_GRID,
+ "visible", TRUE,
+ "row-spacing", 6,
+ "column-spacing", 12,
+ "margin-top", 18,
+ "margin-bottom", 18,
+ "margin-start", 24,
+ "margin-end", 24,
+ NULL);
+ fill_grid_with_event_data (self, GTK_GRID (grid), ical_component);
+ gtk_container_add (GTK_CONTAINER (row), grid);
+
+ gtk_list_box_insert (self->events_listbox, row, -1);
+ }
+}
+
+static GPtrArray*
+filter_event_components (ICalComponent *component)
+{
+ g_autoptr (GPtrArray) event_components = NULL;
+ ICalComponent *aux;
+
+ if (!component)
+ return NULL;
+
+ event_components = g_ptr_array_new_full (20, g_object_unref);
+ aux = i_cal_component_get_first_real_component (component);
+ while (aux)
+ {
+ g_ptr_array_add (event_components, g_object_ref (aux));
+ aux = i_cal_component_get_next_component (component, I_CAL_VEVENT_COMPONENT);
+ }
+
+ return g_steal_pointer (&event_components);
+}
+
+static void
+setup_file (GcalImportFileRow *self)
+{
+ g_autofree gchar *basename = NULL;
+
+ basename = g_file_get_basename (self->file);
+ gtk_label_set_label (self->filename_label, basename);
+
+ gcal_importer_import_file (self->file,
+ self->cancellable,
+ read_calendar_finished_cb,
+ self);
+}
+
+
+/*
+ * Callbacks
+ */
+
+static void
+read_calendar_finished_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ g_autoptr (GPtrArray) event_components = NULL;
+ g_autoptr (GError) error = NULL;
+ g_autofree gchar *subtitle = NULL;
+ ICalComponent *component;
+ GcalImportFileRow *self;
+
+ self = GCAL_IMPORT_FILE_ROW (user_data);
+ component = gcal_importer_import_file_finish (res, &error);
+ event_components = filter_event_components (component);
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self), !error && event_components && event_components->len > 0);
+
+ if (error || !event_components || event_components->len == 0)
+ return;
+
+ add_events_to_listbox (self, event_components);
+
+ self->ical_components = g_ptr_array_ref (event_components);
+
+ g_signal_emit (self, signals[FILE_LOADED], 0, event_components);
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gcal_import_file_row_finalize (GObject *object)
+{
+ GcalImportFileRow *self = (GcalImportFileRow *)object;
+
+ g_cancellable_cancel (self->cancellable);
+ g_clear_object (&self->cancellable);
+ g_clear_object (&self->file);
+ g_clear_pointer (&self->ical_components, g_ptr_array_unref);
+
+ G_OBJECT_CLASS (gcal_import_file_row_parent_class)->finalize (object);
+}
+
+static void
+gcal_import_file_row_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GcalImportFileRow *self = GCAL_IMPORT_FILE_ROW (object);
+
+ switch (prop_id)
+ {
+ case PROP_FILE:
+ g_value_set_object (value, self->file);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gcal_import_file_row_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GcalImportFileRow *self = GCAL_IMPORT_FILE_ROW (object);
+
+ switch (prop_id)
+ {
+ case PROP_FILE:
+ g_assert (self->file == NULL);
+ self->file = g_value_dup_object (value);
+ setup_file (self);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gcal_import_file_row_class_init (GcalImportFileRowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = gcal_import_file_row_finalize;
+ object_class->get_property = gcal_import_file_row_get_property;
+ object_class->set_property = gcal_import_file_row_set_property;
+
+ signals[FILE_LOADED] = g_signal_new ("file-loaded",
+ GCAL_TYPE_IMPORT_FILE_ROW,
+ G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL,
+ g_cclosure_marshal_VOID__BOXED,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_PTR_ARRAY);
+
+ properties[PROP_FILE] = g_param_spec_object ("file",
+ "An ICS file",
+ "An ICS file",
+ G_TYPE_FILE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/ui/gui/importer/gcal-import-file-row.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GcalImportFileRow, events_listbox);
+ gtk_widget_class_bind_template_child (widget_class, GcalImportFileRow, filename_label);
+}
+
+static void
+gcal_import_file_row_init (GcalImportFileRow *self)
+{
+ self->cancellable = g_cancellable_new ();
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+GtkWidget*
+gcal_import_file_row_new (GFile *file,
+ GtkSizeGroup *title_sizegroup)
+{
+ GcalImportFileRow *self;
+
+ self = g_object_new (GCAL_TYPE_IMPORT_FILE_ROW,
+ "file", file,
+ NULL);
+ self->title_sizegroup = title_sizegroup;
+
+ return (GtkWidget*) self;
+}
+
+void
+gcal_import_file_row_show_filename (GcalImportFileRow *self)
+{
+ g_return_if_fail (GCAL_IS_IMPORT_FILE_ROW (self));
+
+ gtk_widget_show (GTK_WIDGET (self->filename_label));
+}
+
+GPtrArray*
+gcal_import_file_row_get_ical_components (GcalImportFileRow *self)
+{
+ g_return_val_if_fail (GCAL_IS_IMPORT_FILE_ROW (self), NULL);
+
+ return self->ical_components;
+}
diff --git a/src/gui/importer/gcal-import-file-row.h b/src/gui/importer/gcal-import-file-row.h
new file mode 100644
index 00000000..0ebff738
--- /dev/null
+++ b/src/gui/importer/gcal-import-file-row.h
@@ -0,0 +1,37 @@
+/* gcal-import-file-row.h
+ *
+ * Copyright 2021 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GCAL_TYPE_IMPORT_FILE_ROW (gcal_import_file_row_get_type())
+G_DECLARE_FINAL_TYPE (GcalImportFileRow, gcal_import_file_row, GCAL, IMPORT_FILE_ROW, GtkListBoxRow)
+
+GtkWidget* gcal_import_file_row_new (GFile *file,
+ GtkSizeGroup *title_sizegroup);
+
+void gcal_import_file_row_show_filename (GcalImportFileRow *self);
+
+GPtrArray* gcal_import_file_row_get_ical_components (GcalImportFileRow *self);
+
+G_END_DECLS
diff --git a/src/gui/importer/gcal-import-file-row.ui b/src/gui/importer/gcal-import-file-row.ui
new file mode 100644
index 00000000..27bbd9f4
--- /dev/null
+++ b/src/gui/importer/gcal-import-file-row.ui
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GcalImportFileRow" parent="GtkListBoxRow">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="activatable">False</property>
+
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+
+ <!-- File name label -->
+ <child>
+ <object class="GtkLabel" id="filename_label">
+ <property name="can-focus">False</property>
+ <property name="margin-top">12</property>
+ <property name="xalign">0.0</property>
+ <attributes>
+ <attribute name="weight" value="bold" />
+ </attributes>
+ </object>
+ </child>
+
+ <!-- Event preview listbox -->
+ <child>
+ <object class="GtkListBox" id="events_listbox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="selection-mode">none</property>
+ <style>
+ <class name="content" />
+ </style>
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+ </template>
+</interface>
diff --git a/src/gui/importer/gcal-importer.c b/src/gui/importer/gcal-importer.c
new file mode 100644
index 00000000..1aab8db3
--- /dev/null
+++ b/src/gui/importer/gcal-importer.c
@@ -0,0 +1,173 @@
+/* gcal-importer.c
+ *
+ * Copyright 2021 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "gcal-importer.h"
+
+#include <glib/gi18n.h>
+
+G_DEFINE_QUARK (ICalErrorEnum, i_cal_error);
+
+static const gchar*
+i_cal_error_enum_to_string (ICalErrorEnum ical_error)
+{
+ switch (ical_error)
+ {
+ case I_CAL_NO_ERROR:
+ return _("No error");
+
+ case I_CAL_BADARG_ERROR:
+ return _("Bad argument to function");
+
+ case I_CAL_NEWFAILED_ERROR:
+ case I_CAL_ALLOCATION_ERROR:
+ return _("Failed to allocate a new object in memory");
+
+ case I_CAL_MALFORMEDDATA_ERROR:
+ return _("File is malformed, invalid, or corrupted");
+
+ case I_CAL_PARSE_ERROR:
+ return _("Failed to parse the calendar contents");
+
+ case I_CAL_FILE_ERROR:
+ return _("Failed to read file");
+
+ case I_CAL_INTERNAL_ERROR:
+ case I_CAL_USAGE_ERROR:
+ case I_CAL_UNIMPLEMENTED_ERROR:
+ case I_CAL_UNKNOWN_ERROR:
+ default:
+ return _("Internal error");
+ }
+}
+
+static void
+read_file_in_thread (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ g_autoptr (GFileInfo) file_info = NULL;
+ g_autoptr (GError) error = NULL;
+ g_autofree gchar *contents = NULL;
+ g_autofree gchar *path = NULL;
+ ICalComponent *component;
+ ICalErrorEnum ical_error;
+ gsize length;
+ GFile *file;
+
+ file = task_data;
+ file_info = g_file_query_info (file,
+ G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
+ G_FILE_QUERY_INFO_NONE,
+ cancellable,
+ &error);
+
+ if (error)
+ {
+ g_task_return_error (task, g_steal_pointer (&error));
+ return;
+ }
+
+ if (g_strcmp0 (g_file_info_get_content_type (file_info), "text/calendar") != 0)
+ {
+ g_task_return_new_error (task,
+ G_FILE_ERROR,
+ G_FILE_ERROR_FAILED,
+ "%s",
+ _("File is not an iCalendar (.ics) file"));
+ return;
+ }
+
+ path = g_file_get_path (file);
+ g_file_get_contents (path, &contents, &length, &error);
+
+ if (error)
+ {
+ g_task_return_error (task, g_steal_pointer (&error));
+ return;
+ }
+
+ component = i_cal_parser_parse_string (contents);
+ ical_error = i_cal_errno_return ();
+
+ if (ical_error != I_CAL_NO_ERROR)
+ {
+ g_task_return_new_error (task,
+ I_CAL_ERROR,
+ ical_error,
+ "%s",
+ i_cal_error_enum_to_string (ical_error));
+ return;
+ }
+
+ if (!component)
+ {
+ g_task_return_new_error (task,
+ I_CAL_ERROR,
+ I_CAL_MALFORMEDDATA_ERROR,
+ "%s",
+ i_cal_error_enum_to_string (I_CAL_MALFORMEDDATA_ERROR));
+ return;
+ }
+
+ g_task_return_pointer (task, g_object_ref (component), g_object_unref);
+}
+
+/**
+ * gcal_importer_import_file:
+ * @file: a #GFile
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: a #GAsyncReadyCallback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Import an ICS file.
+ */
+void
+gcal_importer_import_file (GFile *file,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+
+ g_autoptr (GTask) task = NULL;
+
+ g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ task = g_task_new (NULL, cancellable, callback, user_data);
+ g_task_set_task_data (task, g_object_ref (file), g_object_unref);
+ g_task_set_source_tag (task, gcal_importer_import_file);
+ g_task_run_in_thread (task, read_file_in_thread);
+}
+
+/**
+ * gcal_importer_do_something_finish:
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Returns: (nullable): an #ICalComponent
+ */
+ICalComponent*
+gcal_importer_import_file_finish (GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (g_task_is_valid (result, NULL), FALSE);
+
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
diff --git a/src/gui/importer/gcal-importer.h b/src/gui/importer/gcal-importer.h
new file mode 100644
index 00000000..671b8fa4
--- /dev/null
+++ b/src/gui/importer/gcal-importer.h
@@ -0,0 +1,40 @@
+/* gcal-importer.h
+ *
+ * Copyright 2021 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+#include <libecal/libecal.h>
+
+G_BEGIN_DECLS
+
+#define I_CAL_ERROR i_cal_error_quark ()
+GQuark i_cal_error_quark (void);
+
+void gcal_importer_import_file (GFile *file,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+ICalComponent* gcal_importer_import_file_finish (GAsyncResult *result,
+ GError **error);
+
+G_END_DECLS
diff --git a/src/gui/importer/importer.gresource.xml b/src/gui/importer/importer.gresource.xml
new file mode 100644
index 00000000..d770b48d
--- /dev/null
+++ b/src/gui/importer/importer.gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/calendar/ui/gui/importer">
+ <file compressed="true">gcal-import-dialog.ui</file>
+ <file compressed="true">gcal-import-file-row.ui</file>
+ </gresource>
+</gresources>
diff --git a/src/gui/importer/meson.build b/src/gui/importer/meson.build
new file mode 100644
index 00000000..f635d37a
--- /dev/null
+++ b/src/gui/importer/meson.build
@@ -0,0 +1,13 @@
+calendar_incs += include_directories('.')
+
+built_sources += gnome.compile_resources(
+ 'importer-resources',
+ 'importer.gresource.xml',
+ c_name: 'importer',
+)
+
+sources += files(
+ 'gcal-import-dialog.c',
+ 'gcal-import-file-row.c',
+ 'gcal-importer.c',
+)
diff --git a/src/gui/meson.build b/src/gui/meson.build
index 6a076076..d5670c0c 100644
--- a/src/gui/meson.build
+++ b/src/gui/meson.build
@@ -2,6 +2,7 @@ subdir('calendar-management')
subdir('event-editor')
subdir('gtk')
subdir('icons')
+subdir('importer')
subdir('views')
calendar_incs += include_directories('.')