[calls/wip/ui-manage-accounts: 20/26] Add CallsAccountOverview: A UI to allow managing VoIP accounts




commit d87f217584b800448e0179d5df2870b59ac17312
Author: Evangelos Ribeiro Tzaras <evangelos tzaras puri sm>
Date:   Tue May 11 18:18:35 2021 +0200

    Add CallsAccountOverview: A UI to allow managing VoIP accounts

 src/calls-account-overview.c | 665 +++++++++++++++++++++++++++++++++++++++++++
 src/calls-account-overview.h |  41 +++
 src/calls-account-row.c      | 276 ++++++++++++++++++
 src/calls-account-row.h      |  47 +++
 src/calls-application.c      |  18 ++
 src/calls-main-window.c      |  20 ++
 src/calls-main-window.h      |   9 +-
 src/calls.gresources.xml     |   2 +
 src/meson.build              |   2 +
 src/ui/account-overview.ui   | 250 ++++++++++++++++
 src/ui/account-row.ui        |  41 +++
 src/ui/main-window.ui        |  17 +-
 12 files changed, 1376 insertions(+), 12 deletions(-)
---
diff --git a/src/calls-account-overview.c b/src/calls-account-overview.c
new file mode 100644
index 00000000..feb91958
--- /dev/null
+++ b/src/calls-account-overview.c
@@ -0,0 +1,665 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * This file is part of Calls.
+ *
+ * Calls 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.
+ *
+ * Calls 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 Calls.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Evangelos Ribeiro Tzaras <evangelos tzaras puri sm>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#define G_LOG_DOMAIN "CallsAccountOverview"
+
+#include "calls-account.h"
+#include "calls-account-overview.h"
+#include "calls-account-row.h"
+#include "calls-credentials.h"
+#include "calls-manager.h"
+
+
+/**
+ * Section:calls-account-overview
+ * short_description: A #HdyWindow to manage VoIP accounts
+ * @Title: CallsAccountOverview
+ *
+ * This is a #HdyWindow derived window to display and manage the
+ * VoIP accounts (currently only SIP). This could later become a
+ * #HdyPreferencesPage in a #HdyPreferencesWindow.
+ */
+
+typedef enum {
+  SHOW_INTRO = 0,
+  SHOW_OVERVIEW,
+  SHOW_ADD_ACCOUNT,
+  SHOW_EDIT_ACCOUNT,
+} CallsAccountOverviewState;
+
+
+enum {
+  PROP_0,
+  PROP_MANAGER,
+  PROP_LAST_PROP
+};
+static GParamSpec *props[PROP_LAST_PROP];
+
+struct _CallsAccountOverview {
+  HdyWindow                  parent;
+
+  /* Header bar elements */
+  GtkWidget                 *header_add;
+  GtkWidget                 *header_edit;
+  GtkSpinner                *spinner;
+  GtkWidget                 *login_btn;
+  GtkWidget                 *apply_btn;
+  GtkWidget                 *delete_btn;
+
+  /* UI widgets */
+  GtkStack                  *stack;
+  GtkWidget                 *intro;
+  GtkWidget                 *overview;
+  GtkWidget                 *manage;
+  GtkWidget                 *add_btn;
+  GtkWidget                 *add_row;
+
+  /* add/edit page */
+  GtkWidget                 *form;
+  GtkWidget                 *host;
+  GtkWidget                 *display_name;
+  GtkWidget                 *user;
+  GtkWidget                 *password;
+  GtkWidget                 *port;
+  GtkWidget                 *protocol;
+  GListStore                *protocols; /* bound model for HdyComboRow */
+
+  /* misc */
+  CallsAccountOverviewState  state;
+  CallsAccountManager       *manager;
+
+  /* current credentials which are being edited */
+  CallsCredentials          *current_credentials;
+  const char                *current_uuid;
+  CallsCredentialsType       current_credentials_type;
+
+  gboolean                   user_updated_credentials;
+  gboolean                   connecting;
+  guint                      last_credentials;
+};
+
+G_DEFINE_TYPE (CallsAccountOverview, calls_account_overview, HDY_TYPE_WINDOW)
+
+
+static void
+update_stack_visibility (CallsAccountOverview *self)
+{
+  g_assert (CALLS_IS_ACCOUNT_OVERVIEW (self));
+
+  switch (self->state) {
+  case SHOW_INTRO:
+    gtk_stack_set_visible_child (self->stack, self->intro);
+    gtk_widget_hide (self->form);
+    break;
+  case SHOW_OVERVIEW:
+    gtk_stack_set_visible_child (self->stack, self->overview);
+    gtk_widget_hide (self->form);
+    break;
+  case SHOW_ADD_ACCOUNT:
+  case SHOW_EDIT_ACCOUNT:
+    gtk_window_present (GTK_WINDOW (self->form));
+    break;
+
+  default:
+    g_assert_not_reached();
+  }
+}
+
+/* TODO we might also want a is_form_valid() performing some checks */
+static gboolean
+is_form_filled (CallsAccountOverview *self)
+{
+  g_assert (CALLS_IS_ACCOUNT_OVERVIEW (self));
+
+  return
+    g_strcmp0 (gtk_entry_get_text (GTK_ENTRY (self->host)), "") != 0 &&
+    g_strcmp0 (gtk_entry_get_text (GTK_ENTRY (self->user)), "") != 0 &&
+    g_strcmp0 (gtk_entry_get_text (GTK_ENTRY (self->password)), "") != 0 &&
+    g_strcmp0 (gtk_entry_get_text (GTK_ENTRY (self->port)), "") != 0;
+}
+
+
+static void
+on_text_changed (CallsAccountOverview *self)
+{
+  self->user_updated_credentials = TRUE;
+
+  gtk_widget_set_sensitive (self->login_btn,
+                            is_form_filled (self));
+
+  gtk_widget_set_sensitive (self->apply_btn,
+                            self->user_updated_credentials &&
+                            is_form_filled (self));
+}
+
+
+static void
+update_header_visibility (CallsAccountOverview *self)
+{
+  g_assert (CALLS_IS_ACCOUNT_OVERVIEW (self));
+
+  switch (self->state) {
+  case SHOW_INTRO:
+  case SHOW_OVERVIEW:
+    gtk_widget_hide (self->header_add);
+    gtk_widget_hide (self->header_edit);
+    break;
+
+  case SHOW_ADD_ACCOUNT:
+    gtk_widget_show (self->header_add);
+    gtk_widget_hide (self->header_edit);
+    break;
+
+  case SHOW_EDIT_ACCOUNT:
+    gtk_widget_show (self->header_edit);
+    gtk_widget_hide (self->header_add);
+    break;
+
+  default:
+    g_assert_not_reached ();
+  }
+
+  if (self->connecting)
+    gtk_spinner_start (self->spinner);
+  else
+    gtk_spinner_stop (self->spinner);
+}
+
+
+static void
+update_visibility (CallsAccountOverview *self)
+{
+  g_assert (CALLS_IS_ACCOUNT_OVERVIEW (self));
+
+  update_header_visibility (self);
+  update_stack_visibility (self);
+}
+
+
+static void
+clear_credentials (CallsAccountOverview *self)
+{
+  g_assert (CALLS_IS_ACCOUNT_OVERVIEW (self));
+
+  gtk_entry_set_text (GTK_ENTRY (self->host), "");
+  gtk_entry_set_text (GTK_ENTRY (self->display_name), "");
+  gtk_entry_set_text (GTK_ENTRY (self->user), "");
+  gtk_entry_set_text (GTK_ENTRY (self->password), "");
+  gtk_entry_set_text (GTK_ENTRY (self->port), "0");
+  hdy_combo_row_set_selected_index (HDY_COMBO_ROW (self->protocol), 0);
+
+  self->user_updated_credentials = FALSE;
+
+  g_clear_object (&self->current_credentials);
+
+  self->current_uuid = NULL;
+
+  update_header_visibility (self);
+}
+
+
+static gboolean
+find_protocol (CallsAccountOverview *self,
+               const char           *protocol,
+               guint                *index)
+{
+  guint len;
+  g_assert (CALLS_IS_ACCOUNT_OVERVIEW (self));
+
+  len = g_list_model_get_n_items (G_LIST_MODEL (self->protocols));
+  for (guint i = 0; i < len; i++) {
+    g_autoptr (HdyValueObject) obj =
+      g_list_model_get_item (G_LIST_MODEL (self->protocols), i);
+    const char *prot = hdy_value_object_get_string (obj);
+
+    if (g_strcmp0 (protocol, prot) == 0) {
+      if (index)
+        *index = i;
+      return TRUE;
+    }
+  }
+
+  g_debug ("Could not find protocol '%s'", protocol);
+
+  return FALSE;
+}
+
+
+static void
+edit_credentials (CallsAccountOverview *self,
+                  CallsCredentials     *credentials)
+{
+  g_autofree char *host = NULL;
+  g_autofree char *display_name = NULL;
+  g_autofree char *user = NULL;
+  g_autofree char *password = NULL;
+  gint port;
+  g_autofree char *port_str = NULL;
+  g_autofree char *protocol = NULL;
+  guint protocol_index;
+
+  g_assert (CALLS_IS_ACCOUNT_OVERVIEW (self));
+
+  if (credentials == NULL) {
+    clear_credentials (self);
+    return;
+  }
+
+  g_assert (CALLS_IS_CREDENTIALS (credentials));
+
+  g_object_get (credentials,
+                "host", &host,
+                "display-name", &display_name,
+                "user", &user,
+                "password", &password,
+                "port", &port,
+                "protocol", &protocol,
+                NULL);
+
+  port_str = g_strdup_printf ("%d", port);
+
+  /* The following should always succeed,
+     TODO inform user in the error case
+     related issue #275 https://source.puri.sm/Librem5/calls/-/issues/275
+  */
+  if (!find_protocol (self, protocol, &protocol_index))
+    protocol_index = 0;
+
+  /* set UI elements */
+  gtk_entry_set_text (GTK_ENTRY (self->host), host);
+  gtk_entry_set_text (GTK_ENTRY (self->display_name), display_name);
+  gtk_entry_set_text (GTK_ENTRY (self->user), user);
+  gtk_entry_set_text (GTK_ENTRY (self->password), password);
+  gtk_entry_set_text (GTK_ENTRY (self->port), port_str);
+  hdy_combo_row_set_selected_index (HDY_COMBO_ROW (self->protocol), protocol_index);
+
+  self->user_updated_credentials = FALSE;
+
+  g_clear_object (&self->current_credentials);
+  self->current_credentials = g_object_ref (credentials);
+
+  self->current_uuid = calls_credentials_get_uuid (credentials);
+}
+
+
+static CallsCredentials *
+credentials_from_form (CallsAccountOverview *self)
+{
+  g_autoptr (CallsCredentials) creds = NULL;
+  g_autoptr (HdyValueObject) obj = NULL;
+  GListModel *protocol_model;
+  const char *protocol;
+  g_autofree char *credentials_name = NULL;
+  gint index;
+  gint port;
+
+  g_assert (CALLS_IS_ACCOUNT_OVERVIEW (self));
+
+  credentials_name = g_strdup_printf ("%d-%s", self->last_credentials++,
+                                     gtk_entry_get_text (GTK_ENTRY (self->host)));
+
+  g_debug ("Trying to login");
+
+  protocol_model = hdy_combo_row_get_model (HDY_COMBO_ROW (self->protocol));
+  index = hdy_combo_row_get_selected_index (HDY_COMBO_ROW (self->protocol));
+  obj = g_list_model_get_item (protocol_model, index);
+
+  protocol = hdy_value_object_get_string (obj);
+
+  port = atoi (gtk_entry_get_text (GTK_ENTRY (self->port)));
+
+  creds = calls_credentials_new (self->current_credentials_type, credentials_name);
+
+  g_object_set (creds,
+                "host", gtk_entry_get_text (GTK_ENTRY (self->host)),
+                "user", gtk_entry_get_text (GTK_ENTRY (self->user)),
+                "password", gtk_entry_get_text (GTK_ENTRY (self->password)),
+                "display-name", gtk_entry_get_text (GTK_ENTRY (self->display_name)),
+                "port", port,
+                "protocol", protocol,
+                "auto-connect", TRUE,
+                NULL);
+
+  return g_steal_pointer (&creds);
+}
+
+
+static void
+on_login_clicked (CallsAccountOverview *self)
+{
+  g_autoptr (CallsCredentials) creds = NULL;
+
+  g_assert (CALLS_IS_ACCOUNT_OVERVIEW (self));
+
+  creds = credentials_from_form (self);
+
+  calls_account_manager_add_credentials (self->manager, creds);
+}
+
+
+static void
+on_delete_clicked (CallsAccountOverview *self)
+{
+  g_assert (CALLS_IS_ACCOUNT_OVERVIEW (self));
+
+  g_debug ("Deleting account");
+  calls_account_manager_remove_credentials (self->manager, self->current_uuid);
+}
+
+
+static void
+on_apply_clicked (CallsAccountOverview *self)
+{
+  g_autoptr (CallsCredentials) new_creds = NULL;
+  g_assert (CALLS_IS_ACCOUNT_OVERVIEW (self));
+
+  g_debug ("Applying changes to the account");
+  new_creds = credentials_from_form (self);
+  calls_credentials_update_from_credentials (self->current_credentials, new_creds);
+
+  self->user_updated_credentials = FALSE;
+
+  self->state = SHOW_OVERVIEW;
+
+  update_visibility (self);
+}
+
+
+static void
+on_add_account_clicked (CallsAccountOverview *self)
+{
+  g_assert (CALLS_IS_ACCOUNT_OVERVIEW (self));
+
+  /* TODO this is a hack we can get around, as long as we don't have
+   * additional CallsAccountProvider's */
+  self->current_credentials_type = CALLS_CREDENTIALS_TYPE_SIP;
+
+  clear_credentials (self);
+
+  self->state = SHOW_ADD_ACCOUNT;
+  update_visibility (self);
+}
+
+
+static void
+on_row_activated (GtkListBox    *box,
+                  GtkListBoxRow *row,
+                  CallsAccountOverview *self)
+{
+  CallsAccountRow *acc_row;
+  const char *uuid;
+
+  if (GTK_WIDGET (row) == self->add_row) {
+    on_add_account_clicked (self);
+    return;
+  }
+
+  acc_row = CALLS_ACCOUNT_ROW (row);
+  uuid = calls_account_row_get_uuid (acc_row);
+
+  calls_account_overview_edit_account (self, uuid);
+}
+
+
+static void
+on_manager_state_changed (CallsAccountOverview *self)
+{
+  /* Is there a provider to add an account to? */
+  switch (calls_account_manager_get_state (self->manager)) {
+  case CALLS_ACCOUNT_MANAGER_NULL:
+    return;
+
+  case CALLS_ACCOUNT_MANAGER_INIT:
+    self->state = SHOW_INTRO;
+    gtk_widget_set_sensitive (self->add_btn, FALSE);
+    gtk_widget_set_sensitive (self->add_row, FALSE);
+    break;
+
+  case CALLS_ACCOUNT_MANAGER_ONLY_CREDENTIALS:
+    self->state = SHOW_OVERVIEW;
+    gtk_widget_set_sensitive (self->add_btn, FALSE);
+    gtk_widget_set_sensitive (self->add_row, FALSE);
+    break;
+
+  case CALLS_ACCOUNT_MANAGER_ONLY_PROVIDER:
+    self->state = SHOW_INTRO;
+    gtk_widget_set_sensitive (self->add_btn, TRUE);
+    gtk_widget_set_sensitive (self->add_row, TRUE);
+    break;
+
+  case CALLS_ACCOUNT_MANAGER_READY:
+    self->state = SHOW_OVERVIEW;
+    gtk_widget_set_sensitive (self->add_btn, TRUE);
+    gtk_widget_set_sensitive (self->add_row, TRUE);
+    break;
+
+  default:
+    g_assert_not_reached ();
+  }
+
+  update_visibility (self);
+}
+
+
+static void
+on_credentials_items_changed (GListModel           *credentials,
+                              guint                 position,
+                              guint                 removed,
+                              guint                 added,
+                              CallsAccountOverview *self)
+{
+  g_debug ("Add/remove CallsAccountRows");
+
+  for (gint i = 0; i < removed; i++) {
+    GtkListBoxRow *row =
+      gtk_list_box_get_row_at_index (GTK_LIST_BOX (self->overview), i);
+
+    gtk_widget_destroy (GTK_WIDGET (row));
+  }
+
+  for (guint i = 0; i < added; i++) {
+    g_autoptr (CallsCredentials) creds = NULL;
+    CallsAccountRow *cred_row;
+
+    creds = g_list_model_get_item (credentials, position + i);
+    cred_row = calls_account_row_new (creds);
+    gtk_list_box_insert (GTK_LIST_BOX (self->overview),
+                         GTK_WIDGET (cred_row),
+                         position + i);
+
+    self->last_credentials++;
+  }
+}
+
+
+static void
+calls_account_overview_set_property (GObject      *object,
+                                     guint         property_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+  CallsAccountOverview *self = CALLS_ACCOUNT_OVERVIEW (object);
+
+  switch (property_id) {
+  case PROP_MANAGER:
+    self->manager = g_value_dup_object (value);
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    break;
+  }
+}
+
+
+static void
+calls_account_overview_constructed (GObject *object)
+{
+  CallsAccountOverview *self = CALLS_ACCOUNT_OVERVIEW (object);
+  guint n_items;
+  GListModel *credentials;
+
+  g_signal_connect_swapped (self->manager,
+                            "notify::state",
+                            G_CALLBACK (on_manager_state_changed),
+                            self);
+
+  credentials = calls_account_manager_get_credentials_list (self->manager);
+  g_signal_connect (credentials,
+                    "items-changed",
+                    G_CALLBACK (on_credentials_items_changed),
+                    self);
+
+  n_items = g_list_model_get_n_items (credentials);
+  on_credentials_items_changed (credentials, 0, 0, n_items, self);
+
+  gtk_list_box_insert (GTK_LIST_BOX (self->overview),
+                       GTK_WIDGET (self->add_row),
+                       n_items);
+
+  on_manager_state_changed (self);
+
+  update_visibility (self);
+
+  G_OBJECT_CLASS (calls_account_overview_parent_class)->constructed (object);
+}
+
+
+static void
+calls_account_overview_dispose (GObject *object)
+{
+  CallsAccountOverview *self = CALLS_ACCOUNT_OVERVIEW (object);
+
+  g_clear_object (&self->manager);
+
+  G_OBJECT_CLASS (calls_account_overview_parent_class)->dispose (object);
+}
+
+
+static void
+calls_account_overview_class_init (CallsAccountOverviewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->constructed = calls_account_overview_constructed;
+  object_class->dispose = calls_account_overview_dispose;
+  object_class->set_property = calls_account_overview_set_property;
+
+  props[PROP_MANAGER] =
+    g_param_spec_object ("account-manager",
+                         "Account manager",
+                         "The account manager",
+                         CALLS_TYPE_ACCOUNT_MANAGER,
+                         G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY);
+
+  g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/sm/puri/calls/ui/account-overview.ui");
+  gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, header_add);
+  gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, header_edit);
+
+  gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, spinner);
+  gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, login_btn);
+  gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, delete_btn);
+  gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, apply_btn);
+
+  gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, add_btn);
+  gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, add_row);
+
+  gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, stack);
+  gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, intro);
+  gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, overview);
+  gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, manage);
+
+  gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, form);
+  gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, host);
+  gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, display_name);
+  gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, user);
+  gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, password);
+  gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, port);
+  gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, protocol);
+
+  gtk_widget_class_bind_template_callback (widget_class, on_login_clicked);
+  gtk_widget_class_bind_template_callback (widget_class, on_delete_clicked);
+  gtk_widget_class_bind_template_callback (widget_class, on_apply_clicked);
+  gtk_widget_class_bind_template_callback (widget_class, on_add_account_clicked);
+  gtk_widget_class_bind_template_callback (widget_class, on_row_activated);
+  gtk_widget_class_bind_template_callback (widget_class, on_text_changed);
+}
+
+
+static void
+calls_account_overview_init (CallsAccountOverview *self)
+{
+  HdyValueObject *obj;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->protocols = g_list_store_new (HDY_TYPE_VALUE_OBJECT);
+
+  obj = hdy_value_object_new_string ("TLS");
+  g_list_store_insert (self->protocols, 0, obj);
+  g_clear_object (&obj);
+
+  obj = hdy_value_object_new_string ("TCP");
+  g_list_store_insert (self->protocols, 1, obj);
+  g_clear_object (&obj);
+
+  obj = hdy_value_object_new_string ("UDP");
+  g_list_store_insert (self->protocols, 2, obj);
+  g_clear_object (&obj);
+
+  hdy_combo_row_bind_name_model (HDY_COMBO_ROW (self->protocol),
+                                 G_LIST_MODEL (self->protocols),
+                                 (HdyComboRowGetNameFunc) hdy_value_object_dup_string,
+                                 NULL, NULL);
+}
+
+
+CallsAccountOverview *
+calls_account_overview_new (CallsAccountManager *manager)
+{
+  return g_object_new (CALLS_TYPE_ACCOUNT_OVERVIEW,
+                       "account-manager", manager,
+                       NULL);
+}
+
+
+void
+calls_account_overview_edit_account (CallsAccountOverview *self,
+                                     const char           *uuid)
+{
+  CallsCredentials *creds;
+
+  g_return_if_fail (CALLS_IS_ACCOUNT_OVERVIEW (self));
+  g_return_if_fail (uuid);
+
+  creds = calls_account_manager_get_credentials (self->manager, uuid);
+  if (creds) {
+    edit_credentials (self, creds);
+    self->state = SHOW_EDIT_ACCOUNT;
+    update_visibility (self);
+  }
+}
diff --git a/src/calls-account-overview.h b/src/calls-account-overview.h
new file mode 100644
index 00000000..4c3ab52e
--- /dev/null
+++ b/src/calls-account-overview.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * This file is part of Calls.
+ *
+ * Calls 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.
+ *
+ * Calls 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 Calls.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Evangelos Ribeiro Tzaras <evangelos tzaras puri sm>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#pragma once
+
+#include "calls-account-manager.h"
+
+#include <handy.h>
+
+G_BEGIN_DECLS
+
+#define CALLS_TYPE_ACCOUNT_OVERVIEW (calls_account_overview_get_type ())
+
+G_DECLARE_FINAL_TYPE (CallsAccountOverview, calls_account_overview, CALLS, ACCOUNT_OVERVIEW, HdyWindow)
+
+CallsAccountOverview  *calls_account_overview_new                   (CallsAccountManager  *manager);
+void                   calls_account_overview_edit_account          (CallsAccountOverview *self,
+                                                                     const char           *uuid);
+
+G_END_DECLS
diff --git a/src/calls-account-row.c b/src/calls-account-row.c
new file mode 100644
index 00000000..ddffb589
--- /dev/null
+++ b/src/calls-account-row.c
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * This file is part of Calls.
+ *
+ * Calls 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.
+ *
+ * Calls 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 Calls.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Evangelos Ribeiro Tzaras <evangelos tzaras puri sm>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#define G_LOG_DOMAIN "CallsAccountRow"
+
+#include "calls-account-row.h"
+#include "calls-credentials.h"
+
+
+/**
+ * Section:calls-account-row
+ * short_description: A #HdyActionRow for use in #CallsAccountOverview
+ * @Title: CallsAccountRow
+ *
+ * This is a #HdyActionRow derived widget representing a #CallsCredentials
+ * and associated #CallsAccount in the #CallsAccountOverview.
+ * VoIP accounts (currently only SIP). This could later become a
+ * #HdyPreferencesPage in a #HdyPreferencesWindow.
+ */
+
+
+enum {
+  PROP_0,
+  PROP_CREDENTIALS,
+  PROP_ONLINE,
+  PROP_LAST_PROP
+};
+static GParamSpec *props[PROP_LAST_PROP];
+
+enum {
+  EDIT_ACCOUNT,
+  ONLINE_SWITCHED,
+  N_SIGNALS
+};
+static guint signals[N_SIGNALS];
+
+struct _CallsAccountRow {
+  HdyActionRow          parent;
+
+  CallsCredentials     *credentials;
+  gboolean              active;
+  gboolean              online;
+
+  /* UI elements */
+  HdyAvatar            *avatar;
+  GtkSwitch            *online_switch;
+  GtkButton            *manage_btn;
+};
+
+G_DEFINE_TYPE (CallsAccountRow, calls_account_row, HDY_TYPE_ACTION_ROW);
+
+
+static void
+on_manage_clicked (CallsAccountRow *self)
+{
+  const char *uuid = calls_credentials_get_uuid (self->credentials);
+
+  g_signal_emit (self, signals[EDIT_ACCOUNT], 0, uuid);
+}
+
+
+static void
+on_online_switched (CallsAccountRow *self)
+{
+  const char *uuid = calls_credentials_get_uuid (self->credentials);
+  gboolean online = gtk_switch_get_active (self->online_switch);
+
+  g_signal_emit (self, signals[ONLINE_SWITCHED], 0, uuid, online);
+}
+
+
+static void
+on_credentials_updated (CallsAccountRow *self)
+{
+  g_autofree char *display_name = NULL;
+  g_autofree char *user = NULL;
+  g_autofree char *host = NULL;
+  const char *title;
+  const char *subtitle;
+
+  g_object_get (G_OBJECT (self->credentials),
+                "display-name", &display_name,
+                "user", &user,
+                "host", &host,
+                NULL);
+
+  if (display_name && g_strcmp0 (display_name, "") != 0) {
+    title = display_name;
+    subtitle = host;
+
+  } else {
+    title = user;
+    subtitle = host;
+  }
+
+  hdy_preferences_row_set_title (HDY_PREFERENCES_ROW (self), title);
+  hdy_action_row_set_subtitle (HDY_ACTION_ROW (self), subtitle);
+  hdy_avatar_set_text (self->avatar, user);
+}
+
+static void
+calls_account_row_set_property (GObject      *object,
+                                guint         property_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  CallsAccountRow *self = CALLS_ACCOUNT_ROW (object);
+
+  switch (property_id) {
+  case PROP_CREDENTIALS:
+    self->credentials = g_value_get_object (value);
+    break;
+
+  case PROP_ONLINE:
+    calls_account_row_set_online (self, g_value_get_boolean (value));
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    break;
+  }
+}
+
+static void
+calls_account_row_get_property (GObject      *object,
+                                guint         property_id,
+                                GValue       *value,
+                                GParamSpec   *pspec)
+{
+  CallsAccountRow *self = CALLS_ACCOUNT_ROW (object);
+
+  switch (property_id) {
+  case PROP_ONLINE:
+    g_value_set_boolean (value, calls_account_row_get_online (self));
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    break;
+  }
+}
+
+static void
+calls_account_row_constructed (GObject *object)
+{
+  CallsAccountRow *self = CALLS_ACCOUNT_ROW (object);
+
+  g_signal_connect_swapped (self->credentials,
+                            "credentials-updated",
+                            G_CALLBACK (on_credentials_updated),
+                            self);
+
+  on_credentials_updated (self);
+}
+
+static void
+calls_account_row_class_init (CallsAccountRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->set_property = calls_account_row_set_property;
+  object_class->get_property = calls_account_row_get_property;
+  object_class->constructed = calls_account_row_constructed;
+
+  props[PROP_CREDENTIALS] =
+    g_param_spec_object ("credentials",
+                         "Credentials",
+                         "The credentials this row represents",
+                         CALLS_TYPE_CREDENTIALS,
+                         G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY |
+                         G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  props[PROP_ONLINE] =
+    g_param_spec_boolean ("online",
+                          "online",
+                          "The state of the online switch",
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
+
+  signals[EDIT_ACCOUNT] =
+    g_signal_new ("edit-account",
+                  CALLS_TYPE_ACCOUNT_ROW,
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  1, G_TYPE_STRING);
+
+  signals[ONLINE_SWITCHED] =
+    g_signal_new ("online-switch-changed",
+                  CALLS_TYPE_ACCOUNT_ROW,
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  2, G_TYPE_STRING, G_TYPE_BOOLEAN);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/sm/puri/calls/ui/account-row.ui");
+  gtk_widget_class_bind_template_child (widget_class, CallsAccountRow, avatar);
+  gtk_widget_class_bind_template_child (widget_class, CallsAccountRow, online_switch);
+  gtk_widget_class_bind_template_child (widget_class, CallsAccountRow, manage_btn);
+
+ gtk_widget_class_bind_template_callback (widget_class, on_manage_clicked);
+ gtk_widget_class_bind_template_callback (widget_class, on_online_switched);
+}
+
+
+static void
+calls_account_row_init (CallsAccountRow *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+
+CallsAccountRow *
+calls_account_row_new (CallsCredentials     *credentials)
+{
+  g_return_val_if_fail (CALLS_IS_CREDENTIALS (credentials), NULL);
+
+  return g_object_new (CALLS_TYPE_ACCOUNT_ROW,
+                       "credentials", credentials,
+                       NULL);
+}
+
+
+const char *
+calls_account_row_get_uuid (CallsAccountRow *self)
+{
+  g_return_val_if_fail (CALLS_IS_ACCOUNT_ROW (self), NULL);
+
+  return calls_credentials_get_uuid (self->credentials);
+}
+
+
+gboolean
+calls_account_row_get_online (CallsAccountRow *self)
+{
+  g_return_val_if_fail (CALLS_IS_ACCOUNT_ROW (self), FALSE);
+
+  return gtk_switch_get_active (self->online_switch);
+}
+
+
+void
+calls_account_row_set_online (CallsAccountRow *self,
+                              gboolean         online)
+{
+  g_return_if_fail (CALLS_IS_ACCOUNT_ROW (self));
+
+  if (online == gtk_switch_get_active (self->online_switch))
+    return;
+
+  gtk_switch_set_active (self->online_switch, online);
+}
diff --git a/src/calls-account-row.h b/src/calls-account-row.h
new file mode 100644
index 00000000..eccaa8e9
--- /dev/null
+++ b/src/calls-account-row.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * This file is part of Calls.
+ *
+ * Calls 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.
+ *
+ * Calls 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 Calls.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Evangelos Ribeiro Tzaras <evangelos tzaras puri sm>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#pragma once
+
+#include "calls-credentials.h"
+
+#include <handy.h>
+
+
+G_BEGIN_DECLS
+
+#define CALLS_TYPE_ACCOUNT_ROW (calls_account_row_get_type ())
+
+G_DECLARE_FINAL_TYPE (CallsAccountRow, calls_account_row, CALLS, ACCOUNT_ROW, HdyActionRow)
+
+CallsAccountRow      *calls_account_row_new              (CallsCredentials *credentials);
+const char           *calls_account_row_get_uuid         (CallsAccountRow  *self);
+gboolean              calls_account_row_get_active       (CallsAccountRow  *self);
+void                  calls_account_row_set_active       (CallsAccountRow  *self,
+                                                          gboolean          active);
+gboolean              calls_account_row_get_online       (CallsAccountRow  *self);
+void                  calls_account_row_set_online       (CallsAccountRow  *self,
+                                                          gboolean          online);
+
+G_END_DECLS
diff --git a/src/calls-application.c b/src/calls-application.c
index 7042f0c4..1a86b128 100644
--- a/src/calls-application.c
+++ b/src/calls-application.c
@@ -350,6 +350,22 @@ copy_number (GSimpleAction *action,
   g_debug ("Copied `%s' to clipboard", number);
 }
 
+static void
+show_accounts (GSimpleAction *action,
+               GVariant      *parameter,
+               gpointer       user_data)
+{
+  CallsApplication *app = CALLS_APPLICATION (g_application_get_default ());
+  calls_main_window_show_accounts_overview (app->main_window);
+  //GtkWindow *dialog = calls_main_window_get_accounts_dialog
+  //if (GTK_WINDOW (app->m))
+  //if (GTK_WINDOW (user_data) != gtk_window_get_transient_for (GTK_WINDOW (dialog)))
+  //  gtk_window_set_transient_for (GTK_WINDOW (dialog),
+  //                                GTK_WINDOW (user_data));
+
+  //gtk_window_present
+}
+
 static void
 manager_state_changed_cb (GApplication *application)
 {
@@ -366,6 +382,8 @@ static const GActionEntry actions[] =
   { "set-daemon", set_daemon_action, NULL },
   { "dial", dial_action, "s" },
   { "copy-number", copy_number, "s"},
+  //TODO{ "about", show_about, NULL},
+  { "accounts", show_accounts, NULL},
 };
 
 
diff --git a/src/calls-main-window.c b/src/calls-main-window.c
index fb2da480..73323928 100644
--- a/src/calls-main-window.c
+++ b/src/calls-main-window.c
@@ -23,6 +23,7 @@
  */
 
 #include "calls-main-window.h"
+#include "calls-account-overview.h"
 #include "calls-origin.h"
 #include "calls-ussd.h"
 #include "calls-call-selector-item.h"
@@ -54,6 +55,7 @@ struct _CallsMainWindow
   GtkRevealer *permanent_error_revealer;
   GtkLabel *permanent_error_label;
 
+  CallsAccountOverview *account_overview;
   CallsNewCallBox *new_call;
 
   GtkDialog  *ussd_dialog;
@@ -426,6 +428,7 @@ dispose (GObject *object)
   CallsMainWindow *self = CALLS_MAIN_WINDOW (object);
 
   g_clear_object (&self->record_store);
+  g_clear_object (&self->account_overview);
 
   G_OBJECT_CLASS (calls_main_window_parent_class)->dispose (object);
 }
@@ -531,3 +534,20 @@ calls_main_window_dial (CallsMainWindow *self,
       calls_new_call_box_dial (self->new_call, target);
     }
 }
+
+void
+calls_main_window_show_accounts_overview (CallsMainWindow *self)
+{
+  CallsAccountManager *manager;
+
+  g_return_if_fail (CALLS_IS_MAIN_WINDOW (self));
+
+  if (self->account_overview == NULL) {
+    manager = calls_manager_get_account_manager (calls_manager_get_default ());
+    self->account_overview = calls_account_overview_new (manager);
+    gtk_window_set_transient_for (GTK_WINDOW (self->account_overview),
+                                  GTK_WINDOW (self));
+  }
+
+  gtk_window_present (GTK_WINDOW (self->account_overview));
+}
diff --git a/src/calls-main-window.h b/src/calls-main-window.h
index 26277044..952ccc13 100644
--- a/src/calls-main-window.h
+++ b/src/calls-main-window.h
@@ -33,10 +33,11 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (CallsMainWindow, calls_main_window, CALLS, MAIN_WINDOW, GtkApplicationWindow);
 
-CallsMainWindow *calls_main_window_new  (GtkApplication  *application,
-                                         GListModel      *record_store);
-void             calls_main_window_dial (CallsMainWindow *self,
-                                         const gchar     *target);
+CallsMainWindow       *calls_main_window_new                      (GtkApplication  *application,
+                                                                   GListModel      *record_store);
+void                   calls_main_window_dial                     (CallsMainWindow *self,
+                                                                   const gchar     *target);
+void                   calls_main_window_show_accounts_overview   (CallsMainWindow *self);
 
 G_END_DECLS
 
diff --git a/src/calls.gresources.xml b/src/calls.gresources.xml
index e413bc0c..2b357a57 100644
--- a/src/calls.gresources.xml
+++ b/src/calls.gresources.xml
@@ -14,6 +14,8 @@
     <file preprocess="xml-stripblanks">in-app-notification.ui</file>
     <file preprocess="xml-stripblanks">contacts-row.ui</file>
     <file preprocess="xml-stripblanks">contacts-box.ui</file>
+    <file preprocess="xml-stripblanks">account-overview.ui</file>
+    <file preprocess="xml-stripblanks">account-row.ui</file>
   </gresource>
   <gresource prefix="/sm/puri/calls/">
     <file>style.css</file>
diff --git a/src/meson.build b/src/meson.build
index 65152bd5..552b65cf 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -120,6 +120,8 @@ calls_sources = files(['calls-message-source.c', 'calls-message-source.h',
                        'calls-account.c', 'calls-account.h',
                        'calls-account-manager.c', 'calls-account-manager.h',
                        'calls-account-provider.c', 'calls-account-provider.h',
+                       'calls-account-overview.c', 'calls-account-overview.h',
+                       'calls-account-row.c', 'calls-account-row.h',
                        'calls-settings.c', 'calls-settings.h',
                       ]) + wayland_sources + calls_generated_sources
 
diff --git a/src/ui/account-overview.ui b/src/ui/account-overview.ui
new file mode 100644
index 00000000..e7954776
--- /dev/null
+++ b/src/ui/account-overview.ui
@@ -0,0 +1,250 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.24"/>
+  <requires lib="libhandy" version="1.0"/>
+  <template class="CallsAccountOverview" parent="HdyWindow">
+    <property name="visible">True</property>
+    <property name="default-width">380</property>
+    <property name="default-height">660</property>
+    <signal name="delete-event" handler="gtk_widget_hide_on_delete"/>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="HdyHeaderBar">
+            <property name="title" translatable="yes">VoIP Accounts</property>
+            <property name="show-close-button">True</property>
+            <property name="visible">True</property>
+            <child>
+              <object class="GtkSpinner" id="spinner">
+                <property name="visible">True</property>
+              </object>
+              <packing>
+                <property name="pack_type">end</property>
+              </packing>
+            </child>
+
+          </object>
+        </child>
+
+        <child>
+          <object class="GtkStack" id="stack">
+            <property name="visible">True</property>
+            <property name="vexpand">True</property>
+            <property name="transition-type">crossfade</property>
+
+            <!-- First child type: No accounts present: Show a blurb and a Add button -->
+            <child>
+              <object class="HdyStatusPage" id="intro">
+                <property name="visible">True</property>
+                <property name="title" translatable="yes">Add VoIP Accounts</property>
+                <property name="icon-name">system-users-symbolic</property>
+                <property name="description">You can add VoIP account here. It will allow you to place and 
receive VoIP calls using the SIP protocol. This feature is still relatively new and not yet feature complete 
(i.e. no encrypted media).</property>
+                <child>
+                  <object class="GtkButton" id="add_btn">
+                    <property name="visible">True</property>
+                    <property name="margin">6</property>
+                    <property name="halign">center</property>
+                    <property name="use-underline">True</property>
+                    <property name="label" translatable="yes">_Add Account</property>
+                    <signal name="clicked" handler="on_add_account_clicked" swapped="yes"/>
+                  </object>
+                </child>
+
+              </object>
+              <packing>
+                <property name="name">intro-page</property>
+              </packing>
+            </child>
+
+            <!-- Second child type: Some accounts present: Show a Listbox to manage accounts -->
+            <child>
+              <object class="GtkListBox" id="overview">
+                <property name="visible">True</property>
+                <signal name="row-activated" handler="on_row_activated" swapped="no"/>
+                <!-- placeholder -->
+                <child type="placeholder"/>
+              </object>
+              <packing>
+                <property name="name">overview-page</property>
+              </packing>
+            </child>
+
+          </object>
+        </child>
+
+      </object>
+    </child>
+  </template>
+
+  <object class="HdyActionRow" id="add_row">
+    <property name="visible">True</property>
+    <property name="use-underline">True</property>
+    <property name="activatable">True</property>
+    <property name="title" translatable="yes">_Add Account</property>
+  </object>
+
+  <object class="HdyWindow" id="form">
+    <property name="default-width">380</property>
+    <property name="default-height">660</property>
+    <signal name="delete-event" handler="gtk_widget_hide_on_delete"/>
+    <child>
+      <object class="GtkBox" id="manage">
+        <property name="visible">True</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="HdyHeaderBar" id="header_add">
+            <property name="title" translatable="yes">Add Account</property>
+            <property name="show-close-button">True</property>
+            <child>
+              <object class="GtkButton" id="login_btn">
+                <property name="visible">True</property>
+                <property name="use-underline">True</property>
+                <property name="label" translatable="yes">_Log In</property>
+                <signal name="clicked" handler="on_login_clicked" swapped="yes"/>
+              </object>
+              <packing>
+                <property name="pack_type">end</property>
+              </packing>
+            </child>
+            
+          </object>
+        </child>
+        
+        <child>
+          <object class="HdyHeaderBar" id="header_edit">
+            <property name="visible">True</property>
+            <property name="show-close-button">True</property>
+            <property name="title" translatable="yes">Manage Account</property>
+            <child>
+              <object class="GtkButton" id="delete_btn">
+                <property name="visible">True</property>
+                <property name="use-underline">True</property>
+                <property name="label" translatable="yes">_Delete</property>
+                <signal name="clicked" handler="on_delete_clicked" swapped="yes"/>
+              </object>
+              <packing>
+                <property name="pack_type">end</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkButton" id="apply_btn">
+                <property name="visible">True</property>
+                <property name="use-underline">True</property>
+                <property name="label" translatable="yes">_Apply</property>
+                <signal name="clicked" handler="on_apply_clicked" swapped="yes"/>
+              </object>
+              <packing>
+                <property name="pack_type">end</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+        
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <property name="orientation">vertical</property>
+            <property name="margin">12</property>
+            <property name="spacing">6</property>
+            <child>
+              <object class="GtkLabel">
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">Server</property>
+                <property name="halign">start</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkEntry" id="host">
+                <property name="visible">True</property>
+                <property name="vexpand">False</property>
+                <property name="hexpand">True</property>
+                <signal name="changed" handler="on_text_changed" swapped="yes"/>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="visible">True</property>
+                <property name="margin_top">24</property>
+                <property name="halign">start</property>
+                <property name="label" translatable="yes">Display name (optional)</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkEntry" id="display_name">
+                <property name="visible">True</property>
+                <property name="vexpand">False</property>
+                <property name="hexpand">True</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="visible">True</property>
+                <property name="margin_top">18</property>
+                <property name="halign">start</property>
+                <property name="label" translatable="yes">User ID</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkEntry" id="user">
+                <property name="visible">True</property>
+                <property name="vexpand">False</property>
+                <property name="hexpand">True</property>
+                <signal name="changed" handler="on_text_changed" swapped="yes"/>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="visible">True</property>
+                <property name="margin_top">18</property>
+                <property name="halign">start</property>
+                <property name="label" translatable="yes">Password</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkEntry" id="password">
+                <property name="visible">True</property>
+                <property name="vexpand">False</property>
+                <property name="hexpand">True</property>
+                <property name="input-purpose">password</property>
+                <property name="visibility">False</property>
+                <signal name="changed" handler="on_text_changed" swapped="yes"/>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="visible">True</property>
+                <property name="margin_top">32</property>
+                <property name="halign">start</property>
+                <property name="label" translatable="yes">Port</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkEntry" id="port">
+                <property name="visible">True</property>
+                <property name="vexpand">False</property>
+                <property name="hexpand">True</property>
+                <property name="input-purpose">digits</property>
+                <signal name="changed" handler="on_text_changed" swapped="yes"/>
+              </object>
+            </child>
+            <child>
+              <object class="GtkListBox">
+                <property name="visible">True</property>
+                <property name="selection-mode">none</property>
+                <child>
+                  <object class="HdyComboRow" id="protocol">
+                    <property name="visible">True</property>
+                    <property name="selectable">False</property>
+                    <property name="title" translatable="yes">Protocol</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/src/ui/account-row.ui b/src/ui/account-row.ui
new file mode 100644
index 00000000..9a4a738e
--- /dev/null
+++ b/src/ui/account-row.ui
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.20"/>
+  <requires lib="libhandy" version="1.0"/>
+  <template class="CallsAccountRow" parent="HdyActionRow">
+    <property name="visible">True</property>
+    <property name="title">Title</property>
+    <property name="subtitle">Subtitle</property>
+    <property name="activatable">True</property>
+
+    <child type="prefix">
+      <object class="HdyAvatar" id="avatar">
+        <property name="visible">True</property>
+        <property name="show-initials">True</property>
+        <property name="size">48</property>
+      </object>
+    </child>
+
+    <child>
+      <object class="GtkSwitch" id="online_switch">
+        <property name="valign">center</property>
+        <property name="visible">True</property>
+        <property name="active">False</property>
+        <signal name="notify::active" handler="on_online_switched" swapped="yes"/>
+      </object>
+    </child>
+
+    <child>
+      <object class="GtkButton" id="manage_btn">
+        <property name="visible">True</property>
+        <child>
+          <object class="GtkImage">
+            <property name="visible">True</property>
+            <property name="icon_name">go-next-symbolic</property>
+          </object>
+        </child>
+        <signal name="clicked" handler="on_manage_clicked" swapped="yes"/>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/ui/main-window.ui b/src/ui/main-window.ui
index 68c3edde..95720b16 100644
--- a/src/ui/main-window.ui
+++ b/src/ui/main-window.ui
@@ -80,7 +80,7 @@
         </child>
         <child>
           <object class="GtkMenuButton">
-            <property name="visible">False</property>
+            <property name="visible">True</property>
             <property name="can_focus">False</property>
             <property name="popover">menu_popover</property>
             <child>
@@ -219,8 +219,8 @@
         <child>
           <object class="GtkModelButton">
             <property name="visible">True</property>
-            <property name="text" translatable="yes">VoIP Accounts</property>
-            <!--<property name="action-name">app.voip-accounts</property>-->
+            <property name="text" translatable="yes">_VoIP Accounts</property>
+            <property name="action-name">app.accounts</property>
           </object>
         </child>
         <child>
@@ -231,20 +231,21 @@
         </child>
         <child>
           <object class="GtkModelButton">
-            <property name="visible">True</property>
-            <property name="text" translatable="yes">Keyboard shortcuts</property>
+            <property name="visible">False</property>
+            <property name="text" translatable="yes">_Keyboard shortcuts</property>
           </object>
         </child>
         <child>
           <object class="GtkModelButton">
-            <property name="visible">True</property>
-            <property name="text" translatable="yes">Help</property>
+            <property name="visible">False</property>
+            <property name="text" translatable="yes">_Help</property>
           </object>
         </child>
         <child>
           <object class="GtkModelButton">
             <property name="visible">True</property>
-            <property name="text" translatable="yes">About Calls</property>
+            <property name="text" translatable="yes">_About Calls</property>
+            <property name="action-name">app.about</property>
           </object>
         </child>
 


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