summaryrefslogtreecommitdiffstats
path: root/src/gs-extras-page.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/gs-extras-page.c')
-rw-r--r--src/gs-extras-page.c1220
1 files changed, 1220 insertions, 0 deletions
diff --git a/src/gs-extras-page.c b/src/gs-extras-page.c
new file mode 100644
index 0000000..9b9aeec
--- /dev/null
+++ b/src/gs-extras-page.c
@@ -0,0 +1,1220 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2015-2018 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include "gs-extras-page.h"
+
+#include "gs-app-row.h"
+#include "gs-language.h"
+#include "gs-shell.h"
+#include "gs-common.h"
+#include "gs-utils.h"
+#include "gs-vendor.h"
+
+#include <glib/gi18n.h>
+
+typedef enum {
+ GS_EXTRAS_PAGE_STATE_LOADING,
+ GS_EXTRAS_PAGE_STATE_READY,
+ GS_EXTRAS_PAGE_STATE_NO_RESULTS,
+ GS_EXTRAS_PAGE_STATE_FAILED
+} GsExtrasPageState;
+
+typedef struct {
+ gchar *title;
+ gchar *search;
+ gchar *search_filename;
+ gchar *package_filename;
+ gchar *url_not_found;
+ GsExtrasPage *self;
+} SearchData;
+
+struct _GsExtrasPage
+{
+ GsPage parent_instance;
+
+ GsPluginLoader *plugin_loader;
+ GtkBuilder *builder;
+ GCancellable *search_cancellable;
+ GsShell *shell;
+ GsExtrasPageState state;
+ GtkSizeGroup *sizegroup_image;
+ GtkSizeGroup *sizegroup_name;
+ GtkSizeGroup *sizegroup_desc;
+ GtkSizeGroup *sizegroup_button;
+ GPtrArray *array_search_data;
+ GsExtrasPageMode mode;
+ GsLanguage *language;
+ GsVendor *vendor;
+ guint pending_search_cnt;
+
+ GtkWidget *label_failed;
+ GtkWidget *label_no_results;
+ GtkWidget *list_box_results;
+ GtkWidget *scrolledwindow;
+ GtkWidget *spinner;
+ GtkWidget *stack;
+};
+
+G_DEFINE_TYPE (GsExtrasPage, gs_extras_page, GS_TYPE_PAGE)
+
+static void
+search_data_free (SearchData *search_data)
+{
+ if (search_data->self != NULL)
+ g_object_unref (search_data->self);
+ g_free (search_data->title);
+ g_free (search_data->search);
+ g_free (search_data->search_filename);
+ g_free (search_data->package_filename);
+ g_free (search_data->url_not_found);
+ g_slice_free (SearchData, search_data);
+}
+
+static GsExtrasPageMode
+gs_extras_page_mode_from_string (const gchar *str)
+{
+ if (g_strcmp0 (str, "install-package-files") == 0)
+ return GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_FILES;
+ if (g_strcmp0 (str, "install-provide-files") == 0)
+ return GS_EXTRAS_PAGE_MODE_INSTALL_PROVIDE_FILES;
+ if (g_strcmp0 (str, "install-package-names") == 0)
+ return GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_NAMES;
+ if (g_strcmp0 (str, "install-mime-types") == 0)
+ return GS_EXTRAS_PAGE_MODE_INSTALL_MIME_TYPES;
+ if (g_strcmp0 (str, "install-fontconfig-resources") == 0)
+ return GS_EXTRAS_PAGE_MODE_INSTALL_FONTCONFIG_RESOURCES;
+ if (g_strcmp0 (str, "install-gstreamer-resources") == 0)
+ return GS_EXTRAS_PAGE_MODE_INSTALL_GSTREAMER_RESOURCES;
+ if (g_strcmp0 (str, "install-plasma-resources") == 0)
+ return GS_EXTRAS_PAGE_MODE_INSTALL_PLASMA_RESOURCES;
+ if (g_strcmp0 (str, "install-printer-drivers") == 0)
+ return GS_EXTRAS_PAGE_MODE_INSTALL_PRINTER_DRIVERS;
+
+ g_assert_not_reached ();
+}
+
+const gchar *
+gs_extras_page_mode_to_string (GsExtrasPageMode mode)
+{
+ if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_FILES)
+ return "install-package-files";
+ if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_PROVIDE_FILES)
+ return "install-provide-files";
+ if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_NAMES)
+ return "install-package-names";
+ if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_MIME_TYPES)
+ return "install-mime-types";
+ if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_FONTCONFIG_RESOURCES)
+ return "install-fontconfig-resources";
+ if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_GSTREAMER_RESOURCES)
+ return "install-gstreamer-resources";
+ if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_PLASMA_RESOURCES)
+ return "install-plasma-resources";
+ if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_PRINTER_DRIVERS)
+ return "install-printer-drivers";
+
+ g_assert_not_reached ();
+}
+
+static gchar *
+build_comma_separated_list (gchar **items)
+{
+ guint len;
+
+ len = g_strv_length (items);
+ if (len == 2) {
+ /* TRANSLATORS: separator for a list of items */
+ return g_strjoinv (_(" and "), items);
+ } else {
+ /* TRANSLATORS: separator for a list of items */
+ return g_strjoinv (_(", "), items);
+ }
+}
+
+static gchar *
+build_title (GsExtrasPage *self)
+{
+ guint i;
+ g_autofree gchar *titles = NULL;
+ g_autoptr(GPtrArray) title_array = NULL;
+
+ title_array = g_ptr_array_new ();
+ for (i = 0; i < self->array_search_data->len; i++) {
+ SearchData *search_data;
+
+ search_data = g_ptr_array_index (self->array_search_data, i);
+ g_ptr_array_add (title_array, search_data->title);
+ }
+ g_ptr_array_add (title_array, NULL);
+
+ titles = build_comma_separated_list ((gchar **) title_array->pdata);
+
+ switch (self->mode) {
+ case GS_EXTRAS_PAGE_MODE_INSTALL_FONTCONFIG_RESOURCES:
+ /* TRANSLATORS: Application window title for fonts installation.
+ %s will be replaced by name of the script we're searching for. */
+ return g_strdup_printf (ngettext ("Available fonts for the %s script",
+ "Available fonts for the %s scripts",
+ self->array_search_data->len),
+ titles);
+ break;
+ default:
+ /* TRANSLATORS: Application window title for codec installation.
+ %s will be replaced by actual codec name(s) */
+ return g_strdup_printf (ngettext ("Available software for %s",
+ "Available software for %s",
+ self->array_search_data->len),
+ titles);
+ break;
+ }
+}
+
+static void
+gs_extras_page_update_ui_state (GsExtrasPage *self)
+{
+ GtkWidget *widget;
+ g_autofree gchar *title = NULL;
+
+ if (gs_shell_get_mode (self->shell) != GS_SHELL_MODE_EXTRAS)
+ return;
+
+ /* main spinner */
+ switch (self->state) {
+ case GS_EXTRAS_PAGE_STATE_LOADING:
+ gs_start_spinner (GTK_SPINNER (self->spinner));
+ break;
+ case GS_EXTRAS_PAGE_STATE_READY:
+ case GS_EXTRAS_PAGE_STATE_NO_RESULTS:
+ case GS_EXTRAS_PAGE_STATE_FAILED:
+ gs_stop_spinner (GTK_SPINNER (self->spinner));
+ break;
+ default:
+ g_assert_not_reached ();
+ break;
+ }
+
+ /* headerbar title */
+ widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "application_details_header"));
+ switch (self->state) {
+ case GS_EXTRAS_PAGE_STATE_LOADING:
+ case GS_EXTRAS_PAGE_STATE_READY:
+ title = build_title (self);
+ gtk_label_set_label (GTK_LABEL (widget), title);
+ break;
+ case GS_EXTRAS_PAGE_STATE_NO_RESULTS:
+ case GS_EXTRAS_PAGE_STATE_FAILED:
+ gtk_label_set_label (GTK_LABEL (widget), _("Unable to Find Requested Software"));
+ break;
+ default:
+ g_assert_not_reached ();
+ break;
+ }
+
+ /* stack */
+ switch (self->state) {
+ case GS_EXTRAS_PAGE_STATE_LOADING:
+ gtk_stack_set_visible_child_name (GTK_STACK (self->stack), "spinner");
+ break;
+ case GS_EXTRAS_PAGE_STATE_READY:
+ gtk_stack_set_visible_child_name (GTK_STACK (self->stack), "results");
+ break;
+ case GS_EXTRAS_PAGE_STATE_NO_RESULTS:
+ gtk_stack_set_visible_child_name (GTK_STACK (self->stack), "no-results");
+ break;
+ case GS_EXTRAS_PAGE_STATE_FAILED:
+ gtk_stack_set_visible_child_name (GTK_STACK (self->stack), "failed");
+ break;
+ default:
+ g_assert_not_reached ();
+ break;
+ }
+}
+
+static void
+gs_extras_page_set_state (GsExtrasPage *self,
+ GsExtrasPageState state)
+{
+ self->state = state;
+ gs_extras_page_update_ui_state (self);
+}
+
+static void
+app_row_button_clicked_cb (GsAppRow *app_row,
+ GsExtrasPage *self)
+{
+ GsApp *app = gs_app_row_get_app (app_row);
+
+ if (gs_app_get_state (app) == AS_APP_STATE_UNAVAILABLE &&
+ gs_app_get_url (app, AS_URL_KIND_MISSING) != NULL) {
+ gs_shell_show_uri (self->shell,
+ gs_app_get_url (app, AS_URL_KIND_MISSING));
+ } else if (gs_app_get_state (app) == AS_APP_STATE_AVAILABLE ||
+ gs_app_get_state (app) == AS_APP_STATE_AVAILABLE_LOCAL ||
+ gs_app_get_state (app) == AS_APP_STATE_UNAVAILABLE) {
+ gs_page_install_app (GS_PAGE (self), app, GS_SHELL_INTERACTION_FULL,
+ self->search_cancellable);
+ } else if (gs_app_get_state (app) == AS_APP_STATE_INSTALLED) {
+ gs_page_remove_app (GS_PAGE (self), app, self->search_cancellable);
+ } else {
+ g_critical ("extras: app in unexpected state %u", gs_app_get_state (app));
+ }
+}
+
+static void
+gs_extras_page_add_app (GsExtrasPage *self, GsApp *app, GsAppList *list, SearchData *search_data)
+{
+ GtkWidget *app_row;
+ g_autoptr(GList) existing_apps = NULL;
+
+ /* Don't add same app twice */
+ existing_apps = gtk_container_get_children (GTK_CONTAINER (self->list_box_results));
+ for (GList *l = existing_apps; l != NULL; l = l->next) {
+ GsApp *existing_app;
+
+ existing_app = gs_app_row_get_app (GS_APP_ROW (l->data));
+ if (app == existing_app)
+ gtk_container_remove (GTK_CONTAINER (self->list_box_results),
+ GTK_WIDGET (l->data));
+ }
+
+ app_row = gs_app_row_new (app);
+ gs_app_row_set_colorful (GS_APP_ROW (app_row), TRUE);
+ gs_app_row_set_show_buttons (GS_APP_ROW (app_row), TRUE);
+
+ g_object_set_data_full (G_OBJECT (app_row), "missing-title", g_strdup (search_data->title), g_free);
+
+ g_signal_connect (app_row, "button-clicked",
+ G_CALLBACK (app_row_button_clicked_cb),
+ self);
+
+ gtk_container_add (GTK_CONTAINER (self->list_box_results), app_row);
+ gs_app_row_set_size_groups (GS_APP_ROW (app_row),
+ self->sizegroup_image,
+ self->sizegroup_name,
+ self->sizegroup_desc,
+ self->sizegroup_button);
+ gtk_widget_show (app_row);
+}
+
+static GsApp *
+create_missing_app (SearchData *search_data)
+{
+ GsExtrasPage *self = search_data->self;
+ GsApp *app;
+ GString *summary_missing;
+ g_autofree gchar *name = NULL;
+ g_autofree gchar *url = NULL;
+
+ app = gs_app_new ("missing-codec");
+
+ /* TRANSLATORS: This string is used for codecs that weren't found */
+ name = g_strdup_printf (_("%s not found"), search_data->title);
+ gs_app_set_name (app, GS_APP_QUALITY_HIGHEST, name);
+
+ /* TRANSLATORS: hyperlink title */
+ url = g_strdup_printf ("<a href=\"%s\">%s</a>", search_data->url_not_found, _("on the website"));
+
+ summary_missing = g_string_new ("");
+ switch (self->mode) {
+ case GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_FILES:
+ /* TRANSLATORS: this is when we know about an application or
+ * addon, but it can't be listed for some reason */
+ g_string_append_printf (summary_missing, _("No applications are available that provide the file %s."), search_data->title);
+ g_string_append (summary_missing, "\n");
+ /* TRANSLATORS: first %s is the codec name, and second %s is a
+ * hyperlink with the "on the website" text */
+ g_string_append_printf (summary_missing, _("Information about %s, as well as options "
+ "for how to get missing applications "
+ "might be found %s."), search_data->title, url);
+ break;
+ case GS_EXTRAS_PAGE_MODE_INSTALL_PROVIDE_FILES:
+ /* TRANSLATORS: this is when we know about an application or
+ * addon, but it can't be listed for some reason */
+ g_string_append_printf (summary_missing, _("No applications are available for %s support."), search_data->title);
+ g_string_append (summary_missing, "\n");
+ /* TRANSLATORS: first %s is the codec name, and second %s is a
+ * hyperlink with the "on the website" text */
+ g_string_append_printf (summary_missing, _("Information about %s, as well as options "
+ "for how to get missing applications "
+ "might be found %s."), search_data->title, url);
+ break;
+ case GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_NAMES:
+ /* TRANSLATORS: this is when we know about an application or
+ * addon, but it can't be listed for some reason */
+ g_string_append_printf (summary_missing, _("%s is not available."), search_data->title);
+ g_string_append (summary_missing, "\n");
+ /* TRANSLATORS: first %s is the codec name, and second %s is a
+ * hyperlink with the "on the website" text */
+ g_string_append_printf (summary_missing, _("Information about %s, as well as options "
+ "for how to get missing applications "
+ "might be found %s."), search_data->title, url);
+ break;
+ case GS_EXTRAS_PAGE_MODE_INSTALL_MIME_TYPES:
+ /* TRANSLATORS: this is when we know about an application or
+ * addon, but it can't be listed for some reason */
+ g_string_append_printf (summary_missing, _("No applications are available for %s support."), search_data->title);
+ g_string_append (summary_missing, "\n");
+ /* TRANSLATORS: first %s is the codec name, and second %s is a
+ * hyperlink with the "on the website" text */
+ g_string_append_printf (summary_missing, _("Information about %s, as well as options "
+ "for how to get an application that can support this format "
+ "might be found %s."), search_data->title, url);
+ break;
+ case GS_EXTRAS_PAGE_MODE_INSTALL_FONTCONFIG_RESOURCES:
+ /* TRANSLATORS: this is when we know about an application or
+ * addon, but it can't be listed for some reason */
+ g_string_append_printf (summary_missing, _("No fonts are available for the %s script support."), search_data->title);
+ g_string_append (summary_missing, "\n");
+ /* TRANSLATORS: first %s is the codec name, and second %s is a
+ * hyperlink with the "on the website" text */
+ g_string_append_printf (summary_missing, _("Information about %s, as well as options "
+ "for how to get additional fonts "
+ "might be found %s."), search_data->title, url);
+ break;
+ case GS_EXTRAS_PAGE_MODE_INSTALL_GSTREAMER_RESOURCES:
+ /* TRANSLATORS: this is when we know about an application or
+ * addon, but it can't be listed for some reason */
+ g_string_append_printf (summary_missing, _("No addon codecs are available for the %s format."), search_data->title);
+ g_string_append (summary_missing, "\n");
+ /* TRANSLATORS: first %s is the codec name, and second %s is a
+ * hyperlink with the "on the website" text */
+ g_string_append_printf (summary_missing, _("Information about %s, as well as options "
+ "for how to get a codec that can play this format "
+ "might be found %s."), search_data->title, url);
+ break;
+ case GS_EXTRAS_PAGE_MODE_INSTALL_PLASMA_RESOURCES:
+ /* TRANSLATORS: this is when we know about an application or
+ * addon, but it can't be listed for some reason */
+ g_string_append_printf (summary_missing, _("No Plasma resources are available for %s support."), search_data->title);
+ g_string_append (summary_missing, "\n");
+ /* TRANSLATORS: first %s is the codec name, and second %s is a
+ * hyperlink with the "on the website" text */
+ g_string_append_printf (summary_missing, _("Information about %s, as well as options "
+ "for how to get additional Plasma resources "
+ "might be found %s."), search_data->title, url);
+ break;
+ case GS_EXTRAS_PAGE_MODE_INSTALL_PRINTER_DRIVERS:
+ /* TRANSLATORS: this is when we know about an application or
+ * addon, but it can't be listed for some reason */
+ g_string_append_printf (summary_missing, _("No printer drivers are available for %s."), search_data->title);
+ g_string_append (summary_missing, "\n");
+ /* TRANSLATORS: first %s is the codec name, and second %s is a
+ * hyperlink with the "on the website" text */
+ g_string_append_printf (summary_missing, _("Information about %s, as well as options "
+ "for how to get a driver that supports this printer "
+ "might be found %s."), search_data->title, url);
+
+ break;
+ default:
+ g_assert_not_reached ();
+ break;
+ }
+ gs_app_set_summary_missing (app, g_string_free (summary_missing, FALSE));
+
+ gs_app_set_kind (app, AS_APP_KIND_GENERIC);
+ gs_app_set_state (app, AS_APP_STATE_UNAVAILABLE);
+ gs_app_set_url (app, AS_URL_KIND_MISSING, search_data->url_not_found);
+
+ return app;
+}
+
+static gchar *
+build_no_results_label (GsExtrasPage *self)
+{
+ GsApp *app = NULL;
+ guint num;
+ g_autofree gchar *codec_titles = NULL;
+ g_autofree gchar *url = NULL;
+ g_autoptr(GList) list = NULL;
+ g_autoptr(GPtrArray) array = NULL;
+
+ list = gtk_container_get_children (GTK_CONTAINER (self->list_box_results));
+ num = g_list_length (list);
+
+ g_assert (num > 0);
+
+ array = g_ptr_array_new ();
+ for (GList *l = list; l != NULL; l = l->next) {
+ app = gs_app_row_get_app (GS_APP_ROW (l->data));
+ g_ptr_array_add (array,
+ g_object_get_data (G_OBJECT (l->data), "missing-title"));
+ }
+ g_ptr_array_add (array, NULL);
+
+ url = g_strdup_printf ("<a href=\"%s\">%s</a>",
+ gs_app_get_url (app, AS_URL_KIND_MISSING),
+ /* TRANSLATORS: hyperlink title */
+ _("this website"));
+
+ codec_titles = build_comma_separated_list ((gchar **) array->pdata);
+ /* TRANSLATORS: no codecs were found. First %s will be replaced by actual codec name(s), second %s is a link titled "this website" */
+ return g_strdup_printf (ngettext ("Unfortunately, the %s you were searching for could not be found. Please see %s for more information.",
+ "Unfortunately, the %s you were searching for could not be found. Please see %s for more information.",
+ num),
+ codec_titles,
+ url);
+}
+
+static void
+show_search_results (GsExtrasPage *self)
+{
+ GsApp *app;
+ guint n_children;
+ guint n_missing;
+ g_autoptr(GList) list = NULL;
+
+ list = gtk_container_get_children (GTK_CONTAINER (self->list_box_results));
+ n_children = g_list_length (list);
+
+ /* count the number of rows with missing codecs */
+ n_missing = 0;
+ for (GList *l = list; l != NULL; l = l->next) {
+ app = gs_app_row_get_app (GS_APP_ROW (l->data));
+ if (g_strcmp0 (gs_app_get_id (app), "missing-codec") == 0) {
+ n_missing++;
+ }
+ }
+
+ if (n_children == 0 || n_children == n_missing) {
+ g_autofree gchar *str = NULL;
+
+ /* no results */
+ g_debug ("extras: failed to find any results, %u", n_missing);
+ str = build_no_results_label (self);
+ gtk_label_set_label (GTK_LABEL (self->label_no_results), str);
+ gs_extras_page_set_state (self, GS_EXTRAS_PAGE_STATE_NO_RESULTS);
+ } else if (n_children == 1) {
+ /* switch directly to details view */
+ g_debug ("extras: found one result, showing in details view");
+ g_assert (list != NULL);
+ app = gs_app_row_get_app (GS_APP_ROW (list->data));
+ gs_shell_change_mode (self->shell, GS_SHELL_MODE_DETAILS, app, TRUE);
+ } else {
+ /* show what we got */
+ g_debug ("extras: got %u search results, showing", n_children);
+ gs_extras_page_set_state (self, GS_EXTRAS_PAGE_STATE_READY);
+ }
+}
+
+static void
+search_files_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SearchData *search_data = (SearchData *) user_data;
+ GsExtrasPage *self = search_data->self;
+ g_autoptr(GsAppList) list = NULL;
+ guint i;
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object);
+ g_autoptr(GError) error = NULL;
+
+ list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error);
+ if (list == NULL) {
+ g_autofree gchar *str = NULL;
+ if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) {
+ g_debug ("extras: search files cancelled");
+ return;
+ }
+ g_warning ("failed to find any search results: %s", error->message);
+ str = g_strdup_printf ("%s: %s", _("Failed to find any search results"), error->message);
+ gtk_label_set_label (GTK_LABEL (self->label_failed), str);
+ gs_extras_page_set_state (self, GS_EXTRAS_PAGE_STATE_FAILED);
+ return;
+ }
+
+ /* add missing item */
+ if (gs_app_list_length (list) == 0) {
+ g_autoptr(GsApp) app = NULL;
+ g_debug ("extras: no search result for %s, showing as missing",
+ search_data->title);
+ app = create_missing_app (search_data);
+ gs_app_list_add (list, app);
+ }
+
+ for (i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+
+ g_debug ("%s\n\n", gs_app_to_string (app));
+ gs_extras_page_add_app (self, app, list, search_data);
+ }
+
+ self->pending_search_cnt--;
+
+ /* have all searches finished? */
+ if (self->pending_search_cnt == 0)
+ show_search_results (self);
+}
+
+static void
+file_to_app_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SearchData *search_data = (SearchData *) user_data;
+ GsExtrasPage *self = search_data->self;
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object);
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GsAppList) list = NULL;
+
+ list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error);
+ if (list == NULL) {
+ if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) {
+ g_debug ("extras: search what provides cancelled");
+ return;
+ }
+ if (g_error_matches (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED)) {
+ g_debug ("extras: no search result for %s, showing as missing", search_data->title);
+ app = create_missing_app (search_data);
+ } else {
+ g_autofree gchar *str = NULL;
+
+ g_warning ("failed to find any search results: %s", error->message);
+ str = g_strdup_printf ("%s: %s", _("Failed to find any search results"), error->message);
+ gtk_label_set_label (GTK_LABEL (self->label_failed), str);
+ gs_extras_page_set_state (self, GS_EXTRAS_PAGE_STATE_FAILED);
+ return;
+ }
+ } else {
+ app = g_object_ref (gs_app_list_index (list, 0));
+ }
+
+ g_debug ("%s\n\n", gs_app_to_string (app));
+ gs_extras_page_add_app (self, app, list, search_data);
+
+ self->pending_search_cnt--;
+
+ /* have all searches finished? */
+ if (self->pending_search_cnt == 0)
+ show_search_results (self);
+}
+
+static void
+get_search_what_provides_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SearchData *search_data = (SearchData *) user_data;
+ GsExtrasPage *self = search_data->self;
+ g_autoptr(GsAppList) list = NULL;
+ guint i;
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object);
+ g_autoptr(GError) error = NULL;
+
+ list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error);
+ if (list == NULL) {
+ g_autofree gchar *str = NULL;
+ if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) {
+ g_debug ("extras: search what provides cancelled");
+ return;
+ }
+ g_warning ("failed to find any search results: %s", error->message);
+ str = g_strdup_printf ("%s: %s", _("Failed to find any search results"), error->message);
+ gtk_label_set_label (GTK_LABEL (self->label_failed), str);
+ gs_extras_page_set_state (self, GS_EXTRAS_PAGE_STATE_FAILED);
+ return;
+ }
+
+ /* add missing item */
+ if (gs_app_list_length (list) == 0) {
+ g_autoptr(GsApp) app = NULL;
+ g_debug ("extras: no search result for %s, showing as missing",
+ search_data->title);
+ app = create_missing_app (search_data);
+ gs_app_list_add (list, app);
+ }
+
+ for (i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+
+ g_debug ("%s\n\n", gs_app_to_string (app));
+ gs_extras_page_add_app (self, app, list, search_data);
+ }
+
+ self->pending_search_cnt--;
+
+ /* have all searches finished? */
+ if (self->pending_search_cnt == 0)
+ show_search_results (self);
+}
+
+static void
+gs_extras_page_load (GsExtrasPage *self, GPtrArray *array_search_data)
+{
+ guint i;
+
+ /* cancel any pending searches */
+ g_cancellable_cancel (self->search_cancellable);
+ g_clear_object (&self->search_cancellable);
+ self->search_cancellable = g_cancellable_new ();
+
+ if (array_search_data != NULL) {
+ if (self->array_search_data != NULL)
+ g_ptr_array_unref (self->array_search_data);
+ self->array_search_data = g_ptr_array_ref (array_search_data);
+ }
+
+ self->pending_search_cnt = 0;
+
+ /* remove old entries */
+ gs_container_remove_all (GTK_CONTAINER (self->list_box_results));
+
+ /* set state as loading */
+ self->state = GS_EXTRAS_PAGE_STATE_LOADING;
+
+ /* start new searches, separate one for each codec */
+ for (i = 0; i < self->array_search_data->len; i++) {
+ GsPluginRefineFlags refine_flags;
+ SearchData *search_data;
+
+ refine_flags = GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING |
+ GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES;
+
+ search_data = g_ptr_array_index (self->array_search_data, i);
+ if (search_data->search_filename != NULL) {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH_FILES,
+ "search", search_data->search_filename,
+ "refine-flags", refine_flags,
+ NULL);
+ g_debug ("searching filename: '%s'", search_data->search_filename);
+ gs_plugin_loader_job_process_async (self->plugin_loader,
+ plugin_job,
+ self->search_cancellable,
+ search_files_cb,
+ search_data);
+ } else if (search_data->package_filename != NULL) {
+ g_autoptr (GFile) file = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ file = g_file_new_for_path (search_data->package_filename);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP,
+ "file", file,
+ "refine-flags", refine_flags,
+ NULL);
+ g_debug ("resolving filename to app: '%s'", search_data->package_filename);
+ gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
+ self->search_cancellable,
+ file_to_app_cb,
+ search_data);
+ } else {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ g_debug ("searching what provides: '%s'", search_data->search);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH_PROVIDES,
+ "search", search_data->search,
+ "refine-flags", refine_flags,
+ NULL);
+ gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
+ self->search_cancellable,
+ get_search_what_provides_cb,
+ search_data);
+ }
+ self->pending_search_cnt++;
+ }
+}
+
+static void
+gs_extras_page_reload (GsPage *page)
+{
+ GsExtrasPage *self = GS_EXTRAS_PAGE (page);
+ if (self->array_search_data != NULL)
+ gs_extras_page_load (self, NULL);
+}
+
+static void
+gs_extras_page_search_package_files (GsExtrasPage *self, gchar **files)
+{
+ g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free);
+ guint i;
+
+ for (i = 0; files[i] != NULL; i++) {
+ SearchData *search_data;
+
+ search_data = g_slice_new0 (SearchData);
+ search_data->title = g_strdup (files[i]);
+ search_data->package_filename = g_strdup (files[i]);
+ search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_DEFAULT);
+ search_data->self = g_object_ref (self);
+ g_ptr_array_add (array_search_data, search_data);
+ }
+
+ gs_extras_page_load (self, array_search_data);
+}
+
+static void
+gs_extras_page_search_provide_files (GsExtrasPage *self, gchar **files)
+{
+ g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free);
+ guint i;
+
+ for (i = 0; files[i] != NULL; i++) {
+ SearchData *search_data;
+
+ search_data = g_slice_new0 (SearchData);
+ search_data->title = g_strdup (files[i]);
+ search_data->search_filename = g_strdup (files[i]);
+ search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_DEFAULT);
+ search_data->self = g_object_ref (self);
+ g_ptr_array_add (array_search_data, search_data);
+ }
+
+ gs_extras_page_load (self, array_search_data);
+}
+
+static void
+gs_extras_page_search_package_names (GsExtrasPage *self, gchar **package_names)
+{
+ g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free);
+ guint i;
+
+ for (i = 0; package_names[i] != NULL; i++) {
+ SearchData *search_data;
+
+ search_data = g_slice_new0 (SearchData);
+ search_data->title = g_strdup (package_names[i]);
+ search_data->search = g_strdup (package_names[i]);
+ search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_DEFAULT);
+ search_data->self = g_object_ref (self);
+ g_ptr_array_add (array_search_data, search_data);
+ }
+
+ gs_extras_page_load (self, array_search_data);
+}
+
+static void
+gs_extras_page_search_mime_types (GsExtrasPage *self, gchar **mime_types)
+{
+ g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free);
+ guint i;
+
+ for (i = 0; mime_types[i] != NULL; i++) {
+ SearchData *search_data;
+
+ search_data = g_slice_new0 (SearchData);
+ search_data->title = g_strdup_printf (_("%s file format"), mime_types[i]);
+ search_data->search = g_strdup (mime_types[i]);
+ search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_MIME);
+ search_data->self = g_object_ref (self);
+ g_ptr_array_add (array_search_data, search_data);
+ }
+
+ gs_extras_page_load (self, array_search_data);
+}
+
+static gchar *
+font_tag_to_lang (const gchar *tag)
+{
+ if (g_str_has_prefix (tag, ":lang="))
+ return g_strdup (tag + 6);
+
+ return NULL;
+}
+
+static gchar *
+gs_extras_page_font_tag_to_localised_name (GsExtrasPage *self, const gchar *tag)
+{
+ gchar *name;
+ g_autofree gchar *lang = NULL;
+ g_autofree gchar *language = NULL;
+
+ /* use fontconfig to get the language code */
+ lang = font_tag_to_lang (tag);
+ if (lang == NULL) {
+ g_warning ("Could not parse language tag '%s'", tag);
+ return NULL;
+ }
+
+ /* convert to localisable name */
+ language = gs_language_iso639_to_language (self->language, lang);
+ if (language == NULL) {
+ g_warning ("Could not match language code '%s' to an ISO639 language", lang);
+ return NULL;
+ }
+
+ /* get translation, or return untranslated string */
+ name = g_strdup (dgettext("iso_639", language));
+ if (name == NULL)
+ name = g_strdup (language);
+
+ return name;
+}
+
+static void
+gs_extras_page_search_fontconfig_resources (GsExtrasPage *self, gchar **resources)
+{
+ g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free);
+ guint i;
+
+ for (i = 0; resources[i] != NULL; i++) {
+ SearchData *search_data;
+
+ search_data = g_slice_new0 (SearchData);
+ search_data->title = gs_extras_page_font_tag_to_localised_name (self, resources[i]);
+ search_data->search = g_strdup (resources[i]);
+ search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_FONT);
+ search_data->self = g_object_ref (self);
+ g_ptr_array_add (array_search_data, search_data);
+ }
+
+ gs_extras_page_load (self, array_search_data);
+}
+
+static void
+gs_extras_page_search_gstreamer_resources (GsExtrasPage *self, gchar **resources)
+{
+ g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free);
+ guint i;
+
+ for (i = 0; resources[i] != NULL; i++) {
+ SearchData *search_data;
+ g_auto(GStrv) parts = NULL;
+
+ parts = g_strsplit (resources[i], "|", 2);
+
+ search_data = g_slice_new0 (SearchData);
+ search_data->title = g_strdup (parts[0]);
+ search_data->search = g_strdup (parts[1]);
+ search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_CODEC);
+ search_data->self = g_object_ref (self);
+ g_ptr_array_add (array_search_data, search_data);
+ }
+
+ gs_extras_page_load (self, array_search_data);
+}
+
+static void
+gs_extras_page_search_plasma_resources (GsExtrasPage *self, gchar **resources)
+{
+ g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free);
+ guint i;
+
+ for (i = 0; resources[i] != NULL; i++) {
+ SearchData *search_data;
+
+ search_data = g_slice_new0 (SearchData);
+ search_data->title = g_strdup (resources[i]);
+ search_data->search = g_strdup (resources[i]);
+ search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_DEFAULT);
+ search_data->self = g_object_ref (self);
+ g_ptr_array_add (array_search_data, search_data);
+ }
+
+ gs_extras_page_load (self, array_search_data);
+}
+
+static void
+gs_extras_page_search_printer_drivers (GsExtrasPage *self, gchar **device_ids)
+{
+ g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free);
+ guint i, j;
+ guint len;
+
+ len = g_strv_length (device_ids);
+ if (len > 1)
+ /* hardcode for now as we only support one at a time */
+ len = 1;
+
+ /* make a list of provides tags */
+ for (i = 0; i < len; i++) {
+ SearchData *search_data;
+ gchar *p;
+ guint n_fields;
+ g_autofree gchar *tag = NULL;
+ g_autofree gchar *mfg = NULL;
+ g_autofree gchar *mdl = NULL;
+ g_auto(GStrv) fields = NULL;
+
+ fields = g_strsplit (device_ids[i], ";", 0);
+ n_fields = g_strv_length (fields);
+ mfg = mdl = NULL;
+ for (j = 0; j < n_fields && (!mfg || !mdl); j++) {
+ if (g_str_has_prefix (fields[j], "MFG:"))
+ mfg = g_strdup (fields[j] + 4);
+ else if (g_str_has_prefix (fields[j], "MDL:"))
+ mdl = g_strdup (fields[j] + 4);
+ }
+
+ if (!mfg || !mdl) {
+ g_warning("invalid line '%s', missing field",
+ device_ids[i]);
+ continue;
+ }
+
+ tag = g_strdup_printf ("%s;%s;", mfg, mdl);
+
+ /* Replace spaces with underscores */
+ for (p = tag; *p != '\0'; p++)
+ if (*p == ' ')
+ *p = '_';
+
+ search_data = g_slice_new0 (SearchData);
+ search_data->title = g_strdup_printf ("%s %s", mfg, mdl);
+ search_data->search = g_ascii_strdown (tag, -1);
+ search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_HARDWARE);
+ search_data->self = g_object_ref (self);
+ g_ptr_array_add (array_search_data, search_data);
+ }
+
+ gs_extras_page_load (self, array_search_data);
+}
+
+void
+gs_extras_page_search (GsExtrasPage *self,
+ const gchar *mode_str,
+ gchar **resources)
+{
+ self->mode = gs_extras_page_mode_from_string (mode_str);
+ switch (self->mode) {
+ case GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_FILES:
+ gs_extras_page_search_package_files (self, resources);
+ break;
+ case GS_EXTRAS_PAGE_MODE_INSTALL_PROVIDE_FILES:
+ gs_extras_page_search_provide_files (self, resources);
+ break;
+ case GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_NAMES:
+ gs_extras_page_search_package_names (self, resources);
+ break;
+ case GS_EXTRAS_PAGE_MODE_INSTALL_MIME_TYPES:
+ gs_extras_page_search_mime_types (self, resources);
+ break;
+ case GS_EXTRAS_PAGE_MODE_INSTALL_FONTCONFIG_RESOURCES:
+ gs_extras_page_search_fontconfig_resources (self, resources);
+ break;
+ case GS_EXTRAS_PAGE_MODE_INSTALL_GSTREAMER_RESOURCES:
+ gs_extras_page_search_gstreamer_resources (self, resources);
+ break;
+ case GS_EXTRAS_PAGE_MODE_INSTALL_PLASMA_RESOURCES:
+ gs_extras_page_search_plasma_resources (self, resources);
+ break;
+ case GS_EXTRAS_PAGE_MODE_INSTALL_PRINTER_DRIVERS:
+ gs_extras_page_search_printer_drivers (self, resources);
+ break;
+ default:
+ g_assert_not_reached ();
+ break;
+ }
+}
+
+static void
+gs_extras_page_switch_to (GsPage *page,
+ gboolean scroll_up)
+{
+ GsExtrasPage *self = GS_EXTRAS_PAGE (page);
+ GtkWidget *widget;
+
+ if (gs_shell_get_mode (self->shell) != GS_SHELL_MODE_EXTRAS) {
+ g_warning ("Called switch_to(codecs) when in mode %s",
+ gs_shell_get_mode_string (self->shell));
+ return;
+ }
+
+ widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "application_details_header"));
+ gtk_widget_show (widget);
+
+ if (scroll_up) {
+ GtkAdjustment *adj;
+ adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolledwindow));
+ gtk_adjustment_set_value (adj, gtk_adjustment_get_lower (adj));
+ }
+
+ gs_extras_page_update_ui_state (self);
+}
+
+static void
+row_activated_cb (GtkListBox *list_box,
+ GtkListBoxRow *row,
+ GsExtrasPage *self)
+{
+ GsApp *app;
+
+ app = gs_app_row_get_app (GS_APP_ROW (row));
+
+ if (gs_app_get_state (app) == AS_APP_STATE_UNAVAILABLE &&
+ gs_app_get_url (app, AS_URL_KIND_MISSING) != NULL) {
+ gs_shell_show_uri (self->shell,
+ gs_app_get_url (app, AS_URL_KIND_MISSING));
+ } else {
+ gs_shell_show_app (self->shell, app);
+ }
+}
+
+static gchar *
+get_app_sort_key (GsApp *app)
+{
+ GString *key = NULL;
+ g_autofree gchar *sort_name = NULL;
+
+ key = g_string_sized_new (64);
+
+ /* sort missing applications as last */
+ switch (gs_app_get_state (app)) {
+ case AS_APP_STATE_UNAVAILABLE:
+ g_string_append (key, "9:");
+ break;
+ default:
+ g_string_append (key, "1:");
+ break;
+ }
+
+ /* finally, sort by short name */
+ if (gs_app_get_name (app) != NULL) {
+ sort_name = gs_utils_sort_key (gs_app_get_name (app));
+ g_string_append (key, sort_name);
+ }
+
+ return g_string_free (key, FALSE);
+}
+
+static gint
+list_sort_func (GtkListBoxRow *a,
+ GtkListBoxRow *b,
+ gpointer user_data)
+{
+ GsApp *a1 = gs_app_row_get_app (GS_APP_ROW (a));
+ GsApp *a2 = gs_app_row_get_app (GS_APP_ROW (b));
+ g_autofree gchar *key1 = get_app_sort_key (a1);
+ g_autofree gchar *key2 = get_app_sort_key (a2);
+
+ /* compare the keys according to the algorithm above */
+ return g_strcmp0 (key1, key2);
+}
+
+static void
+list_header_func (GtkListBoxRow *row,
+ GtkListBoxRow *before,
+ gpointer user_data)
+{
+ GtkWidget *header;
+
+ /* first entry */
+ header = gtk_list_box_row_get_header (row);
+ if (before == NULL) {
+ gtk_list_box_row_set_header (row, NULL);
+ return;
+ }
+
+ /* already set */
+ if (header != NULL)
+ return;
+
+ /* set new */
+ header = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
+ gtk_list_box_row_set_header (row, header);
+}
+
+static gboolean
+gs_extras_page_setup (GsPage *page,
+ GsShell *shell,
+ GsPluginLoader *plugin_loader,
+ GtkBuilder *builder,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsExtrasPage *self = GS_EXTRAS_PAGE (page);
+
+ g_return_val_if_fail (GS_IS_EXTRAS_PAGE (self), TRUE);
+
+ self->shell = shell;
+
+ self->plugin_loader = g_object_ref (plugin_loader);
+ self->builder = g_object_ref (builder);
+
+ g_signal_connect (self->list_box_results, "row-activated",
+ G_CALLBACK (row_activated_cb), self);
+ gtk_list_box_set_header_func (GTK_LIST_BOX (self->list_box_results),
+ list_header_func,
+ self, NULL);
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (self->list_box_results),
+ list_sort_func,
+ self, NULL);
+ return TRUE;
+}
+
+static void
+gs_extras_page_dispose (GObject *object)
+{
+ GsExtrasPage *self = GS_EXTRAS_PAGE (object);
+
+ g_cancellable_cancel (self->search_cancellable);
+ g_clear_object (&self->search_cancellable);
+
+ g_clear_object (&self->sizegroup_image);
+ g_clear_object (&self->sizegroup_name);
+ g_clear_object (&self->sizegroup_desc);
+ g_clear_object (&self->sizegroup_button);
+ g_clear_object (&self->language);
+ g_clear_object (&self->vendor);
+ g_clear_object (&self->builder);
+ g_clear_object (&self->plugin_loader);
+
+ g_clear_pointer (&self->array_search_data, g_ptr_array_unref);
+
+ G_OBJECT_CLASS (gs_extras_page_parent_class)->dispose (object);
+}
+
+static void
+gs_extras_page_init (GsExtrasPage *self)
+{
+ g_autoptr(GError) error = NULL;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->state = GS_EXTRAS_PAGE_STATE_LOADING;
+ self->sizegroup_image = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
+ self->sizegroup_name = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
+ self->sizegroup_desc = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
+ self->sizegroup_button = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
+ self->vendor = gs_vendor_new ();
+
+ /* map ISO639 to language names */
+ self->language = gs_language_new ();
+ gs_language_populate (self->language, &error);
+ if (error != NULL)
+ g_error ("Failed to map ISO639 to language names: %s", error->message);
+}
+
+static void
+gs_extras_page_class_init (GsExtrasPageClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GsPageClass *page_class = GS_PAGE_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = gs_extras_page_dispose;
+ page_class->switch_to = gs_extras_page_switch_to;
+ page_class->reload = gs_extras_page_reload;
+ page_class->setup = gs_extras_page_setup;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-extras-page.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GsExtrasPage, label_failed);
+ gtk_widget_class_bind_template_child (widget_class, GsExtrasPage, label_no_results);
+ gtk_widget_class_bind_template_child (widget_class, GsExtrasPage, list_box_results);
+ gtk_widget_class_bind_template_child (widget_class, GsExtrasPage, scrolledwindow);
+ gtk_widget_class_bind_template_child (widget_class, GsExtrasPage, spinner);
+ gtk_widget_class_bind_template_child (widget_class, GsExtrasPage, stack);
+}
+
+GsExtrasPage *
+gs_extras_page_new (void)
+{
+ GsExtrasPage *self;
+ self = g_object_new (GS_TYPE_EXTRAS_PAGE, NULL);
+ return GS_EXTRAS_PAGE (self);
+}