[gnome-software/mwleeds/hardcoded-pwa-list: 1/14] Revive webapp support




commit 89434224a19187149da62ecfda92bb011b697ad6
Author: Phaedrus Leeds <mwleeds protonmail com>
Date:   Mon Nov 29 14:09:46 2021 -0800

    Revive webapp support
    
    This is a re-implementation of the epiphany plugin (dropped in
    ea5eb222f) in a way that should be more robust and maintainable.
    
    This is the first step toward bringing back webapp support. I am doing
    this as a project sponsored by a GNOME Foundation grant. The goal is to
    include a set of Progressive Web Apps in Software that can be installed
    via Epiphany's web app support (and potentially also via Chromium), to
    expand the selection of apps available to users who might not otherwise
    find them. While Epiphany's web apps do not currently implement support
    for PWA manifests, that is hopefully going to be in scope for this
    project.  While the plan is to only include PWAs in the set hard coded
    into Software, non-PWA web apps installed via Epiphany will also show up
    in Software.
    
    The previous implementation of web apps in Software was dropped because
    it was buggy and users did not like it, since they didn't perceive much
    benefit of using sites as web apps versus using a normal browser and
    were confused by the apps not being native desktop apps. The following
    factors should mitigate or address those issues for the new
    implementation:
    1) We're going to use a D-Bus API provided by Epiphany for enumerating,
       installing, and removing web apps rather than re-implementing those
       functions in Software. This avoids bugs that can occur when the
       implementations are out of sync.
    2) We're going to differentiate web apps from native apps in the UI,
       pending design input on how to do so.
    3) The set of PWAs included with Software will be mostly or entirely
       apps that would not otherwise be available to the user, because they
       are only available as PWAs and not native apps.
    4) Once we have support for PWA manifests in Epiphany, or support for
       Chromium-based web apps which already support manifests, the apps
       should be more featureful and closer to native apps than the current
       web app implementation, e.g. they may work offline to some extent.
    
    (This removes the dependency on epiphany-runtime in the spec file,
    because it's not clear yet if this feature will be enabled by default in
    Fedora and the plan is to make this work with flatpak'd Epiphany which
    doesn't have an analogous way to install epiphany without a desktop
    icon; see https://bugzilla.redhat.com/show_bug.cgi?id=1781359 for
    context.)
    
    The file plugins/epiphany/org.gnome.Epiphany.WebAppProvider.xml comes
    from its canonical source:
    
https://gitlab.gnome.org/GNOME/epiphany/-/blob/master/src/webapp-provider/org.gnome.Epiphany.WebAppProvider.xml

 contrib/gnome-software.spec.in                     |   2 +
 meson_options.txt                                  |   1 +
 plugins/core/gs-plugin-icons.c                     |   1 +
 plugins/epiphany/gs-plugin-epiphany.c              | 758 +++++++++++++++++++++
 plugins/epiphany/gs-plugin-epiphany.h              |  20 +
 plugins/epiphany/gs-self-test.c                    | 106 +++
 plugins/epiphany/meson.build                       |  62 ++
 .../epiphany/org.gnome.Epiphany.WebAppProvider.xml |  85 +++
 ....gnome.Software.Plugin.Epiphany.metainfo.xml.in |  11 +
 plugins/meson.build                                |   3 +
 po/POTFILES.in                                     |   1 +
 src/gs-installed-page.c                            |   6 +
 12 files changed, 1056 insertions(+)
---
diff --git a/contrib/gnome-software.spec.in b/contrib/gnome-software.spec.in
index f79448567..2a93b06fb 100644
--- a/contrib/gnome-software.spec.in
+++ b/contrib/gnome-software.spec.in
@@ -144,11 +144,13 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/*.desktop
 %{_datadir}/gnome-software/featured-*.svg
 %{_datadir}/gnome-software/featured-*.jpg
 %{_datadir}/metainfo/org.gnome.Software.appdata.xml
+%{_datadir}/metainfo/org.gnome.Software.Plugin.Epiphany.metainfo.xml
 %{_datadir}/metainfo/org.gnome.Software.Plugin.Flatpak.metainfo.xml
 %{_datadir}/metainfo/org.gnome.Software.Plugin.Fwupd.metainfo.xml
 %dir %{gs_plugin_dir}
 %{gs_plugin_dir}/libgs_plugin_appstream.so
 %{gs_plugin_dir}/libgs_plugin_dummy.so
+%{gs_plugin_dir}/libgs_plugin_epiphany.so
 %{gs_plugin_dir}/libgs_plugin_fedora-langpacks.so
 %{gs_plugin_dir}/libgs_plugin_fedora-pkgdb-collections.so
 %{gs_plugin_dir}/libgs_plugin_flatpak.so
diff --git a/meson_options.txt b/meson_options.txt
index 57658e2fe..030da3e23 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -9,6 +9,7 @@ option('fwupd', type : 'boolean', value : true, description : 'enable fwupd supp
 option('flatpak', type : 'boolean', value : true, description : 'enable Flatpak support')
 option('malcontent', type : 'boolean', value : true, description : 'enable parental controls support using 
libmalcontent')
 option('rpm_ostree', type : 'boolean', value : false, description : 'enable rpm-ostree support')
+option('webapps', type : 'boolean', value : false, description : 'enable webapps support')
 option('gudev', type : 'boolean', value : true, description : 'enable GUdev support')
 option('apt', type : 'boolean', value : false, description : 'enable apt: URL handler in the .desktop file')
 option('snap', type : 'boolean', value : false, description : 'enable Snap support')
diff --git a/plugins/core/gs-plugin-icons.c b/plugins/core/gs-plugin-icons.c
index 567fdfa89..72a235ec6 100644
--- a/plugins/core/gs-plugin-icons.c
+++ b/plugins/core/gs-plugin-icons.c
@@ -46,6 +46,7 @@ gs_plugin_icons_init (GsPluginIcons *self)
 {
        /* needs remote icons downloaded */
        gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "appstream");
+       gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "epiphany");
 }
 
 static void
