[gnome-software/wip/rancell/channels: 4/4] Support snap channels



commit f20a98707bbd993400181167799d2dd24042fc68
Author: Robert Ancell <robert ancell canonical com>
Date:   Thu Nov 23 10:54:20 2017 +1300

    Support snap channels

 lib/gs-app.c                    |   89 +++++++++++++
 lib/gs-app.h                    |    7 +
 lib/gs-channel.c                |  116 +++++++++++++++++
 lib/gs-channel.h                |   44 +++++++
 lib/gs-plugin-job-private.h     |    1 +
 lib/gs-plugin-job.c             |   31 +++++
 lib/gs-plugin-job.h             |    2 +
 lib/gs-plugin-loader.c          |   13 ++
 lib/gs-plugin-types.h           |    2 +
 lib/gs-plugin-vfuncs.h          |   18 +++
 lib/gs-plugin.c                 |    2 +
 lib/meson.build                 |    2 +
 meson.build                     |    2 +-
 plugins/dummy/gs-plugin-dummy.c |   11 ++
 plugins/snap/gs-plugin-snap.c   |  267 ++++++++++++++++++++++++++++++---------
 src/gs-details-page.c           |  176 ++++++++++++++++++++++++++
 src/gs-details-page.ui          |   94 +++++++++++---
 src/gs-page.c                   |   27 ++++
 src/gtk-style.css               |   31 +++++
 19 files changed, 853 insertions(+), 82 deletions(-)
---
diff --git a/lib/gs-app.c b/lib/gs-app.c
index 9c69349..1c15d9a 100644
--- a/lib/gs-app.c
+++ b/lib/gs-app.c
@@ -124,6 +124,8 @@ typedef struct
        AsContentRating         *content_rating;
        GdkPixbuf               *pixbuf;
        GsPrice                 *price;
+       GPtrArray               *channels;
+       GsChannel               *active_channel;
        GCancellable            *cancellable;
 } GsAppPrivate;
 
@@ -579,6 +581,18 @@ gs_app_to_string_append (GsApp *app, GString *str)
                        gs_app_kv_lpad (str, "keyword", tmp);
                }
        }
+       for (i = 0; i < priv->channels->len; i++) {
+               GsChannel *channel = g_ptr_array_index (priv->channels, i);
+               g_autofree gchar *key = NULL;
+               key = g_strdup_printf ("channel-%02u", i);
+               gs_app_kv_printf (str, key, "%s [%s]",
+                                 gs_channel_get_name (channel),
+                                 gs_channel_get_version (channel));
+       }
+       if (priv->active_channel != NULL) {
+               gs_app_kv_printf (str, "active-channel", "%s",
+                                 gs_channel_get_name (priv->active_channel));
+       }
        keys = g_hash_table_get_keys (priv->metadata);
        for (l = keys; l != NULL; l = l->next) {
                GVariant *val;
@@ -3811,6 +3825,78 @@ gs_app_get_priority (GsApp *app)
 }
 
 /**
+ * gs_app_add_channel:
+ * @app: a #GsApp
+ * @channel: a #GsChannel
+ *
+ * Adds a channel to the application.
+ *
+ * Since: 3.28
+ **/
+void
+gs_app_add_channel (GsApp *app, GsChannel *channel)
+{
+       GsAppPrivate *priv = gs_app_get_instance_private (app);
+       g_return_if_fail (GS_IS_APP (app));
+       g_return_if_fail (GS_IS_CHANNEL (channel));
+       g_ptr_array_add (priv->channels, g_object_ref (channel));
+}
+
+/**
+ * gs_app_get_channels:
+ * @app: a #GsApp
+ *
+ * Gets the list of channels.
+ *
+ * Returns: (element-type GsChannel) (transfer none): a list
+ *
+ * Since: 3.28
+ **/
+GPtrArray *
+gs_app_get_channels (GsApp *app)
+{
+       GsAppPrivate *priv = gs_app_get_instance_private (app);
+       g_return_val_if_fail (GS_IS_APP (app), NULL);
+       return priv->channels;
+}
+
+/**
+ * gs_app_set_active_channel:
+ * @app: a #GsApp
+ * @channel: a #GsChannel
+ *
+ * Set the currently active channel.
+ *
+ * Since: 3.28
+ **/
+void
+gs_app_set_active_channel (GsApp *app, GsChannel *channel)
+{
+       GsAppPrivate *priv = gs_app_get_instance_private (app);
+       g_return_if_fail (GS_IS_APP (app));
+       g_return_if_fail (GS_IS_CHANNEL (channel));
+       g_set_object (&priv->active_channel, channel);
+}
+
+/**
+ * gs_app_get_active_channel:
+ * @app: a #GsApp
+ *
+ * Gets the currently active channel.
+ *
+ * Returns: a #GsChannel or %NULL.
+ *
+ * Since: 3.28
+ **/
+GsChannel *
+gs_app_get_active_channel (GsApp *app)
+{
+       GsAppPrivate *priv = gs_app_get_instance_private (app);
+       g_return_val_if_fail (GS_IS_APP (app), NULL);
+       return priv->active_channel;
+}
+
+/**
  * gs_app_get_cancellable:
  * @app: a #GsApp
  *
@@ -3958,6 +4044,8 @@ gs_app_dispose (GObject *object)
        g_clear_pointer (&priv->reviews, g_ptr_array_unref);
        g_clear_pointer (&priv->provides, g_ptr_array_unref);
        g_clear_pointer (&priv->icons, g_ptr_array_unref);
+       g_clear_pointer (&priv->channels, g_ptr_array_unref);
+       g_clear_object (&priv->active_channel);
 
        G_OBJECT_CLASS (gs_app_parent_class)->dispose (object);
 }
@@ -4136,6 +4224,7 @@ gs_app_init (GsApp *app)
        priv->reviews = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
        priv->provides = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
        priv->icons = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+       priv->channels = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
        priv->metadata = g_hash_table_new_full (g_str_hash,
                                                g_str_equal,
                                                g_free,
diff --git a/lib/gs-app.h b/lib/gs-app.h
index 6876bf8..069eaa0 100644
--- a/lib/gs-app.h
+++ b/lib/gs-app.h
@@ -27,6 +27,7 @@
 #include <gdk-pixbuf/gdk-pixbuf.h>
 #include <appstream-glib.h>
 
+#include "gs-channel.h"
 #include "gs-price.h"
 
 G_BEGIN_DECLS
@@ -317,6 +318,12 @@ void                gs_app_remove_quirk            (GsApp          *app,
                                                 AsAppQuirk      quirk);
 gboolean        gs_app_is_installed            (GsApp          *app);
 gboolean        gs_app_is_updatable            (GsApp          *app);
+GPtrArray      *gs_app_get_channels            (GsApp          *app);
+void            gs_app_add_channel             (GsApp          *app,
+                                                GsChannel      *channel);
+void            gs_app_set_active_channel      (GsApp          *app,
+                                                GsChannel      *channel);
+GsChannel      *gs_app_get_active_channel      (GsApp          *app);
 G_END_DECLS
 
 #endif /* __GS_APP_H */
