[gnome-software: 5/7] Add GsAppReviewsDialog




commit ef0650226bdc7953fc2a737c6748d521eb615b5a
Author: Adrien Plazas <kekun plazas laposte net>
Date:   Fri Nov 19 13:55:53 2021 +0100

    Add GsAppReviewsDialog
    
    Fixes: #1362

 po/POTFILES.in                   |   1 +
 src/gnome-software.gresource.xml |   1 +
 src/gs-app-reviews-dialog.c      | 559 +++++++++++++++++++++++++++++++++++++++
 src/gs-app-reviews-dialog.h      |  38 +++
 src/gs-app-reviews-dialog.ui     |  65 +++++
 src/meson.build                  |   1 +
 6 files changed, 665 insertions(+)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 8cf9b138f..9cfd24014 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -10,6 +10,7 @@ src/gs-app-addon-row.c
 src/gs-app-addon-row.ui
 src/gs-app-details-page.c
 src/gs-app-details-page.ui
+src/gs-app-reviews-dialog.ui
 src/gs-app-version-history-dialog.ui
 src/gs-app-version-history-row.c
 src/gs-app-version-history-row.ui
diff --git a/src/gnome-software.gresource.xml b/src/gnome-software.gresource.xml
index f63d374f9..824784d54 100644
--- a/src/gnome-software.gresource.xml
+++ b/src/gnome-software.gresource.xml
@@ -5,6 +5,7 @@
   <file preprocess="xml-stripblanks">gs-app-addon-row.ui</file>
   <file preprocess="xml-stripblanks">gs-app-context-bar.ui</file>
   <file preprocess="xml-stripblanks">gs-app-details-page.ui</file>
+  <file preprocess="xml-stripblanks">gs-app-reviews-dialog.ui</file>
   <file preprocess="xml-stripblanks">gs-app-version-history-dialog.ui</file>
   <file preprocess="xml-stripblanks">gs-app-version-history-row.ui</file>
   <file preprocess="xml-stripblanks">gs-app-row.ui</file>
