diff --git a/po/POTFILES.in b/po/POTFILES.in
index c97bf305..a6591668 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -12,6 +12,8 @@ src/gui/calendar-management/gcal-edit-calendar-page.blp
src/gui/calendar-management/gcal-edit-calendar-page.c
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/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 d1cb4726..bfece87c 100644
--- a/src/gui/calendar-management/calendar-management.gresource.xml
+++ b/src/gui/calendar-management/calendar-management.gresource.xml
@@ -6,5 +6,6 @@
gcal-calendars-page.ui
gcal-edit-calendar-page.ui
gcal-new-calendar-page.ui
+ gcal-new-local-calendar-page.ui
diff --git a/src/gui/calendar-management/gcal-new-local-calendar-page.blp b/src/gui/calendar-management/gcal-new-local-calendar-page.blp
new file mode 100644
index 00000000..5c4867ff
--- /dev/null
+++ b/src/gui/calendar-management/gcal-new-local-calendar-page.blp
@@ -0,0 +1,172 @@
+using Gtk 4.0;
+using Adw 1;
+
+template $GcalNewLocalCalendarPage: Adw.NavigationPage {
+ title: _("New Calendar");
+ tag: "new-local-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-local-calendar-page.c b/src/gui/calendar-management/gcal-new-local-calendar-page.c
new file mode 100644
index 00000000..69a00e9a
--- /dev/null
+++ b/src/gui/calendar-management/gcal-new-local-calendar-page.c
@@ -0,0 +1,550 @@
+/* gcal-new-local-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 "GcalNewLocalCalendarPage"
+
+#include "config.h"
+
+#include "gcal-new-local-calendar-page.h"
+
+#include
+
+#include "gcal-calendar-management-page.h"
+#include "gcal-context.h"
+#include "gcal-debug.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 _GcalNewLocalCalendarPage
+{
+ 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 (GcalNewLocalCalendarPage *self);
+
+static void gcal_calendar_management_page_iface_init (GcalCalendarManagementPageInterface *iface);
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (GcalNewLocalCalendarPage,
+ gcal_new_local_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 (GcalNewLocalCalendarPage *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 (GcalNewLocalCalendarPage *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 (GcalNewLocalCalendarPage *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 (GcalNewLocalCalendarPage *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 (GcalNewLocalCalendarPage *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)
+{
+ GcalNewLocalCalendarPage *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)
+{
+ GcalNewLocalCalendarPage *self;
+ g_autoptr (GPtrArray) sources = NULL;
+ g_autoptr (GError) error = NULL;
+
+ GCAL_ENTRY;
+
+ self = GCAL_NEW_LOCAL_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)
+{
+ GcalNewLocalCalendarPage *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,
+ GcalNewLocalCalendarPage *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,
+ GcalNewLocalCalendarPage *self)
+{
+ cancel_validation_discover (self);
+ validate_url_cb (self);
+}
+
+static void
+on_credentials_dialog_response_cb (AdwMessageDialog *dialog,
+ const gchar *response,
+ GcalNewLocalCalendarPage *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 (GcalNewLocalCalendarPage *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,
+ GcalNewLocalCalendarPage *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,
+ GcalNewLocalCalendarPage *self)
+{
+ update_local_source (self);
+}
+
+static void
+on_local_calendar_color_button_rgba_changed_cb (GtkColorChooser *chooser,
+ GParamSpec *pspec,
+ GcalNewLocalCalendarPage *self)
+{
+ update_local_source (self);
+}
+
+static void
+on_web_description_label_link_activated_cb (GtkLabel *label,
+ gchar *uri,
+ GcalNewLocalCalendarPage *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)
+{
+ GcalNewLocalCalendarPage *self;
+
+ GCAL_ENTRY;
+
+ self = GCAL_NEW_LOCAL_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)
+{
+ GcalNewLocalCalendarPage *self = (GcalNewLocalCalendarPage *)object;
+
+ gtk_widget_dispose_template (GTK_WIDGET (self), GCAL_TYPE_NEW_LOCAL_CALENDAR_PAGE);
+
+ G_OBJECT_CLASS (gcal_new_local_calendar_page_parent_class)->dispose (object);
+}
+
+static void
+gcal_new_calendar_page_finalize (GObject *object)
+{
+ GcalNewLocalCalendarPage *self = (GcalNewLocalCalendarPage *)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_local_calendar_page_parent_class)->finalize (object);
+}
+
+static void
+gcal_new_local_calendar_page_class_init (GcalNewLocalCalendarPageClass *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-local-calendar-page.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GcalNewLocalCalendarPage, add_button);
+ gtk_widget_class_bind_template_child (widget_class, GcalNewLocalCalendarPage, calendar_address_entry);
+ gtk_widget_class_bind_template_child (widget_class, GcalNewLocalCalendarPage, credentials_dialog);
+ gtk_widget_class_bind_template_child (widget_class, GcalNewLocalCalendarPage, credentials_password_entry);
+ gtk_widget_class_bind_template_child (widget_class, GcalNewLocalCalendarPage, credentials_user_entry);
+ gtk_widget_class_bind_template_child (widget_class, GcalNewLocalCalendarPage, local_calendar_color_button);
+ gtk_widget_class_bind_template_child (widget_class, GcalNewLocalCalendarPage, local_calendar_name_row);
+ gtk_widget_class_bind_template_child (widget_class, GcalNewLocalCalendarPage, web_sources_listbox);
+ gtk_widget_class_bind_template_child (widget_class, GcalNewLocalCalendarPage, 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_local_calendar_page_init (GcalNewLocalCalendarPage *self)
+{
+ self->cancellable = g_cancellable_new ();
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
diff --git a/src/gui/calendar-management/gcal-new-local-calendar-page.h b/src/gui/calendar-management/gcal-new-local-calendar-page.h
new file mode 100644
index 00000000..1b1192a0
--- /dev/null
+++ b/src/gui/calendar-management/gcal-new-local-calendar-page.h
@@ -0,0 +1,17 @@
+/* gcal-new-local-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_LOCAL_CALENDAR_PAGE (gcal_new_local_calendar_page_get_type())
+G_DECLARE_FINAL_TYPE (GcalNewLocalCalendarPage, gcal_new_local_calendar_page, GCAL, NEW_LOCAL_CALENDAR_PAGE, AdwNavigationPage)
+
+G_END_DECLS
diff --git a/src/gui/calendar-management/meson.build b/src/gui/calendar-management/meson.build
index bfa75686..3678c94c 100644
--- a/src/gui/calendar-management/meson.build
+++ b/src/gui/calendar-management/meson.build
@@ -8,6 +8,7 @@ blueprints = custom_target(
'gcal-calendars-page.blp',
'gcal-edit-calendar-page.blp',
'gcal-new-calendar-page.blp',
+ 'gcal-new-local-calendar-page.blp',
),
output: '.',
command: [
@@ -32,4 +33,5 @@ sources += files(
'gcal-calendars-page.c',
'gcal-edit-calendar-page.c',
'gcal-new-calendar-page.c',
+ 'gcal-new-local-calendar-page.c',
)