diff --git a/lib/gs-channel.c b/lib/gs-channel.c
new file mode 100644
index 0000000..658a33b
--- /dev/null
+++ b/lib/gs-channel.c
@@ -0,0 +1,116 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2017 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "gs-channel.h"
+
+struct _GsChannel
+{
+       GObject  parent_instance;
+
+       gchar   *name;
+       gchar   *version;
+};
+
+G_DEFINE_TYPE (GsChannel, gs_channel, G_TYPE_OBJECT)
+
+/**
+ * gs_channel_get_name:
+ * @channel: a #GsChannel
+ *
+ * Get the channel name.
+ *
+ * Returns: a channel name.
+ *
+ * Since: 3.28
+ */
+const gchar *
+gs_channel_get_name (GsChannel *channel)
+{
+       g_return_val_if_fail (GS_IS_CHANNEL (channel), NULL);
+       return channel->name;
+}
+
+/**
+ * gs_channel_get_version:
+ * @channel: a #GsChannel
+ *
+ * Get the channel version.
+ *
+ * Returns: a channel version.
+ *
+ * Since: 3.28
+ */
+const gchar *
+gs_channel_get_version (GsChannel *channel)
+{
+       g_return_val_if_fail (GS_IS_CHANNEL (channel), NULL);
+       return channel->version;
+}
+
+static void
+gs_channel_finalize (GObject *object)
+{
+       GsChannel *channel = GS_CHANNEL (object);
+
+       g_free (channel->name);
+       g_free (channel->version);
+
+       G_OBJECT_CLASS (gs_channel_parent_class)->finalize (object);
+}
+
+static void
+gs_channel_class_init (GsChannelClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       object_class->finalize = gs_channel_finalize;
+}
+
+static void
+gs_channel_init (GsChannel *channel)
+{
+}
+
+/**
+ * gs_channel_new:
+ * @name: the name of the channel.
+ * @version: the version this channel is providing.
+ *
+ * Creates a new channel object.
+ *
+ * Return value: a new #GsChannel object.
+ *
+ * Since: 3.28
+ **/
+GsChannel *
+gs_channel_new (const gchar *name, const gchar *version)
+{
+       GsChannel *channel;
+       channel = g_object_new (GS_TYPE_CHANNEL, NULL);
+       channel->name = g_strdup (name);
+       channel->version = g_strdup (version);
+       return GS_CHANNEL (channel);
+}
+
+/* vim: set noexpandtab: */
diff --git a/lib/gs-channel.h b/lib/gs-channel.h
new file mode 100644
index 0000000..64610ab
--- /dev/null
+++ b/lib/gs-channel.h
@@ -0,0 +1,44 @@
+ /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2017 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef __GS_CHANNEL_H
+#define __GS_CHANNEL_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_CHANNEL (gs_channel_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsChannel, gs_channel, GS, CHANNEL, GObject)
+
+GsChannel      *gs_channel_new         (const gchar    *name,
+                                        const gchar    *version);
+
+const gchar    *gs_channel_get_name    (GsChannel      *channel);
+
+const gchar    *gs_channel_get_version (GsChannel      *channel);
+
+G_END_DECLS
+
+#endif /* __GS_CHANNEL_H */
+
+/* vim: set noexpandtab: */
diff --git a/lib/gs-plugin-job-private.h b/lib/gs-plugin-job-private.h
index f238213..414414d 100644
--- a/lib/gs-plugin-job-private.h
+++ b/lib/gs-plugin-job-private.h
@@ -53,6 +53,7 @@ GsPlugin              *gs_plugin_job_get_plugin               (GsPluginJob    *self);
 GsCategory             *gs_plugin_job_get_category             (GsPluginJob    *self);
 AsReview               *gs_plugin_job_get_review               (GsPluginJob    *self);
 GsPrice                        *gs_plugin_job_get_price                (GsPluginJob    *self);
+GsChannel              *gs_plugin_job_get_channel              (GsPluginJob    *self);
 gchar                  *gs_plugin_job_to_string                (GsPluginJob    *self);
 void                    gs_plugin_job_set_action               (GsPluginJob    *self,
                                                                 GsPluginAction  action);
diff --git a/lib/gs-plugin-job.c b/lib/gs-plugin-job.c
index 2c98a6b..406268d 100644
--- a/lib/gs-plugin-job.c
+++ b/lib/gs-plugin-job.c
@@ -48,6 +48,7 @@ struct _GsPluginJob
        GsCategory              *category;
        AsReview                *review;
        GsPrice                 *price;
+       GsChannel               *channel;
        gint64                   time_created;
 };
 
@@ -68,6 +69,7 @@ enum {
        PROP_REVIEW,
        PROP_MAX_RESULTS,
        PROP_PRICE,
+       PROP_CHANNEL,
        PROP_TIMEOUT,
        PROP_LAST
 };
@@ -128,6 +130,9 @@ gs_plugin_job_to_string (GsPluginJob *self)
                g_autofree gchar *price_string = gs_price_to_string (self->price);
                g_string_append_printf (str, " with price=%s", price_string);
        }
+       if (self->channel != NULL) {
+               g_string_append_printf (str, " with channel=%s", gs_channel_get_name (self->channel));
+       }
        if (self->auth != NULL) {
                g_string_append_printf (str, " with auth=%s",
                                        gs_auth_get_provider_id (self->auth));
@@ -452,6 +457,20 @@ gs_plugin_job_get_price (GsPluginJob *self)
        return self->price;
 }
 
+void
+gs_plugin_job_set_channel (GsPluginJob *self, GsChannel *channel)
+{
+       g_return_if_fail (GS_IS_PLUGIN_JOB (self));
+       g_set_object (&self->channel, channel);
+}
+
+GsChannel *
+gs_plugin_job_get_channel (GsPluginJob *self)
+{
+       g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL);
+       return self->channel;
+}
+
 static void
 gs_plugin_job_get_property (GObject *obj, guint prop_id, GValue *value, GParamSpec *pspec)
 {
@@ -500,6 +519,9 @@ gs_plugin_job_get_property (GObject *obj, guint prop_id, GValue *value, GParamSp
        case PROP_PRICE:
                g_value_set_object (value, self->price);
                break;
+       case PROP_CHANNEL:
+               g_value_set_object (value, self->channel);
+               break;
        case PROP_MAX_RESULTS:
                g_value_set_uint (value, self->max_results);
                break;
@@ -566,6 +588,9 @@ gs_plugin_job_set_property (GObject *obj, guint prop_id, const GValue *value, GP
        case PROP_PRICE:
                gs_plugin_job_set_price (self, g_value_get_object (value));
                break;
+       case PROP_CHANNEL:
+               gs_plugin_job_set_channel (self, g_value_get_object (value));
+               break;
        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
                break;
@@ -585,6 +610,7 @@ gs_plugin_job_finalize (GObject *obj)
        g_clear_object (&self->category);
        g_clear_object (&self->review);
        g_clear_object (&self->price);
+       g_clear_object (&self->channel);
        G_OBJECT_CLASS (gs_plugin_job_parent_class)->finalize (obj);
 }
 
@@ -678,6 +704,11 @@ gs_plugin_job_class_init (GsPluginJobClass *klass)
                                     GS_TYPE_PRICE,
                                     G_PARAM_READWRITE);
        g_object_class_install_property (object_class, PROP_PRICE, pspec);
+
+       pspec = g_param_spec_object ("channel", NULL, NULL,
+                                    GS_TYPE_CHANNEL,
+                                    G_PARAM_READWRITE);
+       g_object_class_install_property (object_class, PROP_CHANNEL, pspec);
 }
 
 static void
