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



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

    Support snap channels

 lib/gs-app.c                    |   47 ++++++++++++++++
 lib/gs-app.h                    |    4 +
 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   |   62 +++++++++++++++++----
 src/gs-details-page.c           |   41 ++++++++++++++
 src/gs-details-page.ui          |   94 +++++++++++++++++++++++++------
 src/gtk-style.css               |   31 ++++++++++
 18 files changed, 492 insertions(+), 31 deletions(-)
---
diff --git a/lib/gs-app.c b/lib/gs-app.c
index 9c69349..ba797d5 100644
--- a/lib/gs-app.c
+++ b/lib/gs-app.c
@@ -124,6 +124,7 @@ typedef struct
        AsContentRating         *content_rating;
        GdkPixbuf               *pixbuf;
        GsPrice                 *price;
+       GPtrArray               *channels;
        GCancellable            *cancellable;
 } GsAppPrivate;
 
@@ -579,6 +580,14 @@ 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));
+       }
        keys = g_hash_table_get_keys (priv->metadata);
        for (l = keys; l != NULL; l = l->next) {
                GVariant *val;
@@ -3811,6 +3820,42 @@ 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_get_cancellable:
  * @app: a #GsApp
  *
@@ -3958,6 +4003,7 @@ 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_OBJECT_CLASS (gs_app_parent_class)->dispose (object);
 }
@@ -4136,6 +4182,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..af5f2c5 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,9 @@ 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);
 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..c4349a8 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.26')
 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 5db7243..dd28983 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);
@@ -306,6 +302,12 @@ find_snaps (GsPlugin *plugin, SnapdFindFlags flags, const gchar *section, const
        return g_steal_pointer (&snaps);
 }
 
+static gchar *
+make_version (SnapdSnap *snap)
+{
+       return g_strdup_printf ("%s (%s)", snapd_snap_get_version (snap), snapd_snap_get_revision (snap));
+}
+
 static GsApp *
 snap_to_app (GsPlugin *plugin, SnapdSnap *snap)
 {
@@ -313,6 +315,12 @@ snap_to_app (GsPlugin *plugin, SnapdSnap *snap)
        g_autofree gchar *unique_id = NULL;
        GsApp *cached_app;
        g_autoptr(GsApp) app = NULL;
+       g_autofree gchar *stable_version = NULL;
+       SnapdChannel *channel;
+       g_autoptr(GsChannel) stable_channel = NULL;
+       g_autoptr(GsChannel) candidate_channel = NULL;
+       g_autoptr(GsChannel) beta_channel = NULL;
+       g_autoptr(GsChannel) edge_channel = NULL;
 
        switch (snapd_snap_get_snap_type (snap)) {
        case SNAPD_SNAP_TYPE_APP:
@@ -347,6 +355,17 @@ snap_to_app (GsPlugin *plugin, SnapdSnap *snap)
        if (priv->system_confinement == SNAPD_SYSTEM_CONFINEMENT_STRICT && snapd_snap_get_confinement (snap) 
== SNAPD_CONFINEMENT_STRICT)
                gs_app_add_kudo (app, GS_APP_KUDO_SANDBOXED);
 
+       stable_version = make_version (snap);
+       channel = snapd_snap_match_channel (snap, "stable");
+       stable_channel = gs_channel_new ("stable", stable_version);
+       gs_app_add_channel (app, stable_channel);
+       candidate_channel = gs_channel_new ("candidate", stable_version);
+       gs_app_add_channel (app, candidate_channel);
+       beta_channel = gs_channel_new ("beta", stable_version);
+       gs_app_add_channel (app, beta_channel);
+       edge_channel = gs_channel_new ("edge", stable_version);
+       gs_app_add_channel (app, edge_channel);
+
        return g_steal_pointer (&app);
 }
 
@@ -711,7 +730,7 @@ gs_plugin_refine_app (GsPlugin *plugin,
                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));
+               version = make_version (local_snap);
                gs_app_set_version (app, version);
                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)));
@@ -743,7 +762,7 @@ gs_plugin_refine_app (GsPlugin *plugin,
                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));
-               version = g_strdup_printf ("%s (%s)", snapd_snap_get_version (store_snap), 
snapd_snap_get_revision (store_snap));
+               version = make_version (store_snap);
                gs_app_set_version (app, version);
                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));
@@ -921,6 +940,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,
@@ -962,21 +1006,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..65e7814 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;
@@ -240,6 +244,7 @@ gs_details_page_switch_to (GsPage *page, gboolean scroll_up)
        GsPrice *price;
        g_autofree gchar *text = NULL;
        GtkStyleContext *sc;
+       GPtrArray *channels;
        GtkAdjustment *adj;
 
        if (gs_shell_get_mode (self->shell) != GS_SHELL_MODE_DETAILS) {
@@ -396,6 +401,10 @@ gs_details_page_switch_to (GsPage *page, gboolean scroll_up)
                }
        }
 
+       channels = gs_app_get_channels (self->app);
+       gtk_widget_set_visible (self->label_details_channel_title, channels->len > 0);
+       gtk_widget_set_visible (self->button_details_channel, channels->len > 0);
+
        adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolledwindow_details));
        gtk_adjustment_set_value (adj, gtk_adjustment_get_lower (adj));
 
@@ -1802,6 +1811,31 @@ gs_details_page_app_cancel_button_cb (GtkWidget *widget, GsDetailsPage *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;
+
+               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);
+       }
+
+       gtk_widget_show (self->popover_channel);
+}
+
+static void
 gs_details_page_app_install_button_cb (GtkWidget *widget, GsDetailsPage *self)
 {
        GList *l;
@@ -2265,6 +2299,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 +2398,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/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]