summaryrefslogtreecommitdiffstats
path: root/src/gs-details-page.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/gs-details-page.c')
-rw-r--r--src/gs-details-page.c2856
1 files changed, 2856 insertions, 0 deletions
diff --git a/src/gs-details-page.c b/src/gs-details-page.c
new file mode 100644
index 0000000..6994e6a
--- /dev/null
+++ b/src/gs-details-page.c
@@ -0,0 +1,2856 @@
+/* -*- 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) 2013 Matthias Clasen <mclasen@redhat.com>
+ * Copyright (C) 2014-2019 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include <locale.h>
+#include <string.h>
+#include <glib/gi18n.h>
+
+#include "gs-common.h"
+#include "gs-content-rating.h"
+#include "gs-utils.h"
+
+#include "gs-details-page.h"
+#include "gs-app-addon-row.h"
+#include "gs-history-dialog.h"
+#include "gs-origin-popover-row.h"
+#include "gs-screenshot-image.h"
+#include "gs-star-widget.h"
+#include "gs-review-histogram.h"
+#include "gs-review-dialog.h"
+#include "gs-review-row.h"
+
+/* the number of reviews to show before clicking the 'More Reviews' button */
+#define SHOW_NR_REVIEWS_INITIAL 4
+
+static void gs_details_page_refresh_all (GsDetailsPage *self);
+
+typedef enum {
+ GS_DETAILS_PAGE_STATE_LOADING,
+ GS_DETAILS_PAGE_STATE_READY,
+ GS_DETAILS_PAGE_STATE_FAILED
+} GsDetailsPageState;
+
+struct _GsDetailsPage
+{
+ GsPage parent_instance;
+
+ GsPluginLoader *plugin_loader;
+ GtkBuilder *builder;
+ GCancellable *cancellable;
+ GCancellable *app_cancellable;
+ GsApp *app;
+ GsApp *app_local_file;
+ GsShell *shell;
+ SoupSession *session;
+ gboolean enable_reviews;
+ gboolean show_all_reviews;
+ GSettings *settings;
+ GtkSizeGroup *size_group_origin_popover;
+
+ GtkWidget *application_details_icon;
+ GtkWidget *application_details_summary;
+ GtkWidget *application_details_title;
+ GtkWidget *box_addons;
+ GtkWidget *box_details;
+ GtkWidget *box_details_description;
+ GtkWidget *box_details_support;
+ GtkWidget *box_progress;
+ GtkWidget *box_progress2;
+ GtkWidget *star;
+ GtkWidget *label_review_count;
+ GtkWidget *box_details_screenshot;
+ GtkWidget *box_details_screenshot_main;
+ GtkWidget *box_details_screenshot_scrolledwindow;
+ GtkWidget *box_details_screenshot_thumbnails;
+ GtkWidget *box_details_license_list;
+ GtkWidget *button_details_launch;
+ GtkWidget *button_details_add_shortcut;
+ GtkWidget *button_details_remove_shortcut;
+ GtkWidget *button_details_website;
+ GtkWidget *button_donate;
+ GtkWidget *button_install;
+ GtkWidget *button_update;
+ GtkWidget *button_remove;
+ GtkWidget *button_cancel;
+ GtkWidget *button_more_reviews;
+ GtkWidget *infobar_details_app_norepo;
+ GtkWidget *infobar_details_app_repo;
+ GtkWidget *infobar_details_package_baseos;
+ GtkWidget *infobar_details_repo;
+ GtkWidget *label_progress_percentage;
+ GtkWidget *label_progress_status;
+ GtkWidget *label_addons_uninstalled_app;
+ GtkWidget *label_details_category_title;
+ GtkWidget *label_details_category_value;
+ GtkWidget *label_details_developer_title;
+ GtkWidget *label_details_developer_value;
+ GtkWidget *box_details_developer;
+ GtkWidget *image_details_developer_verified;
+ GtkWidget *button_details_license_free;
+ GtkWidget *button_details_license_nonfree;
+ GtkWidget *button_details_license_unknown;
+ GtkWidget *label_details_license_title;
+ GtkWidget *box_details_license_value;
+ GtkWidget *label_details_channel_title;
+ GtkWidget *label_details_channel_value;
+ GtkWidget *label_details_origin_title;
+ GtkWidget *label_details_origin_value;
+ GtkWidget *label_details_size_installed_title;
+ GtkWidget *label_details_size_installed_value;
+ GtkWidget *label_details_size_download_title;
+ GtkWidget *label_details_size_download_value;
+ GtkWidget *label_details_updated_title;
+ GtkWidget *label_details_updated_value;
+ GtkWidget *label_details_version_title;
+ GtkWidget *label_details_version_value;
+ GtkWidget *label_details_permissions_title;
+ GtkWidget *button_details_permissions_value;
+ GtkWidget *label_failed;
+ GtkWidget *label_license_nonfree_details;
+ GtkWidget *label_licenses_intro;
+ GtkWidget *list_box_addons;
+ GtkWidget *box_reviews;
+ GtkWidget *box_details_screenshot_fallback;
+ GtkWidget *histogram;
+ GtkWidget *button_review;
+ GtkWidget *list_box_reviews;
+ GtkWidget *scrolledwindow_details;
+ GtkWidget *spinner_details;
+ GtkWidget *spinner_remove;
+ GtkWidget *stack_details;
+ GtkWidget *grid_details_kudo;
+ GtkWidget *image_details_kudo_docs;
+ GtkWidget *image_details_kudo_sandboxed;
+ GtkWidget *image_details_kudo_integration;
+ GtkWidget *image_details_kudo_translated;
+ GtkWidget *image_details_kudo_updated;
+ GtkWidget *label_details_kudo_docs;
+ GtkWidget *label_details_kudo_sandboxed;
+ GtkWidget *label_details_kudo_integration;
+ GtkWidget *label_details_kudo_translated;
+ GtkWidget *label_details_kudo_updated;
+ GtkWidget *progressbar_top;
+ guint progress_pulse_id;
+ GtkWidget *popover_license_free;
+ GtkWidget *popover_license_nonfree;
+ GtkWidget *popover_license_unknown;
+ GtkWidget *popover_content_rating;
+ GtkWidget *label_content_rating_title;
+ GtkWidget *label_content_rating_message;
+ GtkWidget *label_content_rating_none;
+ GtkWidget *button_details_rating_value;
+ GtkStyleProvider *button_details_rating_style_provider;
+ GtkWidget *label_details_rating_title;
+ GtkWidget *popover_permissions;
+ GtkWidget *box_permissions_details;
+ GtkWidget *star_eventbox;
+};
+
+G_DEFINE_TYPE (GsDetailsPage, gs_details_page, GS_TYPE_PAGE)
+
+static void
+gs_details_page_set_state (GsDetailsPage *self,
+ GsDetailsPageState state)
+{
+ /* spinner */
+ switch (state) {
+ case GS_DETAILS_PAGE_STATE_LOADING:
+ gs_start_spinner (GTK_SPINNER (self->spinner_details));
+ gtk_widget_show (self->spinner_details);
+ break;
+ case GS_DETAILS_PAGE_STATE_READY:
+ case GS_DETAILS_PAGE_STATE_FAILED:
+ gs_stop_spinner (GTK_SPINNER (self->spinner_details));
+ gtk_widget_hide (self->spinner_details);
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ /* stack */
+ switch (state) {
+ case GS_DETAILS_PAGE_STATE_LOADING:
+ gtk_stack_set_visible_child_name (GTK_STACK (self->stack_details), "spinner");
+ break;
+ case GS_DETAILS_PAGE_STATE_READY:
+ gtk_stack_set_visible_child_name (GTK_STACK (self->stack_details), "ready");
+ break;
+ case GS_DETAILS_PAGE_STATE_FAILED:
+ gtk_stack_set_visible_child_name (GTK_STACK (self->stack_details), "failed");
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+static void
+gs_details_page_update_shortcut_button (GsDetailsPage *self)
+{
+ gboolean add_shortcut_func;
+ gboolean remove_shortcut_func;
+ gboolean has_shortcut;
+
+ gtk_widget_set_visible (self->button_details_add_shortcut,
+ FALSE);
+ gtk_widget_set_visible (self->button_details_remove_shortcut,
+ FALSE);
+
+ if (gs_app_get_kind (self->app) != AS_APP_KIND_DESKTOP)
+ return;
+
+ /* Leave the button hidden if the app can’t be launched by the current
+ * user. */
+ if (gs_app_has_quirk (self->app, GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE))
+ return;
+
+ /* only consider the shortcut button if the app is installed */
+ switch (gs_app_get_state (self->app)) {
+ case AS_APP_STATE_INSTALLED:
+ case AS_APP_STATE_UPDATABLE:
+ case AS_APP_STATE_UPDATABLE_LIVE:
+ break;
+ default:
+ return;
+ }
+
+ add_shortcut_func =
+ gs_plugin_loader_get_plugin_supported (self->plugin_loader,
+ "gs_plugin_add_shortcut");
+ remove_shortcut_func =
+ gs_plugin_loader_get_plugin_supported (self->plugin_loader,
+ "gs_plugin_remove_shortcut");
+
+ has_shortcut = gs_app_has_quirk (self->app, GS_APP_QUIRK_HAS_SHORTCUT);
+
+ if (add_shortcut_func) {
+ gtk_widget_set_visible (self->button_details_add_shortcut,
+ !has_shortcut || !remove_shortcut_func);
+ gtk_widget_set_sensitive (self->button_details_add_shortcut,
+ !has_shortcut);
+ }
+
+ if (remove_shortcut_func) {
+ gtk_widget_set_visible (self->button_details_remove_shortcut,
+ has_shortcut || !add_shortcut_func);
+ gtk_widget_set_sensitive (self->button_details_remove_shortcut,
+ has_shortcut);
+ }
+}
+
+static gboolean
+app_has_pending_action (GsApp *app)
+{
+ /* sanitize the pending state change by verifying we're in one of the
+ * expected states */
+ if (gs_app_get_state (app) != AS_APP_STATE_AVAILABLE &&
+ gs_app_get_state (app) != AS_APP_STATE_UPDATABLE_LIVE &&
+ gs_app_get_state (app) != AS_APP_STATE_UPDATABLE &&
+ gs_app_get_state (app) != AS_APP_STATE_QUEUED_FOR_INSTALL)
+ return FALSE;
+
+ return (gs_app_get_pending_action (app) != GS_PLUGIN_ACTION_UNKNOWN) ||
+ (gs_app_get_state (app) == AS_APP_STATE_QUEUED_FOR_INSTALL);
+}
+
+static void
+gs_details_page_switch_to (GsPage *page, gboolean scroll_up)
+{
+ GsDetailsPage *self = GS_DETAILS_PAGE (page);
+ GtkWidget *widget;
+ GtkAdjustment *adj;
+
+ if (gs_shell_get_mode (self->shell) != GS_SHELL_MODE_DETAILS) {
+ g_warning ("Called switch_to(details) 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_label_set_label (GTK_LABEL (widget), "");
+ gtk_widget_show (widget);
+
+ /* not set, perhaps file-to-app */
+ if (self->app == NULL)
+ return;
+
+ adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolledwindow_details));
+ gtk_adjustment_set_value (adj, gtk_adjustment_get_lower (adj));
+
+ gs_grab_focus_when_mapped (self->scrolledwindow_details);
+}
+
+static gboolean
+_pulse_cb (gpointer user_data)
+{
+ GsDetailsPage *self = GS_DETAILS_PAGE (user_data);
+
+ gtk_progress_bar_pulse (GTK_PROGRESS_BAR (self->progressbar_top));
+
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+stop_progress_pulsing (GsDetailsPage *self)
+{
+ if (self->progress_pulse_id != 0) {
+ g_source_remove (self->progress_pulse_id);
+ self->progress_pulse_id = 0;
+ }
+}
+
+static void
+gs_details_page_refresh_progress (GsDetailsPage *self)
+{
+ guint percentage;
+ AsAppState state;
+
+ /* cancel button */
+ state = gs_app_get_state (self->app);
+ switch (state) {
+ case AS_APP_STATE_INSTALLING:
+ gtk_widget_set_visible (self->button_cancel, TRUE);
+ /* If the app is installing, the user can only cancel it if
+ * 1) They haven't already, and
+ * 2) the plugin hasn't said that they can't, for example if a
+ * package manager has already gone 'too far'
+ */
+ gtk_widget_set_sensitive (self->button_cancel,
+ !g_cancellable_is_cancelled (self->app_cancellable) &&
+ gs_app_get_allow_cancel (self->app));
+ break;
+ default:
+ gtk_widget_set_visible (self->button_cancel, FALSE);
+ break;
+ }
+ if (app_has_pending_action (self->app)) {
+ gtk_widget_set_visible (self->button_cancel, TRUE);
+ gtk_widget_set_sensitive (self->button_cancel,
+ !g_cancellable_is_cancelled (self->app_cancellable) &&
+ gs_app_get_allow_cancel (self->app));
+ }
+
+ /* progress status label */
+ switch (state) {
+ case AS_APP_STATE_REMOVING:
+ gtk_widget_set_visible (self->label_progress_status, TRUE);
+ gtk_label_set_label (GTK_LABEL (self->label_progress_status),
+ _("Removing…"));
+ break;
+ case AS_APP_STATE_INSTALLING:
+ gtk_widget_set_visible (self->label_progress_status, TRUE);
+ gtk_label_set_label (GTK_LABEL (self->label_progress_status),
+ _("Installing"));
+ break;
+ default:
+ gtk_widget_set_visible (self->label_progress_status, FALSE);
+ break;
+ }
+ if (app_has_pending_action (self->app)) {
+ GsPluginAction action = gs_app_get_pending_action (self->app);
+ gtk_widget_set_visible (self->label_progress_status, TRUE);
+ switch (action) {
+ case GS_PLUGIN_ACTION_INSTALL:
+ gtk_label_set_label (GTK_LABEL (self->label_progress_status),
+ /* TRANSLATORS: This is a label on top of the app's progress
+ * bar to inform the user that the app should be installed soon */
+ _("Pending installation…"));
+ break;
+ case GS_PLUGIN_ACTION_UPDATE:
+ case GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD:
+ gtk_label_set_label (GTK_LABEL (self->label_progress_status),
+ /* TRANSLATORS: This is a label on top of the app's progress
+ * bar to inform the user that the app should be updated soon */
+ _("Pending update…"));
+ break;
+ default:
+ gtk_widget_set_visible (self->label_progress_status, FALSE);
+ break;
+ }
+ }
+
+ /* percentage bar */
+ switch (state) {
+ case AS_APP_STATE_INSTALLING:
+ percentage = gs_app_get_progress (self->app);
+ if (percentage == GS_APP_PROGRESS_UNKNOWN) {
+ /* Translators: This string is shown when preparing to download and install an app. */
+ gtk_label_set_label (GTK_LABEL (self->label_progress_status), _("Preparing…"));
+ gtk_widget_set_visible (self->label_progress_status, TRUE);
+ gtk_widget_set_visible (self->label_progress_percentage, FALSE);
+
+ if (self->progress_pulse_id == 0)
+ self->progress_pulse_id = g_timeout_add (50, _pulse_cb, self);
+
+ gtk_widget_set_visible (self->progressbar_top, TRUE);
+ break;
+ } else if (percentage <= 100) {
+ g_autofree gchar *str = g_strdup_printf ("%u%%", percentage);
+ gtk_label_set_label (GTK_LABEL (self->label_progress_percentage), str);
+ gtk_widget_set_visible (self->label_progress_percentage, TRUE);
+ stop_progress_pulsing (self);
+ gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (self->progressbar_top),
+ (gdouble) percentage / 100.f);
+ gtk_widget_set_visible (self->progressbar_top, TRUE);
+ break;
+ }
+ /* FALLTHROUGH */
+ default:
+ gtk_widget_set_visible (self->label_progress_percentage, FALSE);
+ gtk_widget_set_visible (self->progressbar_top, FALSE);
+ stop_progress_pulsing (self);
+ break;
+ }
+ if (app_has_pending_action (self->app)) {
+ gtk_widget_set_visible (self->progressbar_top, TRUE);
+ gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (self->progressbar_top), 0);
+ }
+
+ /* spinner */
+ switch (state) {
+ case AS_APP_STATE_REMOVING:
+ if (!gtk_widget_get_visible (self->spinner_remove)) {
+ gtk_spinner_start (GTK_SPINNER (self->spinner_remove));
+ gtk_widget_set_visible (self->spinner_remove, TRUE);
+ }
+ /* align text together with the spinner if we're showing it */
+ gtk_widget_set_halign (self->box_progress2, GTK_ALIGN_START);
+ break;
+ default:
+ gtk_widget_set_visible (self->spinner_remove, FALSE);
+ gtk_spinner_stop (GTK_SPINNER (self->spinner_remove));
+ gtk_widget_set_halign (self->box_progress2, GTK_ALIGN_CENTER);
+ break;
+ }
+
+ /* progress box */
+ switch (state) {
+ case AS_APP_STATE_REMOVING:
+ case AS_APP_STATE_INSTALLING:
+ gtk_widget_set_visible (self->box_progress, TRUE);
+ break;
+ default:
+ gtk_widget_set_visible (self->box_progress, FALSE);
+ break;
+ }
+ if (app_has_pending_action (self->app))
+ gtk_widget_set_visible (self->box_progress, TRUE);
+}
+
+static gboolean
+gs_details_page_refresh_progress_idle (gpointer user_data)
+{
+ GsDetailsPage *self = GS_DETAILS_PAGE (user_data);
+ gs_details_page_refresh_progress (self);
+ g_object_unref (self);
+ return G_SOURCE_REMOVE;
+}
+
+static void
+gs_details_page_progress_changed_cb (GsApp *app,
+ GParamSpec *pspec,
+ GsDetailsPage *self)
+{
+ g_idle_add (gs_details_page_refresh_progress_idle, g_object_ref (self));
+}
+
+static gboolean
+gs_details_page_allow_cancel_changed_idle (gpointer user_data)
+{
+ GsDetailsPage *self = GS_DETAILS_PAGE (user_data);
+ gtk_widget_set_sensitive (self->button_cancel,
+ gs_app_get_allow_cancel (self->app));
+ g_object_unref (self);
+ return G_SOURCE_REMOVE;
+}
+
+static void
+gs_details_page_allow_cancel_changed_cb (GsApp *app,
+ GParamSpec *pspec,
+ GsDetailsPage *self)
+{
+ g_idle_add (gs_details_page_allow_cancel_changed_idle,
+ g_object_ref (self));
+}
+
+static gboolean
+gs_details_page_switch_to_idle (gpointer user_data)
+{
+ GsDetailsPage *self = GS_DETAILS_PAGE (user_data);
+
+ if (gs_shell_get_mode (self->shell) == GS_SHELL_MODE_DETAILS)
+ gs_page_switch_to (GS_PAGE (self), TRUE);
+
+ /* update widgets */
+ gs_details_page_refresh_all (self);
+
+ g_object_unref (self);
+ return G_SOURCE_REMOVE;
+}
+
+static void
+gs_details_page_notify_state_changed_cb (GsApp *app,
+ GParamSpec *pspec,
+ GsDetailsPage *self)
+{
+ g_idle_add (gs_details_page_switch_to_idle, g_object_ref (self));
+}
+
+static void
+gs_details_page_load_main_screenshot (GsDetailsPage *self,
+ AsScreenshot *screenshot)
+{
+ GsScreenshotImage *ssmain;
+ g_autoptr(GList) children = NULL;
+
+ children = gtk_container_get_children (GTK_CONTAINER (self->box_details_screenshot_main));
+ ssmain = GS_SCREENSHOT_IMAGE (children->data);
+
+ gs_screenshot_image_set_screenshot (ssmain, screenshot);
+ gs_screenshot_image_load_async (ssmain, NULL);
+}
+
+static void
+gs_details_page_screenshot_selected_cb (GtkListBox *list,
+ GtkListBoxRow *row,
+ GsDetailsPage *self)
+{
+ GsScreenshotImage *ssthumb;
+ AsScreenshot *ss;
+ g_autoptr(GList) children = NULL;
+
+ if (row == NULL)
+ return;
+
+ ssthumb = GS_SCREENSHOT_IMAGE (gtk_bin_get_child (GTK_BIN (row)));
+ ss = gs_screenshot_image_get_screenshot (ssthumb);
+
+ gs_details_page_load_main_screenshot (self, ss);
+}
+
+static void
+gs_details_page_refresh_screenshots (GsDetailsPage *self)
+{
+ GPtrArray *screenshots;
+ AsScreenshot *ss;
+ GtkWidget *label;
+ GtkWidget *list;
+ GtkWidget *ssimg;
+ GtkWidget *main_screenshot = NULL;
+ guint i;
+ gboolean is_offline = !gs_plugin_loader_get_network_available (self->plugin_loader);
+ guint num_screenshots_loaded = 0;
+
+ /* reset the visibility of screenshots */
+ gtk_widget_show (self->box_details_screenshot);
+
+ /* treat screenshots differently */
+ if (gs_app_get_kind (self->app) == AS_APP_KIND_FONT) {
+ gs_container_remove_all (GTK_CONTAINER (self->box_details_screenshot_thumbnails));
+ gs_container_remove_all (GTK_CONTAINER (self->box_details_screenshot_main));
+ screenshots = gs_app_get_screenshots (self->app);
+ for (i = 0; i < screenshots->len; i++) {
+ ss = g_ptr_array_index (screenshots, i);
+
+ /* set caption */
+ label = gtk_label_new (as_screenshot_get_caption (ss, NULL));
+ g_object_set (label,
+ "xalign", 0.0,
+ "max-width-chars", 10,
+ "wrap", TRUE,
+ NULL);
+ gtk_container_add (GTK_CONTAINER (self->box_details_screenshot_main), label);
+ gtk_widget_set_visible (label, TRUE);
+
+ /* set images */
+ ssimg = gs_screenshot_image_new (self->session);
+ gs_screenshot_image_set_screenshot (GS_SCREENSHOT_IMAGE (ssimg), ss);
+ gs_screenshot_image_set_size (GS_SCREENSHOT_IMAGE (ssimg),
+ 640,
+ 48);
+ gs_screenshot_image_load_async (GS_SCREENSHOT_IMAGE (ssimg), NULL);
+ gtk_container_add (GTK_CONTAINER (self->box_details_screenshot_main), ssimg);
+ gtk_widget_set_visible (ssimg, TRUE);
+ }
+ gtk_widget_set_visible (self->box_details_screenshot,
+ screenshots->len > 0);
+ gtk_widget_set_visible (self->box_details_screenshot_fallback,
+ screenshots->len == 0 && !is_offline);
+ return;
+ }
+
+ /* fallback warning */
+ screenshots = gs_app_get_screenshots (self->app);
+ switch (gs_app_get_kind (self->app)) {
+ case AS_APP_KIND_GENERIC:
+ case AS_APP_KIND_CODEC:
+ case AS_APP_KIND_ADDON:
+ case AS_APP_KIND_SOURCE:
+ case AS_APP_KIND_FIRMWARE:
+ case AS_APP_KIND_DRIVER:
+ case AS_APP_KIND_INPUT_METHOD:
+ case AS_APP_KIND_LOCALIZATION:
+ case AS_APP_KIND_RUNTIME:
+ gtk_widget_set_visible (self->box_details_screenshot_fallback, FALSE);
+ break;
+ default:
+ gtk_widget_set_visible (self->box_details_screenshot_fallback,
+ screenshots->len == 0 && !is_offline);
+ break;
+ }
+
+ /* reset screenshots */
+ gs_container_remove_all (GTK_CONTAINER (self->box_details_screenshot_main));
+ gs_container_remove_all (GTK_CONTAINER (self->box_details_screenshot_thumbnails));
+
+ list = gtk_list_box_new ();
+ gtk_style_context_add_class (gtk_widget_get_style_context (list), "image-list");
+ gtk_widget_show (list);
+ gtk_widget_show (self->box_details_screenshot_scrolledwindow);
+ gtk_container_add (GTK_CONTAINER (self->box_details_screenshot_thumbnails), list);
+
+ for (i = 0; i < screenshots->len; i++) {
+ ss = g_ptr_array_index (screenshots, i);
+
+ /* we need to load the main screenshot only once if we're online
+ * but all times if we're offline (to check which are cached and
+ * hide those who aren't) */
+ if (is_offline || main_screenshot == NULL) {
+ GtkWidget *ssmain = gs_screenshot_image_new (self->session);
+ gtk_widget_set_can_focus (gtk_bin_get_child (GTK_BIN (ssmain)), FALSE);
+ gs_screenshot_image_set_screenshot (GS_SCREENSHOT_IMAGE (ssmain), ss);
+ gs_screenshot_image_set_size (GS_SCREENSHOT_IMAGE (ssmain),
+ AS_IMAGE_NORMAL_WIDTH,
+ AS_IMAGE_NORMAL_HEIGHT);
+ gs_screenshot_image_load_async (GS_SCREENSHOT_IMAGE (ssmain), NULL);
+
+ /* when we're offline, the load will be immediate, so we
+ * can check if it succeeded, and just skip it and its
+ * thumbnails otherwise */
+ if (is_offline &&
+ !gs_screenshot_image_is_showing (GS_SCREENSHOT_IMAGE (ssmain)))
+ continue;
+
+ /* only set the main_screenshot once */
+ if (main_screenshot == NULL) {
+ main_screenshot = ssmain;
+ gtk_box_pack_start (GTK_BOX (self->box_details_screenshot_main),
+ main_screenshot, FALSE, FALSE, 0);
+ gtk_widget_show (main_screenshot);
+ }
+ }
+
+ ssimg = gs_screenshot_image_new (self->session);
+ gs_screenshot_image_set_screenshot (GS_SCREENSHOT_IMAGE (ssimg), ss);
+ gs_screenshot_image_set_size (GS_SCREENSHOT_IMAGE (ssimg),
+ AS_IMAGE_THUMBNAIL_WIDTH,
+ AS_IMAGE_THUMBNAIL_HEIGHT);
+ gtk_style_context_add_class (gtk_widget_get_style_context (ssimg),
+ "screenshot-image-thumb");
+ gs_screenshot_image_load_async (GS_SCREENSHOT_IMAGE (ssimg), NULL);
+ gtk_list_box_insert (GTK_LIST_BOX (list), ssimg, -1);
+ gtk_widget_set_visible (ssimg, TRUE);
+ ++num_screenshots_loaded;
+ }
+
+ if (main_screenshot == NULL) {
+ gtk_widget_hide (self->box_details_screenshot);
+ return;
+ }
+
+ /* reload the main screenshot with a larger size if it's the only screenshot
+ * available */
+ if (num_screenshots_loaded == 1) {
+ gs_screenshot_image_set_size (GS_SCREENSHOT_IMAGE (main_screenshot),
+ AS_IMAGE_LARGE_WIDTH,
+ AS_IMAGE_LARGE_HEIGHT);
+ gs_screenshot_image_load_async (GS_SCREENSHOT_IMAGE (main_screenshot), NULL);
+ }
+
+ if (num_screenshots_loaded <= 1) {
+ gtk_widget_hide (self->box_details_screenshot_thumbnails);
+ return;
+ }
+
+ gtk_widget_show (self->box_details_screenshot_thumbnails);
+ gtk_list_box_set_selection_mode (GTK_LIST_BOX (list), GTK_SELECTION_BROWSE);
+ g_signal_connect (list, "row-selected",
+ G_CALLBACK (gs_details_page_screenshot_selected_cb),
+ self);
+ gtk_list_box_select_row (GTK_LIST_BOX (list),
+ gtk_list_box_get_row_at_index (GTK_LIST_BOX (list), 0));
+}
+
+static void
+gs_details_page_website_cb (GtkWidget *widget, GsDetailsPage *self)
+{
+ gs_shell_show_uri (self->shell,
+ gs_app_get_url (self->app, AS_URL_KIND_HOMEPAGE));
+}
+
+static void
+gs_details_page_donate_cb (GtkWidget *widget, GsDetailsPage *self)
+{
+ gs_shell_show_uri (self->shell, gs_app_get_url (self->app, AS_URL_KIND_DONATION));
+}
+
+static void
+gs_details_page_set_description (GsDetailsPage *self, const gchar *tmp)
+{
+ GtkStyleContext *style_context;
+ GtkWidget *para;
+ guint i;
+ g_auto(GStrv) split = NULL;
+
+ /* does the description exist? */
+ gtk_widget_set_visible (self->box_details_description, tmp != NULL);
+ if (tmp == NULL)
+ return;
+
+ /* add each paragraph as a new GtkLabel which lets us get the 24px
+ * paragraph spacing */
+ gs_container_remove_all (GTK_CONTAINER (self->box_details_description));
+ split = g_strsplit (tmp, "\n\n", -1);
+ for (i = 0; split[i] != NULL; i++) {
+ para = gtk_label_new (split[i]);
+ gtk_label_set_line_wrap (GTK_LABEL (para), TRUE);
+ gtk_label_set_max_width_chars (GTK_LABEL (para), 40);
+ gtk_label_set_selectable (GTK_LABEL (para), TRUE);
+ gtk_widget_set_visible (para, TRUE);
+ gtk_widget_set_can_focus (para, FALSE);
+ g_object_set (para,
+ "xalign", 0.0,
+ NULL);
+
+ /* add style class for theming */
+ style_context = gtk_widget_get_style_context (para);
+ gtk_style_context_add_class (style_context,
+ "application-details-description");
+
+ gtk_container_add (GTK_CONTAINER (self->box_details_description), para);
+ }
+
+ /* show the webapp warning */
+ if (gs_app_get_kind (self->app) == AS_APP_KIND_WEB_APP) {
+ GtkWidget *label;
+ /* TRANSLATORS: this is the warning box */
+ label = gtk_label_new (_("This application can only be used when there is an active internet connection."));
+ gtk_widget_set_visible (label, TRUE);
+ gtk_label_set_xalign (GTK_LABEL (label), 0.f);
+ gtk_style_context_add_class (gtk_widget_get_style_context (label),
+ "application-details-webapp-warning");
+ gtk_container_add (GTK_CONTAINER (self->box_details_description), label);
+ }
+}
+
+static void
+gs_details_page_set_sensitive (GtkWidget *widget, gboolean is_active)
+{
+ GtkStyleContext *style_context;
+ style_context = gtk_widget_get_style_context (widget);
+ if (!is_active) {
+ gtk_style_context_add_class (style_context, "dim-label");
+ } else {
+ gtk_style_context_remove_class (style_context, "dim-label");
+ }
+}
+
+static gboolean
+gs_details_page_history_cb (GtkLabel *label,
+ gchar *uri,
+ GsDetailsPage *self)
+{
+ GtkWidget *dialog;
+
+ dialog = gs_history_dialog_new ();
+ gs_history_dialog_set_app (GS_HISTORY_DIALOG (dialog), self->app);
+ gs_shell_modal_dialog_present (self->shell, GTK_DIALOG (dialog));
+
+ /* just destroy */
+ g_signal_connect_swapped (dialog, "response",
+ G_CALLBACK (gtk_widget_destroy), dialog);
+
+ return TRUE;
+}
+
+static void
+gs_details_page_refresh_size (GsDetailsPage *self)
+{
+ /* set the installed size */
+ if (gs_app_get_size_installed (self->app) != GS_APP_SIZE_UNKNOWABLE &&
+ gs_app_get_size_installed (self->app) != 0) {
+ g_autofree gchar *size = NULL;
+ size = g_format_size (gs_app_get_size_installed (self->app));
+ gtk_label_set_label (GTK_LABEL (self->label_details_size_installed_value), size);
+ gtk_widget_show (self->label_details_size_installed_title);
+ gtk_widget_show (self->label_details_size_installed_value);
+ } else {
+ gtk_widget_hide (self->label_details_size_installed_title);
+ gtk_widget_hide (self->label_details_size_installed_value);
+ }
+
+ /* set the download size */
+ if (!gs_app_is_installed (self->app) &&
+ gs_app_get_size_download (self->app) != GS_APP_SIZE_UNKNOWABLE) {
+ g_autofree gchar *size = NULL;
+ size = g_format_size (gs_app_get_size_download (self->app));
+ gtk_label_set_label (GTK_LABEL (self->label_details_size_download_value), size);
+ gtk_widget_show (self->label_details_size_download_title);
+ gtk_widget_show (self->label_details_size_download_value);
+ } else {
+ gtk_widget_hide (self->label_details_size_download_title);
+ gtk_widget_hide (self->label_details_size_download_value);
+ }
+}
+
+static void
+gs_details_page_get_alternates_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GsDetailsPage *self = GS_DETAILS_PAGE (user_data);
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object);
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsAppList) list = NULL;
+ GtkWidget *origin_box;
+ GtkWidget *origin_button_label;
+ GtkWidget *origin_popover_list_box;
+ g_autofree gchar *origin_ui = NULL;
+
+ origin_box = GTK_WIDGET (gtk_builder_get_object (self->builder, "origin_box"));
+ origin_button_label = GTK_WIDGET (gtk_builder_get_object (self->builder, "origin_button_label"));
+ origin_popover_list_box = GTK_WIDGET (gtk_builder_get_object (self->builder, "origin_popover_list_box"));
+
+ gs_container_remove_all (GTK_CONTAINER (origin_popover_list_box));
+
+ 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_warning ("failed to get alternates: %s", error->message);
+ gtk_widget_hide (origin_box);
+ return;
+ }
+
+ /* add the local file to the list so that we can carry it over when
+ * switching between alternates */
+ if (self->app_local_file != NULL)
+ gs_app_list_add (list, self->app_local_file);
+
+ /* no alternates to show */
+ if (gs_app_list_length (list) < 2) {
+ gtk_widget_hide (origin_box);
+ return;
+ }
+
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ GtkWidget *row = gs_origin_popover_row_new (app);
+ gtk_widget_show (row);
+ if (app == self->app)
+ gs_origin_popover_row_set_selected (GS_ORIGIN_POPOVER_ROW (row), TRUE);
+ gs_origin_popover_row_set_size_group (GS_ORIGIN_POPOVER_ROW (row),
+ self->size_group_origin_popover);
+ gtk_container_add (GTK_CONTAINER (origin_popover_list_box), row);
+ }
+
+ origin_ui = gs_app_get_origin_ui (self->app);
+ if (origin_ui != NULL)
+ gtk_label_set_text (GTK_LABEL (origin_button_label), origin_ui);
+ else
+ gtk_label_set_text (GTK_LABEL (origin_button_label), "");
+
+ gtk_widget_show (origin_box);
+}
+
+static void
+gs_details_page_refresh_buttons (GsDetailsPage *self)
+{
+ AsAppState state;
+ g_autofree gchar *text = NULL;
+
+ state = gs_app_get_state (self->app);
+
+ /* install button */
+ switch (state) {
+ case AS_APP_STATE_AVAILABLE:
+ case AS_APP_STATE_AVAILABLE_LOCAL:
+ gtk_widget_set_visible (self->button_install, TRUE);
+ /* TRANSLATORS: button text in the header when an application
+ * can be installed */
+ gtk_button_set_label (GTK_BUTTON (self->button_install), _("_Install"));
+ break;
+ case AS_APP_STATE_INSTALLING:
+ gtk_widget_set_visible (self->button_install, FALSE);
+ break;
+ case AS_APP_STATE_UNKNOWN:
+ case AS_APP_STATE_INSTALLED:
+ case AS_APP_STATE_REMOVING:
+ case AS_APP_STATE_UPDATABLE:
+ case AS_APP_STATE_QUEUED_FOR_INSTALL:
+ gtk_widget_set_visible (self->button_install, FALSE);
+ break;
+ case AS_APP_STATE_UPDATABLE_LIVE:
+ if (gs_app_get_kind (self->app) == AS_APP_KIND_FIRMWARE) {
+ gtk_widget_set_visible (self->button_install, TRUE);
+ /* TRANSLATORS: button text in the header when firmware
+ * can be live-installed */
+ gtk_button_set_label (GTK_BUTTON (self->button_install), _("_Install"));
+ } else {
+ gtk_widget_set_visible (self->button_install, FALSE);
+ }
+ break;
+ case AS_APP_STATE_UNAVAILABLE:
+ if (gs_app_get_url (self->app, AS_URL_KIND_MISSING) != NULL) {
+ gtk_widget_set_visible (self->button_install, FALSE);
+ } else {
+ gtk_widget_set_visible (self->button_install, TRUE);
+ /* TRANSLATORS: this is a button that allows the apps to
+ * be installed.
+ * The ellipsis indicates that further steps are required,
+ * e.g. enabling software repositories or the like */
+ gtk_button_set_label (GTK_BUTTON (self->button_install), _("_Install…"));
+ }
+ break;
+ default:
+ g_warning ("App unexpectedly in state %s",
+ as_app_state_to_string (state));
+ g_assert_not_reached ();
+ }
+
+ /* update button */
+ switch (state) {
+ case AS_APP_STATE_UPDATABLE_LIVE:
+ if (gs_app_get_kind (self->app) == AS_APP_KIND_FIRMWARE) {
+ gtk_widget_set_visible (self->button_update, FALSE);
+ } else {
+ gtk_widget_set_visible (self->button_update, TRUE);
+ }
+ break;
+ default:
+ gtk_widget_set_visible (self->button_update, FALSE);
+ break;
+ }
+
+ /* launch button */
+ switch (gs_app_get_state (self->app)) {
+ case AS_APP_STATE_INSTALLED:
+ case AS_APP_STATE_UPDATABLE:
+ case AS_APP_STATE_UPDATABLE_LIVE:
+ if (!gs_app_has_quirk (self->app, GS_APP_QUIRK_NOT_LAUNCHABLE) &&
+ !gs_app_has_quirk (self->app, GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE)) {
+ gtk_widget_set_visible (self->button_details_launch, TRUE);
+ } else {
+ gtk_widget_set_visible (self->button_details_launch, FALSE);
+ }
+ break;
+ default:
+ gtk_widget_set_visible (self->button_details_launch, FALSE);
+ break;
+ }
+
+ gtk_button_set_label (GTK_BUTTON (self->button_details_launch),
+ /* TRANSLATORS: A label for a button to execute the selected application. */
+ _("_Launch"));
+
+ /* don't show the launch and shortcut buttons if the app doesn't have a desktop ID */
+ if (gs_app_get_id (self->app) == NULL) {
+ gtk_widget_set_visible (self->button_details_launch, FALSE);
+ }
+
+ /* remove button */
+ if (gs_app_has_quirk (self->app, GS_APP_QUIRK_COMPULSORY) ||
+ gs_app_get_kind (self->app) == AS_APP_KIND_FIRMWARE) {
+ gtk_widget_set_visible (self->button_remove, FALSE);
+ } else {
+ switch (state) {
+ case AS_APP_STATE_INSTALLED:
+ case AS_APP_STATE_UPDATABLE:
+ case AS_APP_STATE_UPDATABLE_LIVE:
+ gtk_widget_set_visible (self->button_remove, TRUE);
+ gtk_widget_set_sensitive (self->button_remove, TRUE);
+ /* Mark the button as destructive only if Launch is not visible */
+ if (gtk_widget_get_visible (self->button_details_launch))
+ gtk_style_context_remove_class (gtk_widget_get_style_context (self->button_remove), "destructive-action");
+ else
+ gtk_style_context_add_class (gtk_widget_get_style_context (self->button_remove), "destructive-action");
+ /* TRANSLATORS: button text in the header when an application can be erased */
+ gtk_button_set_label (GTK_BUTTON (self->button_remove), _("_Remove"));
+ break;
+ case AS_APP_STATE_AVAILABLE_LOCAL:
+ case AS_APP_STATE_AVAILABLE:
+ case AS_APP_STATE_INSTALLING:
+ case AS_APP_STATE_REMOVING:
+ case AS_APP_STATE_UNAVAILABLE:
+ case AS_APP_STATE_UNKNOWN:
+ case AS_APP_STATE_QUEUED_FOR_INSTALL:
+ gtk_widget_set_visible (self->button_remove, FALSE);
+ break;
+ default:
+ g_warning ("App unexpectedly in state %s",
+ as_app_state_to_string (state));
+ g_assert_not_reached ();
+ }
+ }
+
+ if (app_has_pending_action (self->app)) {
+ gtk_widget_set_visible (self->button_install, FALSE);
+ gtk_widget_set_visible (self->button_update, FALSE);
+ gtk_widget_set_visible (self->button_details_launch, FALSE);
+ gtk_widget_set_visible (self->button_remove, FALSE);
+ }
+}
+
+static struct {
+ GsAppPermissions permission;
+ const char *title;
+ const char *subtitle;
+} permission_display_data[] = {
+ { GS_APP_PERMISSIONS_NETWORK, N_("Network"), N_("Can communicate over the network") },
+ { GS_APP_PERMISSIONS_SYSTEM_BUS, N_("System Services"), N_("Can access D-Bus services on the system bus") },
+ { GS_APP_PERMISSIONS_SESSION_BUS, N_("Session Services"), N_("Can access D-Bus services on the session bus") },
+ { GS_APP_PERMISSIONS_DEVICES, N_("Devices"), N_("Can access system device files") },
+ { GS_APP_PERMISSIONS_HOME_FULL, N_("Home folder"), N_("Can view, edit and create files") },
+ { GS_APP_PERMISSIONS_HOME_READ, N_("Home folder"), N_("Can view files") },
+ { GS_APP_PERMISSIONS_FILESYSTEM_FULL, N_("File system"), N_("Can view, edit and create files") },
+ { GS_APP_PERMISSIONS_FILESYSTEM_READ, N_("File system"), N_("Can view files") },
+ { GS_APP_PERMISSIONS_DOWNLOADS_FULL, N_("Downloads folder"), N_("Can view, edit and create files") },
+ { GS_APP_PERMISSIONS_DOWNLOADS_READ, N_("Downloads folder"), N_("Can view files") },
+ { GS_APP_PERMISSIONS_SETTINGS, N_("Settings"), N_("Can view and change any settings") },
+ { GS_APP_PERMISSIONS_X11, N_("Legacy display system"), N_("Uses an old, insecure display system") },
+ { GS_APP_PERMISSIONS_ESCAPE_SANDBOX, N_("Sandbox escape"), N_("Can escape the sandbox and circumvent any other restrictions") },
+};
+
+static void
+populate_permission_details (GsDetailsPage *self, GsAppPermissions permissions)
+{
+ GList *children;
+
+ children = gtk_container_get_children (GTK_CONTAINER (self->box_permissions_details));
+ for (GList *l = children; l != NULL; l = l->next)
+ gtk_widget_destroy (GTK_WIDGET (l->data));
+ g_list_free (children);
+
+ if (permissions == GS_APP_PERMISSIONS_NONE) {
+ GtkWidget *label;
+ label = gtk_label_new (_("This application is fully sandboxed."));
+ gtk_label_set_xalign (GTK_LABEL (label), 0);
+ gtk_label_set_max_width_chars (GTK_LABEL (label), 40);
+ gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
+ gtk_widget_show (label);
+ gtk_container_add (GTK_CONTAINER (self->box_permissions_details), label);
+ } else if (permissions == GS_APP_PERMISSIONS_UNKNOWN) {
+ GtkWidget *label;
+ label = gtk_label_new (_("Unable to determine which parts of the system "
+ "this application accesses. This is typical for "
+ "older applications."));
+ gtk_label_set_xalign (GTK_LABEL (label), 0);
+ gtk_label_set_max_width_chars (GTK_LABEL (label), 40);
+ gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
+ gtk_widget_show (label);
+ gtk_container_add (GTK_CONTAINER (self->box_permissions_details), label);
+ } else {
+ for (gsize i = 0; i < G_N_ELEMENTS (permission_display_data); i++) {
+ GtkWidget *row, *image, *box, *label;
+
+ if ((permissions & permission_display_data[i].permission) == 0)
+ continue;
+
+ row = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12);
+ gtk_widget_show (row);
+
+ image = gtk_image_new_from_icon_name ("dialog-warning-symbolic", GTK_ICON_SIZE_MENU);
+ if ((permission_display_data[i].permission & ~MEDIUM_PERMISSIONS) == 0)
+ gtk_widget_set_opacity (image, 0);
+
+ gtk_widget_show (image);
+ gtk_container_add (GTK_CONTAINER (row), image);
+
+ box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_show (box);
+ gtk_container_add (GTK_CONTAINER (row), box);
+
+ label = gtk_label_new (_(permission_display_data[i].title));
+ gtk_label_set_xalign (GTK_LABEL (label), 0);
+ gtk_widget_show (label);
+ gtk_container_add (GTK_CONTAINER (box), label);
+
+ label = gtk_label_new (_(permission_display_data[i].subtitle));
+ gtk_label_set_xalign (GTK_LABEL (label), 0);
+ gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label");
+ gtk_widget_show (label);
+ gtk_container_add (GTK_CONTAINER (box), label);
+
+ gtk_container_add (GTK_CONTAINER (self->box_permissions_details), row);
+ }
+ }
+}
+
+static void
+gs_details_page_refresh_all (GsDetailsPage *self)
+{
+ GsAppList *history;
+ GdkPixbuf *pixbuf = NULL;
+ GList *addons;
+ GtkWidget *widget;
+ const gchar *tmp;
+ gboolean ret;
+ gchar **menu_path;
+ guint64 kudos;
+ guint64 updated;
+ guint64 user_integration_bf;
+ gboolean show_support_box = FALSE;
+ g_autofree gchar *origin = NULL;
+
+ /* change widgets */
+ tmp = gs_app_get_name (self->app);
+ widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "application_details_header"));
+ if (tmp != NULL && tmp[0] != '\0') {
+ gtk_label_set_label (GTK_LABEL (self->application_details_title), tmp);
+ gtk_label_set_label (GTK_LABEL (widget), tmp);
+ gtk_widget_set_visible (self->application_details_title, TRUE);
+ } else {
+ gtk_widget_set_visible (self->application_details_title, FALSE);
+ gtk_label_set_label (GTK_LABEL (widget), "");
+ }
+ tmp = gs_app_get_summary (self->app);
+ if (tmp != NULL && tmp[0] != '\0') {
+ gtk_label_set_label (GTK_LABEL (self->application_details_summary), tmp);
+ gtk_widget_set_visible (self->application_details_summary, TRUE);
+ } else {
+ gtk_widget_set_visible (self->application_details_summary, FALSE);
+ }
+
+ /* refresh buttons */
+ gs_details_page_refresh_buttons (self);
+
+ /* set the description */
+ tmp = gs_app_get_description (self->app);
+ gs_details_page_set_description (self, tmp);
+
+ /* set the icon */
+ pixbuf = gs_app_get_pixbuf (self->app);
+ if (pixbuf != NULL) {
+ gs_image_set_from_pixbuf (GTK_IMAGE (self->application_details_icon), pixbuf);
+ } else {
+ gtk_image_set_from_icon_name (GTK_IMAGE (self->application_details_icon),
+ "application-x-executable",
+ GTK_ICON_SIZE_DIALOG);
+ }
+
+ tmp = gs_app_get_url (self->app, AS_URL_KIND_HOMEPAGE);
+ if (tmp != NULL && tmp[0] != '\0') {
+ gtk_widget_set_visible (self->button_details_website, TRUE);
+ show_support_box = TRUE;
+ } else {
+ gtk_widget_set_visible (self->button_details_website, FALSE);
+ }
+ tmp = gs_app_get_url (self->app, AS_URL_KIND_DONATION);
+ if (tmp != NULL && tmp[0] != '\0') {
+ gtk_widget_set_visible (self->button_donate, TRUE);
+ show_support_box = TRUE;
+ } else {
+ gtk_widget_set_visible (self->button_donate, FALSE);
+ }
+ gtk_widget_set_visible (self->box_details_support, show_support_box);
+
+ /* set the developer name, falling back to the project group */
+ tmp = gs_app_get_developer_name (self->app);
+ if (tmp == NULL)
+ tmp = gs_app_get_project_group (self->app);
+ if (tmp == NULL) {
+ gtk_widget_set_visible (self->label_details_developer_title, FALSE);
+ gtk_widget_set_visible (self->box_details_developer, FALSE);
+ } else {
+ gtk_widget_set_visible (self->label_details_developer_title, TRUE);
+ gtk_label_set_label (GTK_LABEL (self->label_details_developer_value), tmp);
+ gtk_widget_set_visible (self->box_details_developer, TRUE);
+ }
+ gtk_widget_set_visible (self->image_details_developer_verified, gs_app_has_quirk (self->app, GS_APP_QUIRK_DEVELOPER_VERIFIED));
+
+ /* set the license buttons */
+ tmp = gs_app_get_license (self->app);
+ if (tmp == NULL) {
+ gtk_widget_set_visible (self->button_details_license_free, FALSE);
+ gtk_widget_set_visible (self->button_details_license_nonfree, FALSE);
+ gtk_widget_set_visible (self->button_details_license_unknown, TRUE);
+ } else if (gs_app_get_license_is_free (self->app)) {
+ gtk_widget_set_visible (self->button_details_license_free, TRUE);
+ gtk_widget_set_visible (self->button_details_license_nonfree, FALSE);
+ gtk_widget_set_visible (self->button_details_license_unknown, FALSE);
+ } else {
+ gtk_widget_set_visible (self->button_details_license_free, FALSE);
+ gtk_widget_set_visible (self->button_details_license_nonfree, TRUE);
+ gtk_widget_set_visible (self->button_details_license_unknown, FALSE);
+ }
+
+ /* set channel for snaps */
+ if (gs_app_get_bundle_kind (self->app) == AS_BUNDLE_KIND_SNAP) {
+ gtk_label_set_label (GTK_LABEL (self->label_details_channel_value), gs_app_get_branch (self->app));
+ gtk_widget_set_visible (self->label_details_channel_title, TRUE);
+ gtk_widget_set_visible (self->label_details_channel_value, TRUE);
+ } else {
+ gtk_widget_set_visible (self->label_details_channel_title, FALSE);
+ gtk_widget_set_visible (self->label_details_channel_value, FALSE);
+ }
+
+ /* set version */
+ tmp = gs_app_get_version (self->app);
+ if (tmp != NULL){
+ gtk_label_set_label (GTK_LABEL (self->label_details_version_value), tmp);
+ } else {
+ /* TRANSLATORS: this is where the version is not known */
+ gtk_label_set_label (GTK_LABEL (self->label_details_version_value), C_("version", "Unknown"));
+ }
+
+ /* refresh size information */
+ gs_details_page_refresh_size (self);
+
+ /* set the updated date */
+ updated = gs_app_get_install_date (self->app);
+ if (updated == GS_APP_INSTALL_DATE_UNSET) {
+ gtk_widget_set_visible (self->label_details_updated_title, FALSE);
+ gtk_widget_set_visible (self->label_details_updated_value, FALSE);
+ } else if (updated == GS_APP_INSTALL_DATE_UNKNOWN) {
+ /* TRANSLATORS: this is where the updated date is not known */
+ gtk_label_set_label (GTK_LABEL (self->label_details_updated_value), C_("updated", "Never"));
+ gtk_widget_set_visible (self->label_details_updated_title, TRUE);
+ gtk_widget_set_visible (self->label_details_updated_value, TRUE);
+ } else {
+ g_autoptr(GDateTime) dt = NULL;
+ g_autofree gchar *updated_str = NULL;
+ dt = g_date_time_new_from_unix_utc ((gint64) updated);
+ updated_str = g_date_time_format (dt, "%x");
+
+ history = gs_app_get_history (self->app);
+
+ if (gs_app_list_length (history) == 0) {
+ gtk_label_set_label (GTK_LABEL (self->label_details_updated_value), updated_str);
+ } else {
+ GString *url;
+
+ url = g_string_new (NULL);
+ g_string_printf (url, "<a href=\"show-history\">%s</a>", updated_str);
+ gtk_label_set_markup (GTK_LABEL (self->label_details_updated_value), url->str);
+ g_string_free (url, TRUE);
+ }
+ gtk_widget_set_visible (self->label_details_updated_title, TRUE);
+ gtk_widget_set_visible (self->label_details_updated_value, TRUE);
+ }
+
+ /* set the category */
+ menu_path = gs_app_get_menu_path (self->app);
+ if (menu_path == NULL || menu_path[0] == NULL || menu_path[0][0] == '\0') {
+ gtk_widget_set_visible (self->label_details_category_title, FALSE);
+ gtk_widget_set_visible (self->label_details_category_value, FALSE);
+ } else {
+ g_autofree gchar *path = NULL;
+ if (gtk_widget_get_direction (self->label_details_category_value) == GTK_TEXT_DIR_RTL)
+ path = g_strjoinv (" ← ", menu_path);
+ else
+ path = g_strjoinv (" → ", menu_path);
+ gtk_label_set_label (GTK_LABEL (self->label_details_category_value), path);
+ gtk_widget_set_visible (self->label_details_category_title, TRUE);
+ gtk_widget_set_visible (self->label_details_category_value, TRUE);
+ }
+
+ /* set the origin */
+ origin = g_strdup (gs_app_get_origin_hostname (self->app));
+ if (origin == NULL)
+ origin = g_strdup (gs_app_get_origin (self->app));
+ if (origin == NULL) {
+ GFile *local_file = gs_app_get_local_file (self->app);
+ if (local_file != NULL)
+ origin = g_file_get_basename (local_file);
+ }
+ if (origin == NULL || origin[0] == '\0') {
+ /* TRANSLATORS: this is where we don't know the origin of the
+ * application */
+ gtk_label_set_label (GTK_LABEL (self->label_details_origin_value), C_("origin", "Unknown"));
+ } else {
+ gtk_label_set_label (GTK_LABEL (self->label_details_origin_value), origin);
+ }
+
+ /* set MyLanguage kudo */
+ kudos = gs_app_get_kudos (self->app);
+ ret = (kudos & GS_APP_KUDO_MY_LANGUAGE) > 0;
+ gtk_widget_set_sensitive (self->image_details_kudo_translated, ret);
+ gs_details_page_set_sensitive (self->label_details_kudo_translated, ret);
+
+ /* set RecentRelease kudo */
+ ret = (kudos & GS_APP_KUDO_RECENT_RELEASE) > 0;
+ gtk_widget_set_sensitive (self->image_details_kudo_updated, ret);
+ gs_details_page_set_sensitive (self->label_details_kudo_updated, ret);
+
+ /* set UserDocs kudo */
+ ret = (kudos & GS_APP_KUDO_INSTALLS_USER_DOCS) > 0;
+ gtk_widget_set_sensitive (self->image_details_kudo_docs, ret);
+ gs_details_page_set_sensitive (self->label_details_kudo_docs, ret);
+
+ /* set sandboxed kudo */
+ ret = (kudos & GS_APP_KUDO_SANDBOXED) > 0;
+ gtk_widget_set_sensitive (self->image_details_kudo_sandboxed, ret);
+ gs_details_page_set_sensitive (self->label_details_kudo_sandboxed, ret);
+
+ /* any of the various integration kudos */
+ user_integration_bf = GS_APP_KUDO_SEARCH_PROVIDER |
+ GS_APP_KUDO_USES_NOTIFICATIONS |
+ GS_APP_KUDO_HIGH_CONTRAST;
+ ret = (kudos & user_integration_bf) > 0;
+ gtk_widget_set_sensitive (self->image_details_kudo_integration, ret);
+ gs_details_page_set_sensitive (self->label_details_kudo_integration, ret);
+
+ /* hide the kudo details for non-desktop software */
+ switch (gs_app_get_kind (self->app)) {
+ case AS_APP_KIND_DESKTOP:
+ gtk_widget_set_visible (self->grid_details_kudo, TRUE);
+ break;
+ default:
+ gtk_widget_set_visible (self->grid_details_kudo, FALSE);
+ break;
+ }
+
+ /* only show permissions for flatpak apps */
+ if (gs_app_get_bundle_kind (self->app) == AS_BUNDLE_KIND_FLATPAK &&
+ gs_app_get_kind (self->app) == AS_APP_KIND_DESKTOP) {
+ GsAppPermissions permissions = gs_app_get_permissions (self->app);
+
+ populate_permission_details (self, permissions);
+
+ if (gs_app_get_permissions (self->app) != GS_APP_PERMISSIONS_UNKNOWN) {
+ if ((permissions & ~LIMITED_PERMISSIONS) == 0)
+ gtk_button_set_label (GTK_BUTTON (self->button_details_permissions_value), _("Low"));
+ else if ((permissions & ~MEDIUM_PERMISSIONS) == 0)
+ gtk_button_set_label (GTK_BUTTON (self->button_details_permissions_value), _("Medium"));
+ else
+ gtk_button_set_label (GTK_BUTTON (self->button_details_permissions_value), _("High"));
+ } else {
+ gtk_button_set_label (GTK_BUTTON (self->button_details_permissions_value), _("Unknown"));
+ }
+
+ gtk_widget_set_visible (self->label_details_permissions_title, TRUE);
+ gtk_widget_set_visible (self->button_details_permissions_value, TRUE);
+ } else {
+ gtk_widget_set_visible (self->label_details_permissions_title, FALSE);
+ gtk_widget_set_visible (self->button_details_permissions_value, FALSE);
+ }
+
+ /* are we trying to replace something in the baseos */
+ gtk_widget_set_visible (self->infobar_details_package_baseos,
+ gs_app_has_quirk (self->app, GS_APP_QUIRK_COMPULSORY) &&
+ gs_app_get_state (self->app) == AS_APP_STATE_AVAILABLE_LOCAL);
+
+ switch (gs_app_get_kind (self->app)) {
+ case AS_APP_KIND_DESKTOP:
+ /* installing an app with a repo file */
+ gtk_widget_set_visible (self->infobar_details_app_repo,
+ gs_app_has_quirk (self->app,
+ GS_APP_QUIRK_HAS_SOURCE) &&
+ gs_app_get_state (self->app) == AS_APP_STATE_AVAILABLE_LOCAL);
+ gtk_widget_set_visible (self->infobar_details_repo, FALSE);
+ break;
+ case AS_APP_KIND_GENERIC:
+ /* installing a repo-release package */
+ gtk_widget_set_visible (self->infobar_details_app_repo, FALSE);
+ gtk_widget_set_visible (self->infobar_details_repo,
+ gs_app_has_quirk (self->app,
+ GS_APP_QUIRK_HAS_SOURCE) &&
+ gs_app_get_state (self->app) == AS_APP_STATE_AVAILABLE_LOCAL);
+ break;
+ default:
+ gtk_widget_set_visible (self->infobar_details_app_repo, FALSE);
+ gtk_widget_set_visible (self->infobar_details_repo, FALSE);
+ break;
+ }
+
+ /* installing a app without a repo file */
+ switch (gs_app_get_kind (self->app)) {
+ case AS_APP_KIND_DESKTOP:
+ if (gs_app_get_kind (self->app) == AS_APP_KIND_FIRMWARE) {
+ gtk_widget_set_visible (self->infobar_details_app_norepo, FALSE);
+ } else {
+ gtk_widget_set_visible (self->infobar_details_app_norepo,
+ !gs_app_has_quirk (self->app,
+ GS_APP_QUIRK_HAS_SOURCE) &&
+ gs_app_get_state (self->app) == AS_APP_STATE_AVAILABLE_LOCAL);
+ }
+ break;
+ default:
+ gtk_widget_set_visible (self->infobar_details_app_norepo, FALSE);
+ break;
+ }
+
+ /* only show the "select addons" string if the app isn't yet installed */
+ switch (gs_app_get_state (self->app)) {
+ case AS_APP_STATE_INSTALLED:
+ case AS_APP_STATE_UPDATABLE:
+ case AS_APP_STATE_UPDATABLE_LIVE:
+ gtk_widget_set_visible (self->label_addons_uninstalled_app, FALSE);
+ break;
+ default:
+ gtk_widget_set_visible (self->label_addons_uninstalled_app, TRUE);
+ break;
+ }
+
+ /* hide fields that don't make sense for sources */
+ switch (gs_app_get_kind (self->app)) {
+ case AS_APP_KIND_SOURCE:
+ gtk_widget_set_visible (self->label_details_license_title, FALSE);
+ gtk_widget_set_visible (self->box_details_license_value, FALSE);
+ gtk_widget_set_visible (self->label_details_version_title, FALSE);
+ gtk_widget_set_visible (self->label_details_version_value, FALSE);
+ break;
+ default:
+ gtk_widget_set_visible (self->label_details_license_title, TRUE);
+ gtk_widget_set_visible (self->box_details_license_value, TRUE);
+ gtk_widget_set_visible (self->label_details_version_title, TRUE);
+ gtk_widget_set_visible (self->label_details_version_value, TRUE);
+ break;
+ }
+
+ gs_details_page_update_shortcut_button (self);
+
+ /* update progress */
+ gs_details_page_refresh_progress (self);
+
+ addons = gtk_container_get_children (GTK_CONTAINER (self->list_box_addons));
+ gtk_widget_set_visible (self->box_addons, addons != NULL);
+ g_list_free (addons);
+}
+
+static void
+list_header_func (GtkListBoxRow *row,
+ GtkListBoxRow *before,
+ gpointer user_data)
+{
+ GtkWidget *header = NULL;
+ if (before != NULL)
+ header = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
+ gtk_list_box_row_set_header (row, header);
+}
+
+static gint
+list_sort_func (GtkListBoxRow *a,
+ GtkListBoxRow *b,
+ gpointer user_data)
+{
+ GsApp *a1 = gs_app_addon_row_get_addon (GS_APP_ADDON_ROW (a));
+ GsApp *a2 = gs_app_addon_row_get_addon (GS_APP_ADDON_ROW (b));
+
+ return gs_utils_sort_strcmp (gs_app_get_name (a1),
+ gs_app_get_name (a2));
+}
+
+static void gs_details_page_addon_selected_cb (GsAppAddonRow *row, GParamSpec *pspec, GsDetailsPage *self);
+
+static void
+gs_details_page_refresh_addons (GsDetailsPage *self)
+{
+ GsAppList *addons;
+ guint i;
+
+ gs_container_remove_all (GTK_CONTAINER (self->list_box_addons));
+
+ addons = gs_app_get_addons (self->app);
+ for (i = 0; i < gs_app_list_length (addons); i++) {
+ GsApp *addon;
+ GtkWidget *row;
+
+ addon = gs_app_list_index (addons, i);
+ if (gs_app_get_state (addon) == AS_APP_STATE_UNKNOWN ||
+ gs_app_get_state (addon) == AS_APP_STATE_UNAVAILABLE)
+ continue;
+
+ row = gs_app_addon_row_new (addon);
+
+ gtk_container_add (GTK_CONTAINER (self->list_box_addons), row);
+ gtk_widget_show (row);
+
+ g_signal_connect (row, "notify::selected",
+ G_CALLBACK (gs_details_page_addon_selected_cb),
+ self);
+ }
+}
+
+static void gs_details_page_refresh_reviews (GsDetailsPage *self);
+
+typedef struct {
+ GsDetailsPage *self;
+ AsReview *review;
+ GsApp *app;
+ GsPluginAction action;
+} GsDetailsPageReviewHelper;
+
+static void
+gs_details_page_review_helper_free (GsDetailsPageReviewHelper *helper)
+{
+ g_object_unref (helper->self);
+ g_object_unref (helper->review);
+ g_object_unref (helper->app);
+ g_free (helper);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsDetailsPageReviewHelper, gs_details_page_review_helper_free);
+
+static void
+gs_details_page_app_set_review_cb (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
+ g_autoptr(GsDetailsPageReviewHelper) helper = (GsDetailsPageReviewHelper *) user_data;
+ g_autoptr(GError) error = NULL;
+
+ if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) {
+ g_warning ("failed to set review on %s: %s",
+ gs_app_get_id (helper->app), error->message);
+ return;
+ }
+ gs_details_page_refresh_reviews (helper->self);
+}
+
+static void
+gs_details_page_review_button_clicked_cb (GsReviewRow *row,
+ GsPluginAction action,
+ GsDetailsPage *self)
+{
+ GsDetailsPageReviewHelper *helper = g_new0 (GsDetailsPageReviewHelper, 1);
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+
+ helper->self = g_object_ref (self);
+ helper->app = g_object_ref (self->app);
+ helper->review = g_object_ref (gs_review_row_get_review (row));
+ helper->action = action;
+ plugin_job = gs_plugin_job_newv (helper->action,
+ "interactive", TRUE,
+ "app", helper->app,
+ "review", helper->review,
+ NULL);
+ gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
+ self->cancellable,
+ gs_details_page_app_set_review_cb,
+ helper);
+}
+
+static void
+gs_details_page_refresh_reviews (GsDetailsPage *self)
+{
+ GArray *review_ratings = NULL;
+ GPtrArray *reviews;
+ gboolean show_review_button = TRUE;
+ gboolean show_reviews = FALSE;
+ guint n_reviews = 0;
+ guint64 possible_actions = 0;
+ guint i;
+ struct {
+ GsPluginAction action;
+ const gchar *plugin_func;
+ } plugin_vfuncs[] = {
+ { GS_PLUGIN_ACTION_REVIEW_UPVOTE, "gs_plugin_review_upvote" },
+ { GS_PLUGIN_ACTION_REVIEW_DOWNVOTE, "gs_plugin_review_downvote" },
+ { GS_PLUGIN_ACTION_REVIEW_REPORT, "gs_plugin_review_report" },
+ { GS_PLUGIN_ACTION_REVIEW_SUBMIT, "gs_plugin_review_submit" },
+ { GS_PLUGIN_ACTION_REVIEW_REMOVE, "gs_plugin_review_remove" },
+ { GS_PLUGIN_ACTION_LAST, NULL }
+ };
+
+ /* nothing to show */
+ if (self->app == NULL)
+ return;
+
+ /* show or hide the entire reviews section */
+ switch (gs_app_get_kind (self->app)) {
+ case AS_APP_KIND_DESKTOP:
+ case AS_APP_KIND_FONT:
+ case AS_APP_KIND_INPUT_METHOD:
+ case AS_APP_KIND_WEB_APP:
+ case AS_APP_KIND_SHELL_EXTENSION:
+ /* don't show a missing rating on a local file */
+ if (gs_app_get_state (self->app) != AS_APP_STATE_AVAILABLE_LOCAL &&
+ self->enable_reviews)
+ show_reviews = TRUE;
+ break;
+ default:
+ break;
+ }
+
+ /* some apps are unreviewable */
+ if (gs_app_has_quirk (self->app, GS_APP_QUIRK_NOT_REVIEWABLE))
+ show_reviews = FALSE;
+
+ /* set the star rating */
+ if (show_reviews) {
+ gtk_widget_set_sensitive (self->star, gs_app_get_rating (self->app) >= 0);
+ gs_star_widget_set_rating (GS_STAR_WIDGET (self->star),
+ gs_app_get_rating (self->app));
+
+ review_ratings = gs_app_get_review_ratings (self->app);
+ if (review_ratings != NULL) {
+ gs_review_histogram_set_ratings (GS_REVIEW_HISTOGRAM (self->histogram),
+ review_ratings);
+ }
+ if (review_ratings != NULL) {
+ for (i = 0; i < review_ratings->len; i++)
+ n_reviews += (guint) g_array_index (review_ratings, guint32, i);
+ } else if (gs_app_get_reviews (self->app) != NULL) {
+ n_reviews = gs_app_get_reviews (self->app)->len;
+ }
+ }
+
+ /* enable appropriate widgets */
+ gtk_widget_set_visible (self->star, show_reviews);
+ gtk_widget_set_visible (self->box_reviews, show_reviews);
+ gtk_widget_set_visible (self->histogram, review_ratings != NULL && review_ratings->len > 0);
+ gtk_widget_set_visible (self->label_review_count, n_reviews > 0);
+
+ /* update the review label next to the star widget */
+ if (n_reviews > 0) {
+ g_autofree gchar *text = NULL;
+ gtk_widget_set_visible (self->label_review_count, TRUE);
+ text = g_strdup_printf ("(%u)", n_reviews);
+ gtk_label_set_text (GTK_LABEL (self->label_review_count), text);
+ }
+
+ /* no point continuing */
+ if (!show_reviews)
+ return;
+
+ /* find what the plugins support */
+ for (i = 0; plugin_vfuncs[i].action != GS_PLUGIN_ACTION_LAST; i++) {
+ if (gs_plugin_loader_get_plugin_supported (self->plugin_loader,
+ plugin_vfuncs[i].plugin_func)) {
+ possible_actions |= 1u << plugin_vfuncs[i].action;
+ }
+ }
+
+ /* add all the reviews */
+ gs_container_remove_all (GTK_CONTAINER (self->list_box_reviews));
+ reviews = gs_app_get_reviews (self->app);
+ for (i = 0; i < reviews->len; i++) {
+ AsReview *review = g_ptr_array_index (reviews, i);
+ GtkWidget *row = gs_review_row_new (review);
+ guint64 actions;
+
+ g_signal_connect (row, "button-clicked",
+ G_CALLBACK (gs_details_page_review_button_clicked_cb), self);
+ if (as_review_get_flags (review) & AS_REVIEW_FLAG_SELF) {
+ actions = possible_actions & 1 << GS_PLUGIN_ACTION_REVIEW_REMOVE;
+ show_review_button = FALSE;
+ } else {
+ actions = possible_actions & ~(1u << GS_PLUGIN_ACTION_REVIEW_REMOVE);
+ }
+ gs_review_row_set_actions (GS_REVIEW_ROW (row), actions);
+ gtk_container_add (GTK_CONTAINER (self->list_box_reviews), row);
+ gtk_widget_set_visible (row, self->show_all_reviews ||
+ i < SHOW_NR_REVIEWS_INITIAL);
+ gs_review_row_set_network_available (GS_REVIEW_ROW (row),
+ gs_plugin_loader_get_network_available (self->plugin_loader));
+ }
+
+ /* only show the button if there are more to show */
+ gtk_widget_set_visible (self->button_more_reviews,
+ !self->show_all_reviews &&
+ reviews->len > SHOW_NR_REVIEWS_INITIAL);
+
+ /* show the button only if the user never reviewed */
+ gtk_widget_set_visible (self->button_review, show_review_button);
+ if (gs_plugin_loader_get_network_available (self->plugin_loader)) {
+ gtk_widget_set_sensitive (self->button_review, TRUE);
+ gtk_widget_set_sensitive (self->star_eventbox, TRUE);
+ gtk_widget_set_tooltip_text (self->button_review, NULL);
+ } else {
+ gtk_widget_set_sensitive (self->button_review, FALSE);
+ gtk_widget_set_sensitive (self->star_eventbox, FALSE);
+ gtk_widget_set_tooltip_text (self->button_review,
+ /* TRANSLATORS: we need a remote server to process */
+ _("You need internet access to write a review"));
+ }
+}
+
+static void
+gs_details_page_app_refine_cb (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
+ GsDetailsPage *self = GS_DETAILS_PAGE (user_data);
+ g_autoptr(GError) error = NULL;
+ if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) {
+ g_warning ("failed to refine %s: %s",
+ gs_app_get_id (self->app),
+ error->message);
+ return;
+ }
+ gs_details_page_refresh_size (self);
+ gs_details_page_refresh_reviews (self);
+}
+
+static void
+gs_details_page_content_rating_set_css (GsDetailsPage *page, guint age)
+{
+ g_autoptr(GString) css = g_string_new (NULL);
+ const gchar *color_bg = NULL;
+ const gchar *color_fg = "#ffffff";
+ if (age >= 18) {
+ color_bg = "#ee2222";
+ } else if (age >= 15) {
+ color_bg = "#f1c000";
+ } else if (age >= 12) {
+ color_bg = "#2a97c9";
+ } else if (age >= 5) {
+ color_bg = "#3f756c";
+ } else {
+ color_bg = "#009d66";
+ }
+ g_string_append_printf (css, "color: %s;\n", color_fg);
+ g_string_append_printf (css, "background-color: %s;\n", color_bg);
+
+ gs_utils_widget_set_css (page->button_details_rating_value,
+ (GtkCssProvider **) &page->button_details_rating_style_provider,
+ "content-rating-custom", css->str);
+}
+
+static void
+gs_details_page_refresh_content_rating (GsDetailsPage *self)
+{
+ AsContentRating *content_rating;
+ GsContentRatingSystem system;
+ guint age = 0;
+ g_autofree gchar *display = NULL;
+ const gchar *locale;
+
+ /* get the content rating system from the locale */
+ locale = setlocale (LC_MESSAGES, NULL);
+ system = gs_utils_content_rating_system_from_locale (locale);
+ g_debug ("content rating system is guessed as %s from %s",
+ gs_content_rating_system_to_str (system),
+ locale);
+
+ /* only show the button if a game and has a content rating */
+ content_rating = gs_app_get_content_rating (self->app);
+ if (content_rating != NULL) {
+ age = as_content_rating_get_minimum_age (content_rating);
+ display = gs_utils_content_rating_age_to_str (system, age);
+ }
+ if (display != NULL) {
+ gtk_button_set_label (GTK_BUTTON (self->button_details_rating_value), display);
+ gtk_widget_set_visible (self->button_details_rating_value, TRUE);
+ gtk_widget_set_visible (self->label_details_rating_title, TRUE);
+ gs_details_page_content_rating_set_css (self, age);
+ } else {
+ gtk_widget_set_visible (self->button_details_rating_value, FALSE);
+ gtk_widget_set_visible (self->label_details_rating_title, FALSE);
+ }
+}
+
+static void
+_set_app (GsDetailsPage *self, GsApp *app)
+{
+ /* do not show all the reviews by default */
+ self->show_all_reviews = FALSE;
+
+ /* disconnect the old handlers */
+ if (self->app != NULL) {
+ g_signal_handlers_disconnect_by_func (self->app, gs_details_page_notify_state_changed_cb, self);
+ g_signal_handlers_disconnect_by_func (self->app, gs_details_page_progress_changed_cb, self);
+ g_signal_handlers_disconnect_by_func (self->app, gs_details_page_allow_cancel_changed_cb,
+ self);
+ }
+
+ /* save app */
+ g_set_object (&self->app, app);
+ if (self->app == NULL) {
+ /* switch away from the details view that failed to load */
+ gs_shell_set_mode (self->shell, GS_SHELL_MODE_OVERVIEW);
+ return;
+ }
+ g_set_object (&self->app_cancellable, gs_app_get_cancellable (app));
+ g_signal_connect_object (self->app, "notify::state",
+ G_CALLBACK (gs_details_page_notify_state_changed_cb),
+ self, 0);
+ g_signal_connect_object (self->app, "notify::size",
+ G_CALLBACK (gs_details_page_notify_state_changed_cb),
+ self, 0);
+ g_signal_connect_object (self->app, "notify::license",
+ G_CALLBACK (gs_details_page_notify_state_changed_cb),
+ self, 0);
+ g_signal_connect_object (self->app, "notify::quirk",
+ G_CALLBACK (gs_details_page_notify_state_changed_cb),
+ self, 0);
+ g_signal_connect_object (self->app, "notify::progress",
+ G_CALLBACK (gs_details_page_progress_changed_cb),
+ self, 0);
+ g_signal_connect_object (self->app, "notify::allow-cancel",
+ G_CALLBACK (gs_details_page_allow_cancel_changed_cb),
+ self, 0);
+ g_signal_connect_object (self->app, "notify::pending-action",
+ G_CALLBACK (gs_details_page_notify_state_changed_cb),
+ self, 0);
+}
+
+/* show the UI and do operations that should not block page load */
+static void
+gs_details_page_load_stage2 (GsDetailsPage *self)
+{
+ g_autofree gchar *tmp = NULL;
+ g_autoptr(GsPluginJob) plugin_job1 = NULL;
+ g_autoptr(GsPluginJob) plugin_job2 = NULL;
+
+ /* print what we've got */
+ tmp = gs_app_to_string (self->app);
+ g_debug ("%s", tmp);
+
+ /* update UI */
+ gs_details_page_set_state (self, GS_DETAILS_PAGE_STATE_READY);
+ gs_details_page_refresh_screenshots (self);
+ gs_details_page_refresh_addons (self);
+ gs_details_page_refresh_reviews (self);
+ gs_details_page_refresh_all (self);
+ gs_details_page_refresh_content_rating (self);
+
+ /* if these tasks fail (e.g. because we have no networking) then it's
+ * of no huge importance if we don't get the required data */
+ plugin_job1 = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE,
+ "app", self->app,
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE,
+ NULL);
+ plugin_job2 = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_ALTERNATES,
+ "interactive", TRUE,
+ "app", self->app,
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE,
+ "dedupe-flags", GS_APP_LIST_FILTER_FLAG_NONE,
+ NULL);
+ gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job1,
+ self->cancellable,
+ gs_details_page_app_refine_cb,
+ self);
+ gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job2,
+ self->cancellable,
+ gs_details_page_get_alternates_cb,
+ self);
+}
+
+static void
+gs_details_page_load_stage1_cb (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
+ GsDetailsPage *self = GS_DETAILS_PAGE (user_data);
+ g_autoptr(GError) error = NULL;
+
+ if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) {
+ g_warning ("failed to refine %s: %s",
+ gs_app_get_id (self->app),
+ error->message);
+ }
+ if (gs_app_get_kind (self->app) == AS_APP_KIND_UNKNOWN ||
+ gs_app_get_state (self->app) == AS_APP_STATE_UNKNOWN) {
+ g_autofree gchar *str = NULL;
+ const gchar *id = gs_app_get_id (self->app);
+ str = g_strdup_printf (_("Unable to find “%s”"), id == NULL ? gs_app_get_source_default (self->app) : id);
+ gtk_label_set_text (GTK_LABEL (self->label_failed), str);
+ gs_details_page_set_state (self, GS_DETAILS_PAGE_STATE_FAILED);
+ return;
+ }
+
+ /* Hide the app if it’s not suitable for the user, but only if it’s not
+ * already installed — a parent could have decided that a particular
+ * app *is* actually suitable for their child, despite its age rating.
+ *
+ * Make it look like the app doesn’t exist, to not tantalise the
+ * child. */
+ if (!gs_app_is_installed (self->app) &&
+ gs_app_has_quirk (self->app, GS_APP_QUIRK_PARENTAL_FILTER)) {
+ g_autofree gchar *str = NULL;
+ const gchar *id = gs_app_get_id (self->app);
+ str = g_strdup_printf (_("Unable to find “%s”"), id == NULL ? gs_app_get_source_default (self->app) : id);
+ gtk_label_set_text (GTK_LABEL (self->label_failed), str);
+ gs_details_page_set_state (self, GS_DETAILS_PAGE_STATE_FAILED);
+ return;
+ }
+
+ /* do 2nd stage refine */
+ gs_details_page_load_stage2 (self);
+}
+
+static void
+gs_details_page_file_to_app_cb (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
+ GsDetailsPage *self = GS_DETAILS_PAGE (user_data);
+ g_autoptr(GsAppList) list = NULL;
+ g_autoptr(GError) error = NULL;
+
+ list = gs_plugin_loader_job_process_finish (plugin_loader,
+ res,
+ &error);
+ if (list == NULL) {
+ g_warning ("failed to convert file to GsApp: %s", error->message);
+ /* go back to the overview */
+ gs_shell_set_mode (self->shell, GS_SHELL_MODE_OVERVIEW);
+ } else {
+ GsApp *app = gs_app_list_index (list, 0);
+ g_set_object (&self->app_local_file, app);
+ _set_app (self, app);
+ gs_details_page_load_stage2 (self);
+ }
+}
+
+static void
+gs_details_page_url_to_app_cb (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
+ GsDetailsPage *self = GS_DETAILS_PAGE (user_data);
+ g_autoptr(GsAppList) list = NULL;
+ g_autoptr(GError) error = NULL;
+
+ list = gs_plugin_loader_job_process_finish (plugin_loader,
+ res,
+ &error);
+ if (list == NULL) {
+ g_warning ("failed to convert URL to GsApp: %s", error->message);
+ /* go back to the overview */
+ gs_shell_set_mode (self->shell, GS_SHELL_MODE_OVERVIEW);
+ } else {
+ GsApp *app = gs_app_list_index (list, 0);
+ _set_app (self, app);
+ gs_details_page_load_stage2 (self);
+ }
+}
+
+void
+gs_details_page_set_local_file (GsDetailsPage *self, GFile *file)
+{
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ gs_details_page_set_state (self, GS_DETAILS_PAGE_STATE_LOADING);
+ g_clear_object (&self->app_local_file);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP,
+ "file", file,
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_CONTENT_RATING |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS,
+ NULL);
+ gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
+ self->cancellable,
+ gs_details_page_file_to_app_cb,
+ self);
+}
+
+void
+gs_details_page_set_url (GsDetailsPage *self, const gchar *url)
+{
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ gs_details_page_set_state (self, GS_DETAILS_PAGE_STATE_LOADING);
+ g_clear_object (&self->app_local_file);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_URL_TO_APP,
+ "search", url,
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_CONTENT_RATING |
+ GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES,
+ NULL);
+ gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
+ self->cancellable,
+ gs_details_page_url_to_app_cb,
+ self);
+}
+
+/* refines a GsApp */
+static void
+gs_details_page_load_stage1 (GsDetailsPage *self)
+{
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+
+ /* update UI */
+ gs_page_switch_to (GS_PAGE (self), TRUE);
+ gs_details_page_set_state (self, GS_DETAILS_PAGE_STATE_LOADING);
+
+ /* get extra details about the app */
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE,
+ "app", self->app,
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_CONTENT_RATING |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS,
+ NULL);
+ gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
+ self->cancellable,
+ gs_details_page_load_stage1_cb,
+ self);
+
+ /* update UI with loading page */
+ gs_details_page_refresh_all (self);
+}
+
+static void
+gs_details_page_reload (GsPage *page)
+{
+ GsDetailsPage *self = GS_DETAILS_PAGE (page);
+ if (self->app != NULL)
+ gs_details_page_load_stage1 (self);
+}
+
+static gint
+origin_popover_list_sort_func (GtkListBoxRow *a,
+ GtkListBoxRow *b,
+ gpointer user_data)
+{
+ GsApp *a1 = gs_origin_popover_row_get_app (GS_ORIGIN_POPOVER_ROW (a));
+ GsApp *a2 = gs_origin_popover_row_get_app (GS_ORIGIN_POPOVER_ROW (b));
+ g_autofree gchar *a1_origin = gs_app_get_origin_ui (a1);
+ g_autofree gchar *a2_origin = gs_app_get_origin_ui (a2);
+
+ return gs_utils_sort_strcmp (a1_origin, a2_origin);
+}
+
+static void
+origin_popover_row_activated_cb (GtkListBox *list_box,
+ GtkListBoxRow *row,
+ gpointer user_data)
+{
+ GsDetailsPage *self = GS_DETAILS_PAGE (user_data);
+ GsApp *app;
+ GtkWidget *popover;
+
+ popover = GTK_WIDGET (gtk_builder_get_object (self->builder, "origin_popover"));
+ gtk_popover_popdown (GTK_POPOVER (popover));
+
+ app = gs_origin_popover_row_get_app (GS_ORIGIN_POPOVER_ROW (row));
+ if (app != self->app) {
+ _set_app (self, app);
+ gs_details_page_load_stage1 (self);
+ }
+}
+
+static void
+settings_changed_cb (GsDetailsPage *self, const gchar *key, gpointer data)
+{
+ if (self->app == NULL)
+ return;
+ if (g_strcmp0 (key, "show-nonfree-ui") == 0) {
+ gs_details_page_refresh_all (self);
+ }
+}
+
+/* this is being called from GsShell */
+void
+gs_details_page_set_app (GsDetailsPage *self, GsApp *app)
+{
+ g_return_if_fail (GS_IS_DETAILS_PAGE (self));
+ g_return_if_fail (GS_IS_APP (app));
+
+ /* clear old state */
+ g_clear_object (&self->app_local_file);
+
+ /* save GsApp */
+ _set_app (self, app);
+ gs_details_page_load_stage1 (self);
+}
+
+GsApp *
+gs_details_page_get_app (GsDetailsPage *self)
+{
+ return self->app;
+}
+
+static void
+gs_details_page_remove_app (GsDetailsPage *self)
+{
+ g_set_object (&self->app_cancellable, gs_app_get_cancellable (self->app));
+ gs_page_remove_app (GS_PAGE (self), self->app, self->app_cancellable);
+}
+
+static void
+gs_details_page_app_remove_button_cb (GtkWidget *widget, GsDetailsPage *self)
+{
+ gs_details_page_remove_app (self);
+}
+
+static void
+gs_details_page_app_cancel_button_cb (GtkWidget *widget, GsDetailsPage *self)
+{
+ g_cancellable_cancel (self->app_cancellable);
+ gtk_widget_set_sensitive (widget, FALSE);
+
+ /* reset the pending-action from the app if needed */
+ gs_app_set_pending_action (self->app, GS_PLUGIN_ACTION_UNKNOWN);
+
+ /* FIXME: We should be able to revert the QUEUED_FOR_INSTALL without
+ * having to pretend to remove the app */
+ if (gs_app_get_state (self->app) == AS_APP_STATE_QUEUED_FOR_INSTALL)
+ gs_details_page_remove_app (self);
+}
+
+static void
+gs_details_page_app_install_button_cb (GtkWidget *widget, GsDetailsPage *self)
+{
+ g_autoptr(GList) addons = NULL;
+
+ /* Mark ticked addons to be installed together with the app */
+ addons = gtk_container_get_children (GTK_CONTAINER (self->list_box_addons));
+ for (GList *l = addons; l; l = l->next) {
+ if (gs_app_addon_row_get_selected (l->data)) {
+ GsApp *addon = gs_app_addon_row_get_addon (l->data);
+
+ if (gs_app_get_state (addon) == AS_APP_STATE_AVAILABLE)
+ gs_app_set_to_be_installed (addon, TRUE);
+ }
+ }
+
+ g_set_object (&self->app_cancellable, gs_app_get_cancellable (self->app));
+
+ if (gs_app_get_state (self->app) == AS_APP_STATE_UPDATABLE_LIVE) {
+ gs_page_update_app (GS_PAGE (self), self->app, self->app_cancellable);
+ return;
+ }
+
+ gs_page_install_app (GS_PAGE (self), self->app, GS_SHELL_INTERACTION_FULL,
+ self->app_cancellable);
+}
+
+static void
+gs_details_page_app_update_button_cb (GtkWidget *widget, GsDetailsPage *self)
+{
+ g_set_object (&self->app_cancellable, gs_app_get_cancellable (self->app));
+ gs_page_update_app (GS_PAGE (self), self->app, self->app_cancellable);
+}
+
+static void
+gs_details_page_addon_selected_cb (GsAppAddonRow *row,
+ GParamSpec *pspec,
+ GsDetailsPage *self)
+{
+ GsApp *addon;
+
+ addon = gs_app_addon_row_get_addon (row);
+
+ /* If the main app is already installed, ticking the addon checkbox
+ * triggers an immediate install. Otherwise we'll install the addon
+ * together with the main app. */
+ switch (gs_app_get_state (self->app)) {
+ case AS_APP_STATE_INSTALLED:
+ case AS_APP_STATE_UPDATABLE:
+ case AS_APP_STATE_UPDATABLE_LIVE:
+ if (gs_app_addon_row_get_selected (row)) {
+ g_set_object (&self->app_cancellable, gs_app_get_cancellable (addon));
+ gs_page_install_app (GS_PAGE (self), addon, GS_SHELL_INTERACTION_FULL,
+ self->app_cancellable);
+ } else {
+ g_set_object (&self->app_cancellable, gs_app_get_cancellable (addon));
+ gs_page_remove_app (GS_PAGE (self), addon, self->app_cancellable);
+ /* make sure the addon checkboxes are synced if the
+ * user clicks cancel in the remove confirmation dialog */
+ gs_details_page_refresh_addons (self);
+ gs_details_page_refresh_all (self);
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+static void
+gs_details_page_app_launch_button_cb (GtkWidget *widget, GsDetailsPage *self)
+{
+ g_autoptr(GCancellable) cancellable = g_cancellable_new ();
+
+ /* hide the notification */
+ g_application_withdraw_notification (g_application_get_default (),
+ "installed");
+
+ g_set_object (&self->cancellable, cancellable);
+ gs_page_launch_app (GS_PAGE (self), self->app, self->cancellable);
+}
+
+static void
+gs_details_page_app_add_shortcut_button_cb (GtkWidget *widget,
+ GsDetailsPage *self)
+{
+ g_autoptr(GCancellable) cancellable = g_cancellable_new ();
+ g_set_object (&self->cancellable, cancellable);
+ gs_page_shortcut_add (GS_PAGE (self), self->app, self->cancellable);
+}
+
+static void
+gs_details_page_app_remove_shortcut_button_cb (GtkWidget *widget,
+ GsDetailsPage *self)
+{
+ g_autoptr(GCancellable) cancellable = g_cancellable_new ();
+ g_set_object (&self->cancellable, cancellable);
+ gs_page_shortcut_remove (GS_PAGE (self), self->app, self->cancellable);
+}
+
+static void
+gs_details_page_review_response_cb (GtkDialog *dialog,
+ gint response,
+ GsDetailsPage *self)
+{
+ g_autofree gchar *text = NULL;
+ g_autoptr(GDateTime) now = NULL;
+ g_autoptr(AsReview) review = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ GsDetailsPageReviewHelper *helper;
+ GsReviewDialog *rdialog = GS_REVIEW_DIALOG (dialog);
+
+ /* not agreed */
+ if (response != GTK_RESPONSE_OK) {
+ gtk_widget_destroy (GTK_WIDGET (dialog));
+ return;
+ }
+
+ review = as_review_new ();
+ as_review_set_summary (review, gs_review_dialog_get_summary (rdialog));
+ text = gs_review_dialog_get_text (rdialog);
+ as_review_set_description (review, text);
+ as_review_set_rating (review, gs_review_dialog_get_rating (rdialog));
+ as_review_set_version (review, gs_app_get_version (self->app));
+ now = g_date_time_new_now_local ();
+ as_review_set_date (review, now);
+
+ /* call into the plugins to set the new value */
+ helper = g_new0 (GsDetailsPageReviewHelper, 1);
+ helper->self = g_object_ref (self);
+ helper->app = g_object_ref (self->app);
+ helper->review = g_object_ref (review);
+ helper->action = GS_PLUGIN_ACTION_REVIEW_SUBMIT;
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REVIEW_SUBMIT,
+ "interactive", TRUE,
+ "app", helper->app,
+ "review", helper->review,
+ NULL);
+ gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
+ self->cancellable,
+ gs_details_page_app_set_review_cb,
+ helper);
+
+ /* unmap the dialog */
+ gtk_widget_destroy (GTK_WIDGET (dialog));
+}
+
+static void
+gs_details_page_write_review_cb (GtkButton *button,
+ GsDetailsPage *self)
+{
+ GtkWidget *dialog;
+ dialog = gs_review_dialog_new ();
+ g_signal_connect (dialog, "response",
+ G_CALLBACK (gs_details_page_review_response_cb), self);
+ gs_shell_modal_dialog_present (self->shell, GTK_DIALOG (dialog));
+}
+
+static void
+gs_details_page_app_installed (GsPage *page, GsApp *app)
+{
+ gs_details_page_reload (page);
+}
+
+static void
+gs_details_page_app_removed (GsPage *page, GsApp *app)
+{
+ gs_details_page_reload (page);
+}
+
+static void
+show_all_cb (GtkWidget *widget, gpointer user_data)
+{
+ gtk_widget_show (widget);
+}
+
+static void
+gs_details_page_more_reviews_button_cb (GtkWidget *widget, GsDetailsPage *self)
+{
+ self->show_all_reviews = TRUE;
+ gtk_container_foreach (GTK_CONTAINER (self->list_box_reviews),
+ show_all_cb, NULL);
+ gtk_widget_set_visible (self->button_more_reviews, FALSE);
+}
+
+static void
+gs_details_page_content_rating_button_cb (GtkWidget *widget, GsDetailsPage *self)
+{
+ AsContentRating *cr;
+ AsContentRatingValue value_bad = AS_CONTENT_RATING_VALUE_NONE;
+ const gchar *tmp;
+ g_autofree const gchar **ids = NULL;
+ g_autoptr(GString) str = g_string_new (NULL);
+
+ /* Ordered from worst to best */
+ const gchar *violence_group[] = {
+ "violence-bloodshed",
+ "violence-realistic",
+ "violence-fantasy",
+ "violence-cartoon",
+ NULL
+ };
+ const gchar *social_group[] = {
+ "social-audio",
+ "social-chat",
+ "social-contacts",
+ "social-info",
+ NULL
+ };
+
+ cr = gs_app_get_content_rating (self->app);
+ if (cr == NULL)
+ return;
+
+ ids = gs_content_rating_get_all_rating_ids ();
+
+ /* get the worst thing */
+ for (gsize i = 0; ids[i] != NULL; i++) {
+ AsContentRatingValue value;
+ value = as_content_rating_get_value (cr, ids[i]);
+ if (value > value_bad)
+ value_bad = value;
+ }
+
+ /* get the content rating description for the worst things about the app;
+ * handle the groups separately*/
+ for (gsize i = 0; ids[i] != NULL; i++) {
+ if (!g_strv_contains (violence_group, ids[i]) &&
+ !g_strv_contains (social_group, ids[i])) {
+ AsContentRatingValue value;
+ value = as_content_rating_get_value (cr, ids[i]);
+ if (value < value_bad)
+ continue;
+ tmp = gs_content_rating_key_value_to_str (ids[i], value);
+ g_string_append_printf (str, "• %s\n", tmp);
+ }
+ }
+
+ for (gsize i = 0; violence_group[i] != NULL; i++) {
+ AsContentRatingValue value;
+ value = as_content_rating_get_value (cr, violence_group[i]);
+ if (value < value_bad)
+ continue;
+ tmp = gs_content_rating_key_value_to_str (violence_group[i], value);
+ g_string_append_printf (str, "• %s\n", tmp);
+ break;
+ }
+
+ for (gsize i = 0; social_group[i] != NULL; i++) {
+ AsContentRatingValue value;
+ value = as_content_rating_get_value (cr, social_group[i]);
+ if (value < value_bad)
+ continue;
+ tmp = gs_content_rating_key_value_to_str (social_group[i], value);
+ g_string_append_printf (str, "• %s\n", tmp);
+ break;
+ }
+
+ if (str->len > 0)
+ g_string_truncate (str, str->len - 1);
+
+ /* enable the details if there are any */
+ gtk_label_set_label (GTK_LABEL (self->label_content_rating_message), str->str);
+ gtk_widget_set_visible (self->label_content_rating_title, str->len > 0);
+ gtk_widget_set_visible (self->label_content_rating_message, str->len > 0);
+ gtk_widget_set_visible (self->label_content_rating_none, str->len == 0);
+
+ /* show popover */
+ gtk_popover_set_relative_to (GTK_POPOVER (self->popover_content_rating), widget);
+ gtk_widget_show (self->popover_content_rating);
+}
+
+static void
+gs_details_page_permissions_button_cb (GtkWidget *widget, GsDetailsPage *self)
+{
+ gtk_widget_show (self->popover_permissions);
+}
+
+static gboolean
+gs_details_page_activate_link_cb (GtkLabel *label,
+ const gchar *uri,
+ GsDetailsPage *self)
+{
+ gs_shell_show_uri (self->shell, uri);
+ return TRUE;
+}
+
+static GtkWidget *
+gs_details_page_label_widget (GsDetailsPage *self,
+ const gchar *title,
+ const gchar *url)
+{
+ GtkWidget *w;
+ g_autofree gchar *markup = NULL;
+
+ markup = g_strdup_printf ("<a href=\"%s\">%s</a>", url, title);
+ w = gtk_label_new (markup);
+ g_signal_connect (w, "activate-link",
+ G_CALLBACK (gs_details_page_activate_link_cb),
+ self);
+ gtk_label_set_use_markup (GTK_LABEL (w), TRUE);
+ gtk_label_set_xalign (GTK_LABEL (w), 0.f);
+ gtk_widget_set_visible (w, TRUE);
+ return w;
+}
+
+static GtkWidget *
+gs_details_page_license_widget_for_token (GsDetailsPage *self, const gchar *token)
+{
+ /* public domain */
+ if (g_strcmp0 (token, "@LicenseRef-public-domain") == 0) {
+ /* TRANSLATORS: see the wikipedia page */
+ return gs_details_page_label_widget (self, _("Public domain"),
+ /* TRANSLATORS: Replace the link with a version in your language,
+ * e.g. https://de.wikipedia.org/wiki/Gemeinfreiheit */
+ _("https://en.wikipedia.org/wiki/Public_domain"));
+ }
+
+ /* free software, license unspecified */
+ if (g_str_has_prefix (token, "@LicenseRef-free")) {
+ /* TRANSLATORS: Replace the link with a version in your language,
+ * e.g. https://www.gnu.org/philosophy/free-sw.de */
+ const gchar *url = _("https://www.gnu.org/philosophy/free-sw");
+ gchar *tmp;
+
+ /* we support putting a custom URL in the
+ * token string, e.g. @LicenseRef-free=http://ubuntu.com */
+ tmp = g_strstr_len (token, -1, "=");
+ if (tmp != NULL)
+ url = tmp + 1;
+
+ /* TRANSLATORS: see GNU page */
+ return gs_details_page_label_widget (self, _("Free Software"), url);
+ }
+
+ /* SPDX value */
+ if (g_str_has_prefix (token, "@")) {
+ g_autofree gchar *uri = NULL;
+ uri = g_strdup_printf ("http://spdx.org/licenses/%s",
+ token + 1);
+ return gs_details_page_label_widget (self, token + 1, uri);
+ }
+
+ /* new SPDX value the extractor didn't know about */
+ if (as_utils_is_spdx_license_id (token)) {
+ g_autofree gchar *uri = NULL;
+ uri = g_strdup_printf ("http://spdx.org/licenses/%s",
+ token);
+ return gs_details_page_label_widget (self, token, uri);
+ }
+
+ /* nothing to show */
+ return NULL;
+}
+
+static void
+gs_details_page_license_free_cb (GtkWidget *widget, GsDetailsPage *self)
+{
+ guint cnt = 0;
+ guint i;
+ g_auto(GStrv) tokens = NULL;
+
+ /* URLify any SPDX IDs */
+ gs_container_remove_all (GTK_CONTAINER (self->box_details_license_list));
+ tokens = as_utils_spdx_license_tokenize (gs_app_get_license (self->app));
+ for (i = 0; tokens[i] != NULL; i++) {
+ GtkWidget *w = NULL;
+
+ /* translated join */
+ if (g_strcmp0 (tokens[i], "&") == 0)
+ continue;
+ if (g_strcmp0 (tokens[i], "|") == 0)
+ continue;
+ if (g_strcmp0 (tokens[i], "+") == 0)
+ continue;
+
+ /* add widget */
+ w = gs_details_page_license_widget_for_token (self, tokens[i]);
+ if (w == NULL)
+ continue;
+ gtk_container_add (GTK_CONTAINER (self->box_details_license_list), w);
+
+ /* one more license */
+ cnt++;
+ }
+
+ /* use the correct plural */
+ gtk_label_set_label (GTK_LABEL (self->label_licenses_intro),
+ /* TRANSLATORS: for the free software popover */
+ ngettext ("Users are bound by the following license:",
+ "Users are bound by the following licenses:",
+ cnt));
+ gtk_widget_set_visible (self->label_licenses_intro, cnt > 0);
+
+ gtk_widget_show (self->popover_license_free);
+}
+
+static void
+gs_details_page_license_nonfree_cb (GtkWidget *widget, GsDetailsPage *self)
+{
+ g_autofree gchar *str = NULL;
+ g_autofree gchar *uri = NULL;
+ g_auto(GStrv) tokens = NULL;
+
+ /* license specified as a link */
+ tokens = as_utils_spdx_license_tokenize (gs_app_get_license (self->app));
+ for (guint i = 0; tokens[i] != NULL; i++) {
+ if (g_str_has_prefix (tokens[i], "@LicenseRef-proprietary=")) {
+ uri = g_strdup (tokens[i] + 24);
+ break;
+ }
+ }
+ if (uri == NULL)
+ uri = g_settings_get_string (self->settings, "nonfree-software-uri");
+ str = g_strdup_printf ("<a href=\"%s\">%s</a>",
+ uri,
+ _("More information"));
+ gtk_label_set_label (GTK_LABEL (self->label_license_nonfree_details), str);
+ gtk_widget_show (self->popover_license_nonfree);
+}
+
+static void
+gs_details_page_license_unknown_cb (GtkWidget *widget, GsDetailsPage *self)
+{
+ gtk_widget_show (self->popover_license_unknown);
+}
+
+static void
+gs_details_page_network_available_notify_cb (GsPluginLoader *plugin_loader,
+ GParamSpec *pspec,
+ GsDetailsPage *self)
+{
+ gs_details_page_refresh_reviews (self);
+}
+
+static void
+gs_details_page_star_pressed_cb(GtkWidget *widget, GdkEventButton *event, GsDetailsPage *self)
+{
+ gs_details_page_write_review_cb(GTK_BUTTON (self->button_review), self);
+}
+
+static gboolean
+gs_details_page_setup (GsPage *page,
+ GsShell *shell,
+ GsPluginLoader *plugin_loader,
+ GtkBuilder *builder,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsDetailsPage *self = GS_DETAILS_PAGE (page);
+ GtkAdjustment *adj;
+ GtkWidget *origin_popover_list_box;
+
+ g_return_val_if_fail (GS_IS_DETAILS_PAGE (self), TRUE);
+
+ self->shell = shell;
+
+ self->plugin_loader = g_object_ref (plugin_loader);
+ self->builder = g_object_ref (builder);
+ self->cancellable = g_object_ref (cancellable);
+
+ /* show review widgets if we have plugins that provide them */
+ self->enable_reviews =
+ gs_plugin_loader_get_plugin_supported (plugin_loader,
+ "gs_plugin_review_submit");
+ g_signal_connect (self->button_review, "clicked",
+ G_CALLBACK (gs_details_page_write_review_cb),
+ self);
+ g_signal_connect (self->star_eventbox, "button-press-event",
+ G_CALLBACK (gs_details_page_star_pressed_cb),
+ self);
+
+ /* hide some UI when offline */
+ g_signal_connect_object (self->plugin_loader, "notify::network-available",
+ G_CALLBACK (gs_details_page_network_available_notify_cb),
+ self, 0);
+
+ /* setup details */
+ g_signal_connect (self->button_install, "clicked",
+ G_CALLBACK (gs_details_page_app_install_button_cb),
+ self);
+ g_signal_connect (self->button_update, "clicked",
+ G_CALLBACK (gs_details_page_app_update_button_cb),
+ self);
+ g_signal_connect (self->button_remove, "clicked",
+ G_CALLBACK (gs_details_page_app_remove_button_cb),
+ self);
+ g_signal_connect (self->button_cancel, "clicked",
+ G_CALLBACK (gs_details_page_app_cancel_button_cb),
+ self);
+ g_signal_connect (self->button_more_reviews, "clicked",
+ G_CALLBACK (gs_details_page_more_reviews_button_cb),
+ self);
+ g_signal_connect (self->button_details_rating_value, "clicked",
+ G_CALLBACK (gs_details_page_content_rating_button_cb),
+ self);
+ g_signal_connect (self->button_details_permissions_value, "clicked",
+ G_CALLBACK (gs_details_page_permissions_button_cb),
+ self);
+ g_signal_connect (self->label_details_updated_value, "activate-link",
+ G_CALLBACK (gs_details_page_history_cb),
+ self);
+ g_signal_connect (self->button_details_launch, "clicked",
+ G_CALLBACK (gs_details_page_app_launch_button_cb),
+ self);
+ g_signal_connect (self->button_details_add_shortcut, "clicked",
+ G_CALLBACK (gs_details_page_app_add_shortcut_button_cb),
+ self);
+ g_signal_connect (self->button_details_remove_shortcut, "clicked",
+ G_CALLBACK (gs_details_page_app_remove_shortcut_button_cb),
+ self);
+ g_signal_connect (self->button_details_website, "clicked",
+ G_CALLBACK (gs_details_page_website_cb),
+ self);
+ g_signal_connect (self->button_donate, "clicked",
+ G_CALLBACK (gs_details_page_donate_cb),
+ self);
+ g_signal_connect (self->button_details_license_free, "clicked",
+ G_CALLBACK (gs_details_page_license_free_cb),
+ self);
+ g_signal_connect (self->button_details_license_nonfree, "clicked",
+ G_CALLBACK (gs_details_page_license_nonfree_cb),
+ self);
+ g_signal_connect (self->button_details_license_unknown, "clicked",
+ G_CALLBACK (gs_details_page_license_unknown_cb),
+ self);
+ g_signal_connect (self->label_license_nonfree_details, "activate-link",
+ G_CALLBACK (gs_details_page_activate_link_cb),
+ self);
+ origin_popover_list_box = GTK_WIDGET (gtk_builder_get_object (self->builder, "origin_popover_list_box"));
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (origin_popover_list_box),
+ origin_popover_list_sort_func,
+ NULL, NULL);
+ gtk_list_box_set_header_func (GTK_LIST_BOX (origin_popover_list_box),
+ list_header_func,
+ NULL, NULL);
+ g_signal_connect (origin_popover_list_box, "row-activated",
+ G_CALLBACK (origin_popover_row_activated_cb),
+ self);
+
+ adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolledwindow_details));
+ gtk_container_set_focus_vadjustment (GTK_CONTAINER (self->box_details), adj);
+ return TRUE;
+}
+
+static void
+gs_details_page_dispose (GObject *object)
+{
+ GsDetailsPage *self = GS_DETAILS_PAGE (object);
+
+ stop_progress_pulsing (self);
+
+ if (self->app != NULL) {
+ g_signal_handlers_disconnect_by_func (self->app, gs_details_page_notify_state_changed_cb, self);
+ g_signal_handlers_disconnect_by_func (self->app, gs_details_page_progress_changed_cb, self);
+ g_clear_object (&self->app);
+ }
+ g_clear_object (&self->app_local_file);
+ g_clear_object (&self->builder);
+ g_clear_object (&self->plugin_loader);
+ g_clear_object (&self->cancellable);
+ g_clear_object (&self->app_cancellable);
+ g_clear_object (&self->session);
+ g_clear_object (&self->size_group_origin_popover);
+ g_clear_object (&self->button_details_rating_style_provider);
+
+ G_OBJECT_CLASS (gs_details_page_parent_class)->dispose (object);
+}
+
+static void
+gs_details_page_class_init (GsDetailsPageClass *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_details_page_dispose;
+ page_class->app_installed = gs_details_page_app_installed;
+ page_class->app_removed = gs_details_page_app_removed;
+ page_class->switch_to = gs_details_page_switch_to;
+ page_class->reload = gs_details_page_reload;
+ page_class->setup = gs_details_page_setup;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-details-page.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, application_details_icon);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, application_details_summary);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, application_details_title);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_addons);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_description);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_support);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_progress);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_progress2);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, star);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_review_count);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_screenshot);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_screenshot_main);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_screenshot_scrolledwindow);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_screenshot_thumbnails);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_license_list);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_launch);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_add_shortcut);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_remove_shortcut);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_website);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_donate);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_install);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_update);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_remove);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_cancel);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_more_reviews);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, infobar_details_app_norepo);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, infobar_details_app_repo);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, infobar_details_package_baseos);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, infobar_details_repo);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_addons_uninstalled_app);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_progress_percentage);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_progress_status);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_category_title);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_category_value);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_developer_title);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_developer_value);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_developer);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, image_details_developer_verified);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_license_free);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_license_nonfree);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_license_unknown);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_license_title);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_license_value);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_channel_title);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_channel_value);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_origin_title);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_origin_value);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_size_download_title);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_size_download_value);
+ 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_title);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_updated_value);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_version_title);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_version_value);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_permissions_title);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_permissions_value);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_failed);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, list_box_addons);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_reviews);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_screenshot_fallback);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, histogram);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_review);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, list_box_reviews);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, scrolledwindow_details);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, spinner_details);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, spinner_remove);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, stack_details);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, grid_details_kudo);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, image_details_kudo_docs);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, image_details_kudo_sandboxed);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, image_details_kudo_integration);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, image_details_kudo_translated);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, image_details_kudo_updated);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_kudo_docs);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_kudo_sandboxed);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_kudo_integration);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_kudo_translated);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_kudo_updated);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, progressbar_top);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, popover_license_free);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, popover_license_nonfree);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, popover_license_unknown);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_license_nonfree_details);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_licenses_intro);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, popover_content_rating);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_content_rating_title);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_content_rating_message);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_content_rating_none);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_rating_value);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_rating_title);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, popover_permissions);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_permissions_details);
+ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, star_eventbox);
+}
+
+static void
+gs_details_page_init (GsDetailsPage *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ /* setup networking */
+ self->session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT, gs_user_agent (),
+ NULL);
+ self->settings = g_settings_new ("org.gnome.software");
+ g_signal_connect_swapped (self->settings, "changed",
+ G_CALLBACK (settings_changed_cb),
+ self);
+ self->size_group_origin_popover = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
+
+ gtk_list_box_set_header_func (GTK_LIST_BOX (self->list_box_addons),
+ list_header_func,
+ self, NULL);
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (self->list_box_addons),
+ list_sort_func,
+ self, NULL);
+}
+
+GsDetailsPage *
+gs_details_page_new (void)
+{
+ GsDetailsPage *self;
+ self = g_object_new (GS_TYPE_DETAILS_PAGE, NULL);
+ return GS_DETAILS_PAGE (self);
+}