diff --git a/lib/gs-plugin-job.h b/lib/gs-plugin-job.h
index 2edd285..48c3fa3 100644
--- a/lib/gs-plugin-job.h
+++ b/lib/gs-plugin-job.h
@@ -72,6 +72,8 @@ void           gs_plugin_job_set_review               (GsPluginJob    *self,
                                                         AsReview       *review);
 void            gs_plugin_job_set_price                (GsPluginJob    *self,
                                                         GsPrice        *price);
+void            gs_plugin_job_set_channel              (GsPluginJob    *self,
+                                                        GsChannel      *channel);
 
 #define                 gs_plugin_job_newv(a,...)              
GS_PLUGIN_JOB(g_object_new(GS_TYPE_PLUGIN_JOB, "action", a, __VA_ARGS__))
 
diff --git a/lib/gs-plugin-loader.c b/lib/gs-plugin-loader.c
index daddf2c..845c984 100644
--- a/lib/gs-plugin-loader.c
+++ b/lib/gs-plugin-loader.c
@@ -128,6 +128,11 @@ typedef gboolean    (*GsPluginPurchaseFunc)        (GsPlugin       *plugin,
                                                         GsPrice        *price,
                                                         GCancellable   *cancellable,
                                                         GError         **error);
+typedef gboolean        (*GsPluginSwitchChannelFunc)   (GsPlugin       *plugin,
+                                                        GsApp          *app,
+                                                        GsChannel      *channel,
+                                                        GCancellable   *cancellable,
+                                                        GError         **error);
 typedef gboolean        (*GsPluginReviewFunc)          (GsPlugin       *plugin,
                                                         GsApp          *app,
                                                         AsReview       *review,
@@ -616,6 +621,14 @@ gs_plugin_loader_call_vfunc (GsPluginLoaderHelper *helper,
                                           cancellable, &error_local);
                }
                break;
+       case GS_PLUGIN_ACTION_SWITCH_CHANNEL:
+               {
+                       GsPluginSwitchChannelFunc plugin_func = func;
+                       ret = plugin_func (plugin, app,
+                                          gs_plugin_job_get_channel (helper->plugin_job),
+                                          cancellable, &error_local);
+               }
+               break;
        case GS_PLUGIN_ACTION_REVIEW_SUBMIT:
        case GS_PLUGIN_ACTION_REVIEW_UPVOTE:
        case GS_PLUGIN_ACTION_REVIEW_DOWNVOTE:
diff --git a/lib/gs-plugin-types.h b/lib/gs-plugin-types.h
index 2804216..47911f6 100644
--- a/lib/gs-plugin-types.h
+++ b/lib/gs-plugin-types.h
@@ -267,6 +267,7 @@ typedef enum {
  * @GS_PLUGIN_ACTION_INITIALIZE:               Initialize the plugin
  * @GS_PLUGIN_ACTION_DESTROY:                  Destroy the plugin
  * @GS_PLUGIN_ACTION_PURCHASE:                 Purchase an app
+ * @GS_PLUGIN_ACTION_SWITCH_CHANNEL:           Switch app channel
  *
  * The plugin action.
  **/
@@ -314,6 +315,7 @@ typedef enum {
        GS_PLUGIN_ACTION_INITIALIZE,
        GS_PLUGIN_ACTION_DESTROY,
        GS_PLUGIN_ACTION_PURCHASE,
+       GS_PLUGIN_ACTION_SWITCH_CHANNEL,
        /*< private >*/
        GS_PLUGIN_ACTION_LAST
 } GsPluginAction;
diff --git a/lib/gs-plugin-vfuncs.h b/lib/gs-plugin-vfuncs.h
index f8d48e7..6144715 100644
--- a/lib/gs-plugin-vfuncs.h
+++ b/lib/gs-plugin-vfuncs.h
@@ -612,6 +612,24 @@ gboolean    gs_plugin_app_install                  (GsPlugin       *plugin,
                                                         GError         **error);
 
 /**
+ * gs_plugin_app_switch_channel:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ * @channel: a #GsChannel
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Set the app chanel.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean        gs_plugin_app_switch_channel           (GsPlugin       *plugin,
+                                                        GsApp          *app,
+                                                        GsChannel      *channel,
+                                                        GCancellable   *cancellable,
+                                                        GError         **error);
+
+/**
  * gs_plugin_app_remove:
  * @plugin: a #GsPlugin
  * @app: a #GsApp
diff --git a/lib/gs-plugin.c b/lib/gs-plugin.c
index 9b1fe7a..61a06e3 100644
--- a/lib/gs-plugin.c
+++ b/lib/gs-plugin.c
@@ -1766,6 +1766,8 @@ gs_plugin_action_to_function_name (GsPluginAction action)
                return "gs_plugin_destroy";
        if (action == GS_PLUGIN_ACTION_PURCHASE)
                return "gs_plugin_app_purchase";
+       if (action == GS_PLUGIN_ACTION_SWITCH_CHANNEL)
+               return "gs_plugin_app_switch_channel";
        return NULL;
 }
 
diff --git a/lib/meson.build b/lib/meson.build
index 47b71a9..c745554 100644
--- a/lib/meson.build
+++ b/lib/meson.build
@@ -42,6 +42,7 @@ install_headers([
     'gs-app-list.h',
     'gs-auth.h',
     'gs-category.h',
+    'gs-channel.h',
     'gs-os-release.h',
     'gs-plugin.h',
     'gs-plugin-event.h',
@@ -76,6 +77,7 @@ libgnomesoftware = static_library(
     'gs-app-list.c',
     'gs-auth.c',
     'gs-category.c',
+    'gs-channel.c',
     'gs-debug.c',
     'gs-os-release.c',
     'gs-plugin.c',
diff --git a/meson.build b/meson.build
index e305dfb..2066aaa 100644
--- a/meson.build
+++ b/meson.build
@@ -168,7 +168,7 @@ if get_option('enable-gudev')
 endif
 
 if get_option('enable-snap')
-  snap = dependency('snapd-glib', version : '>= 1.19')
+  snap = dependency('snapd-glib', version : '>= 1.30')
 endif
 
 gnome = import('gnome')
diff --git a/plugins/dummy/gs-plugin-dummy.c b/plugins/dummy/gs-plugin-dummy.c
index 6962852..fe119d0 100644
--- a/plugins/dummy/gs-plugin-dummy.c
+++ b/plugins/dummy/gs-plugin-dummy.c
@@ -839,6 +839,17 @@ gs_plugin_refresh (GsPlugin *plugin,
 }
 
 gboolean
+gs_plugin_app_switch_channel (GsPlugin *plugin,
+                             GsApp *app,
+                             GsChannel *channel,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       g_debug ("Switching channel to %s", gs_channel_get_name (channel));
+       return TRUE;
+}
+
+gboolean
 gs_plugin_app_upgrade_download (GsPlugin *plugin, GsApp *app,
                                GCancellable *cancellable, GError **error)
 {
diff --git a/plugins/snap/gs-plugin-snap.c b/plugins/snap/gs-plugin-snap.c
index fce680c..8f1727c 100644
--- a/plugins/snap/gs-plugin-snap.c
+++ b/plugins/snap/gs-plugin-snap.c
@@ -47,10 +47,6 @@ get_client (GsPlugin *plugin, GError **error)
 
        client = snapd_client_new ();
        snapd_client_set_allow_interaction (client, TRUE);
-#ifndef SNAPD_GLIB_VERSION_1_24
-       if (!snapd_client_connect_sync (client, NULL, error))
-               return NULL;
-#endif
        old_user_agent = snapd_client_get_user_agent (client);
        user_agent = g_strdup_printf ("%s %s", gs_user_agent (), old_user_agent);
        snapd_client_set_user_agent (client, user_agent);
@@ -301,7 +297,10 @@ find_snaps (GsPlugin *plugin, SnapdFindFlags flags, const gchar *section, const
                return NULL;
        }
 
-       store_snap_cache_update (plugin, snaps);
+       /* Only cache name results because they return channel information
+        * 
https://forum.snapcraft.io/t/channel-maps-list-is-empty-when-using-v1-snaps-search-as-opposed-to-using-v2-snaps-details
 */
+       if ((flags & SNAPD_FIND_FLAGS_MATCH_NAME) != 0)
+               store_snap_cache_update (plugin, snaps);
 
        return g_steal_pointer (&snaps);
 }
