summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Ivascu <ivascu.gabriel59@gmail.com>2017-06-07 01:10:44 +0300
committerGabriel Ivascu <ivascu.gabriel59@gmail.com>2017-07-05 16:27:19 +0300
commit76164e2b00bf405780556af3ecb424d7907c47d9 (patch)
treec1ab9e0397d1912c287c426e91f2b839e6b7f35f
parent1383b6e8f3c8a3afe551bcf37a4ca309e1fcda0a (diff)
downloadepiphany-76164e2b00bf405780556af3ecb424d7907c47d9.tar.gz
sync: Implement open tabs sync
-rw-r--r--data/org.gnome.epiphany.gschema.xml10
-rw-r--r--lib/ephy-prefs.h2
-rw-r--r--lib/sync/ephy-open-tabs-manager.c257
-rw-r--r--lib/sync/ephy-open-tabs-manager.h38
-rw-r--r--lib/sync/ephy-open-tabs-record.c300
-rw-r--r--lib/sync/ephy-open-tabs-record.h41
-rw-r--r--lib/sync/ephy-sync-service.c7
-rw-r--r--lib/sync/meson.build2
-rw-r--r--po/POTFILES.in2
-rw-r--r--src/ephy-shell.c18
-rw-r--r--src/ephy-shell.h3
-rw-r--r--src/meson.build1
-rw-r--r--src/prefs-dialog.c74
-rw-r--r--src/resources/epiphany.gresource.xml1
-rw-r--r--src/resources/gtk/prefs-dialog.ui36
-rw-r--r--src/resources/gtk/synced-tabs-dialog.ui92
-rw-r--r--src/synced-tabs-dialog.c355
-rw-r--r--src/synced-tabs-dialog.h36
18 files changed, 1255 insertions, 20 deletions
diff --git a/data/org.gnome.epiphany.gschema.xml b/data/org.gnome.epiphany.gschema.xml
index e35466413..e476ebaeb 100644
--- a/data/org.gnome.epiphany.gschema.xml
+++ b/data/org.gnome.epiphany.gschema.xml
@@ -337,6 +337,16 @@
<summary>Initial sync or normal sync</summary>
<description>TRUE if history collection needs to be synced for the first time, FALSE otherwise.</description>
</key>
+ <key type="b" name="sync-open-tabs-enabled">
+ <default>false</default>
+ <summary>Enable open tabs sync</summary>
+ <description>TRUE if open tabs collection should be synced, FALSE otherwise.</description>
+ </key>
+ <key type="d" name="sync-open-tabs-time">
+ <default>0</default>
+ <summary>Open tabs sync timestamp</summary>
+ <description>The timestamp at which last open tabs sync was made.</description>
+ </key>
</schema>
<enum id="org.gnome.Epiphany.Permission">
<value nick="undecided" value="-1"/>
diff --git a/lib/ephy-prefs.h b/lib/ephy-prefs.h
index fde68f913..2cf31c2e6 100644
--- a/lib/ephy-prefs.h
+++ b/lib/ephy-prefs.h
@@ -165,6 +165,8 @@ static const char * const ephy_prefs_web_schema[] = {
#define EPHY_PREFS_SYNC_HISTORY_ENABLED "sync-history-enabled"
#define EPHY_PREFS_SYNC_HISTORY_TIME "sync-history-time"
#define EPHY_PREFS_SYNC_HISTORY_INITIAL "sync-history-initial"
+#define EPHY_PREFS_SYNC_OPEN_TABS_ENABLED "sync-open-tabs-enabled"
+#define EPHY_PREFS_SYNC_OPEN_TABS_TIME "sync-open-tabs-time"
static struct {
const char *schema;
diff --git a/lib/sync/ephy-open-tabs-manager.c b/lib/sync/ephy-open-tabs-manager.c
new file mode 100644
index 000000000..b9d73176f
--- /dev/null
+++ b/lib/sync/ephy-open-tabs-manager.c
@@ -0,0 +1,257 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2017 Gabriel Ivascu <ivascu.gabriel59@gmail.com>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany 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.
+ *
+ * Epiphany 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 Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include "ephy-open-tabs-manager.h"
+
+#include "ephy-embed-container.h"
+#include "ephy-embed-shell.h"
+#include "ephy-settings.h"
+#include "ephy-synchronizable-manager.h"
+
+struct _EphyOpenTabsManager {
+ GObject parent_instance;
+
+ /* A list of EphyOpenTabsRecord objects describing the open tabs
+ * of other sync clients. This is updated at every sync. */
+ GSList *remote_records;
+};
+
+static void ephy_synchronizable_manager_iface_init (EphySynchronizableManagerInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (EphyOpenTabsManager, ephy_open_tabs_manager, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (EPHY_TYPE_SYNCHRONIZABLE_MANAGER,
+ ephy_synchronizable_manager_iface_init))
+
+static void
+ephy_open_tabs_manager_dispose (GObject *object)
+{
+ EphyOpenTabsManager *self = EPHY_OPEN_TABS_MANAGER (object);
+
+ g_slist_free_full (self->remote_records, g_object_unref);
+ self->remote_records = NULL;
+
+ G_OBJECT_CLASS (ephy_open_tabs_manager_parent_class)->dispose (object);
+}
+
+static void
+ephy_open_tabs_manager_class_init (EphyOpenTabsManagerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = ephy_open_tabs_manager_dispose;
+}
+
+static void
+ephy_open_tabs_manager_init (EphyOpenTabsManager *self)
+{
+}
+
+EphyOpenTabsManager *
+ephy_open_tabs_manager_new (void)
+{
+ return EPHY_OPEN_TABS_MANAGER (g_object_new (EPHY_TYPE_OPEN_TABS_MANAGER, NULL));
+}
+
+EphyOpenTabsRecord *
+ephy_open_tabs_manager_get_local_tabs (EphyOpenTabsManager *self)
+{
+ EphyOpenTabsRecord *local_tabs;
+ WebKitFaviconDatabase *database;
+ EphyEmbedShell *embed_shell;
+ GList *windows;
+ GList *tabs;
+ char *favicon;
+ char *client_id;
+ char *client_name;
+ const char *title;
+ const char *url;
+
+ g_return_val_if_fail (EPHY_IS_OPEN_TABS_MANAGER (self), NULL);
+
+ embed_shell = ephy_embed_shell_get_default ();
+ windows = gtk_application_get_windows (GTK_APPLICATION (embed_shell));
+ database = webkit_web_context_get_favicon_database (ephy_embed_shell_get_web_context (embed_shell));
+ client_id = g_settings_get_string (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_CLIENT_ID);
+ client_name = g_strdup_printf ("%s on Epiphany", client_id);
+ local_tabs = ephy_open_tabs_record_new (client_id, client_name);
+
+ for (GList *l = windows; l && l->data; l = l->next) {
+ tabs = ephy_embed_container_get_children (l->data);
+
+ for (GList *t = tabs; t && t->data; t = t->next) {
+ title = ephy_embed_get_title (t->data);
+
+ if (!g_strcmp0 (title, "Blank page") || !g_strcmp0 (title, "Most Visited"))
+ continue;
+
+ url = ephy_web_view_get_display_address (ephy_embed_get_web_view (t->data));
+ favicon = webkit_favicon_database_get_favicon_uri (database, url);
+ ephy_open_tabs_record_add_tab (local_tabs, title, url, favicon);
+
+ g_free (favicon);
+ }
+
+ g_list_free (tabs);
+ }
+
+ g_free (client_id);
+ g_free (client_name);
+
+ return local_tabs;
+}
+
+GSList *
+ephy_open_tabs_manager_get_remote_tabs (EphyOpenTabsManager *self)
+{
+ g_return_val_if_fail (EPHY_IS_OPEN_TABS_MANAGER (self), NULL);
+
+ return self->remote_records;
+}
+
+void
+ephy_open_tabs_manager_clear_cache (EphyOpenTabsManager *self)
+{
+ g_return_if_fail (EPHY_IS_OPEN_TABS_MANAGER (self));
+
+ g_slist_free_full (self->remote_records, g_object_unref);
+ self->remote_records = NULL;
+}
+
+const char *
+synchronizable_manager_get_collection_name (EphySynchronizableManager *manager)
+{
+ gboolean sync_with_firefox = g_settings_get_boolean (EPHY_SETTINGS_SYNC,
+ EPHY_PREFS_SYNC_WITH_FIREFOX);
+
+ return sync_with_firefox ? "tabs" : "ephy-tabs";
+}
+
+static GType
+synchronizable_manager_get_synchronizable_type (EphySynchronizableManager *manager)
+{
+ return EPHY_TYPE_OPEN_TABS_RECORD;
+}
+
+static gboolean
+synchronizable_manager_is_initial_sync (EphySynchronizableManager *manager)
+{
+ /* Initial sync will always be true.
+ * We always want all records when syncing open tabs.
+ */
+ return TRUE;
+}
+
+static void
+synchronizable_manager_set_is_initial_sync (EphySynchronizableManager *manager,
+ gboolean is_initial)
+{
+ /* Initial sync will always be true. */
+}
+
+static double
+synchronizable_manager_get_sync_time (EphySynchronizableManager *manager)
+{
+ return g_settings_get_double (EPHY_SETTINGS_SYNC,
+ EPHY_PREFS_SYNC_OPEN_TABS_TIME);
+}
+
+static void
+synchronizable_manager_set_sync_time (EphySynchronizableManager *manager,
+ double sync_time)
+{
+ g_settings_set_double (EPHY_SETTINGS_SYNC,
+ EPHY_PREFS_SYNC_OPEN_TABS_TIME,
+ sync_time);
+}
+
+static void
+synchronizable_manager_add (EphySynchronizableManager *manager,
+ EphySynchronizable *synchronizable)
+{
+ /* Every sync of open tabs is an initial sync so we don't need this. */
+}
+
+static void
+synchronizable_manager_remove (EphySynchronizableManager *manager,
+ EphySynchronizable *synchronizable)
+{
+ /* Every sync of open tabs is an initial sync so we don't need this. */
+}
+
+static void
+synchronizable_manager_save (EphySynchronizableManager *manager,
+ EphySynchronizable *synchronizable)
+{
+ /* No implementation.
+ * We don't care about the server time modified of open tabs records.
+ */
+}
+
+static void
+synchronizable_manager_merge (EphySynchronizableManager *manager,
+ gboolean is_initial,
+ GSList *remotes_deleted,
+ GSList *remotes_updated,
+ EphySynchronizableManagerMergeCallback callback,
+ gpointer user_data)
+{
+ EphyOpenTabsManager *self = EPHY_OPEN_TABS_MANAGER (manager);
+ EphyOpenTabsRecord *local_tabs;
+ GSList *to_upload = NULL;
+ char *client_id;
+
+ client_id = g_settings_get_string (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_CLIENT_ID);
+ g_slist_free_full (self->remote_records, g_object_unref);
+ self->remote_records = NULL;
+
+ for (GSList *l = remotes_updated; l && l->data; l = l->next) {
+ /* Exclude the record which describes the local open tabs. */
+ if (!g_strcmp0 (client_id, ephy_open_tabs_record_get_id (l->data)))
+ continue;
+
+ self->remote_records = g_slist_prepend (self->remote_records, g_object_ref (l->data));
+ }
+
+ /* Only upload the local open tabs, we don't want to alter open tabs of
+ * other clients. Also, overwrite any previous value by doing a force upload.
+ */
+ local_tabs = ephy_open_tabs_manager_get_local_tabs (self);
+ to_upload = g_slist_prepend (to_upload, local_tabs);
+
+ g_free (client_id);
+
+ callback (to_upload, TRUE, user_data);
+}
+
+static void
+ephy_synchronizable_manager_iface_init (EphySynchronizableManagerInterface *iface)
+{
+ iface->get_collection_name = synchronizable_manager_get_collection_name;
+ iface->get_synchronizable_type = synchronizable_manager_get_synchronizable_type;
+ iface->is_initial_sync = synchronizable_manager_is_initial_sync;
+ iface->set_is_initial_sync = synchronizable_manager_set_is_initial_sync;
+ iface->get_sync_time = synchronizable_manager_get_sync_time;
+ iface->set_sync_time = synchronizable_manager_set_sync_time;
+ iface->add = synchronizable_manager_add;
+ iface->remove = synchronizable_manager_remove;
+ iface->save = synchronizable_manager_save;
+ iface->merge = synchronizable_manager_merge;
+}
diff --git a/lib/sync/ephy-open-tabs-manager.h b/lib/sync/ephy-open-tabs-manager.h
new file mode 100644
index 000000000..166539ead
--- /dev/null
+++ b/lib/sync/ephy-open-tabs-manager.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2017 Gabriel Ivascu <ivascu.gabriel59@gmail.com>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany 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.
+ *
+ * Epiphany 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 Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ephy-open-tabs-record.h"
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define EPHY_TYPE_OPEN_TABS_MANAGER (ephy_open_tabs_manager_get_type ())
+
+G_DECLARE_FINAL_TYPE (EphyOpenTabsManager, ephy_open_tabs_manager, EPHY, OPEN_TABS_MANAGER, GObject)
+
+EphyOpenTabsManager *ephy_open_tabs_manager_new (void);
+EphyOpenTabsRecord *ephy_open_tabs_manager_get_local_tabs (EphyOpenTabsManager *self);
+GSList *ephy_open_tabs_manager_get_remote_tabs (EphyOpenTabsManager *self);
+void ephy_open_tabs_manager_clear_cache (EphyOpenTabsManager *self);
+
+G_END_DECLS
diff --git a/lib/sync/ephy-open-tabs-record.c b/lib/sync/ephy-open-tabs-record.c
new file mode 100644
index 000000000..c10a7ec46
--- /dev/null
+++ b/lib/sync/ephy-open-tabs-record.c
@@ -0,0 +1,300 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2017 Gabriel Ivascu <ivascu.gabriel59@gmail.com>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany 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.
+ *
+ * Epiphany 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 Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include "ephy-open-tabs-record.h"
+
+#include "ephy-synchronizable.h"
+
+struct _EphyOpenTabsRecord {
+ GObject parent_instance;
+
+ char *id;
+ char *client_name;
+
+ /* List of JSON objects. Each object describes a tab and has the fields:
+ * @title: a string representing the title of the current page
+ * @urlHistory: a JSON array of strings (the page URLs in tab's history)
+ * @icon: a string representing the favicon URI of the tab, i.e. the favicon
+ * URI of the most recent website in tab (the first item in urlHistory)
+ * @lastUsed: an integer representing the UNIX time in seconds at which the
+ * tab was last accessed, or 0
+ */
+ GSList *tabs;
+};
+
+static void json_serializable_iface_init (JsonSerializableIface *iface);
+static void ephy_synchronizable_iface_init (EphySynchronizableInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (EphyOpenTabsRecord, ephy_open_tabs_record, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (JSON_TYPE_SERIALIZABLE,
+ json_serializable_iface_init)
+ G_IMPLEMENT_INTERFACE (EPHY_TYPE_SYNCHRONIZABLE,
+ ephy_synchronizable_iface_init))
+
+enum {
+ PROP_0,
+ PROP_ID,
+ PROP_CLIENT_NAME,
+ PROP_TABS,
+ LAST_PROP
+};
+
+static GParamSpec *obj_properties[LAST_PROP];
+
+static void
+ephy_open_tabs_record_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ EphyOpenTabsRecord *self = EPHY_OPEN_TABS_RECORD (object);
+
+ switch (prop_id) {
+ case PROP_ID:
+ g_free (self->id);
+ self->id = g_strdup (g_value_get_string (value));
+ break;
+ case PROP_CLIENT_NAME:
+ g_free (self->client_name);
+ self->client_name = g_strdup (g_value_get_string (value));
+ break;
+ case PROP_TABS:
+ g_slist_free_full (self->tabs, (GDestroyNotify)json_object_unref);
+ self->tabs = g_value_get_pointer (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ephy_open_tabs_record_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ EphyOpenTabsRecord *self = EPHY_OPEN_TABS_RECORD (object);
+
+ switch (prop_id) {
+ case PROP_ID:
+ g_value_set_string (value, self->id);
+ break;
+ case PROP_CLIENT_NAME:
+ g_value_set_string (value, self->client_name);
+ break;
+ case PROP_TABS:
+ g_value_set_pointer (value, self->tabs);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ephy_open_tabs_record_dispose (GObject *object)
+{
+ EphyOpenTabsRecord *self = EPHY_OPEN_TABS_RECORD (object);
+
+ g_clear_pointer (&self->id, g_free);
+ g_clear_pointer (&self->client_name, g_free);
+ g_slist_free_full (self->tabs, (GDestroyNotify)json_object_unref);
+ self->tabs = NULL;
+
+ G_OBJECT_CLASS (ephy_open_tabs_record_parent_class)->dispose (object);
+}
+
+static void
+ephy_open_tabs_record_class_init (EphyOpenTabsRecordClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->set_property = ephy_open_tabs_record_set_property;
+ object_class->get_property = ephy_open_tabs_record_get_property;
+ object_class->dispose = ephy_open_tabs_record_dispose;
+
+ obj_properties[PROP_ID] =
+ g_param_spec_string ("id",
+ "Id",
+ "Id of the open tabs record",
+ "Default id",
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+ obj_properties[PROP_CLIENT_NAME] =
+ g_param_spec_string ("clientName",
+ "Client name",
+ "Name of the sync client providing the tabs",
+ "Default client name",
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+ obj_properties[PROP_TABS] =
+ g_param_spec_pointer ("tabs",
+ "Tabs",
+ "A list of JSON objects describing the tabs",
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, LAST_PROP, obj_properties);
+}
+
+static void
+ephy_open_tabs_record_init (EphyOpenTabsRecord *self)
+{
+}
+
+EphyOpenTabsRecord *
+ephy_open_tabs_record_new (const char *id,
+ const char *client_name)
+{
+ return EPHY_OPEN_TABS_RECORD (g_object_new (EPHY_TYPE_OPEN_TABS_RECORD,
+ "id", id,
+ "clientName", client_name,
+ NULL));
+}
+
+const char *
+ephy_open_tabs_record_get_id (EphyOpenTabsRecord *self)
+{
+ g_return_val_if_fail (EPHY_IS_OPEN_TABS_RECORD (self), NULL);
+
+ return self->id;
+}
+
+const char *
+ephy_open_tabs_record_get_client_name (EphyOpenTabsRecord *self)
+{
+ g_return_val_if_fail (EPHY_IS_OPEN_TABS_RECORD (self), NULL);
+
+ return self->client_name;
+}
+
+GSList *
+ephy_open_tabs_record_get_tabs (EphyOpenTabsRecord *self)
+{
+ g_return_val_if_fail (EPHY_IS_OPEN_TABS_RECORD (self), NULL);
+
+ return self->tabs;
+}
+
+void
+ephy_open_tabs_record_add_tab (EphyOpenTabsRecord *self,
+ const char *title,
+ const char *url,
+ const char *favicon)
+{
+ JsonObject *tab;
+ JsonArray *url_history;
+
+ g_return_if_fail (EPHY_IS_OPEN_TABS_RECORD (self));
+ g_return_if_fail (title);
+ g_return_if_fail (url);
+
+ tab = json_object_new ();
+ json_object_set_string_member (tab, "title", title);
+ /* Only use the most recent URL. */
+ url_history = json_array_new ();
+ json_array_add_string_element (url_history, url);
+ json_object_set_array_member (tab, "urlHistory", url_history);
+ json_object_set_string_member (tab, "icon", favicon);
+ json_object_set_int_member (tab, "lastUsed", g_get_real_time () / 1000000);
+
+ self->tabs = g_slist_prepend (self->tabs, tab);
+}
+
+static JsonNode *
+serializable_serialize_property (JsonSerializable *serializable,
+ const char *name,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ if (!g_strcmp0 (name, "tabs")) {
+ JsonNode *node = json_node_new (JSON_NODE_ARRAY);
+ JsonArray *array = json_array_new ();
+
+ for (GList *l = g_value_get_pointer (value); l && l->data; l = l->next)
+ json_array_add_object_element (array, json_object_ref (l->data));
+
+ json_node_set_array (node, array);
+
+ return node;
+ }
+
+ return json_serializable_default_serialize_property (serializable, name, value, pspec);
+}
+
+static gboolean
+serializable_deserialize_property (JsonSerializable *serializable,
+ const char *name,
+ GValue *value,
+ GParamSpec *pspec,
+ JsonNode *node)
+{
+ if (!g_strcmp0 (name, "tabs")) {
+ JsonArray *array;
+ GSList *tabs = NULL;
+
+ array = json_node_get_array (node);
+ for (guint i = 0; i < json_array_get_length (array); i++)
+ tabs = g_slist_prepend (tabs, json_object_ref (json_array_get_object_element (array, i)));
+
+ g_value_set_pointer (value, tabs);
+
+ return TRUE;
+ }
+
+ return json_serializable_default_deserialize_property (serializable, name, value, pspec, node);
+}
+
+static void
+json_serializable_iface_init (JsonSerializableIface *iface)
+{
+ iface->serialize_property = serializable_serialize_property;
+ iface->deserialize_property = serializable_deserialize_property;
+}
+
+static const char *
+synchronizable_get_id (EphySynchronizable *synchronizable)
+{
+ return EPHY_OPEN_TABS_RECORD (synchronizable)->id;
+}
+
+static double
+synchronizable_get_server_time_modified (EphySynchronizable *synchronizable)
+{
+ /* No implementation.
+ * We don't care about the server time modified of open tabs records.
+ */
+ return 0;
+}
+
+static void
+synchronizable_set_server_time_modified (EphySynchronizable *synchronizable,
+ double server_time_modified)
+{
+ /* No implementation.
+ * We don't care about the server time modified of open tabs records.
+ */
+}
+
+static void
+ephy_synchronizable_iface_init (EphySynchronizableInterface *iface)
+{
+ iface->get_id = synchronizable_get_id;
+ iface->get_server_time_modified = synchronizable_get_server_time_modified;
+ iface->set_server_time_modified = synchronizable_set_server_time_modified;
+ iface->to_bso = ephy_synchronizable_default_to_bso;
+}
diff --git a/lib/sync/ephy-open-tabs-record.h b/lib/sync/ephy-open-tabs-record.h
new file mode 100644
index 000000000..b31fc82b5
--- /dev/null
+++ b/lib/sync/ephy-open-tabs-record.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2017 Gabriel Ivascu <ivascu.gabriel59@gmail.com>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany 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.
+ *
+ * Epiphany 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 Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define EPHY_TYPE_OPEN_TABS_RECORD (ephy_open_tabs_record_get_type ())
+
+G_DECLARE_FINAL_TYPE (EphyOpenTabsRecord, ephy_open_tabs_record, EPHY, OPEN_TABS_RECORD, GObject)
+
+EphyOpenTabsRecord *ephy_open_tabs_record_new (const char *id,
+ const char *client_name);
+const char *ephy_open_tabs_record_get_id (EphyOpenTabsRecord *self);
+const char *ephy_open_tabs_record_get_client_name (EphyOpenTabsRecord *self);
+GSList *ephy_open_tabs_record_get_tabs (EphyOpenTabsRecord *self);
+void ephy_open_tabs_record_add_tab (EphyOpenTabsRecord *self,
+ const char *title,
+ const char *url,
+ const char *favicon);
+
+G_END_DECLS
diff --git a/lib/sync/ephy-sync-service.c b/lib/sync/ephy-sync-service.c
index d1f299c49..fea3ad1f0 100644
--- a/lib/sync/ephy-sync-service.c
+++ b/lib/sync/ephy-sync-service.c
@@ -999,7 +999,7 @@ ephy_sync_service_delete_synchronizable (EphySyncService *self,
json_object_set_boolean_member (object, "deleted", TRUE);
record = json_to_string (node, FALSE);
bundle = ephy_sync_service_get_key_bundle (self, collection);
- payload = ephy_sync_crypto_encrypt_record (record, bundle);
+ payload = ephy_sync_crypto_encrypt_record (record, bundle);
json_object_remove_member (object, "deleted");
json_object_set_string_member (object, "payload", payload);
body = json_to_string (node, FALSE);
@@ -1337,6 +1337,10 @@ ephy_sync_service_unregister_client_id (EphySyncService *self)
ephy_sync_service_queue_storage_request (self, endpoint, SOUP_METHOD_DELETE,
NULL, -1, -1, NULL, NULL);
+ g_free (endpoint);
+ endpoint = g_strdup_printf ("storage/tabs/%s", client_id);
+ ephy_sync_service_queue_storage_request (self, endpoint, SOUP_METHOD_DELETE,
+ NULL, -1, -1, NULL, NULL);
g_settings_set_string (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_CLIENT_ID, "");
g_free (endpoint);
@@ -1897,7 +1901,6 @@ ephy_sync_service_upload_meta_global_record (EphySyncService *self)
declined = json_array_new ();
json_array_add_string_element (declined, "addons");
json_array_add_string_element (declined, "prefs");
- json_array_add_string_element (declined, "tabs");
json_object_set_array_member (payload, "declined", declined);
json_object_set_object_member (engines, "clients", make_engine_object (1));
json_object_set_object_member (engines, "bookmarks", make_engine_object (2));
diff --git a/lib/sync/meson.build b/lib/sync/meson.build
index 728ab1288..ae6f72d6f 100644
--- a/lib/sync/meson.build
+++ b/lib/sync/meson.build
@@ -2,6 +2,8 @@ libephysync_sources = [
'debug/ephy-sync-debug.c',
'ephy-history-manager.c',
'ephy-history-record.c',
+ 'ephy-open-tabs-manager.c',
+ 'ephy-open-tabs-record.c',
'ephy-password-manager.c',
'ephy-password-record.c',
'ephy-sync-crypto.c',
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 04ee4ee2d..f9308f092 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -60,5 +60,7 @@ src/resources/gtk/prefs-dialog.ui
src/resources/gtk/prefs-lang-dialog.ui
src/resources/gtk/search-engine-dialog.ui
src/resources/gtk/shortcuts-dialog.ui
+src/resources/gtk/synced-tabs-dialog.ui
src/search-provider/ephy-search-provider.c
+src/synced-tabs-dialog.c
src/window-commands.c
diff --git a/src/ephy-shell.c b/src/ephy-shell.c
index 886f63954..4b70a61ba 100644
--- a/src/ephy-shell.c
+++ b/src/ephy-shell.c
@@ -58,6 +58,7 @@ struct _EphyShell {
EphyBookmarksManager *bookmarks_manager;
EphyPasswordManager *password_manager;
EphyHistoryManager *history_manager;
+ EphyOpenTabsManager *open_tabs_manager;
GNetworkMonitor *network_monitor;
GtkWidget *history_dialog;
GObject *prefs_dialog;
@@ -363,6 +364,11 @@ ephy_shell_startup (GApplication *application)
manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_history_manager (shell));
ephy_sync_service_register_manager (ephy_shell_get_sync_service (shell), manager);
}
+
+ if (g_settings_get_boolean (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_OPEN_TABS_ENABLED)) {
+ manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_open_tabs_manager (shell));
+ ephy_sync_service_register_manager (ephy_shell_get_sync_service (shell), manager);
+ }
}
gtk_application_set_app_menu (GTK_APPLICATION (application),
@@ -635,6 +641,7 @@ ephy_shell_dispose (GObject *object)
g_clear_object (&shell->bookmarks_manager);
g_clear_object (&shell->password_manager);
g_clear_object (&shell->history_manager);
+ g_clear_object (&shell->open_tabs_manager);
g_slist_free_full (shell->open_uris_idle_ids, remove_open_uris_idle_cb);
shell->open_uris_idle_ids = NULL;
@@ -868,6 +875,17 @@ ephy_shell_get_history_manager (EphyShell *shell)
return shell->history_manager;
}
+EphyOpenTabsManager *
+ephy_shell_get_open_tabs_manager (EphyShell *shell)
+{
+ g_return_val_if_fail (EPHY_IS_SHELL (shell), NULL);
+
+ if (shell->open_tabs_manager == NULL)
+ shell->open_tabs_manager = ephy_open_tabs_manager_new ();
+
+ return shell->open_tabs_manager;
+}
+
/**
* ephy_shell_get_net_monitor:
*
diff --git a/src/ephy-shell.h b/src/ephy-shell.h
index b7d4800d2..b07bb4dad 100644
--- a/src/ephy-shell.h
+++ b/src/ephy-shell.h
@@ -26,6 +26,7 @@
#include "ephy-embed-shell.h"
#include "ephy-embed.h"
#include "ephy-history-manager.h"
+#include "ephy-open-tabs-manager.h"
#include "ephy-password-manager.h"
#include "ephy-session.h"
#include "ephy-sync-service.h"
@@ -108,6 +109,8 @@ EphyPasswordManager *ephy_shell_get_password_manager (EphyShell *shell);
EphyHistoryManager *ephy_shell_get_history_manager (EphyShell *shell);
+EphyOpenTabsManager *ephy_shell_get_open_tabs_manager (EphyShell *shell);
+
EphySyncService *ephy_shell_get_sync_service (EphyShell *shell);
GtkWidget *ephy_shell_get_history_dialog (EphyShell *shell);
diff --git a/src/meson.build b/src/meson.build
index a06356b2b..8c9e97e18 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -40,6 +40,7 @@ libephymain_sources = [
'passwords-dialog.c',
'popup-commands.c',
'prefs-dialog.c',
+ 'synced-tabs-dialog.c',
'window-commands.c',
enums
]
diff --git a/src/prefs-dialog.c b/src/prefs-dialog.c
index 9d0164dc9..9126bc644 100644
--- a/src/prefs-dialog.c
+++ b/src/prefs-dialog.c
@@ -44,6 +44,7 @@
#include "cookies-dialog.h"
#include "languages.h"
#include "passwords-dialog.h"
+#include "synced-tabs-dialog.h"
#include <glib/gi18n.h>
#include <gtk/gtk.h>
@@ -125,11 +126,13 @@ struct _PrefsDialog {
GtkWidget *sync_bookmarks_checkbutton;
GtkWidget *sync_passwords_checkbutton;
GtkWidget *sync_history_checkbutton;
+ GtkWidget *sync_open_tabs_checkbutton;
GtkWidget *sync_frequency_5_min_radiobutton;
GtkWidget *sync_frequency_15_min_radiobutton;
GtkWidget *sync_frequency_30_min_radiobutton;
GtkWidget *sync_frequency_60_min_radiobutton;
GtkWidget *sync_now_button;
+ GtkWidget *synced_tabs_button;
gboolean sync_was_signed_in;
WebKitWebView *fxa_web_view;
@@ -181,15 +184,20 @@ sync_collection_toggled_cb (GtkToggleButton *button,
PrefsDialog *dialog)
{
EphySynchronizableManager *manager = NULL;
-
- if (GTK_WIDGET (button) == dialog->sync_bookmarks_checkbutton)
- manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_bookmarks_manager (ephy_shell_get_default ()));
- else if (GTK_WIDGET (button) == dialog->sync_passwords_checkbutton)
- manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_password_manager (ephy_shell_get_default ()));
- else if (GTK_WIDGET (button) == dialog->sync_history_checkbutton)
- manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_history_manager (ephy_shell_get_default ()));
- else
+ EphyShell *shell = ephy_shell_get_default ();
+
+ if (GTK_WIDGET (button) == dialog->sync_bookmarks_checkbutton) {
+ manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_bookmarks_manager (shell));
+ } else if (GTK_WIDGET (button) == dialog->sync_passwords_checkbutton) {
+ manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_password_manager (shell));
+ } else if (GTK_WIDGET (button) == dialog->sync_history_checkbutton) {
+ manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_history_manager (shell));
+ } else if (GTK_WIDGET (button) == dialog->sync_open_tabs_checkbutton) {
+ manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_open_tabs_manager (shell));
+ ephy_open_tabs_manager_clear_cache (EPHY_OPEN_TABS_MANAGER (manager));
+ } else {
g_assert_not_reached ();
+ }
if (gtk_toggle_button_get_active (button)) {
ephy_sync_service_register_manager (dialog->sync_service, manager);
@@ -258,9 +266,8 @@ sync_secrets_store_finished_cb (EphySyncService *service,
GError *error,
PrefsDialog *dialog)
{
- EphyBookmarksManager *bookmarks_manager;
- EphyPasswordManager *password_manager;
- EphyHistoryManager *history_manager;
+ EphySynchronizableManager *manager;
+ EphyShell *shell = ephy_shell_get_default ();
g_assert (EPHY_IS_SYNC_SERVICE (service));
g_assert (EPHY_IS_PREFS_DIALOG (dialog));
@@ -288,16 +295,20 @@ sync_secrets_store_finished_cb (EphySyncService *service,
ephy_sync_service_get_sync_user (service));
if (g_settings_get_boolean (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_BOOKMARKS_ENABLED)) {
- bookmarks_manager = ephy_shell_get_bookmarks_manager (ephy_shell_get_default ());
- ephy_sync_service_register_manager (service, EPHY_SYNCHRONIZABLE_MANAGER (bookmarks_manager));
+ manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_bookmarks_manager (shell));
+ ephy_sync_service_register_manager (service, manager);
}
if (g_settings_get_boolean (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_PASSWORDS_ENABLED)) {
- password_manager = ephy_shell_get_password_manager (ephy_shell_get_default ());
- ephy_sync_service_register_manager (service, EPHY_SYNCHRONIZABLE_MANAGER (password_manager));
+ manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_password_manager (shell));
+ ephy_sync_service_register_manager (service, manager);
}
if (g_settings_get_boolean (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_HISTORY_ENABLED)) {
- history_manager = ephy_shell_get_history_manager (ephy_shell_get_default ());
- ephy_sync_service_register_manager (service, EPHY_SYNCHRONIZABLE_MANAGER (history_manager));
+ manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_history_manager (shell));
+ ephy_sync_service_register_manager (service, manager);
+ }
+ if (g_settings_get_boolean (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_OPEN_TABS_ENABLED)) {
+ manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_open_tabs_manager (shell));
+ ephy_sync_service_register_manager (service, manager);
}
g_free (text);
@@ -551,6 +562,20 @@ on_sync_sync_now_button_clicked (GtkWidget *button,
}
static void
+on_sync_synced_tabs_button_clicked (GtkWidget *button,
+ PrefsDialog *dialog)
+{
+ EphyOpenTabsManager *manager;
+ SyncedTabsDialog *synced_tabs_dialog;
+
+ manager = ephy_shell_get_open_tabs_manager (ephy_shell_get_default ());
+ synced_tabs_dialog = synced_tabs_dialog_new (manager);
+ gtk_window_set_transient_for (GTK_WINDOW (synced_tabs_dialog), GTK_WINDOW (dialog));
+ gtk_window_set_modal (GTK_WINDOW (synced_tabs_dialog), TRUE);
+ gtk_window_present (GTK_WINDOW (synced_tabs_dialog));
+}
+
+static void
on_manage_cookies_button_clicked (GtkWidget *button,
PrefsDialog *dialog)
{
@@ -657,17 +682,20 @@ prefs_dialog_class_init (PrefsDialogClass *klass)
gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_bookmarks_checkbutton);
gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_passwords_checkbutton);
gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_history_checkbutton);
+ gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_open_tabs_checkbutton);
gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_frequency_5_min_radiobutton);
gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_frequency_15_min_radiobutton);
gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_frequency_30_min_radiobutton);
gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_frequency_60_min_radiobutton);
gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_now_button);
+ gtk_widget_class_bind_template_child (widget_class, PrefsDialog, synced_tabs_button);
gtk_widget_class_bind_template_callback (widget_class, on_manage_cookies_button_clicked);
gtk_widget_class_bind_template_callback (widget_class, on_manage_passwords_button_clicked);
gtk_widget_class_bind_template_callback (widget_class, on_search_engine_dialog_button_clicked);
gtk_widget_class_bind_template_callback (widget_class, on_sync_sign_out_button_clicked);
gtk_widget_class_bind_template_callback (widget_class, on_sync_sync_now_button_clicked);
+ gtk_widget_class_bind_template_callback (widget_class, on_sync_synced_tabs_button_clicked);
}
static void
@@ -1721,6 +1749,11 @@ setup_sync_page (PrefsDialog *dialog)
dialog->sync_history_checkbutton,
"active",
G_SETTINGS_BIND_DEFAULT);
+ g_settings_bind (sync_settings,
+ EPHY_PREFS_SYNC_OPEN_TABS_ENABLED,
+ dialog->sync_open_tabs_checkbutton,
+ "active",
+ G_SETTINGS_BIND_DEFAULT);
g_settings_bind_with_mapping (sync_settings,
EPHY_PREFS_SYNC_FREQUENCY,
dialog->sync_frequency_5_min_radiobutton,
@@ -1758,6 +1791,10 @@ setup_sync_page (PrefsDialog *dialog)
GINT_TO_POINTER (60),
NULL);
+ g_object_bind_property (dialog->sync_open_tabs_checkbutton, "active",
+ dialog->synced_tabs_button, "sensitive",
+ G_BINDING_SYNC_CREATE);
+
g_signal_connect_object (dialog->sync_service, "sync-secrets-store-finished",
G_CALLBACK (sync_secrets_store_finished_cb),
dialog, 0);
@@ -1779,6 +1816,9 @@ setup_sync_page (PrefsDialog *dialog)
g_signal_connect_object (dialog->sync_history_checkbutton, "toggled",
G_CALLBACK (sync_collection_toggled_cb),
dialog, 0);
+ g_signal_connect_object (dialog->sync_open_tabs_checkbutton, "toggled",
+ G_CALLBACK (sync_collection_toggled_cb),
+ dialog, 0);
}
static void
diff --git a/src/resources/epiphany.gresource.xml b/src/resources/epiphany.gresource.xml
index 8580f8f6f..80c9e7d0b 100644
--- a/src/resources/epiphany.gresource.xml
+++ b/src/resources/epiphany.gresource.xml
@@ -27,6 +27,7 @@
<file preprocess="xml-stripblanks" compressed="true">gtk/prefs-dialog.ui</file>
<file preprocess="xml-stripblanks" compressed="true">gtk/prefs-lang-dialog.ui</file>
<file preprocess="xml-stripblanks" compressed="true">gtk/search-engine-dialog.ui</file>
+ <file preprocess="xml-stripblanks" compressed="true">gtk/synced-tabs-dialog.ui</file>
<file preprocess="xml-stripblanks" compressed="true">gtk/shortcuts-dialog.ui</file>
</gresource>
<gresource prefix="/org/gnome/Epiphany/icons">
diff --git a/src/resources/gtk/prefs-dialog.ui b/src/resources/gtk/prefs-dialog.ui
index 1db23d145..ac32223b1 100644
--- a/src/resources/gtk/prefs-dialog.ui
+++ b/src/resources/gtk/prefs-dialog.ui
@@ -905,7 +905,7 @@
<child>
<object class="GtkBox">
<property name="visible">True</property>
- <property name="orientation">vertical</property>
+ <property name="orientation">horizontal</property>
<property name="spacing">6</property>
<property name="margin-start">12</property>
<child>
@@ -929,6 +929,13 @@
<property name="use-underline">True</property>
</object>
</child>
+ <child>
+ <object class="GtkCheckButton" id="sync_open_tabs_checkbutton">
+ <property name="label" translatable="yes">Open _Tabs</property>
+ <property name="visible">True</property>
+ <property name="use-underline">True</property>
+ </object>
+ </child>
</object>
</child>
<child>
@@ -994,6 +1001,33 @@
</child>
</object>
</child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Misc</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <property name="margin-start">12</property>
+ <child>
+ <object class="GtkButton" id="synced_tabs_button">
+ <property name="label" translatable="yes">_View synced tabs</property>
+ <property name="visible">True</property>
+ <property name="use-underline">True</property>
+ <property name="halign">start</property>
+ <signal name="clicked" handler="on_sync_synced_tabs_button_clicked"/>
+ </object>
+ </child>
+ </object>
+ </child>
</object>
</child>
</object>
diff --git a/src/resources/gtk/synced-tabs-dialog.ui b/src/resources/gtk/synced-tabs-dialog.ui
new file mode 100644
index 000000000..86410f2cb
--- /dev/null
+++ b/src/resources/gtk/synced-tabs-dialog.ui
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.10"/>
+ <object class="GtkTreeStore" id="treestore">
+ <columns>
+ <!-- column-name ICON -->
+ <column type="GdkPixbuf"/>
+ <!-- column-name TITLE -->
+ <column type="gchararray"/>
+ <!-- column-name URL -->
+ <column type="gchararray"/>
+ </columns>
+ </object>
+ <template class="SyncedTabsDialog" parent="GtkDialog">
+ <property name="height_request">500</property>
+ <property name="modal">True</property>
+ <property name="window_position">center</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="type_hint">dialog</property>
+ <child internal-child="headerbar">
+ <object class="GtkHeaderBar">
+ <property name="title" translatable="yes">Synced Tabs</property>
+ <property name="show-close-button">True</property>
+ </object>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="border_width">15</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="halign">start</property>
+ <property name="wrap">True</property>
+ <property name="max-width-chars">50</property>
+ <property name="label" translatable="yes">Below are the synced open tabs of your other devices that use Firefox Sync with this account. Except for the tabs under Local Tabs, all the other tabs can be opened by double clicking on their name.</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="visible">True</property>
+ <property name="expand">True</property>
+ <child>
+ <object class="GtkTreeView" id="treeview">
+ <property name="visible">True</property>
+ <property name="model">treestore</property>
+ <property name="headers-visible">False</property>
+ <signal name="row-activated" handler="treeview_row_activated_cb"/>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection">
+ <property name="mode">single</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkTreeViewColumn">
+ <child>
+ <object class="GtkCellRendererPixbuf"/>
+ <attributes>
+ <attribute name="pixbuf">0</attribute>
+ </attributes>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkTreeViewColumn">
+ <child>
+ <object class="GtkCellRendererText">
+ <property name="ellipsize">end</property>
+ </object>
+ <attributes>
+ <attribute name="text">1</attribute>
+ </attributes>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/synced-tabs-dialog.c b/src/synced-tabs-dialog.c
new file mode 100644
index 000000000..d6cd8eb1e
--- /dev/null
+++ b/src/synced-tabs-dialog.c
@@ -0,0 +1,355 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2017 Gabriel Ivascu <ivascu.gabriel59@gmail.com>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany 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.
+ *
+ * Epiphany 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 Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include "synced-tabs-dialog.h"
+
+#include "ephy-embed-prefs.h"
+#include "ephy-embed-shell.h"
+#include "ephy-favicon-helpers.h"
+#include "ephy-shell.h"
+
+#include <json-glib/json-glib.h>
+
+#define PIXBUF_MISSING_PATH "/org/gnome/epiphany/missing-thumbnail.png"
+
+struct _SyncedTabsDialog {
+ GtkDialog parent_instance;
+
+ EphyOpenTabsManager *manager;
+
+ WebKitFaviconDatabase *database;
+ GdkPixbuf *pixbuf_root;
+ GdkPixbuf *pixbuf_missing;
+
+ GtkTreeModel *treestore;
+ GtkWidget *treeview;
+};
+
+G_DEFINE_TYPE (SyncedTabsDialog, synced_tabs_dialog, GTK_TYPE_DIALOG)
+
+enum {
+ ICON_COLUMN,
+ TITLE_COLUMN,
+ URL_COLUMN
+};
+
+enum {
+ PROP_0,
+ PROP_OPEN_TABS_MANAGER,
+ LAST_PROP
+};
+
+static GParamSpec *obj_properties[LAST_PROP];
+
+typedef struct {
+ SyncedTabsDialog *dialog;
+ char *title;
+ char *url;
+ guint parent_index;
+} PopulateRowAsyncData;
+
+static PopulateRowAsyncData *
+populate_row_async_data_new (SyncedTabsDialog *dialog,
+ const char *title,
+ const char *url,
+ guint parent_index)
+{
+ PopulateRowAsyncData *data;
+
+ data = g_slice_new (PopulateRowAsyncData);
+ data->dialog = g_object_ref (dialog);
+ data->title = g_strdup (title);
+ data->url = g_strdup (url);
+ data->parent_index = parent_index;
+
+ return data;
+}
+
+static void
+populate_row_async_data_free (PopulateRowAsyncData *data)
+{
+ g_object_unref (data->dialog);
+ g_free (data->title);
+ g_free (data->url);
+ g_slice_free (PopulateRowAsyncData, data);
+}
+
+static void
+treeview_row_activated_cb (GtkTreeView *view,
+ GtkTreePath *path,
+ GtkTreeViewColumn *column,
+ gpointer user_data)
+{
+ EphyShell *shell;
+ EphyEmbed *embed;
+ GtkWindow *window;
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+ char *url;
+ char *path_str;
+
+ /* No action on top-level rows. */
+ if (gtk_tree_path_get_depth (path) == 1)
+ return;
+
+ /* No action on local tabs, i.e. children of first top-level row. */
+ path_str = gtk_tree_path_to_string (path);
+ if (g_str_has_prefix (path_str, "0:"))
+ goto out;
+
+ model = gtk_tree_view_get_model (view);
+ gtk_tree_model_get_iter (model, &iter, path);
+ gtk_tree_model_get (model, &iter, URL_COLUMN, &url, -1);
+
+ shell = ephy_shell_get_default ();
+ window = gtk_application_get_active_window (GTK_APPLICATION (shell));
+ embed = ephy_shell_new_tab (shell, EPHY_WINDOW (window),
+ NULL, EPHY_NEW_TAB_APPEND_LAST);
+ ephy_web_view_load_url (ephy_embed_get_web_view (embed), url);
+
+ g_free (url);
+out:
+ g_free (path_str);
+}
+
+static void
+synced_tabs_dialog_favicon_loaded_cb (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ WebKitFaviconDatabase *database = WEBKIT_FAVICON_DATABASE (source);
+ PopulateRowAsyncData *data = (PopulateRowAsyncData *)user_data;
+ cairo_surface_t *surface;
+ GdkPixbuf *favicon = NULL;
+ GtkTreeIter parent_iter;
+ char *escaped_url;
+
+ surface = webkit_favicon_database_get_favicon_finish (database, result, NULL);
+ if (surface) {
+ favicon = ephy_pixbuf_get_from_surface_scaled (surface, FAVICON_SIZE, FAVICON_SIZE);
+ cairo_surface_destroy (surface);
+ }
+
+ gtk_tree_model_get_iter_first (data->dialog->treestore, &parent_iter);
+ for (guint i = 0; i < data->parent_index; i++)
+ gtk_tree_model_iter_next (data->dialog->treestore, &parent_iter);
+
+ favicon = favicon ? favicon : data->dialog->pixbuf_missing;
+ escaped_url = g_markup_escape_text (data->url, -1);
+ gtk_tree_store_insert_with_values (GTK_TREE_STORE (data->dialog->treestore),
+ NULL, &parent_iter, -1,
+ ICON_COLUMN, favicon,
+ TITLE_COLUMN, data->title,
+ URL_COLUMN, escaped_url,
+ -1);
+
+ g_free (escaped_url);
+ populate_row_async_data_free (data);
+}
+
+static void
+synced_tabs_dialog_populate_from_record (SyncedTabsDialog *dialog,
+ EphyOpenTabsRecord *record,
+ gboolean is_local,
+ guint index)
+{
+ PopulateRowAsyncData *data;
+ JsonArray *url_history;
+ GSList *tabs;
+ const char *title;
+ const char *url;
+
+ g_assert (EPHY_IS_SYNCED_TABS_DIALOG (dialog));
+ g_assert (EPHY_IS_OPEN_TABS_RECORD (record));
+
+ if (is_local)
+ title = _("Local Tabs");
+ else
+ title = ephy_open_tabs_record_get_client_name (record);
+
+ /* Insert top-level row. */
+ gtk_tree_store_insert_with_values (GTK_TREE_STORE (dialog->treestore),
+ NULL, NULL, -1,
+ ICON_COLUMN, dialog->pixbuf_root,
+ TITLE_COLUMN, title,
+ URL_COLUMN, NULL,
+ -1);
+
+ tabs = ephy_open_tabs_record_get_tabs (record);
+ for (GSList *l = tabs; l && l->data; l = l->next) {
+ title = json_object_get_string_member (l->data, "title");
+ url_history = json_object_get_array_member (l->data, "urlHistory");
+ url = json_array_get_string_element (url_history, 0);
+
+ data = populate_row_async_data_new (dialog, title, url, index);
+ webkit_favicon_database_get_favicon (dialog->database, url, NULL,
+ synced_tabs_dialog_favicon_loaded_cb,
+ data);
+ }
+}
+
+static void
+synced_tabs_dialog_populate_model (SyncedTabsDialog *dialog)
+{
+ EphyOpenTabsRecord *record;
+ GSList *remotes;
+ guint index = 0;
+
+ /* Insert local tabs. */
+ record = ephy_open_tabs_manager_get_local_tabs (dialog->manager);
+ synced_tabs_dialog_populate_from_record (dialog, record, TRUE, index++);
+
+ /* Insert remote tabs. */
+ remotes = ephy_open_tabs_manager_get_remote_tabs (dialog->manager);
+ for (GSList *l = remotes; l && l->data; l = l->next)
+ synced_tabs_dialog_populate_from_record (dialog, l->data, FALSE, index++);
+
+ g_object_unref (record);
+}
+
+static void
+synced_tabs_dialog_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ SyncedTabsDialog *dialog = EPHY_SYNCED_TABS_DIALOG (object);
+
+ switch (prop_id) {
+ case PROP_OPEN_TABS_MANAGER:
+ g_clear_object (&dialog->manager);
+ dialog->manager = g_object_ref (g_value_get_object (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+synced_tabs_dialog_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SyncedTabsDialog *dialog = EPHY_SYNCED_TABS_DIALOG (object);
+
+ switch (prop_id) {
+ case PROP_OPEN_TABS_MANAGER:
+ g_value_set_object (value, dialog->manager);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+synced_tabs_dialog_constructed (GObject *object)
+{
+ SyncedTabsDialog *dialog = EPHY_SYNCED_TABS_DIALOG (object);
+
+ G_OBJECT_CLASS (synced_tabs_dialog_parent_class)->constructed (object);
+
+ synced_tabs_dialog_populate_model (dialog);
+}
+
+static void
+synced_tabs_dialog_dispose (GObject *object)
+{
+ SyncedTabsDialog *dialog = EPHY_SYNCED_TABS_DIALOG (object);
+
+ g_clear_object (&dialog->manager);
+ g_clear_object (&dialog->pixbuf_root);
+ g_clear_object (&dialog->pixbuf_missing);
+
+ G_OBJECT_CLASS (synced_tabs_dialog_parent_class)->dispose (object);
+}
+
+static void
+synced_tabs_dialog_class_init (SyncedTabsDialogClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->set_property = synced_tabs_dialog_set_property;
+ object_class->get_property = synced_tabs_dialog_get_property;
+ object_class->constructed = synced_tabs_dialog_constructed;
+ object_class->dispose = synced_tabs_dialog_dispose;
+
+ obj_properties[PROP_OPEN_TABS_MANAGER] =
+ g_param_spec_object ("open-tabs-manager",
+ "Open tabs manager",
+ "Open Tabs Manager",
+ EPHY_TYPE_OPEN_TABS_MANAGER,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, LAST_PROP, obj_properties);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/org/gnome/epiphany/gtk/synced-tabs-dialog.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, SyncedTabsDialog, treestore);
+ gtk_widget_class_bind_template_child (widget_class, SyncedTabsDialog, treeview);
+ gtk_widget_class_bind_template_callback (widget_class, treeview_row_activated_cb);
+}
+
+static void
+synced_tabs_dialog_init (SyncedTabsDialog *dialog)
+{
+ WebKitWebContext *context;
+ GdkPixbuf *pixbuf;
+ GError *error = NULL;
+
+ gtk_widget_init_template (GTK_WIDGET (dialog));
+
+ gtk_tree_view_set_tooltip_column (GTK_TREE_VIEW (dialog->treeview), URL_COLUMN);
+
+ context = ephy_embed_shell_get_web_context (ephy_embed_shell_get_default ());
+ dialog->database = webkit_web_context_get_favicon_database (context);
+
+ dialog->pixbuf_root = gtk_icon_theme_load_icon (gtk_icon_theme_get_default (),
+ "computer-symbolic",
+ FAVICON_SIZE, 0, &error);
+ if (error) {
+ g_warning ("Failed to build pixbuf from theme icon: %s", error->message);
+ g_error_free (error);
+ error = NULL;
+ }
+
+ pixbuf = gdk_pixbuf_new_from_resource (PIXBUF_MISSING_PATH, &error);
+ if (pixbuf) {
+ dialog->pixbuf_missing = gdk_pixbuf_scale_simple (pixbuf,
+ FAVICON_SIZE, FAVICON_SIZE,
+ GDK_INTERP_BILINEAR);
+ g_object_unref (pixbuf);
+ } else {
+ g_warning ("Failed to build pixbuf from resource: %s", error->message);
+ g_error_free (error);
+ }
+}
+
+SyncedTabsDialog *
+synced_tabs_dialog_new (EphyOpenTabsManager *manager)
+{
+ return EPHY_SYNCED_TABS_DIALOG (g_object_new (EPHY_TYPE_SYNCED_TABS_DIALOG,
+ "use-header-bar", TRUE,
+ "open-tabs-manager", manager,
+ NULL));
+}
diff --git a/src/synced-tabs-dialog.h b/src/synced-tabs-dialog.h
new file mode 100644
index 000000000..9081a0924
--- /dev/null
+++ b/src/synced-tabs-dialog.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2017 Gabriel Ivascu <ivascu.gabriel59@gmail.com>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany 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.
+ *
+ * Epiphany 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 Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ephy-open-tabs-manager.h"
+
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define EPHY_TYPE_SYNCED_TABS_DIALOG (synced_tabs_dialog_get_type ())
+
+G_DECLARE_FINAL_TYPE (SyncedTabsDialog, synced_tabs_dialog, EPHY, SYNCED_TABS_DIALOG, GtkDialog)
+
+SyncedTabsDialog *synced_tabs_dialog_new (EphyOpenTabsManager *manager);
+
+G_END_DECLS