diff options
author | Niels De Graef <nielsdegraef@gmail.com> | 2023-01-14 15:22:10 +0100 |
---|---|---|
committer | Niels De Graef <ndegraef@redhat.com> | 2023-01-14 15:22:10 +0100 |
commit | 23056a22f54bdcec7b3a5229b2a30a934aaa0f2f (patch) | |
tree | cca4eb10e9f5b6cca441aeaf0ced2c3f90dcdf29 | |
parent | c4ac635f4461f725bc7a5f007e302de73cf24613 (diff) | |
download | gnome-contacts-nielsdg/address-books.tar.gz |
preferences: Allow adding CardDAV address booksnielsdg/address-books
-rw-r--r-- | data/contacts.gresource.xml | 1 | ||||
-rw-r--r-- | data/ui/contacts-add-address-book-dialog.ui | 154 | ||||
-rw-r--r-- | src/contacts-add-address-book-dialog.vala | 270 | ||||
-rw-r--r-- | src/contacts-address-book-type.vala | 52 | ||||
-rw-r--r-- | src/contacts-preferences-window.vala | 40 | ||||
-rw-r--r-- | src/meson.build | 2 |
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', |