@@ -674,6 +673,75 @@ gs_plugin_snap_get_description_safe (SnapdSnap *snap)
        return g_string_free (str, FALSE);
 }
 
+static gchar *
+make_version (SnapdSnap *snap)
+{
+       return g_strdup_printf ("%s (%s)", snapd_snap_get_version (snap), snapd_snap_get_revision (snap));
+}
+
+static SnapdChannel *
+find_channel (SnapdSnap *snap, const gchar *name)
+{
+       GPtrArray *channels;
+       guint i;
+
+       channels = snapd_snap_get_channels (snap);
+       for (i = 0; i < channels->len; i++) {
+               SnapdChannel *channel = channels->pdata[i];
+               if (strcmp (snapd_channel_get_name (channel), name) == 0)
+                       return channel;
+       }
+
+       return NULL;
+}
+
+static void
+refine_channels (GsApp *app, SnapdSnap *snap, const gchar *tracking_channel)
+{
+       gchar **tracks;
+       guint i;
+
+       /* already refined... */
+       if (gs_app_get_channels (app)->len > 0)
+               return;
+
+       tracks = snapd_snap_get_tracks (snap);
+       for (i = 0; tracks[i] != NULL; i++) {
+               const gchar *risks[] = {"stable", "candidate", "beta", "edge", NULL};
+               g_autofree gchar *last_version = NULL;
+               guint j;
+
+               last_version = make_version (snap);
+               for (j = 0; risks[j] != NULL; j++) {
+                       g_autofree gchar *full_name = NULL;
+                       const gchar *name;
+                       SnapdChannel *channel;
+                       g_autofree gchar *version = NULL;
+                       g_autoptr(GsChannel) c = NULL;
+
+                       full_name = g_strdup_printf ("%s/%s", tracks[i], risks[j]);
+                       if (strcmp (tracks[i], "latest") == 0)
+                               name = risks[j];
+                       else
+                               name = full_name;
+                       channel = find_channel (snap, full_name);
+                       if (channel != NULL)
+                               version = g_strdup_printf ("%s (%s)", snapd_channel_get_version (channel), 
snapd_channel_get_revision (channel));
+                       else
+                               version = g_strdup (last_version);
+                       c = gs_channel_new (name, version);
+                       gs_app_add_channel (app, c);
+                       g_free (last_version);
+                       last_version = g_strdup (version);
+
+                       if (g_strcmp0 (tracking_channel, name) == 0)
+                               gs_app_set_active_channel (app, c);
+
+                       // FIXME: Add any branches for this track/risk
+               }
+       }
+}
+
 gboolean
 gs_plugin_refine_app (GsPlugin *plugin,
                      GsApp *app,
@@ -683,9 +751,12 @@ gs_plugin_refine_app (GsPlugin *plugin,
 {
        GsPluginData *priv = gs_plugin_get_data (plugin);
        g_autoptr(SnapdClient) client = NULL;
-       const gchar *id, *icon_url = NULL;
+       const gchar *id, *tracking_channel = NULL, *store_version = NULL, *name;
        g_autoptr(SnapdSnap) local_snap = NULL;
        g_autoptr(SnapdSnap) store_snap = NULL;
+       SnapdSnap *snap;
+       g_autofree gchar *description = NULL;
+       g_autofree gchar *version = NULL;
 
        /* not us */
        if (g_strcmp0 (gs_app_get_management_plugin (app), "snap") != 0)
@@ -701,67 +772,80 @@ gs_plugin_refine_app (GsPlugin *plugin,
        if (id == NULL)
                return TRUE;
 
-       /* get information from installed snaps */
+       /* get information from local snaps and store */
        local_snap = snapd_client_list_one_sync (client, id, cancellable, NULL);
-       if (local_snap != NULL) {
-               const gchar *name;
-               g_autofree gchar *description = NULL;
-               g_autofree gchar *version = NULL;
+       store_snap = get_store_snap (plugin, id, cancellable, NULL);
 
-               if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN)
+       /* get latest upstream version */
+       if (local_snap != NULL)
+               tracking_channel = snapd_snap_get_tracking_channel (local_snap);
+       if (tracking_channel == NULL)
+               tracking_channel = "stable";
+       if (store_snap != NULL) {
+               SnapdChannel *c = snapd_snap_match_channel (store_snap, tracking_channel);
+               if (c != NULL)
+                       store_version = snapd_channel_get_version (c);
+               else
+                       store_version = snapd_snap_get_version (store_snap);
+               refine_channels (app, store_snap, tracking_channel);
+       }
+
+       gs_app_set_update_version (app, NULL);
+       switch (gs_app_get_state (app)) {
+       case AS_APP_STATE_UNKNOWN:
+               if (local_snap != NULL)
                        gs_app_set_state (app, AS_APP_STATE_INSTALLED);
-               name = snapd_snap_get_title (local_snap);
-               if (name == NULL || g_strcmp0 (name, "") == 0)
-                       name = snapd_snap_get_name (local_snap);
-               gs_app_set_name (app, GS_APP_QUALITY_NORMAL, name);
-               gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, snapd_snap_get_summary (local_snap));
-               description = gs_plugin_snap_get_description_safe (local_snap);
-               if (description != NULL)
-                       gs_app_set_description (app, GS_APP_QUALITY_NORMAL, description);
-               gs_app_set_license (app, GS_APP_QUALITY_NORMAL, snapd_snap_get_license (local_snap));
-               version = g_strdup_printf ("%s (%s)", snapd_snap_get_version (local_snap), 
snapd_snap_get_revision (local_snap));
-               gs_app_set_version (app, version);
+               else
+                       gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+               break;
+       case AS_APP_STATE_INSTALLED:
+       case AS_APP_STATE_UPDATABLE_LIVE:
+               // FIXME: Should the store version be greater? Or revision? What about rollbacks?
+               if (store_version != NULL && g_strcmp0 (store_version, snapd_snap_get_version (local_snap)) 
!= 0) {
+                       gs_app_set_update_version (app, store_version);
+                       gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE);
+               }
+               else {
+                       if (gs_app_get_state (app) == AS_APP_STATE_UPDATABLE_LIVE)
+                               gs_app_set_state (app, AS_APP_STATE_UNKNOWN);
+                       gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+               }
+               break;
+       default:
+               break;
+       }
+
+       /* use store information for basic metadata over local information */
+       snap = store_snap != NULL ? store_snap : local_snap;
+       name = snapd_snap_get_title (snap);
+       if (name == NULL || g_strcmp0 (name, "") == 0)
+               name = snapd_snap_get_name (snap);
+       gs_app_set_name (app, GS_APP_QUALITY_NORMAL, name);
+       gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, snapd_snap_get_summary (snap));
+       description = gs_plugin_snap_get_description_safe (snap);
+       if (description != NULL)
+               gs_app_set_description (app, GS_APP_QUALITY_NORMAL, description);
+       gs_app_set_license (app, GS_APP_QUALITY_NORMAL, snapd_snap_get_license (snap));
+       gs_app_set_developer_name (app, snapd_snap_get_developer (snap));
+
+       snap = local_snap != NULL ? local_snap : store_snap;
+       version = make_version (snap);
+       gs_app_set_version (app, version);
+
+       /* add information specific to installed snaps */
+       if (local_snap != NULL) {
                gs_app_set_size_installed (app, snapd_snap_get_installed_size (local_snap));
                gs_app_set_install_date (app, g_date_time_to_unix (snapd_snap_get_install_date (local_snap)));
-               gs_app_set_developer_name (app, snapd_snap_get_developer (local_snap));
-               icon_url = snapd_snap_get_icon (local_snap);
-               if (g_strcmp0 (icon_url, "") == 0)
-                       icon_url = NULL;
 
                find_launch_app (app, local_snap);
        }
 
-       /* get information from snap store */
-       store_snap = get_store_snap (plugin, id, cancellable, NULL);
+       /* add information specific to store snaps */
        if (store_snap != NULL) {
                GPtrArray *screenshots;
-               const gchar *name;
-               g_autofree gchar *description = NULL;
-
-               if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN)
-                       gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
 
-               name = snapd_snap_get_title (store_snap);
-               if (name == NULL || g_strcmp0 (name, "") == 0)
-                       name = snapd_snap_get_name (store_snap);
-               gs_app_set_name (app, GS_APP_QUALITY_NORMAL, name);
-               gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, snapd_snap_get_summary (store_snap));
-               description = gs_plugin_snap_get_description_safe (store_snap);
-               if (description != NULL)
-                       gs_app_set_description (app, GS_APP_QUALITY_NORMAL, description);
-               gs_app_set_license (app, GS_APP_QUALITY_NORMAL, snapd_snap_get_license (store_snap));
-               if (gs_app_get_version (app) == NULL) {
-                       g_autofree gchar *version = NULL;
-                       version = g_strdup_printf ("%s (%s)", snapd_snap_get_version (store_snap), 
snapd_snap_get_revision (store_snap));
-                       gs_app_set_version (app, version);
-               }
+               gs_app_set_origin (app, priv->store_name);
                gs_app_set_size_download (app, snapd_snap_get_download_size (store_snap));