diff --git a/plugins/epiphany/gs-plugin-epiphany.c b/plugins/epiphany/gs-plugin-epiphany.c
new file mode 100644
index 000000000..23f9692a0
--- /dev/null
+++ b/plugins/epiphany/gs-plugin-epiphany.c
@@ -0,0 +1,758 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2021-2022 Matthew Leeds <mwleeds protonmail com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include <config.h>
+#include <glib/gi18n.h>
+#include <gnome-software.h>
+#include <fcntl.h>
+#include <gio/gunixfdlist.h>
+#include <glib/gstdio.h>
+
+#include "gs-epiphany-generated.h"
+#include "gs-plugin-epiphany.h"
+
+/*
+ * SECTION:
+ * This plugin uses Epiphany to install, launch, and uninstall web applications.
+ *
+ * If the org.gnome.Epiphany.WebAppProvider D-Bus interface is not present or
+ * the DynamicLauncher portal is not available then it self-disables. This
+ * should work with both Flatpak'd and not Flatpak'd Epiphany, for new enough
+ * versions of Epiphany.
+ *
+ * Since: 43
+ */
+
+struct _GsPluginEpiphany
+{
+       GsPlugin parent;
+
+       GsWorkerThread *worker;  /* (owned) */
+
+       GsEphyWebAppProvider *epiphany_proxy;  /* (owned) */
+       GDBusProxy *launcher_portal_proxy;  /* (owned) */
+       GFileMonitor *monitor; /* (owned) */
+       guint changed_id;
+       GMutex installed_apps_mutex;
+};
+
+G_DEFINE_TYPE (GsPluginEpiphany, gs_plugin_epiphany, GS_TYPE_PLUGIN)
+
+#define assert_in_worker(self) \
+       g_assert (gs_worker_thread_is_in_worker_context (self->worker))
+
+static void
+gs_epiphany_error_convert (GError **perror)
+{
+       GError *error = perror != NULL ? *perror : NULL;
+
+       /* not set */
+       if (error == NULL)
+               return;
+
+       /* parse remote epiphany-webapp-provider error */
+       if (g_dbus_error_is_remote_error (error)) {
+               g_autofree gchar *remote_error = g_dbus_error_get_remote_error (error);
+
+               g_dbus_error_strip_remote_error (error);
+
+               if (g_str_equal (remote_error, "org.freedesktop.DBus.Error.ServiceUnknown")) {
+                       error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED;
+               } else if (g_str_has_prefix (remote_error, "org.gnome.Epiphany.WebAppProvider.Error")) {
+                       error->code = GS_PLUGIN_ERROR_FAILED;
+               } else {
+                       g_warning ("Can’t reliably fixup remote error ‘%s’", remote_error);
+                       error->code = GS_PLUGIN_ERROR_FAILED;
+               }
+               error->domain = GS_PLUGIN_ERROR;
+               return;
+       }
+
+       /* this is allowed for low-level errors */
+       if (gs_utils_error_convert_gio (perror))
+               return;
+
+       /* this is allowed for low-level errors */
+       if (gs_utils_error_convert_gdbus (perror))
+               return;
+}
+
+/* Run in the main thread. */
+static void
+gs_plugin_epiphany_changed_cb (GFileMonitor      *monitor,
+                               GFile             *file,
+                               GFile             *other_file,
+                               GFileMonitorEvent  event_type,
+                               gpointer           user_data)
+{
+       GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (user_data);
+
+       /* FIXME: With the current API this is the only way to reload the list
+        * of installed apps.
+        */
+       gs_plugin_reload (GS_PLUGIN (self));
+}
+
+static void setup_thread_cb (GTask        *task,
+                             gpointer      source_object,
+                             gpointer      task_data,
+                             GCancellable *cancellable);
+
+static void
+gs_plugin_epiphany_setup_async (GsPlugin            *plugin,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+       GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin);
+       g_autoptr(GTask) task = NULL;
+       g_autoptr(GError) local_error = NULL;
+       g_autofree char *portal_apps_path = NULL;
+       g_autoptr(GFile) portal_apps_file = NULL;
+
+       task = g_task_new (plugin, cancellable, callback, user_data);
+       g_task_set_source_tag (task, gs_plugin_epiphany_setup_async);
+
+       g_debug ("%s", G_STRFUNC);
+
+       /* Watch for changes to the set of installed apps in the main thread.
+        * This will also trigger when other apps' dynamic launchers are
+        * installed or removed but that is expected to be infrequent.
+        */
+       portal_apps_path = g_build_filename (g_get_user_data_dir (), "xdg-desktop-portal", "applications", 
NULL);
+       g_mkdir_with_parents (portal_apps_path, 0700);
+       portal_apps_file = g_file_new_for_path (portal_apps_path);
+       self->monitor = g_file_monitor_directory (portal_apps_file, G_FILE_MONITOR_WATCH_MOVES,
+                                                 cancellable, &local_error);
+       if (self->monitor == NULL) {
+               gs_epiphany_error_convert (&local_error);
+               g_task_return_error (task, g_steal_pointer (&local_error));
+               return;
+       }
+
+       self->changed_id = g_signal_connect (self->monitor, "changed",
+                                            G_CALLBACK (gs_plugin_epiphany_changed_cb), self);
+
+       /* Start up a worker thread to process all the plugin’s function calls. */
+       self->worker = gs_worker_thread_new ("gs-plugin-epiphany");
+
+       /* Queue a job to set up D-Bus proxies */
+       gs_worker_thread_queue (self->worker, G_PRIORITY_DEFAULT,
+                               setup_thread_cb, g_steal_pointer (&task));
+}
+
+/* Run in @worker */
+static void
+setup_thread_cb (GTask        *task,
+                gpointer      source_object,
+                gpointer      task_data,
+                GCancellable *cancellable)
+{
+       GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (source_object);
+       g_autofree gchar *name_owner = NULL;
+       g_autoptr(GError) local_error = NULL;
+       g_autoptr(GVariant) version = NULL;
+
+       assert_in_worker (self);
+
+       /* Check that the proxy exists (and is owned; it should auto-start) so
+        * we can disable the plugin for systems which don’t have new enough
+        * Epiphany.
+        */
+       self->epiphany_proxy = gs_ephy_web_app_provider_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION,
+                                                                               G_DBUS_PROXY_FLAGS_NONE,
+                                                                               
"org.gnome.Epiphany.WebAppProvider",
+                                                                               
"/org/gnome/Epiphany/WebAppProvider",
+                                                                               g_task_get_cancellable (task),
+                                                                               &local_error);
+       if (self->epiphany_proxy == NULL) {
+               gs_epiphany_error_convert (&local_error);
+               g_task_return_error (task, g_steal_pointer (&local_error));
+               return;
+       }
+
+       name_owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (self->epiphany_proxy));
+       if (name_owner == NULL) {
+               g_task_return_new_error (task, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED,
+                                        "Couldn’t create Epiphany WebAppProvider proxy: couldn’t get name 
owner");
+               return;
+       }
+
+       /* Check if the dynamic launcher portal is available and disable otherwise */
+       self->launcher_portal_proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION,
+                                                                    
G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS,
+                                                                    NULL,
+                                                                    "org.freedesktop.portal.Desktop",
+                                                                    "/org/freedesktop/portal/desktop",
+                                                                    "org.freedesktop.portal.DynamicLauncher",
+                                                                    g_task_get_cancellable (task),
+                                                                    &local_error);
+       if (self->launcher_portal_proxy == NULL) {
+               gs_epiphany_error_convert (&local_error);
+               g_task_return_error (task, g_steal_pointer (&local_error));
+               return;
+       }
+
+       version = g_dbus_proxy_get_cached_property (self->launcher_portal_proxy, "version");
+       if (version == NULL) {
+               g_task_return_new_error (task, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED,
+                                        "Dynamic launcher portal not available");
+               return;
+       } else {
+               g_debug ("Found version %" G_GUINT32_FORMAT " of the dynamic launcher portal",
+                        g_variant_get_uint32 (version));
+       }
+
+       g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_epiphany_setup_finish (GsPlugin      *plugin,
+                                 GAsyncResult  *result,
+                                 GError       **error)
+{
+       return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void shutdown_cb (GObject      *source_object,
+                         GAsyncResult *result,
+                         gpointer      user_data);
+
+static void
+gs_plugin_epiphany_shutdown_async (GsPlugin            *plugin,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
+{
+       GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin);
+       g_autoptr(GTask) task = NULL;
+
+       task = g_task_new (self, cancellable, callback, user_data);
+       g_task_set_source_tag (task, gs_plugin_epiphany_shutdown_async);
+
+       /* Stop the worker thread. */
+       gs_worker_thread_shutdown_async (self->worker, cancellable, shutdown_cb, g_steal_pointer (&task));
+}
+
+static void
+shutdown_cb (GObject      *source_object,
+             GAsyncResult *result,
+             gpointer      user_data)
+{
+       g_autoptr(GTask) task = G_TASK (user_data);
+       GsPluginEpiphany *self = g_task_get_source_object (task);
+       g_autoptr(GsWorkerThread) worker = NULL;
+       g_autoptr(GError) local_error = NULL;
+
+       worker = g_steal_pointer (&self->worker);
+
+       if (!gs_worker_thread_shutdown_finish (worker, result, &local_error)) {
+               g_task_return_error (task, g_steal_pointer (&local_error));
+               return;
+       }
+
+       g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_epiphany_shutdown_finish (GsPlugin      *plugin,
+                                    GAsyncResult  *result,
+                                    GError       **error)
+{
+       return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+gs_plugin_epiphany_init (GsPluginEpiphany *self)
+{
+       /* set name of MetaInfo file */
+       gs_plugin_set_appstream_id (GS_PLUGIN (self), "org.gnome.Software.Plugin.Epiphany");
+
+       /* need help from appstream */
+       gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "appstream");
+
+       /* prioritize over packages */
+       gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_BETTER_THAN, "packagekit");
+}
+
+static void
+gs_plugin_epiphany_dispose (GObject *object)
+{
+       GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (object);
+
+       if (self->changed_id > 0) {
+               g_signal_handler_disconnect (self->monitor, self->changed_id);
+               self->changed_id = 0;
+       }
+
+       g_clear_object (&self->epiphany_proxy);
+       g_clear_object (&self->launcher_portal_proxy);
+       g_clear_object (&self->monitor);
+       g_clear_object (&self->worker);
+
+       G_OBJECT_CLASS (gs_plugin_epiphany_parent_class)->dispose (object);
+}
+
+static void
+gs_plugin_epiphany_finalize (GObject *object)
+{
+       GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (object);
+
+       g_mutex_clear (&self->installed_apps_mutex);
+
+       G_OBJECT_CLASS (gs_plugin_epiphany_parent_class)->finalize (object);
+}
+
+void
+gs_plugin_adopt_app (GsPlugin *plugin,
+                    GsApp    *app)
+{
+       if (gs_app_get_kind (app) == AS_COMPONENT_KIND_WEB_APP &&
+           gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_PACKAGE) {
+               gs_app_set_management_plugin (app, plugin);
+       }
+}
+
+static void list_installed_apps_thread_cb (GTask        *task,
+                                           gpointer      source_object,
+                                           gpointer      task_data,
+                                           GCancellable *cancellable);
+
+static void
+gs_plugin_epiphany_list_installed_apps_async (GsPlugin                       *plugin,
+                                             GsPluginListInstalledAppsFlags  flags,
+                                             GCancellable                   *cancellable,
+                                             GAsyncReadyCallback             callback,
+                                             gpointer                        user_data)
+{
+       GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin);
+       g_autoptr(GTask) task = NULL;
+
+       task = g_task_new (plugin, cancellable, callback, user_data);
+       g_task_set_source_tag (task, gs_plugin_epiphany_list_installed_apps_async);
+
+       /* Queue a job to get the installed apps. */
+       gs_worker_thread_queue (self->worker, G_PRIORITY_DEFAULT,
+                               list_installed_apps_thread_cb, g_steal_pointer (&task));
+}
+
+/* Run in @worker */
+static void
+set_license_from_hostname (GsApp      *app,
+                          const char *hostname)
+{
+       const struct {
+               const gchar *hostname;
+               const gchar *license_spdx;
+       } app_licenses[] = {
+       /* Keep in alphabetical order by hostname */
+       { "app.diagrams.net", "Apache-2.0" },
+       { "devdocs.io", "MPL-2.0" },
+       { "discourse.flathub.org", "GPL-2.0-or-later" },
+       { "discourse.gnome.org", "GPL-2.0-or-later" },
+       { "excalidraw.com", "MIT" },
+       { "pinafore.social", "AGPL-3.0-only" },
+       { "snapdrop.net", "GPL-3.0-only" },
+       { "stackedit.io", "Apache-2.0" },
+       { "squoosh.app", "Apache-2.0" },
+       };
+
+       g_return_if_fail (GS_IS_APP (app));
+
+       if (hostname == NULL || *hostname == '\0')
+               return;
+
+       /* Hard-code the licenses as it's hard to get them programmatically. We
+        * can move them to an AppStream file if needed.
+        */
+       if (gs_app_get_license (app) == NULL) {
+               for (gsize i = 0; i < G_N_ELEMENTS (app_licenses); i++) {
+                       if (g_str_equal (hostname, app_licenses[i].hostname)) {
+                               gs_app_set_license (app, GS_APP_QUALITY_NORMAL,
+                                                   app_licenses[i].license_spdx);
+                               break;
+                       }
+               }
+       }
+}
+
+/* Run in @worker */
+static GsApp *
+gs_epiphany_create_app (GsPluginEpiphany *self,
+                       const char       *id)
+{
+       g_autoptr(GsApp) app_cached = NULL;
+       g_autoptr(GsApp) tmp_app = NULL;
+
+       assert_in_worker (self);
+
+       tmp_app = gs_app_new (id);
+       gs_app_set_management_plugin (tmp_app, GS_PLUGIN (self));
+       gs_app_set_origin (tmp_app, "gnome-web");
+       gs_app_set_origin_ui (tmp_app, _("GNOME Web"));
+       gs_app_set_kind (tmp_app, AS_COMPONENT_KIND_WEB_APP);
+       gs_app_set_scope (tmp_app, AS_COMPONENT_SCOPE_USER);
+
+       app_cached = gs_plugin_cache_lookup (GS_PLUGIN (self), id);
+       if (app_cached != NULL)
+               return g_steal_pointer (&app_cached);
+
+       gs_plugin_cache_add (GS_PLUGIN (self), id, tmp_app);
+       return g_steal_pointer (&tmp_app);
+}
+
+/* Run in @worker */
+static void
+list_installed_apps_thread_cb (GTask        *task,
+                               gpointer      source_object,
+                               gpointer      task_data,
+                               GCancellable *cancellable)
+{
+       GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (source_object);
+       g_autoptr(GsAppList) list = gs_app_list_new ();
+       g_autoptr(GsAppList) installed_cache = gs_app_list_new ();
+       g_autoptr(GError) local_error = NULL;
+       g_autoptr(GVariant) webapps_v = NULL;
+       g_auto(GStrv) webapps = NULL;
+       g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->installed_apps_mutex);
+       guint n_webapps;
+
+       assert_in_worker (self);
+
+       if (!gs_ephy_web_app_provider_call_get_installed_apps_sync (self->epiphany_proxy,
+                                                                   &webapps,
+                                                                   cancellable,
+                                                                   &local_error)) {
+               gs_epiphany_error_convert (&local_error);
+               g_task_return_error (task, g_steal_pointer (&local_error));
+               return;
+       }
+
+       n_webapps = g_strv_length (webapps);
+       g_debug ("%s: epiphany-webapp-provider returned %u installed web apps", G_STRFUNC, n_webapps);
+       for (guint i = 0; i < n_webapps; i++) {
+               const gchar *desktop_file_id = webapps[i];
+               const gchar *desktop_path;
+               const gchar *name;
+               const gchar *url = NULL;
+               g_autofree char *icon_path = NULL;
+               const gchar *exec;
+               const gchar *host;
+               int argc;
+               g_auto(GStrv) argv = NULL;
+               guint64 install_date = 0;
+               goffset desktop_size = 0, icon_size = 0;
+               g_autoptr(GsApp) app = NULL;
+               g_autoptr(GDesktopAppInfo) desktop_info = NULL;
+               g_autoptr(GFileInfo) file_info = NULL;
+               g_autoptr(GFile) desktop_file = NULL;
+               g_autoptr(GUri) uri = NULL;
+
+               g_debug ("%s: Working on installed web app %s", G_STRFUNC, desktop_file_id);
+
+               desktop_info = g_desktop_app_info_new (desktop_file_id);
+               if (desktop_info == NULL) {
+                       g_warning ("Epiphany returned a non-existent desktop ID %s", desktop_file_id);
+                       continue;
+               }
+
+               name = g_app_info_get_name (G_APP_INFO (desktop_info));
+
+               /* This way of getting the URL is a bit hacky but it's what Epiphany does */
+               exec = g_app_info_get_commandline (G_APP_INFO (desktop_info));
+               if (g_shell_parse_argv (exec, &argc, &argv, NULL)) {
+                       g_assert (argc > 0);
+                       url = argv[argc - 1];
+               }
+               if (!url || !(uri = g_uri_parse (url, G_URI_FLAGS_NONE, NULL))) {
+                       g_warning ("Failed to parse URL for web app %s", desktop_file_id);
+                       continue;
+               }
+
+               icon_path = g_desktop_app_info_get_string (desktop_info, "Icon");
+
+               desktop_path = g_desktop_app_info_get_filename (desktop_info);
+               g_assert (desktop_path);
+               desktop_file = g_file_new_for_path (desktop_path);
+
+               file_info = g_file_query_info (desktop_file,
+                                              G_FILE_ATTRIBUTE_TIME_CREATED "," 
G_FILE_ATTRIBUTE_STANDARD_SIZE,
+                                              0, NULL, NULL);
+               if (file_info) {
+                       install_date = g_file_info_get_attribute_uint64 (file_info, 
G_FILE_ATTRIBUTE_TIME_CREATED);
+                       desktop_size = g_file_info_get_size (file_info);
+               }
+
+               app = gs_epiphany_create_app (self, desktop_file_id);
+               gs_app_set_state (app, GS_APP_STATE_INSTALLED);
+               gs_app_set_name (app, GS_APP_QUALITY_NORMAL, name);
+               gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, url);
+               gs_app_set_permissions (app, GS_APP_PERMISSIONS_NETWORK);
+
+               /* Use the domain name as a fallback summary.
+                * FIXME: Fetch the summary from the site's webapp manifest.
+                */
+               host = g_uri_get_host (uri);
+               gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, host ? host : url);
+
+               set_license_from_hostname (app, host);
+
+               if (icon_path) {
+                       g_autoptr(GFile) icon_file = g_file_new_for_path (icon_path);
+                       g_autoptr(GIcon) icon = g_file_icon_new (icon_file);
+                       g_autofree char *icon_dir = g_path_get_dirname (icon_path);
+                       g_autofree char *icon_dir_basename = g_path_get_basename (icon_dir);
+                       const char *x;
+                       guint64 size = 0;
+
+                       g_clear_object (&file_info);
+                       file_info = g_file_query_info (icon_file,
+                                                      G_FILE_ATTRIBUTE_STANDARD_SIZE,
+                                                      0, NULL, NULL);
+                       if (file_info)
+                               icon_size = g_file_info_get_size (file_info);
+
+                       /* dir should be either scalable or e.g. 512x512 */
+                       if (g_strcmp0 (icon_dir_basename, "scalable") == 0) {
+                               /* Ensure scalable icons are preferred */
+                               size = 4096;
+                       } else if ((x = strchr (icon_dir_basename, 'x')) != NULL) {
+                               g_ascii_string_to_unsigned (x + 1, 10, 1, G_MAXINT, &size, NULL);
+                       }
+                       if (size > 0 && size <= 4096) {
+                               gs_icon_set_width (icon, size);
+                               gs_icon_set_height (icon, size);
+                       } else {
+                               g_warning ("Unexpectedly unable to determine size of icon %s", icon_path);
+                       }
+
+                       gs_app_add_icon (app, icon);
+               }
+               if (install_date) {
+                       gs_app_set_install_date (app, install_date);
+               }
+               if (desktop_size > 0 || icon_size > 0) {
+                       gs_app_set_size_installed (app, desktop_size + icon_size);
+               }
+               gs_app_list_add (list, app);
+       }
+
+       /* Update the state on any apps that were uninstalled outside
+        * gnome-software
+        */
+       gs_plugin_cache_lookup_by_state (GS_PLUGIN (self), installed_cache, GS_APP_STATE_INSTALLED);
+       for (guint i = 0; i < gs_app_list_length (installed_cache); i++) {
+               GsApp *app = gs_app_list_index (installed_cache, i);
+               const char *app_id = gs_app_get_id (app);
+               g_autoptr(GsApp) app_cached = NULL;
+
+               if (g_strv_contains ((const char * const *)webapps, app_id))
+                       continue;
+
+               gs_app_set_state (app, GS_APP_STATE_UNKNOWN);
+               gs_plugin_cache_remove (GS_PLUGIN (self), app_id);
+       }
+
+       g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref);
+}
+
+static GsAppList *
+gs_plugin_epiphany_list_installed_apps_finish (GsPlugin      *plugin,
+                                               GAsyncResult  *result,
+                                               GError       **error)
+{
+       g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == 
gs_plugin_epiphany_list_installed_apps_async, FALSE);
+       return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static GVariant *
+get_serialized_icon (GsApp *app,
+                    GIcon *icon)
+{
+       g_autofree char *icon_path = NULL;
+       g_autoptr(GInputStream) stream = NULL;
+       g_autoptr(GBytes) bytes = NULL;
+       g_autoptr(GIcon) bytes_icon = NULL;
+       g_autoptr(GVariant) icon_v = NULL;
+
+       /* Note: GsRemoteIcon will work on this GFileIcon code path.
+        * The icons plugin should have called
+        * gs_app_ensure_icons_downloaded() for us.
+        */
+       if (!G_IS_FILE_ICON (icon))
+               return NULL;
+
+       icon_path = g_file_get_path (g_file_icon_get_file (G_FILE_ICON (icon)));
+       if (!g_str_has_suffix (icon_path, ".png") &&
+           !g_str_has_suffix (icon_path, ".svg") &&
+           !g_str_has_suffix (icon_path, ".jpeg") &&
+           !g_str_has_suffix (icon_path, ".jpg")) {
+               g_warning ("Icon for app %s has unsupported file extension: %s",
+                          gs_app_get_id (app), icon_path);
+               return NULL;
+       }
+
+       /* Serialize the icon as a #GBytesIcon since that's
+        * what the dynamic launcher portal requires.
+        */
+       stream = g_loadable_icon_load (G_LOADABLE_ICON (icon), 0, NULL, NULL, NULL);
+
+       /* Icons are usually smaller than 1 MiB. Set a 10 MiB
+        * limit so we can't use a huge amount of memory or hit
+        * the D-Bus message size limit
+        */
+       if (stream)
+               bytes = g_input_stream_read_bytes (stream, 10485760 /* 10 MiB */, NULL, NULL);
+       if (bytes)
+               bytes_icon = g_bytes_icon_new (bytes);
+       if (bytes_icon)
+               icon_v = g_icon_serialize (bytes_icon);
+
+       return g_steal_pointer (&icon_v);
+}
+
+gboolean
+gs_plugin_app_install (GsPlugin      *plugin,
+                      GsApp         *app,
+                      GCancellable  *cancellable,
+                      GError       **error)
+{
+       GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin);
+       const char *url;
+       const char *name;
+       const char *token = NULL;
+       g_autoptr(GVariant) token_v = NULL;
+       g_autoptr(GVariant) icon_v = NULL;
+       GVariantBuilder opt_builder;
+       const int icon_sizes[] = {512, 192, 128, 1};
+
+       if (!gs_app_has_management_plugin (app, plugin))
+               return TRUE;
+
+       url = gs_app_get_url (app, AS_URL_KIND_HOMEPAGE);
+       if (url == NULL || *url == '\0') {
+               g_set_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED,
+                            "Can't install web app %s without url",
+                            gs_app_get_id (app));
+               return FALSE;
+       }
+       name = gs_app_get_name (app);
+       if (name == NULL || *name == '\0') {
+               g_set_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED,
+                            "Can't install web app %s without name",
+                            gs_app_get_id (app));
+               return FALSE;
+       }
+       for (guint i = 0; i < G_N_ELEMENTS (icon_sizes); i++) {
+               GIcon *icon = gs_app_get_icon_for_size (app, icon_sizes[i], 1, NULL);
+               if (icon != NULL)
+                       icon_v = get_serialized_icon (app, icon);
+               if (icon_v != NULL)
+                       break;
+       }
+       if (icon_v == NULL) {
+               g_set_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED,
+                            "Can't install web app %s without icon",
+                            gs_app_get_id (app));
+               return FALSE;
+       }
+
+       gs_app_set_state (app, GS_APP_STATE_INSTALLING);
+       /* First get a token from xdg-desktop-portal so Epiphany can do the
+        * installation without user confirmation
+        */
+       g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT);
+       token_v = g_dbus_proxy_call_sync (self->launcher_portal_proxy,
+                                         "RequestInstallToken",
+                                         g_variant_new ("(sva{sv})",
+                                                        name, icon_v, &opt_builder),
+                                         G_DBUS_CALL_FLAGS_NONE,
+                                         -1, cancellable, error);
+       if (token_v == NULL) {
+               gs_epiphany_error_convert (error);
+               gs_app_set_state_recover (app);
+               return FALSE;
+       }
+
+       /* Then pass the token to Epiphany which will use xdg-desktop-portal to
+        * complete the installation
+        */
+       g_variant_get (token_v, "(&s)", &token);
+       if (!gs_ephy_web_app_provider_call_install_sync (self->epiphany_proxy,
+                                                        url, name, token,
+                                                        NULL,
+                                                        cancellable,
+                                                        error)) {
+               gs_epiphany_error_convert (error);
+               gs_app_set_state_recover (app);
+               return FALSE;
+       }
+       gs_app_set_state (app, GS_APP_STATE_INSTALLED);
+
+       return TRUE;
+}
+
+gboolean
+gs_plugin_app_remove (GsPlugin      *plugin,
+                     GsApp         *app,
+                     GCancellable  *cancellable,
+                     GError       **error)
+{
+       GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin);
+
+       if (!gs_app_has_management_plugin (app, plugin))
+               return TRUE;
+
+       gs_app_set_state (app, GS_APP_STATE_REMOVING);
+       if (!gs_ephy_web_app_provider_call_uninstall_sync (self->epiphany_proxy,
+                                                          gs_app_get_id (app),
+                                                          cancellable,
+                                                          error)) {
+               gs_epiphany_error_convert (error);
+               gs_app_set_state_recover (app);
+               return FALSE;
+       }
+       gs_app_set_state (app, GS_APP_STATE_AVAILABLE);
+
+       return TRUE;
+}
+
+gboolean
+gs_plugin_launch (GsPlugin      *plugin,
+                 GsApp         *app,
+                 GCancellable  *cancellable,
+                 GError       **error)
+{
+       if (!gs_app_has_management_plugin (app, plugin))
+               return TRUE;
+
+       return gs_plugin_app_launch (plugin, app, error);
+}
+
+static void
+gs_plugin_epiphany_class_init (GsPluginEpiphanyClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass);
+
+       object_class->dispose = gs_plugin_epiphany_dispose;
+       object_class->finalize = gs_plugin_epiphany_finalize;
+
+       plugin_class->setup_async = gs_plugin_epiphany_setup_async;
+       plugin_class->setup_finish = gs_plugin_epiphany_setup_finish;
+       plugin_class->shutdown_async = gs_plugin_epiphany_shutdown_async;
+       plugin_class->shutdown_finish = gs_plugin_epiphany_shutdown_finish;
+       plugin_class->list_installed_apps_async = gs_plugin_epiphany_list_installed_apps_async;
+       plugin_class->list_installed_apps_finish = gs_plugin_epiphany_list_installed_apps_finish;
+}
+
+GType
+gs_plugin_query_type (void)
+{
+       return GS_TYPE_PLUGIN_EPIPHANY;
+}
diff --git a/plugins/epiphany/gs-plugin-epiphany.h b/plugins/epiphany/gs-plugin-epiphany.h
new file mode 100644
index 000000000..955d0593c
--- /dev/null
+++ b/plugins/epiphany/gs-plugin-epiphany.h
@@ -0,0 +1,20 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2021 Matthew Leeds <mwleeds protonmail com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_PLUGIN_EPIPHANY (gs_plugin_epiphany_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsPluginEpiphany, gs_plugin_epiphany, GS, PLUGIN_EPIPHANY, GsPlugin)
+
+G_END_DECLS
diff --git a/plugins/epiphany/gs-self-test.c b/plugins/epiphany/gs-self-test.c
new file mode 100644
index 000000000..4bea3e740
--- /dev/null
+++ b/plugins/epiphany/gs-self-test.c
@@ -0,0 +1,106 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013-2017 Richard Hughes <richard hughsie com>
+ * Copyright (C) 2021 Matthew Leeds <mwleeds protonmail com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include "gnome-software-private.h"
+
+#include "gs-test.h"
+
+static void
+gs_plugins_epiphany_func (GsPluginLoader *plugin_loader)
+{
+       gboolean ret;
+       g_autoptr(GError) error = NULL;
+       g_autoptr(GsApp) app = NULL;
+       g_autoptr(GsPluginJob) plugin_job = NULL;
+       GsPlugin *plugin;
+
+       /* no epiphany, abort */
+       if (!gs_plugin_loader_get_enabled (plugin_loader, "epiphany"))
+               return;
+
+       /* a webapp with a local icon */
+       app = gs_app_new ("app.squoosh.webapp.desktop");
+       gs_app_set_kind (app, AS_COMPONENT_KIND_WEB_APP);
+       plugin = gs_plugin_loader_find_plugin (plugin_loader, "epiphany");
+       gs_app_set_management_plugin (app, plugin);
+       plugin_job = gs_plugin_job_refine_new_for_app (app,
+                                                      GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON);
+       ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+       gs_test_flush_main_context ();
+       g_assert_no_error (error);
+       g_assert_true (ret);
+
+       g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE);
+       g_assert_nonnull (gs_app_get_icons (app));
+}
+
+int
+main (int argc, char **argv)
+{
+       gboolean ret;
+       g_autofree gchar *fn = NULL;
+       g_autofree gchar *xml = NULL;
+       g_autoptr(GError) error = NULL;
+       g_autoptr(GsPluginLoader) plugin_loader = NULL;
+       const gchar *allowlist[] = {
+               "appstream",
+               "epiphany",
+               "icons",
+               NULL
+       };
+
+       g_test_init (&argc, &argv, NULL);
+       g_setenv ("G_MESSAGES_DEBUG", "all", TRUE);
+       g_setenv ("GS_XMLB_VERBOSE", "1", TRUE);
+
+       /* Use an icon we already have locally */
+       fn = gs_test_get_filename (TESTDATADIR, "icons/hicolor/scalable/org.gnome.Software.svg");
+       g_assert (fn != NULL);
+       xml = g_strdup_printf ("<?xml version=\"1.0\"?>\n"
+               "<components version=\"0.14\">\n"
+               "  <component type=\"webapp\">\n"
+               "    <id>app.squoosh.webapp.desktop</id>\n"
+               "    <metadata_license>CC0-1.0</metadata_license>\n"
+               "    <project_license>Apache-2.0</project_license>\n"
+               "    <name>Squoosh</name>\n"
+               "    <summary>Compress and compare images with different codecs, right in your 
browser</summary>\n"
+               "    <launchable type=\"url\">https://squoosh.app/</launchable>\n"
+               "    <icon type=\"remote\">file://%s</icon>\n"
+               "    <categories>\n"
+               "      <category>Utility</category>\n"
+               "    </categories>\n"
+               "    <pkgname>test</pkgname>\n"
+               "  </component>\n"
+               "  <info>\n"
+               "    <scope>user</scope>\n"
+               "  </info>\n"
+               "</components>\n", fn);
+       g_setenv ("GS_SELF_TEST_APPSTREAM_XML", xml, TRUE);
+
+       /* we can only load this once per process */
+       plugin_loader = gs_plugin_loader_new ();
+       gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR);
+       gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR_CORE);
+       ret = gs_plugin_loader_setup (plugin_loader,
+                                     allowlist,
+                                     NULL,
+                                     NULL,
+                                     &error);
+       g_assert_no_error (error);
+       g_assert_true (ret);
+
+       /* plugin tests go here */
+       g_test_add_data_func ("/gnome-software/plugins/epiphany",
+                             plugin_loader,
+                             (GTestDataFunc) gs_plugins_epiphany_func);
+
+       return g_test_run ();
+}
diff --git a/plugins/epiphany/meson.build b/plugins/epiphany/meson.build
new file mode 100644
index 000000000..794920c18
--- /dev/null
+++ b/plugins/epiphany/meson.build
@@ -0,0 +1,62 @@
+cargs = ['-DG_LOG_DOMAIN="GsPluginEpiphany"']
+
+# The WebAppProvider interface xml comes from:
+# 
https://gitlab.gnome.org/GNOME/epiphany/-/blob/master/src/webapp-provider/org.gnome.Epiphany.WebAppProvider.xml
+epiphany_generated = gnome.gdbus_codegen(
+  'gs-epiphany-generated',
+  sources : ['org.gnome.Epiphany.WebAppProvider.xml'],
+  interface_prefix : 'org.gnome.Epiphany',
+  namespace : 'GsEphy',
+)
+
+shared_module(
+  'gs_plugin_epiphany',
+  epiphany_generated,
+  sources : 'gs-plugin-epiphany.c',
+  include_directories : [
+    include_directories('../..'),
+    include_directories('../../lib'),
+  ],
+  install : true,
+  install_dir: plugin_dir,
+  c_args : cargs,
+  dependencies : plugin_libs,
+  link_with : [
+    libgnomesoftware,
+  ],
+)
+metainfo = 'org.gnome.Software.Plugin.Epiphany.metainfo.xml'
+
+i18n.merge_file(
+  input: metainfo + '.in',
+  output: metainfo,
+  type: 'xml',
+  po_dir: join_paths(meson.source_root(), 'po'),
+  install: true,
+  install_dir: join_paths(get_option('datadir'), 'metainfo'),
+)
+
+if get_option('tests')
+  cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"']
+  cargs += ['-DLOCALPLUGINDIR_CORE="' + meson.current_build_dir() + '/../core"']
+  cargs += ['-DTESTDATADIR="' + join_paths(meson.current_source_dir(), '..', '..', 'data') + '"']
+  e = executable(
+    'gs-self-test-epiphany',
+    compiled_schemas,
+    sources : [
+      'gs-self-test.c',
+    ],
+    include_directories : [
+      include_directories('../..'),
+      include_directories('../../lib'),
+    ],
+    dependencies : [
+      plugin_libs,
+    ],
+    link_with : [
+      libgnomesoftware
+    ],
+    c_args : cargs,
+  )
+  test('gs-self-test-epiphany', e, suite: ['plugins', 'epiphany'], env: test_env)
+endif
diff --git a/plugins/epiphany/org.gnome.Epiphany.WebAppProvider.xml 
b/plugins/epiphany/org.gnome.Epiphany.WebAppProvider.xml
new file mode 100644
index 000000000..6c2954dc3
--- /dev/null
+++ b/plugins/epiphany/org.gnome.Epiphany.WebAppProvider.xml
@@ -0,0 +1,85 @@
+<!DOCTYPE node PUBLIC
+'-//freedesktop//DTD D-BUS Object Introspection 1.0//EN'
+'http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd'>
+<node>
+
+  <!--
+      org.gnome.Epiphany.WebAppProvider:
+      @short_description: Webapp provider interface
+
+      The interface used for handling Epiphany Webapps in GNOME Software, or other
+      clients (version 1).
+  -->
+  <interface name="org.gnome.Epiphany.WebAppProvider">
+    <!--
+        GetInstalledApps:
+        @desktop_file_ids: An array of .desktop file names, one for each
+          installed web app, with the .desktop suffix included
+
+        Returns the set of installed Epiphany web applications. The caller can
+        use them with g_desktop_app_info_new() if outside the sandbox.
+    -->
+    <method name="GetInstalledApps">
+      <arg type="as" name="webapps" direction="out" />
+    </method>
+
+    <!--
+        Install:
+        @url: the URL of the web app
+        @name: the human readable name of the web app
+        @install_token: the token acquired via org.freedesktop.portal.InstallDynamicLauncher
+        @desktop_file_id: the desktop file id of the installed app, with a
+          ".desktop" suffix
+
+        Installs a web app. This interface is expected to be used by trusted
+        system components such as GNOME Software, which can acquire an
+        @install_token using the portal method
+        org.freedesktop.portal.DynamicLauncher.RequestInstallToken(). This allows Epiphany
+        to install the web app without user interaction and despite being sandboxed.
+        This is desirable because the user would've already clicked "Install" in
+        Software; they should not have to confirm the operation again in a different
+        app (Epiphany).
+
+        The @install_token must be provided so that Epiphany can complete the
+        installation without a user-facing dialog. The icon given to
+        org.freedesktop.portal.InstallDynamicLauncher.RequestInstallToken() will
+        be used, and the name given to that method should match the @name given here.
+
+        If the arguments passed are invalid this method returns the error
+        `org.gnome.Epiphany.WebAppProvider.Error.InvalidArgs`, and otherwise
+        `org.gnome.Epiphany.WebAppProvider.Error.Failed`.
+    -->
+    <method name="Install">
+      <arg type="s" name="url" direction="in" />
+      <arg type="s" name="name" direction="in" />
+      <arg type="s" name="install_token" direction="in" />
+      <arg type="s" name="desktop_file_id" direction="out" />
+    </method>
+
+    <!--
+        Uninstall:
+        @desktop_file_id: the filename of the .desktop file for an installed web
+          app, with the .desktop suffix
+
+        Uninstalls a web app. Note that the @desktop_file_id is just a filename
+        not a full path, and it's the same one returned by the
+        GetInstalledWebApps() method.
+
+        The error `org.gnome.Epiphany.WebAppProvider.Error.NotInstalled` will be
+        returned if the specified web app is not installed. The other possible
+        error values are `org.gnome.Epiphany.WebAppProvider.Error.InvalidArgs`
+        and `org.gnome.Epiphany.WebAppProvider.Error.Failed`.
+    -->
+    <method name="Uninstall">
+      <arg type="s" name="desktop_file_id" direction="in" />
+    </method>
+    <!--
+        Version:
+
+        The API version number, to be incremented for backwards compatible
+        changes so clients can determine which features are available. For
+        backwards incompatible changes, the interface name will change.
+    -->
+    <property name="Version" type="u" access="read"/>
+  </interface>
+</node>
diff --git a/plugins/epiphany/org.gnome.Software.Plugin.Epiphany.metainfo.xml.in 
b/plugins/epiphany/org.gnome.Software.Plugin.Epiphany.metainfo.xml.in
new file mode 100644
index 000000000..626381f90
--- /dev/null
+++ b/plugins/epiphany/org.gnome.Software.Plugin.Epiphany.metainfo.xml.in
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2013-2016 Richard Hughes <richard hughsie com> -->
+<component type="addon">
+  <id>org.gnome.Software.Plugin.Epiphany</id>
+  <extends>org.gnome.Software.desktop</extends>
+  <name>Web Apps Support</name>
+  <summary>Run popular web applications in a browser</summary>
+  <metadata_license>CC0-1.0</metadata_license>
+  <project_license>GPL-2.0+</project_license>
+  <update_contact>mwleeds_at_protonmail.com</update_contact>
+</component>
diff --git a/plugins/meson.build b/plugins/meson.build
index 01fc42f17..711b488e2 100644
--- a/plugins/meson.build
+++ b/plugins/meson.build
@@ -38,3 +38,6 @@ endif
 if get_option('snap')
   subdir('snap')
 endif