diff --git a/src/gs-app-reviews-dialog.c b/src/gs-app-reviews-dialog.c
new file mode 100644
index 000000000..a00bef07a
--- /dev/null
+++ b/src/gs-app-reviews-dialog.c
@@ -0,0 +1,559 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) Adrien Plazas <adrien plazas puri sm>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include "gs-app-reviews-dialog.h"
+
+#include "gnome-software-private.h"
+#include "gs-common.h"
+#include "gs-review-row.h"
+#include <glib/gi18n.h>
+
+struct _GsAppReviewsDialog
+{
+       GtkDialog        parent_instance;
+       GtkWidget       *listbox;
+       GtkWidget       *stack;
+
+       GsPluginLoader  *plugin_loader;  /* (owned) (nullable) */
+       GsApp           *app;  /* (owned) (nullable) */
+       GCancellable    *cancellable;  /* (owned) */
+       GCancellable    *refine_cancellable;  /* (owned) (nullable) */
+       GsOdrsProvider  *odrs_provider;  /* (nullable) (owned), NULL if reviews are disabled */
+};
+
+G_DEFINE_TYPE (GsAppReviewsDialog, gs_app_reviews_dialog, GTK_TYPE_DIALOG)
+
+typedef enum {
+       PROP_APP = 1,
+       PROP_ODRS_PROVIDER,
+       PROP_PLUGIN_LOADER,
+} GsAppReviewsDialogProperty;
+
+static GParamSpec *obj_props[PROP_PLUGIN_LOADER + 1] = { NULL, };
+
+enum {
+       SIGNAL_REVIEWS_UPDATED,
+       SIGNAL_LAST
+};
+
+static guint signals[SIGNAL_LAST] = { 0 };
+
+static void refresh_reviews (GsAppReviewsDialog *self);
+
+static gint
+sort_reviews (AsReview **a, AsReview **b)
+{
+       return -g_date_time_compare (as_review_get_date (*a), as_review_get_date (*b));
+}
+
+static void
+review_button_clicked_cb (GsReviewRow        *row,
+                          GsReviewAction      action,
+                          GsAppReviewsDialog *self)
+{
+       AsReview *review = gs_review_row_get_review (row);
+       g_autoptr(GError) local_error = NULL;
+
+       g_assert (self->odrs_provider != NULL);
+
+       /* FIXME: Make this async */
+       switch (action) {
+       case GS_REVIEW_ACTION_UPVOTE:
+               gs_odrs_provider_upvote_review (self->odrs_provider, self->app,
+                                               review, self->cancellable,
+                                               &local_error);
+               break;
+       case GS_REVIEW_ACTION_DOWNVOTE:
+               gs_odrs_provider_downvote_review (self->odrs_provider, self->app,
+                                                 review, self->cancellable,
+                                                 &local_error);
+               break;
+       case GS_REVIEW_ACTION_REPORT:
+               gs_odrs_provider_report_review (self->odrs_provider, self->app,
+                                               review, self->cancellable,
+                                               &local_error);
+               break;
+       case GS_REVIEW_ACTION_REMOVE:
+               gs_odrs_provider_remove_review (self->odrs_provider, self->app,
+                                               review, self->cancellable,
+                                               &local_error);
+               break;
+       case GS_REVIEW_ACTION_DISMISS:
+               /* The dismiss action is only used from the moderate page. */
+       default:
+               g_assert_not_reached ();
+       }
+
+       if (local_error != NULL) {
+               g_warning ("failed to set review on %s: %s",
+                          gs_app_get_id (self->app), local_error->message);
+               return;
+       }
+
+       refresh_reviews (self);
+}
+
+static void
+populate_reviews (GsAppReviewsDialog *self)
+{
+       GPtrArray *reviews;
+       gboolean show_reviews = FALSE;
+       guint64 possible_actions = 0;
+       guint i;
+       GsReviewAction all_actions[] = {
+               GS_REVIEW_ACTION_UPVOTE,
+               GS_REVIEW_ACTION_DOWNVOTE,
+               GS_REVIEW_ACTION_REPORT,
+               GS_REVIEW_ACTION_REMOVE,
+       };
+
+       /* nothing to show */
+       if (self->app == NULL) {
+               gtk_stack_set_visible_child_name (GTK_STACK (self->stack), "empty");
+
+               return;
+       }
+
+       /* show or hide the entire reviews section */
+       switch (gs_app_get_kind (self->app)) {
+       case AS_COMPONENT_KIND_DESKTOP_APP:
+       case AS_COMPONENT_KIND_FONT:
+       case AS_COMPONENT_KIND_INPUT_METHOD:
+       case AS_COMPONENT_KIND_WEB_APP:
+               /* don't show a missing rating on a local file */
+               if (gs_app_get_state (self->app) != GS_APP_STATE_AVAILABLE_LOCAL &&
+                   self->odrs_provider != NULL)
+                       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;
+
+       /* check that reviews are available */
+       reviews = gs_app_get_reviews (self->app);
+       if (reviews->len == 0)
+               show_reviews = FALSE;
+
+       if (!show_reviews) {
+               gtk_stack_set_visible_child_name (GTK_STACK (self->stack), "empty");
+
+               return;
+       }
+
+       /* find what the plugins support */
+       for (i = 0; i < G_N_ELEMENTS (all_actions); i++) {
+               if (self->odrs_provider != NULL)
+                       possible_actions |= (1u << all_actions[i]);
+       }
+
+       /* add all the reviews */
+       gs_widget_remove_all (self->listbox, (GsRemoveFunc) gtk_list_box_remove);
+       g_ptr_array_sort (reviews, (GCompareFunc) sort_reviews);
+       for (i = 0; i < reviews->len; i++) {
+               AsReview *review = g_ptr_array_index (reviews, i);
+               GtkWidget *row = gs_review_row_new (review);
+               guint64 actions;
+
+               gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (row), FALSE);
+               gtk_list_box_append (GTK_LIST_BOX (self->listbox), row);
+
+               g_signal_connect (row, "button-clicked",
+                                 G_CALLBACK (review_button_clicked_cb), self);
+               if (as_review_get_flags (review) & AS_REVIEW_FLAG_SELF)
+                       actions = possible_actions & (1 << GS_REVIEW_ACTION_REMOVE);
+               else
+                       actions = possible_actions & ~(1u << GS_REVIEW_ACTION_REMOVE);
+               gs_review_row_set_actions (GS_REVIEW_ROW (row), actions);
+               gs_review_row_set_network_available (GS_REVIEW_ROW (row),
+                                                    GS_IS_PLUGIN_LOADER (self->plugin_loader) && 
gs_plugin_loader_get_network_available (self->plugin_loader));
+       }
+
+       gtk_stack_set_visible_child_name (GTK_STACK (self->stack), "reviews");
+}
+
+static void
+refresh_reviews (GsAppReviewsDialog *self)
+{
+       if (!gtk_widget_get_realized (GTK_WIDGET (self)))
+               return;
+
+       populate_reviews (self);
+
+       g_signal_emit (self, signals[SIGNAL_REVIEWS_UPDATED], 0);
+}
+
+static void
+gs_app_reviews_dialog_app_refine_cb (GObject      *source,
+                                     GAsyncResult *res,
+                                     gpointer      user_data)
+{
+       GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
+       GsAppReviewsDialog *self = GS_APP_REVIEWS_DIALOG (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;
+       }
+
+       refresh_reviews (self);
+}
+
+static void
+gs_app_reviews_dialog_app_refine (GsAppReviewsDialog *self)
+{
+       g_autoptr(GsPluginJob) plugin_job = NULL;
+
+       if (self->refine_cancellable != NULL) {
+               g_cancellable_cancel (self->refine_cancellable);
+               g_clear_object (&self->refine_cancellable);
+       }
+
+       if (self->plugin_loader == NULL || self->app == NULL)
+               return;
+
+       self->refine_cancellable = g_cancellable_new ();
+
+       /* If this task fails (e.g. because we have no networking) then
+        * it's of no huge importance if we don't get the required data.
+        */
+       plugin_job = gs_plugin_job_refine_new_for_app (self->app,
+                                                      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);
+       gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
+                                           self->refine_cancellable,
+                                           gs_app_reviews_dialog_app_refine_cb,
+                                           self);
+}
+
+static void
+gs_app_reviews_dialog_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+       GsAppReviewsDialog *self = GS_APP_REVIEWS_DIALOG (object);
+
+       switch ((GsAppReviewsDialogProperty) prop_id) {
+       case PROP_APP:
+               g_value_set_object (value, gs_app_reviews_dialog_get_app (self));
+               break;
+       case PROP_ODRS_PROVIDER:
+               g_value_set_object (value, gs_app_reviews_dialog_get_odrs_provider (self));
+               break;
+       case PROP_PLUGIN_LOADER:
+               g_value_set_object (value, gs_app_reviews_dialog_get_plugin_loader (self));
+               break;
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+               break;
+       }
+}
+
+static void
+gs_app_reviews_dialog_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+       GsAppReviewsDialog *self = GS_APP_REVIEWS_DIALOG (object);
+
+       switch ((GsAppReviewsDialogProperty) prop_id) {
+       case PROP_APP:
+               gs_app_reviews_dialog_set_app (self, g_value_get_object (value));
+               break;
+       case PROP_ODRS_PROVIDER:
+               gs_app_reviews_dialog_set_odrs_provider (self, g_value_get_object (value));
+               break;
+       case PROP_PLUGIN_LOADER:
+               gs_app_reviews_dialog_set_plugin_loader (self, g_value_get_object (value));
+               break;
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+               break;
+       }
+}
+
+static void
+gs_app_reviews_dialog_dispose (GObject *object)
+{
+       GsAppReviewsDialog *self = GS_APP_REVIEWS_DIALOG (object);
+
+       g_cancellable_cancel (self->cancellable);
+       g_clear_object (&self->cancellable);
+
+       if (self->refine_cancellable != NULL) {
+               g_cancellable_cancel (self->refine_cancellable);
+               g_clear_object (&self->refine_cancellable);
+       }
+
+       if (self->plugin_loader)
+               g_signal_handlers_disconnect_by_func (self->plugin_loader,
+                                                     refresh_reviews, self);
+       g_clear_object (&self->plugin_loader);
+
+       g_clear_object (&self->app);
+       g_clear_object (&self->odrs_provider);
+
+       G_OBJECT_CLASS (gs_app_reviews_dialog_parent_class)->dispose (object);
+}
+
+static void
+gs_app_reviews_dialog_class_init (GsAppReviewsDialogClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+       object_class->get_property = gs_app_reviews_dialog_get_property;
+       object_class->set_property = gs_app_reviews_dialog_set_property;
+       object_class->dispose = gs_app_reviews_dialog_dispose;
+
+       /**
+        * GsAppReviewsDialog:app: (nullable)
+        *
+        * An app whose reviews should be displayed.
+        *
+        * If this is %NULL, ratings and reviews will be disabled.
+        *
+        * Since: 42
+        */
+       obj_props[PROP_APP] =
+               g_param_spec_object ("app", NULL, NULL,
+                                    GS_TYPE_APP,
+                                    G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+       /**
+        * GsAppReviewsDialog:odrs-provider: (nullable)
+        *
+        * An ODRS provider to give access to ratings and reviews information
+        * for the app being displayed.
+        *
+        * If this is %NULL, ratings and reviews will be disabled.
+        *
+        * Since: 42
+        */
+       obj_props[PROP_ODRS_PROVIDER] =
+               g_param_spec_object ("odrs-provider", NULL, NULL,
+                                    GS_TYPE_ODRS_PROVIDER,
+                                    G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+       /**
+        * GsAppReviewsDialog:plugin-loader: (nullable)
+        *
+        * A plugin loader to provide network availability.
+        *
+        * If this is %NULL, ratings and reviews will be disabled.
+        *
+        * Since: 42
+        */
+       obj_props[PROP_PLUGIN_LOADER] =
+               g_param_spec_object ("plugin-loader", NULL, NULL,
+                                    GS_TYPE_PLUGIN_LOADER,
+                                    G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+       g_object_class_install_properties (object_class, G_N_ELEMENTS (obj_props), obj_props);
+
+       /**
+        * GsAppReviewsDialog::reviews-updated:
+        *
+        * Emitted when reviews are updated.
+        *
+        * Since: 42
+        */
+       signals[SIGNAL_REVIEWS_UPDATED] =
+               g_signal_new ("reviews-updated",
+                             G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+                             0, NULL, NULL, g_cclosure_marshal_generic,
+                             G_TYPE_NONE, 0);
+
+       gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/Software/gs-app-reviews-dialog.ui");
+
+       gtk_widget_class_bind_template_child (widget_class, GsAppReviewsDialog, listbox);
+       gtk_widget_class_bind_template_child (widget_class, GsAppReviewsDialog, stack);
+}
+
+static void
+gs_app_reviews_dialog_init (GsAppReviewsDialog *self)
+{
+       self->cancellable = g_cancellable_new ();
+
+       gtk_widget_init_template (GTK_WIDGET (self));
+
+       g_signal_connect_swapped (self, "realize",
+                                 G_CALLBACK (refresh_reviews), self);
+}
+
+/**
+ * gs_app_reviews_dialog_new:
+ * @parent: (nullable): a #GtkWindow, or %NULL
+ * @app: (nullable): a #GsApp, or %NULL
+ * @odrs_provider: (nullable): a #GsOdrsProvider, or %NULL
+ * @plugin_loader: (nullable): a #GsPluginLoader, or %NULL
+ *
+ * Create a new #GsAppReviewsDialog transient for @parent, and set its initial
+ * app, ODRS provider and plugin loader to @app, @odrs_provider and
+ * @plugin_loader respectively.
+ *
+ * Returns: (transfer full): a new #GsAppReviewsDialog
+ * Since: 42
+ */
+GtkWidget *
+gs_app_reviews_dialog_new (GtkWindow *parent, GsApp *app, GsOdrsProvider *odrs_provider, GsPluginLoader 
*plugin_loader)
+{
+       GsAppReviewsDialog *self;
+
+       g_return_val_if_fail (parent == NULL || GTK_IS_WINDOW (parent), NULL);
+       g_return_val_if_fail (app == NULL || GS_IS_APP (app), NULL);
+       g_return_val_if_fail (odrs_provider == NULL || GS_IS_ODRS_PROVIDER (odrs_provider), NULL);
+       g_return_val_if_fail (plugin_loader == NULL ||GS_IS_PLUGIN_LOADER (plugin_loader), NULL);
+
+       self = g_object_new (GS_TYPE_APP_REVIEWS_DIALOG,
+                            "app", app,
+                            "modal", TRUE,
+                            "odrs-provider", odrs_provider,
+                            "plugin-loader", plugin_loader,
+                            "transient-for", parent,
+                            "use-header-bar", TRUE,
+                            NULL);
+
+       return GTK_WIDGET (self);
+}
+
+/**
+ * gs_app_reviews_dialog_get_app:
+ * @self: a #GsAppReviewsDialog
+ *
+ * Get the value of #GsAppReviewsDialog:app.
+ *
+ * Returns: (nullable) (transfer none): a #GsApp, or %NULL if unset
+ * Since: 42
+ */
+GsApp *
+gs_app_reviews_dialog_get_app (GsAppReviewsDialog *self)
+{
+       g_return_val_if_fail (GS_IS_APP_REVIEWS_DIALOG (self), NULL);
+
+       return self->app;
+}
+
+/**
+ * gs_app_reviews_dialog_set_app:
+ * @self: a #GsAppReviewsDialog
+ * @app: (nullable) (transfer none): new #GsApp or %NULL
+ *
+ * Set the value of #GsAppReviewsDialog:app.
+ *
+ * Since: 42
+ */
+void
+gs_app_reviews_dialog_set_app (GsAppReviewsDialog *self,
+                               GsApp              *app)
+{
+       g_return_if_fail (GS_IS_APP_REVIEWS_DIALOG (self));
+       g_return_if_fail (app == NULL || GS_IS_APP (app));
+
+       if (g_set_object (&self->app, app)) {
+               gs_app_reviews_dialog_app_refine (self);
+               refresh_reviews (self);
+               g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_APP]);
+       }
+}
+
+/**
+ * gs_app_reviews_dialog_get_odrs_provider:
+ * @self: a #GsAppReviewsDialog
+ *
+ * Get the value of #GsAppReviewsDialog:odrs-provider.
+ *
+ * Returns: (nullable) (transfer none): a #GsOdrsProvider, or %NULL if unset
+ * Since: 42
+ */
+GsOdrsProvider *
+gs_app_reviews_dialog_get_odrs_provider (GsAppReviewsDialog *self)
+{
+       g_return_val_if_fail (GS_IS_APP_REVIEWS_DIALOG (self), NULL);
+
+       return self->odrs_provider;
+}
+
+/**
+ * gs_app_reviews_dialog_set_odrs_provider:
+ * @self: a #GsAppReviewsDialog
+ * @odrs_provider: (nullable) (transfer none): new #GsOdrsProvider or %NULL
+ *
+ * Set the value of #GsAppReviewsDialog:odrs-provider.
+ *
+ * Since: 42
+ */
+void
+gs_app_reviews_dialog_set_odrs_provider (GsAppReviewsDialog *self,
+                                         GsOdrsProvider     *odrs_provider)
+{
+       g_return_if_fail (GS_IS_APP_REVIEWS_DIALOG (self));
+       g_return_if_fail (odrs_provider == NULL || GS_IS_ODRS_PROVIDER (odrs_provider));
+
+       if (g_set_object (&self->odrs_provider, odrs_provider)) {
+               refresh_reviews (self);
+               g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_ODRS_PROVIDER]);
+       }
+}
+
+/**
+ * gs_app_reviews_dialog_get_plugin_loader:
+ * @self: a #GsAppReviewsDialog
+ *
+ * Get the value of #GsAppReviewsDialog:plugin-loader.
+ *
+ * Returns: (nullable) (transfer none): a #GsPluginLoader, or %NULL if unset
+ * Since: 42
+ */
+GsPluginLoader *
+gs_app_reviews_dialog_get_plugin_loader (GsAppReviewsDialog *self)
+{
+       g_return_val_if_fail (GS_IS_APP_REVIEWS_DIALOG (self), NULL);
+
+       return self->plugin_loader;
+}
+
+/**
+ * gs_app_reviews_dialog_set_plugin_loader:
+ * @self: a #GsAppReviewsDialog
+ * @plugin_loader: (nullable) (transfer none): new #GsPluginLoader or %NULL
+ *
+ * Set the value of #GsAppReviewsDialog:plugin-loader.
+ *
+ * Since: 42
+ */
+void
+gs_app_reviews_dialog_set_plugin_loader (GsAppReviewsDialog *self,
+                                         GsPluginLoader     *plugin_loader)
+{
+       g_return_if_fail (GS_IS_APP_REVIEWS_DIALOG (self));
+       g_return_if_fail (plugin_loader == NULL || GS_IS_PLUGIN_LOADER (plugin_loader));
+
+       if (self->plugin_loader)
+               g_signal_handlers_disconnect_by_func (self->plugin_loader,
+                                                     refresh_reviews, self);
+
+       if (g_set_object (&self->plugin_loader, plugin_loader)) {
+               gs_app_reviews_dialog_app_refine (self);
+               g_signal_connect_swapped (self->plugin_loader, "notify::network-available",
+                                         G_CALLBACK (refresh_reviews), self);
+               g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_PLUGIN_LOADER]);
+       }
+}
diff --git a/src/gs-app-reviews-dialog.h b/src/gs-app-reviews-dialog.h
new file mode 100644
index 000000000..76dd5dc1b
--- /dev/null
+++ b/src/gs-app-reviews-dialog.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2021 Adrien Plazas <adrien plazas puri sm>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "gnome-software-private.h"
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_APP_REVIEWS_DIALOG (gs_app_reviews_dialog_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsAppReviewsDialog, gs_app_reviews_dialog, GS, APP_REVIEWS_DIALOG, GtkDialog)
+
+GtkWidget      *gs_app_reviews_dialog_new      (GtkWindow      *parent,
+                                                GsApp          *app,
+                                                GsOdrsProvider *odrs_provider,
+                                                GsPluginLoader *plugin_loader);
+
+GsApp  *gs_app_reviews_dialog_get_app  (GsAppReviewsDialog     *self);
+void    gs_app_reviews_dialog_set_app  (GsAppReviewsDialog     *self,
+                                        GsApp                  *app);
+
+GsOdrsProvider *gs_app_reviews_dialog_get_odrs_provider        (GsAppReviewsDialog     *self);
+void            gs_app_reviews_dialog_set_odrs_provider        (GsAppReviewsDialog     *self,
+                                                                GsOdrsProvider         *odrs_provider);
+
+GsPluginLoader *gs_app_reviews_dialog_get_plugin_loader        (GsAppReviewsDialog     *self);
+void            gs_app_reviews_dialog_set_plugin_loader        (GsAppReviewsDialog     *self,
+                                                                GsPluginLoader         *plugin_loader);
+
+G_END_DECLS
diff --git a/src/gs-app-reviews-dialog.ui b/src/gs-app-reviews-dialog.ui
new file mode 100644
index 000000000..2ceee8902
--- /dev/null
+++ b/src/gs-app-reviews-dialog.ui
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.10"/>
+  <requires lib="handy" version="1.0"/>
+  <template class="GsAppReviewsDialog" parent="GtkDialog">
+    <property name="title" translatable="yes">Reviews</property>
+    <property name="default_width">550</property>
+    <property name="default_height">600</property>
+    <property name="width_request">360</property>
+    <property name="height_request">400</property>
+    <property name="use_header_bar">1</property>
+    <child internal-child="headerbar">
+      <object class="AdwHeaderBar"/>
+    </child>
+    <child internal-child="content_area">
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkStack" id="stack">
+            <child>
+              <object class="GtkStackPage">
+                <property name="name">empty</property>
+                <property name="child">
+                  <object class="AdwStatusPage">
+                    <property name="description" translatable="yes">No reviews were found for this 
application.</property>
+                    <property name="icon-name">review-symbolic</property>
+                    <property name="title" translatable="yes">No Reviews</property>
+                  </object>
+                </property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkStackPage">
+                <property name="name">reviews</property>
+                <property name="child">
+                  <object class="GtkScrolledWindow">
+                    <child>
+                      <object class="AdwClamp">
+                        <property name="vexpand">True</property>
+                        <property name="hexpand">False</property>
+                        <property name="margin-start">12</property>
+                        <property name="margin-end">12</property>
+                        <property name="margin-bottom">18</property>
+                        <property name="margin-top">18</property>
+                        <child>
+                          <object class="GtkListBox" id="listbox">
+                            <property name="selection-mode">none</property>
+                            <property name="valign">start</property>
+                            <style>
+                              <class name="boxed-list"/>
+                            </style>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/meson.build b/src/meson.build
index 47b17d25e..bfc8f0c9a 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -25,6 +25,7 @@ enums = gnome.mkenums_simple('gs-enums',
 gnome_software_sources = [
   'gs-age-rating-context-dialog.c',
   'gs-app-addon-row.c',
+  'gs-app-reviews-dialog.c',
   'gs-app-version-history-dialog.c',
   'gs-app-version-history-row.c',
   'gs-application.c',


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