-               gs_app_set_developer_name (app, snapd_snap_get_developer (store_snap));
-               if (icon_url == NULL) {
-                       icon_url = snapd_snap_get_icon (store_snap);
-                       if (g_strcmp0 (icon_url, "") == 0)
-                               icon_url = NULL;
-               }
 
                screenshots = snapd_snap_get_screenshots (store_snap);
                if (screenshots != NULL && gs_app_get_screenshots (app)->len == 0) {
@@ -791,12 +875,20 @@ gs_plugin_refine_app (GsPlugin *plugin,
                                gs_app_add_screenshot (app, ss);
                        }
                }
-
-               gs_app_set_origin (app, priv->store_name);
        }
 
        /* load icon if requested */
        if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON) {
+               const gchar *icon_url = NULL;
+
+               if (local_snap != NULL)
+                       icon_url = snapd_snap_get_icon (local_snap);
+               if (g_strcmp0 (icon_url, "") == 0)
+                       icon_url = NULL;
+               if (icon_url == NULL && store_snap != NULL)
+                       icon_url = snapd_snap_get_icon (store_snap);
+               if (g_strcmp0 (icon_url, "") == 0)
+                       icon_url = NULL;
                if (!load_icon (plugin, client, app, icon_url, cancellable, error)) {
                        snapd_error_convert (error);
                        return FALSE;
@@ -832,6 +924,7 @@ gs_plugin_app_install (GsPlugin *plugin,
 {
        g_autoptr(SnapdClient) client = NULL;
        SnapdInstallFlags flags = SNAPD_INSTALL_FLAGS_NONE;
+       const gchar *channel = NULL;
 
        /* We can only install apps we know of */
        if (g_strcmp0 (gs_app_get_management_plugin (app), "snap") != 0)
@@ -841,10 +934,39 @@ gs_plugin_app_install (GsPlugin *plugin,
        if (client == NULL)
                return FALSE;
 
+       if (gs_app_get_active_channel (app) != NULL)
+               channel = gs_channel_get_name (gs_app_get_active_channel (app));
+
        gs_app_set_state (app, AS_APP_STATE_INSTALLING);
        if (g_strcmp0 (gs_app_get_metadata_item (app, "snap::confinement"), "classic") == 0)
                flags |= SNAPD_INSTALL_FLAGS_CLASSIC;
-       if (!snapd_client_install2_sync (client, flags, gs_app_get_id (app), NULL, NULL, progress_cb, app, 
cancellable, error)) {
+       if (!snapd_client_install2_sync (client, flags, gs_app_get_id (app), channel, NULL, progress_cb, app, 
cancellable, error)) {
+               gs_app_set_state_recover (app);
+               snapd_error_convert (error);
+               return FALSE;
+       }
+       gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+       return TRUE;
+}
+
+gboolean
+gs_plugin_update_app (GsPlugin *plugin,
+                     GsApp *app,
+                     GCancellable *cancellable,
+                     GError **error)
+{
+       g_autoptr(SnapdClient) client = NULL;
+
+       /* We can only install apps we know of */
+       if (g_strcmp0 (gs_app_get_management_plugin (app), "snap") != 0)
+               return TRUE;
+
+       client = get_client (plugin, error);
+       if (client == NULL)
+               return FALSE;
+
+       gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+       if (!snapd_client_refresh_sync (client, gs_app_get_id (app), NULL, progress_cb, app, cancellable, 
error)) {
                gs_app_set_state_recover (app);
                snapd_error_convert (error);
                return FALSE;
@@ -934,6 +1056,31 @@ gs_plugin_launch (GsPlugin *plugin,
 }
 
 gboolean
+gs_plugin_app_switch_channel (GsPlugin *plugin,
+                             GsApp *app,
+                             GsChannel *channel,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       g_autoptr(SnapdClient) client = NULL;
+
+       /* We can only modify apps we know of */
+       if (g_strcmp0 (gs_app_get_management_plugin (app), "snap") != 0)
+               return TRUE;
+
+       client = get_client (plugin, error);
+       if (client == NULL)
+               return FALSE;
+
+       if (!snapd_client_switch_sync (client, gs_app_get_id (app), gs_channel_get_name (channel), 
progress_cb, app, cancellable, error)) {
+               snapd_error_convert (error);
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+gboolean
 gs_plugin_app_remove (GsPlugin *plugin,
                      GsApp *app,
                      GCancellable *cancellable,
@@ -975,21 +1122,15 @@ gs_plugin_auth_login (GsPlugin *plugin, GsAuth *auth,
        g_clear_object (&priv->auth_data);
        if (priv->snapd_supports_polkit) {
                g_autoptr(SnapdClient) client = NULL;
-#ifdef SNAPD_GLIB_VERSION_1_26
                g_autoptr(SnapdUserInformation) user_information = NULL;
-#endif
 
                client = get_client (plugin, error);
                if (client == NULL)
                        return FALSE;
 
-#ifdef SNAPD_GLIB_VERSION_1_26
                user_information = snapd_client_login2_sync (client, gs_auth_get_username (auth), 
gs_auth_get_password (auth), gs_auth_get_pin (auth), NULL, error);
                if (user_information != NULL)
                        priv->auth_data = g_object_ref (snapd_user_information_get_auth_data 
(user_information));
-#else
-               priv->auth_data = snapd_client_login_sync (client, gs_auth_get_username (auth), 
gs_auth_get_password (auth), gs_auth_get_pin (auth), NULL, error);
-#endif
        }
        else
                priv->auth_data = snapd_login_sync (gs_auth_get_username (auth), gs_auth_get_password (auth), 
gs_auth_get_pin (auth), NULL, error);
diff --git a/src/gs-details-page.c b/src/gs-details-page.c
index d13932b..54b66e6 100644
--- a/src/gs-details-page.c
+++ b/src/gs-details-page.c
@@ -107,6 +107,8 @@ struct _GsDetailsPage
        GtkWidget               *label_details_size_download_title;
        GtkWidget               *label_details_size_download_value;
        GtkWidget               *label_details_updated_value;
+       GtkWidget               *label_details_channel_title;
+       GtkWidget               *button_details_channel;
        GtkWidget               *label_details_version_value;
        GtkWidget               *label_failed;
        GtkWidget               *label_pending;
@@ -123,6 +125,7 @@ struct _GsDetailsPage
        GtkWidget               *spinner_remove;
        GtkWidget               *stack_details;
        GtkWidget               *grid_details_kudo;
+       GtkWidget               *grid_popover_channel;
        GtkWidget               *image_details_kudo_docs;
        GtkWidget               *image_details_kudo_sandboxed;
        GtkWidget               *image_details_kudo_integration;
@@ -134,6 +137,7 @@ struct _GsDetailsPage
        GtkWidget               *label_details_kudo_translated;
        GtkWidget               *label_details_kudo_updated;
        GtkWidget               *progressbar_top;
+       GtkWidget               *popover_channel;
        GtkWidget               *popover_license_free;
        GtkWidget               *popover_license_nonfree;
        GtkWidget               *popover_license_unknown;
@@ -825,6 +829,7 @@ gs_details_page_refresh_all (GsDetailsPage *self)
        guint64 kudos;
        guint64 updated;
        guint64 user_integration_bf;
+       GsChannel *channel;
        g_autoptr(GError) error = NULL;
 
        /* change widgets */
@@ -901,6 +906,13 @@ gs_details_page_refresh_all (GsDetailsPage *self)
                gtk_widget_set_visible (self->button_details_license_unknown, FALSE);
        }
 
+       /* set channel */
+       channel = gs_app_get_active_channel (self->app);
+       gtk_widget_set_visible (self->label_details_channel_title, channel != NULL);
+       gtk_widget_set_visible (self->button_details_channel, channel != NULL);
+       if (channel != NULL)
+               gtk_button_set_label (GTK_BUTTON (self->button_details_channel), gs_channel_get_name 
(channel));
+
        /* set version */
        tmp = gs_app_get_version (self->app);
        if (tmp != NULL){
@@ -1801,6 +1813,163 @@ gs_details_page_app_cancel_button_cb (GtkWidget *widget, GsDetailsPage *self)
        gtk_widget_set_sensitive (widget, FALSE);
 }
 
+typedef struct {
+       GsDetailsPage   *self;
+       GsChannel       *channel;
+} GsDetailsPageChannelHelper;
+
+static void
+gs_details_page_channel_helper_free (GsDetailsPageChannelHelper *helper)
+{
+       g_object_unref (helper->self);
+       g_object_unref (helper->channel);
+       g_free (helper);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsDetailsPageChannelHelper, gs_details_page_channel_helper_free);
+
+static void
+gs_page_channel_switch_refine_cb (GObject *source,
+                                  GAsyncResult *res,
+                                  gpointer user_data)
+{
+       g_autoptr(GsDetailsPageChannelHelper) helper = (GsDetailsPageChannelHelper *) user_data;
+       GsDetailsPage *self = helper->self;
+       GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
+       gboolean ret;
+       g_autoptr(GError) error = NULL;
+
+       ret = gs_plugin_loader_job_action_finish (plugin_loader,
+                                                 res,
+                                                 &error);
+       if (g_error_matches (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_CANCELLED)) {
+               g_debug ("%s", error->message);
+               return;
+       }
+       if (!ret) {
+               g_warning ("failed to refine %s: %s",
+                          gs_app_get_id (self->app),
+                          error->message);
+               return;
+       }
+
+       gs_details_page_refresh_all (self);
+}
+
+static void
+gs_page_channel_switched_cb (GObject *source,
+                             GAsyncResult *res,
+                             gpointer user_data)
+{
+       g_autoptr(GsDetailsPageChannelHelper) helper = (GsDetailsPageChannelHelper *) user_data;
+       GsDetailsPage *self = helper->self;
+       GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
+       gboolean ret;
+       g_autoptr(GsPluginJob) plugin_job = NULL;
+       g_autoptr(GError) error = NULL;
+
+       ret = gs_plugin_loader_job_action_finish (plugin_loader,
+                                                 res,
+                                                 &error);
+       if (g_error_matches (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_CANCELLED)) {
+               g_debug ("%s", error->message);
+               return;
+       }
+       if (!ret) {
+               g_warning ("failed to switch channel %s: %s",
+                          gs_app_get_id (self->app),
+                          error->message);
+               return;
+       }
+
+       plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE,
+                                        "app", self->app,
+                                        "failure-flags", GS_PLUGIN_FAILURE_FLAGS_NONE,
+                                        "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION,
+                                        NULL);
+       gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
+                                           self->app_cancellable,
+                                           gs_page_channel_switch_refine_cb,
+                                           g_steal_pointer (&helper));
+}
+
+static void
+gs_details_page_switch_channel_cb (GtkWidget *widget, gpointer user_data)
+{
+       g_autoptr(GsDetailsPageChannelHelper) helper = (GsDetailsPageChannelHelper *) user_data;
+       GsDetailsPage *self = helper->self;
+       g_autoptr(GsPluginJob) plugin_job = NULL;
+
+       gtk_widget_hide (self->popover_channel);
+
+       gs_app_set_active_channel (self->app, helper->channel);
+
+       switch (gs_app_get_state (self->app)) {
+       case AS_APP_STATE_INSTALLED:
+       case AS_APP_STATE_UPDATABLE:
+       case AS_APP_STATE_UPDATABLE_LIVE:
+               plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SWITCH_CHANNEL,
+                                                "app", self->app,
+                                                "channel", helper->channel,
+                                                NULL);
+               gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
+                                                   self->app_cancellable,
+                                                   gs_page_channel_switched_cb,
+                                                   g_steal_pointer (&helper));
+               break;
+       default:
+               /* not yet installed, just update what we would install */
+               gs_app_set_version (self->app, gs_channel_get_version (helper->channel));
+               break;
+       }
+
+       gs_details_page_refresh_all (self);
+}
+
+static void
+gs_details_page_channel_cb (GtkWidget *widget, GsDetailsPage *self)
+{
+       GPtrArray *channels;
+       guint i;
+
+       gs_container_remove_all (GTK_CONTAINER (self->grid_popover_channel));
+       channels = gs_app_get_channels (self->app);
+       for (i = 0; i < channels->len; i++) {
+               GsChannel *channel = g_ptr_array_index (channels, i);
+               GtkWidget *label;
+               GtkWidget *button;
+
+               label = gtk_label_new (gs_channel_get_name (channel));
+               gtk_label_set_xalign (GTK_LABEL (label), 0.0);
+               gtk_widget_show (label);
+               gtk_grid_attach (GTK_GRID (self->grid_popover_channel), label, 0, i, 1, 1);
+
+               label = gtk_label_new (gs_channel_get_version (channel));
+               gtk_label_set_xalign (GTK_LABEL (label), 0.0);
+               gtk_widget_show (label);
+               gtk_grid_attach (GTK_GRID (self->grid_popover_channel), label, 1, i, 1, 1);
+
+               if (channel != gs_app_get_active_channel (self->app)) {
+                       GsDetailsPageChannelHelper *helper = g_new0 (GsDetailsPageChannelHelper, 1);
+
+                       button = gtk_button_new_with_label (_("Switch"));
+                       gtk_widget_show (button);
+                       gtk_grid_attach (GTK_GRID (self->grid_popover_channel), button, 2, i, 1, 1);
+                       helper->self = g_object_ref (self);
+                       helper->channel = g_object_ref (channel);
+                       g_signal_connect (button, "clicked",
+                                         G_CALLBACK (gs_details_page_switch_channel_cb),
+                                         helper);
+               }
+       }
+
+       gtk_widget_show (self->popover_channel);
+}
+
 static void
 gs_details_page_app_install_button_cb (GtkWidget *widget, GsDetailsPage *self)
 {
@@ -2265,6 +2434,9 @@ gs_details_page_setup (GsPage *page,
        g_signal_connect (self->button_donate, "clicked",
                          G_CALLBACK (gs_details_page_donate_cb),
                          self);
+       g_signal_connect (self->button_details_channel, "clicked",
+                         G_CALLBACK (gs_details_page_channel_cb),
+                         self);
        g_signal_connect (self->button_details_license_free, "clicked",
                          G_CALLBACK (gs_details_page_license_free_cb),
                          self);
@@ -2361,6 +2533,10 @@ gs_details_page_class_init (GsDetailsPageClass *klass)
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, 
label_details_size_installed_title);
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, 
label_details_size_installed_value);
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_updated_value);
+       gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_channel_title);
+       gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_channel);
+       gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, popover_channel);
+       gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, grid_popover_channel);
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_version_value);
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_failed);
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_pending);
diff --git a/src/gs-details-page.ui b/src/gs-details-page.ui
index 68a9fb9..df40ebb 100644
--- a/src/gs-details-page.ui
+++ b/src/gs-details-page.ui
@@ -780,6 +780,51 @@
                                 <property name="row_spacing">9</property>
                                 <property name="column_spacing">24</property>
                                 <child>
