summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNiels De Graef <nielsdegraef@gmail.com>2023-01-14 15:22:10 +0100
committerNiels De Graef <ndegraef@redhat.com>2023-01-14 15:22:10 +0100
commit23056a22f54bdcec7b3a5229b2a30a934aaa0f2f (patch)
treecca4eb10e9f5b6cca441aeaf0ced2c3f90dcdf29
parentc4ac635f4461f725bc7a5f007e302de73cf24613 (diff)
downloadgnome-contacts-nielsdg/address-books.tar.gz
preferences: Allow adding CardDAV address booksnielsdg/address-books
-rw-r--r--data/contacts.gresource.xml1
-rw-r--r--data/ui/contacts-add-address-book-dialog.ui154
-rw-r--r--src/contacts-add-address-book-dialog.vala270
-rw-r--r--src/contacts-address-book-type.vala52
-rw-r--r--src/contacts-preferences-window.vala40
-rw-r--r--src/meson.build2
6 files changed, 507 insertions, 12 deletions
diff --git a/data/contacts.gresource.xml b/data/contacts.gresource.xml
index 89c8052..1510f67 100644
--- a/data/contacts.gresource.xml
+++ b/data/contacts.gresource.xml
@@ -15,6 +15,7 @@
<file compressed="true" preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/contacts-avatar-selector.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks">ui/contacts-add-address-book-dialog.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/contacts-contact-pane.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/contacts-crop-dialog.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/contacts-editor-menu.ui</file>
diff --git a/data/ui/contacts-add-address-book-dialog.ui b/data/ui/contacts-add-address-book-dialog.ui
new file mode 100644
index 0000000..28e9115
--- /dev/null
+++ b/data/ui/contacts-add-address-book-dialog.ui
@@ -0,0 +1,154 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="ContactsAddAddressBookDialog" parent="GtkDialog">
+ <property name="modal">True</property>
+ <property name="title" translatable="yes">Add CardDAV account</property>
+
+ <child type="action">
+ <object class="GtkButton" id="cancel_button">
+ <property name="label" translatable="yes">_Cancel</property>
+ <property name="use-underline">True</property>
+ <property name="action-name">window.close</property>
+ </object>
+ </child>
+ <child type="action">
+ <object class="GtkButton" id="add_button">
+ <property name="label" translatable="yes">_Add</property>
+ <property name="use-underline">True</property>
+ <property name="action-name">address-book.save</property>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="cancel">cancel_button</action-widget>
+ <action-widget response="accept" default="true">add_button</action-widget>
+ </action-widgets>
+
+ <child internal-child="content_area">
+ <object class="GtkBox">
+ <child>
+ <object class="AdwClamp">
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="spacing">18</property>
+ <property name="margin-top">18</property>
+ <property name="margin-bottom">18</property>
+ <property name="margin-start">18</property>
+ <property name="margin-end">18</property>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <property name="title" translatable="yes">Address book name</property>
+ <property name="description" translatable="yes">Provide a name for the address book</property>
+ <child>
+ <object class="AdwEntryRow" id="address_book_name_row">
+ <property name="title" translatable="yes">Name</property>
+ <signal name="notify::text" handler="on_address_book_name_row_notify_text"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <property name="title" translatable="yes">Server details</property>
+ <child>
+ <object class="AdwEntryRow" id="server_row">
+ <property name="title" translatable="yes">URL</property>
+ <property name="text">https://</property>
+ <property name="input-purpose">url</property>
+ <signal name="notify::text" handler="on_server_row_notify_text"/>
+ </object>
+ </child>
+ <child>
+ <object class="AdwEntryRow" id="username_row">
+ <property name="title" translatable="yes">Username</property>
+ <signal name="notify::text" handler="on_username_row_notify_text"/>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPasswordEntryRow" id="password_row">
+ <property name="title" translatable="yes">Password</property>
+ <signal name="notify::text" handler="on_password_row_notify_text"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="connect_button">
+ <property name="sensitive">False</property>
+ <property name="halign">end</property>
+ <property name="label" translatable="yes">_Connect</property>
+ <property name="use-underline">True</property>
+ <property name="action-name">address-book.connect</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStack" id="bottom_stack">
+ <child>
+ <object class="GtkStackPage">
+ <property name="name">normal</property>
+ <property name="child">
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Please fill in all fields to connect to the server</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStackPage">
+ <property name="name">error</property>
+ <property name="child">
+ <object class="GtkLabel" id="error_label">
+ <style>
+ <class name="error"/>
+ </style>
+ </object>
+ </property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStackPage" id="loading_page">
+ <property name="name">loading</property>
+ <property name="child">
+ <object class="GtkBox" id="loading_box">
+ <property name="halign">center</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkSpinner">
+ <property name="spinning" bind-source="loading_box" bind-property="visible" bind-flags="sync-create"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Contacting the server</property>
+ </object>
+ </child>
+ </object>
+ </property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStackPage">
+ <property name="name">address_books</property>
+ <property name="child">
+ <object class="AdwPreferencesGroup" id="address_books_group">
+ <property name="title" translatable="yes">Address books</property>
+ </object>
+ </property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/contacts-add-address-book-dialog.vala b/src/contacts-add-address-book-dialog.vala
new file mode 100644
index 0000000..323fffb
--- /dev/null
+++ b/src/contacts-add-address-book-dialog.vala
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2022 Niels De Graef <nielsdegraef@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 2 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/>.
+ */
+
+using Folks;
+
+[GtkTemplate (ui = "/org/gnome/Contacts/ui/contacts-add-address-book-dialog.ui")]
+public class Contacts.AddAddressBookDialog : Gtk.Dialog {
+
+ // The "scratch source" that we will end up saving
+ private E.Source e_source;
+
+ public AddressBookType address_book_type { get; construct set; }
+
+ [GtkChild]
+ private unowned Adw.EntryRow address_book_name_row;
+
+ [GtkChild]
+ private unowned Adw.EntryRow server_row;
+ [GtkChild]
+ private unowned Adw.EntryRow username_row;
+ [GtkChild]
+ private unowned Adw.PasswordEntryRow password_row;
+ [GtkChild]
+ private unowned Gtk.Button connect_button;
+
+ [GtkChild]
+ private unowned Gtk.Stack bottom_stack;
+ [GtkChild]
+ private unowned Gtk.Label error_label;
+
+ [GtkChild]
+ private unowned Adw.PreferencesGroup address_books_group;
+ private DiscoveredSourceRow? selected_source_row = null;
+
+ [GtkChild]
+ private unowned Gtk.Button add_button;
+ //XXX only make sensitive once selected-source-row is set
+
+ static construct {
+ install_action ("address-book.connect", null, (Gtk.WidgetActionActivateFunc) connect_action);
+ install_action ("address-book.save", null, (Gtk.WidgetActionActivateFunc) save_action);
+ }
+
+ construct {
+ // Initial widget state
+ this.use_header_bar = 1;
+
+ // Create a so-called scratch source
+ this.e_source = new E.Source (null, null);
+
+ // Set the backend name depending on the address book type
+ var ab_extension = (E.SourceAddressBook)
+ this.e_source.get_extension (E.SOURCE_EXTENSION_ADDRESS_BOOK);
+ ab_extension.set_backend_name (this.address_book_type.to_e_backend_name ());
+ }
+
+ public AddAddressBookDialog (Gtk.Window? parent_window) {
+ // NOTE: we only support CardDAV for now
+ Object (address_book_type: AddressBookType.CARDDAV,
+ transient_for: parent_window);
+ }
+
+ private void connect_action (string action_name, Variant? parameter) {
+ this.connect_button.sensitive = false;
+
+ connect_to_server.begin (null, (obj, res) => {
+ try {
+ connect_to_server.end (res);
+ } catch (Error e) {
+ warning ("Couldn't connect: %s[%d] %s", e.domain.to_string(), e.code, e.message);
+
+ // We can use ESoupSessionError to find out errors that match a
+ // specific HTTP code
+ if (e.domain == E.SoupSession.error_quark ()) {
+ if (e.code == 401) {
+ show_error (_("Error: Invalid username or password"));
+ } else {
+ show_error (_("Error: server returned HTTP %u").printf (e.code));
+ }
+ } else if (e.domain == UriError.quark ()) {
+ show_error (_("Error: Invalid URL"));
+ } else {
+ show_error (_("Error connecting to the server"));
+ }
+ } finally {
+ this.connect_button.sensitive = true;
+ }
+ });
+ }
+
+ private void save_action (string action_name, Variant? parameter) {
+ save.begin (null, (obj, res) => {
+ try {
+ save.end (res);
+ } catch (Error e) {
+ //XXX show something in the UI
+ warning ("Couldn't save: %s", e.message);
+ }
+ });
+ close ();
+ }
+
+ [GtkCallback]
+ private void on_address_book_name_row_notify_text (Object object, ParamSpec pspec) {
+ check_connect_button ();
+ }
+
+ [GtkCallback]
+ private void on_server_row_notify_text (Object object, ParamSpec pspec) {
+ check_connect_button ();
+ }
+
+ [GtkCallback]
+ private void on_username_row_notify_text (Object object, ParamSpec pspec) {
+ check_connect_button ();
+ }
+
+ [GtkCallback]
+ private void on_password_row_notify_text (Object object, ParamSpec pspec) {
+ check_connect_button ();
+ }
+
+ private void check_connect_button () {
+ // XXX check also if uri is valid
+ // XXX maybe set warning label?
+ this.connect_button.sensitive =
+ (this.username_row.text.strip () != "" &&
+ this.password_row.text != "" &&
+ this.server_row.text.strip () != "");
+ }
+
+ private void show_error (string label) {
+ this.error_label.label = label;
+ this.bottom_stack.visible_child_name = "error";
+ }
+
+ private async void connect_to_server (Cancellable? cancellable) throws Error {
+ // First of all try to parse the URI (if it fails, it throws an error)
+ var uri = Uri.parse (this.server_row.text, UriFlags.NONE);
+
+ // Do a basic check that we have a connection to E-D-S
+ //XXX maybe we can just create a registry here?
+ if (!ensure_eds_accounts (true)) {
+ warning ("Can't connect to evolution-data-server");
+ return;// XXX throw error
+ }
+
+ // The basic details
+ var ab_name = this.address_book_name_row.text;
+ debug ("Connecting to source '%s' (type: %s)", ab_name, this.address_book_type.to_string ());
+
+ if (ab_name.strip () != "")
+ this.e_source.display_name = ab_name;
+
+ // Get the webdav and auth extensions to set those
+ var webdav_ext = (E.SourceWebdav)
+ this.e_source.get_extension (E.SOURCE_EXTENSION_WEBDAV_BACKEND);
+ webdav_ext.uri = uri;
+
+ var auth_ext = (E.SourceAuthentication)
+ this.e_source.get_extension (E.SOURCE_EXTENSION_AUTHENTICATION);
+ auth_ext.user = this.username_row.text;
+
+ // Credentials
+ var credentials = new E.NamedParameters ();
+ credentials.set (E.SOURCE_CREDENTIAL_USERNAME, this.username_row.text);
+ credentials.set (E.SOURCE_CREDENTIAL_PASSWORD, this.password_row.text);
+
+ debug ("Discovering address books for '%s'", ab_name);
+ this.bottom_stack.visible_child_name = "loading";
+
+ // Discover which address books are available at the server
+ string cert_pem;
+ TlsCertificateFlags cert_errors;
+ SList<E.WebDAVDiscoveredSource> discovered_sources;
+ SList<string> discovered_email_addrs;
+ yield this.e_source.webdav_discover_sources (null,
+ E.WebDAVDiscoverSupports.CONTACTS,
+ credentials,
+ cancellable,
+ out cert_pem,
+ out cert_errors,
+ out discovered_sources,
+ out discovered_email_addrs);
+ debug ("Discovered %u address books", discovered_sources.length ());
+
+ if (discovered_sources.length () == 0) {
+ show_error (_("No address books found"));
+ return;
+ }
+
+ // Give the user the option to select which one to import
+ this.bottom_stack.visible_child_name = "address_books";
+ this.address_books_group.description = ngettext (
+ "Found %u address book to import",
+ "Found %u address books. Please select the address book you want to import",
+ discovered_sources.length ()).printf (discovered_sources.length ());
+ foreach (unowned var s in discovered_sources) {
+ debug ("SOURCE '%s' (%s): '%s'", s.display_name, s.href, s.description);
+
+ var source_row = new DiscoveredSourceRow (s);
+ this.address_books_group.add (source_row);
+ source_row.activated.connect ((row) => {
+ unowned var src_row = (DiscoveredSourceRow) row;
+ if (src_row == this.selected_source_row)
+ return;
+
+ this.selected_source_row.selected = false;
+ src_row.selected = true;
+ this.selected_source_row = src_row;
+ });
+ }
+ }
+
+ public async void save (Cancellable? cancellable) throws Error
+ requires (this.selected_source_row != null) {
+ // Note that eds_source_registry is guaranteed to be non-null due to
+ // ensure_eds_accounts()
+ //XXX maybe do create our own registry?
+ if (eds_source_registry == null)
+ error ("eds_source_registry is null");
+
+ var selected_source = this.selected_source_row.source;
+ if (selected_source.href != null && selected_source.href != "") {
+ var webdav_ext = (E.SourceWebdav)
+ this.e_source.get_extension (E.SOURCE_EXTENSION_WEBDAV_BACKEND);
+ var uri = Uri.parse (this.server_row.text, UriFlags.NONE);
+ webdav_ext.uri = uri;
+ }
+
+ debug ("Saving source '%s'", this.e_source.display_name);
+ yield eds_source_registry.commit_source (this.e_source, cancellable);
+ debug ("Saved source '%s'", this.e_source.display_name);
+ }
+
+ // Helper class to show a remote address book that can be imported
+ private class DiscoveredSourceRow : Adw.ActionRow {
+
+ public E.WebDAVDiscoveredSource source { get; construct set; }
+
+ public bool selected { get; set; default = false; }
+
+ public DiscoveredSourceRow (E.WebDAVDiscoveredSource source) {
+ Object (source: source);
+
+ this.title = source.display_name;
+ if (source.description != null && source.description != "")
+ this.subtitle = source.description;
+
+ var checkmark = new Gtk.Image.from_icon_name ("object-select-symbolic");
+ bind_property ("selected", checkmark, "visible", BindingFlags.SYNC_CREATE);
+ add_suffix (checkmark);
+ this.activatable_widget = checkmark;
+ }
+ }
+}
diff --git a/src/contacts-address-book-type.vala b/src/contacts-address-book-type.vala
new file mode 100644
index 0000000..b7203a0
--- /dev/null
+++ b/src/contacts-address-book-type.vala
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2022 Niels De Graef <nielsdegraef@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 2 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/>.
+ */
+
+public enum Contacts.AddressBookType {
+ LOCAL,
+ CARDDAV,
+ LDAP;
+
+ /**
+ * Returns a user-facing string representation of the type
+ */
+ public unowned string to_string () {
+ switch (this) {
+ case LOCAL:
+ return _("Local");
+ case CARDDAV:
+ return _("CardDAV");
+ case LDAP:
+ return _("LDAP");
+ }
+ return_val_if_reached (null);
+ }
+
+ /**
+ * Returns the string as expected by E.SourceBackend.set_name()
+ */
+ public unowned string to_e_backend_name () {
+ switch (this) {
+ case LOCAL:
+ return "local";
+ case CARDDAV:
+ return "carddav";
+ case LDAP:
+ return "ldap";
+ }
+ return_val_if_reached (null);
+ }
+}
diff --git a/src/contacts-preferences-window.vala b/src/contacts-preferences-window.vala
index 9c51210..5b3f1aa 100644
--- a/src/contacts-preferences-window.vala
+++ b/src/contacts-preferences-window.vala
@@ -27,8 +27,8 @@ public class Contacts.PreferencesWindow : Adw.PreferencesWindow {
Object (transient_for: transient_for, search_enabled: false);
var acc_list = new AccountsList (contacts_store);
- acc_list.title = _("Primary Address Book");
- acc_list.description = _("New contacts will be added to the selected address book. You are able to view and edit contacts from other address books.");
+ acc_list.title = _("Address Books");
+ acc_list.description = _("New contacts will be stored in the selected primary address book");
this.address_books_page.add (acc_list);
acc_list.notify["selected-store"].connect ((obj, pspec) => {
@@ -36,18 +36,34 @@ public class Contacts.PreferencesWindow : Adw.PreferencesWindow {
contacts_store.set_primary_address_book (edsf_store);
});
- var goa_button_content = new Adw.ButtonContent ();
- goa_button_content.label = _("_Online Accounts");
- goa_button_content.use_underline = true;
- goa_button_content.icon_name = "external-link-symbolic";
- var goa_button = new Gtk.Button ();
- goa_button.set_child (goa_button_content);
+ var add_accounts_group = new Adw.PreferencesGroup ();
+ add_accounts_group.title = _("Add address book");
+
+ var goa_row = new Adw.ActionRow ();
+ goa_row.title = _("GNOME Online Accounts");
+ var goa_button = new Gtk.Button.from_icon_name ("external-link-symbolic");
goa_button.tooltip_text = _("Opens the Online Accounts panel in GNOME Settings");
- goa_button.margin_top = 36;
- goa_button.halign = Gtk.Align.CENTER;
- goa_button.add_css_class ("pill");
+ goa_button.add_css_class ("flat");
goa_button.clicked.connect (on_goa_button_clicked);
- acc_list.add (goa_button);
+ goa_row.add_suffix (goa_button);
+ goa_row.activatable_widget = goa_button;
+ add_accounts_group.add (goa_row);
+
+ var carddav_row = new Adw.ActionRow ();
+ carddav_row.title = _("CardDAV account");
+ var carddav_button = new Gtk.Button.from_icon_name ("go-next-symbolic");
+ carddav_button.clicked.connect (on_carddav_button_clicked);
+ carddav_button.add_css_class ("flat");
+ carddav_row.add_suffix (carddav_button);
+ carddav_row.activatable_widget = carddav_button;
+ add_accounts_group.add (carddav_row);
+
+ this.address_books_page.add (add_accounts_group);
+ }
+
+ private void on_carddav_button_clicked (Gtk.Button add_account_button) {
+ var dialog = new AddAddressBookDialog (this);
+ dialog.present ();
}
private void on_goa_button_clicked (Gtk.Button goa_button) {
diff --git a/src/meson.build b/src/meson.build
index 87a9fd0..24abaa0 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -83,6 +83,8 @@ libcontacts_dep = declare_dependency(
# The gnome-contacts binary
contacts_vala_sources = files(
'contacts-accounts-list.vala',
+ 'contacts-address-book-type.vala',
+ 'contacts-add-address-book-dialog.vala',
'contacts-app.vala',
'contacts-avatar.vala',
'contacts-avatar-selector.vala',