[gnome-calendar/gbsneto/open-files: 1/2] Add file importing dialog




commit e689f18b1e5847a74772d5243721f035386e3565
Author: Georges Basile Stavracas Neto <georges stavracas gmail com>
Date:   Mon Aug 16 15:27:46 2021 -0300

    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

 po/POTFILES.in                           |   4 +
 src/gui/gcal-application.c               |  54 ++-
 src/gui/gcal-window.c                    |  17 +
 src/gui/gcal-window.h                    |   4 +
 src/gui/importer/gcal-import-dialog.c    | 569 +++++++++++++++++++++++++++++++
 src/gui/importer/gcal-import-dialog.h    |  36 ++
 src/gui/importer/gcal-import-dialog.ui   | 197 +++++++++++
 src/gui/importer/gcal-import-file-row.c  | 388 +++++++++++++++++++++
 src/gui/importer/gcal-import-file-row.h  |  37 ++
 src/gui/importer/gcal-import-file-row.ui |  43 +++
 src/gui/importer/gcal-importer.c         | 173 ++++++++++
 src/gui/importer/gcal-importer.h         |  40 +++
 src/gui/importer/importer.gresource.xml  |   7 +
 src/gui/importer/meson.build             |  13 +
 src/gui/meson.build                      |   1 +
 15 files changed, 1582 insertions(+), 1 deletion(-)
---
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);
 }
 
@@ -574,6 +608,23 @@ gcal_application_dbus_unregister (GApplication    *application,
   GCAL_EXIT;
 }
 
+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)
 {
@@ -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('.')


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]