+                                  <object class="GtkLabel" id="label_details_channel_title">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">False</property>
+                                    <property name="label" translatable="yes">Channel</property>
+                                    <property name="xalign">0</property>
+                                    <property name="yalign">0.5</property>
+                                    <property name="vexpand">True</property>
+                                    <style>
+                                      <class name="dim-label"/>
+                                    </style>
+                                  </object>
+                                  <packing>
+                                    <property name="left_attach">0</property>
+                                    <property name="top_attach">0</property>
+                                  </packing>
+                                </child>
+                                <child>
+                                  <object class="GtkBox" id="box_details_channel_value">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">False</property>
+                                    <property name="orientation">vertical</property>
+                                    <child>
+                                      <object class="GtkButton" id="button_details_channel">
+                                        <property name="label" translatable="no">stable</property>
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">True</property>
+                                        <property name="receives_default">True</property>
+                                        <property name="halign">start</property>
+                                        <style>
+                                          <class name="details-channel"/>
+                                        </style>
+                                      </object>
+                                      <packing>
+                                        <property name="expand">False</property>
+                                        <property name="fill">False</property>
+                                        <property name="position">0</property>
+                                      </packing>
+                                    </child>
+                                  </object>
+                                  <packing>
+                                    <property name="left_attach">1</property>
+                                    <property name="top_attach">0</property>
+                                  </packing>
+                                </child>
+                                <child>
                                   <object class="GtkLabel" id="label_details_version_title">
                                     <property name="visible">True</property>
                                     <property name="can_focus">False</property>
