diff --git a/po/POTFILES.in b/po/POTFILES.in index a6591668..4555f7a8 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -14,6 +14,8 @@ src/gui/calendar-management/gcal-new-calendar-page.blp src/gui/calendar-management/gcal-new-calendar-page.c src/gui/calendar-management/gcal-new-local-calendar-page.blp src/gui/calendar-management/gcal-new-local-calendar-page.c +src/gui/calendar-management/gcal-new-online-calendar-page.blp +src/gui/calendar-management/gcal-new-online-calendar-page.c src/gui/common/gcal-calendar-combo-row.blp src/gui/common/gcal-calendar-row.c src/gui/common/gcal-date-chooser.blp diff --git a/src/gui/calendar-management/calendar-management.gresource.xml b/src/gui/calendar-management/calendar-management.gresource.xml index bfece87c..fbac02b7 100644 --- a/src/gui/calendar-management/calendar-management.gresource.xml +++ b/src/gui/calendar-management/calendar-management.gresource.xml @@ -7,5 +7,6 @@ gcal-edit-calendar-page.ui gcal-new-calendar-page.ui gcal-new-local-calendar-page.ui + gcal-new-online-calendar-page.ui diff --git a/src/gui/calendar-management/gcal-new-online-calendar-page.blp b/src/gui/calendar-management/gcal-new-online-calendar-page.blp new file mode 100644 index 00000000..14ea3ab1 --- /dev/null +++ b/src/gui/calendar-management/gcal-new-online-calendar-page.blp @@ -0,0 +1,172 @@ +using Gtk 4.0; +using Adw 1; + +template $GcalNewOnlineCalendarPage: Adw.NavigationPage { + title: _("New Calendar"); + tag: "new-online-calendar"; + + child: Adw.ToolbarView { + [top] + Adw.HeaderBar { + show-end-title-buttons: false; + show-start-title-buttons: false; + } + + content: Adw.PreferencesPage { + Adw.PreferencesGroup { + title: _("Create a Local Calendar"); + + Adw.EntryRow local_calendar_name_row { + title: _("Calendar Name"); + notify::text => $on_local_calendar_name_row_text_changed_cb(template) not-swapped; + } + + Adw.ActionRow { + title: _("Color"); + activatable-widget: local_calendar_color_button; + + ColorDialogButton local_calendar_color_button { + valign: center; + + dialog: ColorDialog { + modal: true; + with-alpha: false; + }; + + notify::rgba => $on_local_calendar_color_button_rgba_changed_cb(template) not-swapped; + } + } + } + + Adw.PreferencesGroup { + title: _("Connect to an Online Calendar"); + + Grid { + row-spacing: 12; + column-spacing: 12; + + Label { + label: _( + "HTTPS, CalDAV, or Webcal URLs are supported. Online calendars that can only be updated by the provider will be added as read-only." + ); + wrap: true; + xalign: 0; + + layout { + column: 0; + row: 0; + column-span: 2; + } + } + + Entry calendar_address_entry { + hexpand: true; + placeholder-text: "https://example.com/calendar.ics"; + input-purpose: url; + notify::text => $on_url_entry_text_changed_cb(template) not-swapped; + activate => $on_calendar_address_activated_cb(template) not-swapped; + + layout { + column: 0; + row: 1; + } + } + + Revealer web_sources_revealer { + transition-type: slide_down; + transition-duration: 200; + + layout { + column: 0; + row: 2; + column-span: 2; + } + + Box web_sources_listbox_box { + orientation: vertical; + spacing: 6; + + Label web_sources_calendar_label { + label: _("Calendars"); + xalign: 0; + + styles [ + "heading", + ] + } + + ListBox web_sources_listbox { + activate-on-single-click: false; + selection-mode: none; + + styles [ + "calendar-list", + ] + } + } + } + + Label web_description_label { + label: _( + "If the calendar belongs to one of your online accounts, you can add it through the online account settings." + ); + use-markup: true; + wrap: true; + xalign: 0; + activate-link => $on_web_description_label_link_activated_cb(template) not-swapped; + + layout { + column: 0; + row: 3; + column-span: 2; + } + } + } + } + + Adw.PreferencesGroup { + Button add_button { + halign: center; + label: C_("button", "Add Calendar"); + sensitive: false; + clicked => $on_add_button_clicked_cb(template) not-swapped; + + styles [ + "pill", + "suggested-action", + ] + } + } + }; + }; +} + +Adw.MessageDialog credentials_dialog { + heading: _("Credentials"); + hide-on-close: true; + + extra-child: Adw.Clamp { + ListBox { + Adw.EntryRow credentials_user_entry { + title: _("User"); + } + + Adw.PasswordEntryRow credentials_password_entry { + title: _("Password"); + } + + styles [ + "boxed-list", + ] + } + }; + + responses [ + cancel: _("_Cancel"), + connect: _("_Connect") suggested, + ] + + default-response: "connect"; + close-response: "cancel"; + response => $on_credentials_dialog_response_cb(template) not-swapped; +} diff --git a/src/gui/calendar-management/gcal-new-online-calendar-page.c b/src/gui/calendar-management/gcal-new-online-calendar-page.c new file mode 100644 index 00000000..2d52790d --- /dev/null +++ b/src/gui/calendar-management/gcal-new-online-calendar-page.c @@ -0,0 +1,551 @@ +/* gcal-new-online-calendar-page.c + * + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2019 Georges Basile Stavracas Neto + * SPDX-FileCopyrightText: 2026 The GNOME Calendar authors + */ + + +#define G_LOG_DOMAIN "GcalNewOnlineCalendarPage" + +#include "config.h" + +#include "gcal-new-online-calendar-page.h" + +#include + +#include "gcal-calendar-management-page.h" +#include "gcal-context.h" +#include "gcal-debug.h" +#include "gcal-source-discoverer.h" +#include "gcal-utils.h" + +#define ENTRY_PROGRESS_TIMEOUT 100 // ms + +typedef enum +{ + ENTRY_STATE_EMPTY, + ENTRY_STATE_VALIDATING, + ENTRY_STATE_VALID, + ENTRY_STATE_INVALID +} EntryState; + +struct _GcalNewOnlineCalendarPage +{ + AdwNavigationPage parent; + + GtkWidget *add_button; + GtkEntry *calendar_address_entry; + EntryState calendar_address_entry_state; + AdwMessageDialog *credentials_dialog; + GtkEntry *credentials_password_entry; + GtkEntry *credentials_user_entry; + GtkColorDialogButton *local_calendar_color_button; + AdwEntryRow *local_calendar_name_row; + GtkWidget *web_sources_listbox; + GtkWidget *web_sources_revealer; + + guint calendar_address_id; + GPtrArray *remote_sources; + guint validate_url_resource_id; + + GCancellable *cancellable; + + ESource *local_source; +}; + +static gboolean pulse_web_entry (gpointer data); + +static void sources_discovered_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void cancel_validation_discover (GcalNewOnlineCalendarPage *self); + +static void gcal_calendar_management_page_iface_init (GcalCalendarManagementPageInterface *iface); + +G_DEFINE_FINAL_TYPE_WITH_CODE (GcalNewOnlineCalendarPage, + gcal_new_online_calendar_page, + ADW_TYPE_NAVIGATION_PAGE, + G_IMPLEMENT_INTERFACE (GCAL_TYPE_CALENDAR_MANAGEMENT_PAGE, + gcal_calendar_management_page_iface_init)) + +/* + * Auxiliary methods + */ + +static void +update_add_button (GcalNewOnlineCalendarPage *self) +{ + g_autofree gchar *add_button_label = NULL; + gboolean valid; + uint32_t n_calendars; + + valid = (self->local_source != NULL || self->remote_sources != NULL) && + self->calendar_address_entry_state != ENTRY_STATE_VALIDATING && + self->calendar_address_entry_state != ENTRY_STATE_INVALID; + + gtk_widget_set_sensitive (self->add_button, valid); + + n_calendars = 0; + if (self->remote_sources) + n_calendars += self->remote_sources->len; + else if (self->local_source) + n_calendars++; + + if (n_calendars > 0) + { + add_button_label = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, + "Add Calendar", + "Add %1$u Calendars", + n_calendars), + n_calendars); + } + else + { + add_button_label = g_strdup (C_("button", "Add Calendar")); + } + + gtk_button_set_label (GTK_BUTTON (self->add_button), add_button_label); +} + +static void +update_local_source (GcalNewOnlineCalendarPage *self) +{ + g_autofree gchar *calendar_name = NULL; + + g_clear_object (&self->local_source); + + calendar_name = g_strdup (gtk_editable_get_text (GTK_EDITABLE (self->local_calendar_name_row))); + calendar_name = g_strstrip (calendar_name); + + if (calendar_name && g_utf8_strlen (calendar_name, -1) > 0) + { + g_autofree gchar *color_string = NULL; + ESourceExtension *ext; + const GdkRGBA *color; + ESource *source; + + color = gtk_color_dialog_button_get_rgba (self->local_calendar_color_button); + color_string = gdk_rgba_to_string (color); + + /* Create the new source and add the needed extensions */ + source = e_source_new (NULL, NULL, NULL); + e_source_set_parent (source, "local-stub"); + e_source_set_display_name (source, calendar_name); + + ext = e_source_get_extension (source, E_SOURCE_EXTENSION_CALENDAR); + e_source_backend_set_backend_name (E_SOURCE_BACKEND (ext), "local"); + e_source_selectable_set_color (E_SOURCE_SELECTABLE (ext), color_string); + + e_source_backend_set_backend_name (E_SOURCE_BACKEND (ext), "local"); + + self->local_source = source; + } + + update_add_button (self); +} + +static void +toggle_url_entry_pulsing (GcalNewOnlineCalendarPage *self, + gboolean pulsing) +{ + if (pulsing && self->calendar_address_id == 0) + { + gtk_entry_set_progress_fraction (self->calendar_address_entry, 0.1); + self->calendar_address_id = g_timeout_add (ENTRY_PROGRESS_TIMEOUT, pulse_web_entry, self); + } + else if (!pulsing && self->calendar_address_id != 0) + { + gtk_entry_set_progress_fraction (self->calendar_address_entry, 0.0); + g_clear_handle_id (&self->calendar_address_id, g_source_remove); + } +} + +static void +update_url_entry_state (GcalNewOnlineCalendarPage *self, + EntryState state, + const gchar *error_message) +{ + self->calendar_address_entry_state = state; + + if (state == ENTRY_STATE_INVALID) + { + gtk_widget_add_css_class (GTK_WIDGET (self->calendar_address_entry), "error"); + if (error_message) + { + g_autofree gchar *multiline_message = NULL; + g_auto(GStrv) split_message = NULL; + + split_message = g_strsplit (error_message, ": ", -1); + multiline_message = g_strjoinv (":\n", split_message); + + gtk_entry_set_icon_from_icon_name (self->calendar_address_entry, + GTK_ENTRY_ICON_SECONDARY, + "dialog-error"); + gtk_entry_set_icon_tooltip_text (self->calendar_address_entry, + GTK_ENTRY_ICON_SECONDARY, + multiline_message); + } + } + else + { + gtk_widget_remove_css_class (GTK_WIDGET (self->calendar_address_entry), "error"); + gtk_entry_set_icon_from_icon_name (self->calendar_address_entry, + GTK_ENTRY_ICON_SECONDARY, + NULL); + gtk_entry_set_icon_tooltip_text (self->calendar_address_entry, + GTK_ENTRY_ICON_SECONDARY, + NULL); + } + + update_add_button (self); + toggle_url_entry_pulsing (self, state == ENTRY_STATE_VALIDATING); +} + +static void +discover_sources (GcalNewOnlineCalendarPage *self) +{ + const gchar *username; + const gchar *password; + const gchar *url; + + GCAL_ENTRY; + + self->validate_url_resource_id = 0; + + url = gtk_editable_get_text (GTK_EDITABLE (self->calendar_address_entry)); + username = gtk_editable_get_text (GTK_EDITABLE (self->credentials_user_entry)); + password = gtk_editable_get_text (GTK_EDITABLE (self->credentials_password_entry)); + + gcal_discover_sources_from_uri (url, + username, + password, + self->cancellable, + sources_discovered_cb, + self); + update_url_entry_state (self, ENTRY_STATE_VALIDATING, NULL); + + GCAL_EXIT; +} + +/* + * Callbacks + */ + +static gboolean +pulse_web_entry (gpointer data) +{ + GcalNewOnlineCalendarPage *self = data; + + gtk_entry_progress_pulse (self->calendar_address_entry); + return G_SOURCE_CONTINUE; +} + +static void +sources_discovered_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GcalNewOnlineCalendarPage *self; + g_autoptr (GPtrArray) sources = NULL; + g_autoptr (GError) error = NULL; + + GCAL_ENTRY; + + self = GCAL_NEW_ONLINE_CALENDAR_PAGE (user_data); + sources = gcal_discover_sources_from_uri_finish (result, &error); + + if (error) + { + if (g_error_matches (error, + GCAL_SOURCE_DISCOVERER_ERROR, + GCAL_SOURCE_DISCOVERER_ERROR_UNAUTHORIZED)) + { + GtkRoot *root; + + g_debug ("Unauthorized, asking for credentials..."); + + root = gtk_widget_get_root (GTK_WIDGET (self)); + gtk_window_set_transient_for (GTK_WINDOW (self->credentials_dialog), GTK_WINDOW (root)); + gtk_window_present (GTK_WINDOW (self->credentials_dialog)); + } + /* + * If the operation was cancelled by the user, no error message + * or error style class will be applied + */ + else if (!g_error_matches (error, + G_IO_ERROR, + G_IO_ERROR_CANCELLED)) + { + g_warning ("Error finding sources: %s", error->message); + update_url_entry_state (self, ENTRY_STATE_INVALID, error->message); + } + GCAL_RETURN (); + } + + g_debug ("Found %u sources", sources->len); + + self->remote_sources = g_steal_pointer (&sources); + update_url_entry_state (self, ENTRY_STATE_VALID, NULL); + + GCAL_EXIT; +} + +static gboolean +validate_url_cb (gpointer data) +{ + GcalNewOnlineCalendarPage *self = data; + g_autoptr (GUri) guri = NULL; + + GCAL_ENTRY; + + update_url_entry_state (self, ENTRY_STATE_VALIDATING, NULL); + self->validate_url_resource_id = 0; + + guri = g_uri_parse (gtk_editable_get_text (GTK_EDITABLE (self->calendar_address_entry)), SOUP_HTTP_URI_FLAGS | G_URI_FLAGS_PARSE_RELAXED, NULL); + + if (guri != NULL && g_uri_get_scheme (guri) && g_uri_get_host (guri)) + { + discover_sources (self); + } + else + { + update_url_entry_state (self, ENTRY_STATE_INVALID, _("The URL you have entered appears to be invalid.")); + g_debug ("Invalid URL passed"); + } + + GCAL_RETURN (G_SOURCE_REMOVE); +} + +static void +on_add_button_clicked_cb (GtkWidget *button, + GcalNewOnlineCalendarPage *self) +{ + GcalContext *context; + GcalManager *manager; + + context = gcal_application_get_context (GCAL_DEFAULT_APPLICATION); + manager = gcal_context_get_manager (context); + + /* Commit each new remote source */ + if (self->remote_sources) + { + guint i; + + for (i = 0; i < self->remote_sources->len; i++) + gcal_manager_save_source (manager, g_ptr_array_index (self->remote_sources, i)); + + g_clear_pointer (&self->remote_sources, g_ptr_array_unref); + } + + else + { + gcal_manager_save_source (manager, self->local_source); + } + + gcal_calendar_management_page_switch_page (GCAL_CALENDAR_MANAGEMENT_PAGE (self), + "calendars", + NULL); +} + +static void +on_calendar_address_activated_cb (GtkEntry *entry, + GcalNewOnlineCalendarPage *self) +{ + cancel_validation_discover (self); + validate_url_cb (self); +} + +static void +on_credentials_dialog_response_cb (AdwMessageDialog *dialog, + const gchar *response, + GcalNewOnlineCalendarPage *self) +{ + if (g_str_equal (response, "connect")) + discover_sources (self); + else if (g_str_equal (response, "cancel")) + update_url_entry_state (self, ENTRY_STATE_INVALID, + _("A valid user and password are required to connect to this calendar.")); + + gtk_editable_set_text (GTK_EDITABLE (self->credentials_user_entry), ""); + gtk_editable_set_text (GTK_EDITABLE (self->credentials_password_entry), ""); +} + +static void +cancel_validation_discover (GcalNewOnlineCalendarPage *self) +{ + if (self->calendar_address_id != 0) + { + gtk_entry_set_progress_fraction (self->calendar_address_entry, 0); + g_clear_handle_id (&self->calendar_address_id, g_source_remove); + } + + /* Cleanup previous remote sources */ + g_clear_pointer (&self->remote_sources, g_ptr_array_unref); + + if (self->validate_url_resource_id != 0) + g_clear_handle_id (&self->validate_url_resource_id, g_source_remove); + + /* Cancel a validation in case it is on-going */ + if (self->calendar_address_entry_state == ENTRY_STATE_VALIDATING) + { + g_cancellable_cancel (self->cancellable); + g_clear_object (&self->cancellable); + self->cancellable = g_cancellable_new (); + } +} + +static void +on_url_entry_text_changed_cb (GtkEntry *entry, + GParamSpec *pspec, + GcalNewOnlineCalendarPage *self) +{ + const gchar *text; + + GCAL_ENTRY; + + text = gtk_editable_get_text (GTK_EDITABLE (entry)); + + cancel_validation_discover (self); + + if (text && g_utf8_strlen (text, -1) > 0) + { + /* + * Only validate the text after the timeout, as we don't want to bother them + * as soon as they type into the entry. + */ + self->validate_url_resource_id = g_timeout_add (750, validate_url_cb, self); + } + else + { + gtk_entry_set_progress_fraction (self->calendar_address_entry, 0); + update_url_entry_state (self, ENTRY_STATE_EMPTY, NULL); + } + + GCAL_EXIT; +} + +static void +on_local_calendar_name_row_text_changed_cb (AdwEntryRow *entry_row, + GParamSpec *pspec, + GcalNewOnlineCalendarPage *self) +{ + update_local_source (self); +} + +static void +on_local_calendar_color_button_rgba_changed_cb (GtkColorChooser *chooser, + GParamSpec *pspec, + GcalNewOnlineCalendarPage *self) +{ + update_local_source (self); +} + +static void +on_web_description_label_link_activated_cb (GtkLabel *label, + gchar *uri, + GcalNewOnlineCalendarPage *self) +{ + GDBusConnection *connection = g_application_get_dbus_connection (g_application_get_default ()); + + gcal_util_launch_gnome_settings (connection, "online-accounts", NULL); +} + + +/* + * GcalCalendarManagementPage iface + */ + +static void +gcal_new_calendar_page_deactivate (GcalCalendarManagementPage *page) +{ + GcalNewOnlineCalendarPage *self; + + GCAL_ENTRY; + + self = GCAL_NEW_ONLINE_CALENDAR_PAGE (page); + + g_clear_object (&self->local_source); + g_clear_pointer (&self->remote_sources, g_ptr_array_unref); + update_add_button (self); + + gtk_editable_set_text (GTK_EDITABLE (self->local_calendar_name_row), ""); + gtk_editable_set_text (GTK_EDITABLE (self->calendar_address_entry), ""); + + toggle_url_entry_pulsing (self, FALSE); + + GCAL_EXIT; +} + +static void +gcal_calendar_management_page_iface_init (GcalCalendarManagementPageInterface *iface) +{ + iface->deactivate = gcal_new_calendar_page_deactivate; +} + +/* + * GObject overrides + */ + +static void +gcal_new_calendar_page_dispose (GObject *object) +{ + GcalNewOnlineCalendarPage *self = (GcalNewOnlineCalendarPage *)object; + + gtk_widget_dispose_template (GTK_WIDGET (self), GCAL_TYPE_NEW_ONLINE_CALENDAR_PAGE); + + G_OBJECT_CLASS (gcal_new_online_calendar_page_parent_class)->dispose (object); +} + +static void +gcal_new_calendar_page_finalize (GObject *object) +{ + GcalNewOnlineCalendarPage *self = (GcalNewOnlineCalendarPage *)object; + + g_clear_handle_id (&self->calendar_address_id, g_source_remove); + g_clear_handle_id (&self->validate_url_resource_id, g_source_remove); + + g_cancellable_cancel (self->cancellable); + g_clear_object (&self->cancellable); + + G_OBJECT_CLASS (gcal_new_online_calendar_page_parent_class)->finalize (object); +} + +static void +gcal_new_online_calendar_page_class_init (GcalNewOnlineCalendarPageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gcal_new_calendar_page_dispose; + object_class->finalize = gcal_new_calendar_page_finalize; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/ui/gui/calendar-management/gcal-new-online-calendar-page.ui"); + + gtk_widget_class_bind_template_child (widget_class, GcalNewOnlineCalendarPage, add_button); + gtk_widget_class_bind_template_child (widget_class, GcalNewOnlineCalendarPage, calendar_address_entry); + gtk_widget_class_bind_template_child (widget_class, GcalNewOnlineCalendarPage, credentials_dialog); + gtk_widget_class_bind_template_child (widget_class, GcalNewOnlineCalendarPage, credentials_password_entry); + gtk_widget_class_bind_template_child (widget_class, GcalNewOnlineCalendarPage, credentials_user_entry); + gtk_widget_class_bind_template_child (widget_class, GcalNewOnlineCalendarPage, local_calendar_color_button); + gtk_widget_class_bind_template_child (widget_class, GcalNewOnlineCalendarPage, local_calendar_name_row); + gtk_widget_class_bind_template_child (widget_class, GcalNewOnlineCalendarPage, web_sources_listbox); + gtk_widget_class_bind_template_child (widget_class, GcalNewOnlineCalendarPage, web_sources_revealer); + + gtk_widget_class_bind_template_callback (widget_class, on_add_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, on_calendar_address_activated_cb); + gtk_widget_class_bind_template_callback (widget_class, on_credentials_dialog_response_cb); + gtk_widget_class_bind_template_callback (widget_class, on_local_calendar_name_row_text_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, on_local_calendar_color_button_rgba_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, on_url_entry_text_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, on_web_description_label_link_activated_cb); +} + +static void +gcal_new_online_calendar_page_init (GcalNewOnlineCalendarPage *self) +{ + self->cancellable = g_cancellable_new (); + + gtk_widget_init_template (GTK_WIDGET (self)); +} diff --git a/src/gui/calendar-management/gcal-new-online-calendar-page.h b/src/gui/calendar-management/gcal-new-online-calendar-page.h new file mode 100644 index 00000000..44fab324 --- /dev/null +++ b/src/gui/calendar-management/gcal-new-online-calendar-page.h @@ -0,0 +1,17 @@ +/* gcal-new-online-calendar-page.h + * + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2019 Georges Basile Stavracas Neto + * SPDX-FileCopyrightText: 2026 The GNOME Calendar authors + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define GCAL_TYPE_NEW_ONLINE_CALENDAR_PAGE (gcal_new_online_calendar_page_get_type()) +G_DECLARE_FINAL_TYPE (GcalNewOnlineCalendarPage, gcal_new_online_calendar_page, GCAL, NEW_ONLINE_CALENDAR_PAGE, AdwNavigationPage) + +G_END_DECLS diff --git a/src/gui/calendar-management/meson.build b/src/gui/calendar-management/meson.build index 3678c94c..dcfc05ff 100644 --- a/src/gui/calendar-management/meson.build +++ b/src/gui/calendar-management/meson.build @@ -9,6 +9,7 @@ blueprints = custom_target( 'gcal-edit-calendar-page.blp', 'gcal-new-calendar-page.blp', 'gcal-new-local-calendar-page.blp', + 'gcal-new-online-calendar-page.blp', ), output: '.', command: [ @@ -34,4 +35,5 @@ sources += files( 'gcal-edit-calendar-page.c', 'gcal-new-calendar-page.c', 'gcal-new-local-calendar-page.c', + 'gcal-new-online-calendar-page.c', )