[gnome-software/wip/rancell/channels: 4/4] Support snap channels
- From: Robert Ancell <rancell src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-software/wip/rancell/channels: 4/4] Support snap channels
- Date: Tue, 28 Nov 2017 03:41:41 +0000 (UTC)
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]