@@ -793,7 +838,7 @@
                                   </object>
                                   <packing>
                                     <property name="left_attach">0</property>
-                                    <property name="top_attach">0</property>
+                                    <property name="top_attach">1</property>
                                   </packing>
                                 </child>
                                 <child>
@@ -812,7 +857,7 @@
                                   </object>
                                   <packing>
                                     <property name="left_attach">1</property>
-                                    <property name="top_attach">0</property>
+                                    <property name="top_attach">1</property>
                                   </packing>
                                 </child>
 
@@ -830,7 +875,7 @@
                                   </object>
                                   <packing>
                                     <property name="left_attach">0</property>
-                                    <property name="top_attach">8</property>
+                                    <property name="top_attach">9</property>
                                   </packing>
                                 </child>
                                 <child>
@@ -850,7 +895,7 @@
                                   </object>
                                   <packing>
                                     <property name="left_attach">1</property>
-                                    <property name="top_attach">8</property>
+                                    <property name="top_attach">9</property>
                                   </packing>
                                 </child>
 
@@ -868,7 +913,7 @@
                                   </object>
                                   <packing>
                                     <property name="left_attach">0</property>
-                                    <property name="top_attach">1</property>
+                                    <property name="top_attach">2</property>
                                   </packing>
                                 </child>
                                 <child>