+if get_option('webapps')
+  subdir('epiphany')
+endif
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 9cfd24014..a7fc21084 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -103,6 +103,7 @@ src/org.gnome.Software.desktop.in
 plugins/core/gs-plugin-generic-updates.c
 plugins/core/gs-plugin-rewrite-resource.c
 plugins/eos-updater/gs-plugin-eos-updater.c
+plugins/epiphany/gs-plugin-epiphany.c
 plugins/fedora-pkgdb-collections/gs-plugin-fedora-pkgdb-collections.c
 plugins/flatpak/org.gnome.Software.Plugin.Flatpak.metainfo.xml.in
 plugins/flatpak/gs-flatpak.c
diff --git a/src/gs-installed-page.c b/src/gs-installed-page.c
index b93dc0e92..875dc9eab 100644
--- a/src/gs-installed-page.c
+++ b/src/gs-installed-page.c
@@ -309,9 +309,15 @@ gs_installed_page_is_actual_app (GsApp *app)
 {
        if (gs_app_get_description (app) != NULL)
                return TRUE;
+
        /* special snowflake */
        if (g_strcmp0 (gs_app_get_id (app), "google-chrome.desktop") == 0)
                return TRUE;
+
+       /* web apps sometimes don't have descriptions */
+       if (gs_app_get_kind (app) == AS_COMPONENT_KIND_WEB_APP)
+               return TRUE;
+
        g_debug ("%s is not an actual app", gs_app_get_unique_id (app));
        return FALSE;
 }


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