@@ -886,7 +931,7 @@
                                   </object>
                                   <packing>
                                     <property name="left_attach">1</property>
-                                    <property name="top_attach">1</property>
+                                    <property name="top_attach">2</property>
                                   </packing>
                                 </child>
                                 <child>
@@ -903,7 +948,7 @@
                                   </object>
                                   <packing>
                                     <property name="left_attach">0</property>
-                                    <property name="top_attach">2</property>
+                                    <property name="top_attach">3</property>
                                   </packing>
                                 </child>
                                 <child>
@@ -923,7 +968,7 @@
                                   </object>
                                   <packing>
                                     <property name="left_attach">1</property>
-                                    <property name="top_attach">2</property>
+                                    <property name="top_attach">3</property>
                                   </packing>
                                 </child>
 
@@ -941,7 +986,7 @@
                                   </object>
                                   <packing>
                                     <property name="left_attach">0</property>
-                                    <property name="top_attach">6</property>
+                                    <property name="top_attach">7</property>
                                   </packing>
                                 </child>
                                 <child>
@@ -956,7 +1001,7 @@
                                   </object>
                                   <packing>
                                     <property name="left_attach">1</property>
-                                    <property name="top_attach">6</property>
+                                    <property name="top_attach">7</property>
                                   </packing>
                                 </child>
 
@@ -974,7 +1019,7 @@
                                   </object>
                                   <packing>
                                     <property name="left_attach">0</property>
-                                    <property name="top_attach">7</property>
+                                    <property name="top_attach">8</property>
                                   </packing>
                                 </child>
                                 <child>
@@ -989,7 +1034,7 @@
                                   </object>
                                   <packing>
                                     <property name="left_attach">1</property>
-                                    <property name="top_attach">7</property>
+                                    <property name="top_attach">8</property>
                                   </packing>
                                 </child>
 
@@ -1007,7 +1052,7 @@
                                   </object>
                                   <packing>
                                     <property name="left_attach">0</property>
-                                    <property name="top_attach">5</property>
+                                    <property name="top_attach">6</property>
                                   </packing>
                                 </child>
                                 <child>
@@ -1023,7 +1068,7 @@
                                   </object>
                                   <packing>
                                     <property name="left_attach">1</property>
-                                    <property name="top_attach">5</property>
+                                    <property name="top_attach">6</property>
                                   </packing>
                                 </child>
                                 <child>
@@ -1040,7 +1085,7 @@
                                   </object>
                                   <packing>
                                     <property name="left_attach">0</property>
-                                    <property name="top_attach">4</property>
+                                    <property name="top_attach">5</property>
                                   </packing>
                                 </child>
                                 <child>
@@ -1057,7 +1102,7 @@
                                   </object>
                                   <packing>
                                     <property name="left_attach">1</property>
-                                    <property name="top_attach">4</property>
+                                    <property name="top_attach">5</property>
                                   </packing>
                                 </child>
                                 <child>
@@ -1074,7 +1119,7 @@
                                   </object>
                                   <packing>
                                     <property name="left_attach">0</property>
-                                    <property name="top_attach">3</property>
+                                    <property name="top_attach">4</property>
                                   </packing>
                                 </child>
                                 <child>
@@ -1136,7 +1181,7 @@
                                   </object>
                                   <packing>
                                     <property name="left_attach">1</property>
-                                    <property name="top_attach">3</property>
+                                    <property name="top_attach">4</property>
                                   </packing>
                                 </child>
                               </object>
@@ -1377,6 +1422,19 @@
       <widget name="button_details_license_unknown"/>
     </widgets>
   </object>
+  <object class="GtkPopover" id="popover_channel">
+    <property name="can_focus">False</property>
+    <property name="border_width">21</property>
+    <property name="relative_to">button_details_channel</property>
+    <child>
+      <object class="GtkGrid" id="grid_popover_channel">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="row_spacing">9</property>
+        <property name="column_spacing">12</property>
+      </object>
+    </child>
+  </object>
   <object class="GtkPopover" id="popover_license_free">
     <property name="can_focus">False</property>
     <property name="border_width">21</property>
diff --git a/src/gs-page.c b/src/gs-page.c
index 6102303..42344f1 100644
--- a/src/gs-page.c
+++ b/src/gs-page.c
@@ -607,6 +607,33 @@ gs_page_update_app (GsPage *page, GsApp *app, GCancellable *cancellable)
 }
 
 static void
+gs_page_channel_switched_cb (GObject *source,
+                             GAsyncResult *res,
+                             gpointer user_data)
+{
+       g_autoptr(GsPageHelper) helper = (GsPageHelper *) user_data;
+       GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
+       gboolean ret;
+       g_autoptr(GError) error = NULL;
+
+       ret = gs_plugin_loader_job_action_finish (plugin_loader,
+                                                 res,
+                                                 &error);
+       if (g_error_matches (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_CANCELLED)) {
+               g_debug ("%s", error->message);
+               return;
+       }
+       if (!ret) {
+               g_warning ("failed to switch channel %s: %s",
+                          gs_app_get_id (helper->app),
+                          error->message);
+               return;
+       }
+}
+
+static void
 gs_page_remove_app_response_cb (GtkDialog *dialog,
                                gint response,
                                GsPageHelper *helper)
diff --git a/src/gtk-style.css b/src/gtk-style.css
index f6ddfaa..2c7facd 100644
--- a/src/gtk-style.css
+++ b/src/gtk-style.css
@@ -53,6 +53,37 @@
        border-radius: 16px;
 }
 
+.details-channel,
+.details-channel:backdrop {
+       outline-offset: 0;
+       background-image: none;
+       border-image: none;
+       border-radius: 4px;
+       border-width: 0 0 2px 0;
+       padding: 1px 9px;
+       box-shadow: none;
+       text-shadow: none;
+       color: #ffffff;
+}
+
+.details-channel label,
+.details-channel:backdrop label,
+.details-channel:hover label {
+       color: #fff;
+}
+
+.details-channel {
+       background-color: #4e9a06;
+       border-color: #3e7905;
+}
+.details-channel:hover {
+       background-color: #5db807;
+       border-color: #4d9606;
+}
+.details-channel:backdrop {
+       border-color: #4e9a06;
+}
+
 .details-license-free,
 .details-license-nonfree,
 .details-license-unknown,


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