[gnome-software/package-rebase: 1/2] Add review UI and plugin API. Add Ubuntu review plugin.
- From: Robert Ancell <rancell src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-software/package-rebase: 1/2] Add review UI and plugin API. Add Ubuntu review plugin.
- Date: Fri, 15 Jan 2016 02:18:12 +0000 (UTC)
commit d15c39c805e6093ec64fca136c20826db68c9c7c
Author: Robert Ancell <robert ancell canonical com>
Date: Thu Oct 15 14:32:31 2015 +0100
Add review UI and plugin API.
Add Ubuntu review plugin.
configure.ac | 2 +
po/POTFILES.in | 4 +
src/Makefile.am | 8 +
src/gnome-software.gresource.xml | 2 +
src/gs-app-review-dialog.c | 99 ++++
src/gs-app-review-dialog.h | 44 ++
src/gs-app-review-dialog.ui | 236 +++++++++
src/gs-app-review-row.c | 154 ++++++
src/gs-app-review-row.h | 41 ++
src/gs-app-review-row.ui | 75 +++
src/gs-app-review.c | 337 +++++++++++++
src/gs-app-review.h | 64 +++
src/gs-app.c | 48 ++
src/gs-app.h | 8 +
src/gs-plugin-loader.c | 57 ++-
src/gs-plugin-loader.h | 3 +
src/gs-plugin.h | 9 +
src/gs-shell-details.c | 100 ++++-
src/gs-shell-details.ui | 34 ++-
src/gtk-style-hc.css | 9 +
src/gtk-style.css | 9 +
src/plugins/Makefile.am | 8 +
src/plugins/gs-plugin-local-ratings.c | 8 +
src/plugins/gs-plugin-ubuntu-reviews.c | 860 ++++++++++++++++++++++++++++++++
24 files changed, 2214 insertions(+), 5 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index db16013..13c0ac4 100644
--- a/configure.ac
+++ b/configure.ac
@@ -67,6 +67,8 @@ PKG_CHECK_MODULES(SOUP, libsoup-2.4 >= 2.51.92)
PKG_CHECK_MODULES(GSETTINGS_DESKTOP_SCHEMAS, gsettings-desktop-schemas >= 3.11.5)
PKG_CHECK_MODULES(GNOME_DESKTOP, gnome-desktop-3.0 >= 3.17.92)
PKG_CHECK_MODULES(POLKIT, polkit-gobject-1)
+PKG_CHECK_MODULES(JSON_GLIB, json-glib-1.0 >= 0.12)
+PKG_CHECK_MODULES(OAUTH, oauth)
AC_PATH_PROG(APPSTREAM_UTIL, [appstream-util], [unfound])
AC_ARG_ENABLE(man,
[AS_HELP_STRING([--enable-man],
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 358f343..a083269 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -7,6 +7,10 @@ src/gnome-software-local-file.desktop.in
src/gs-app-folder-dialog.c
src/gs-application.c
src/gs-app-addon-row.c
+src/gs-app-review-dialog.c
+[type: gettext/glade]src/gs-app-review-dialog.ui
+src/gs-app-review-row.c
+[type: gettext/glade]src/gs-app-review-row.ui
src/gs-app.c
src/gs-app-row.c
src/gs-app-tile.c
diff --git a/src/Makefile.am b/src/Makefile.am
index 903caf7..05eddeb 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -35,6 +35,8 @@ UI_FILES = \
gnome-software.ui \
gs-app-addon-row.ui \
gs-app-row.ui \
+ gs-app-review-dialog.ui \
+ gs-app-review-row.ui \
gs-first-run-dialog.ui \
gs-history-dialog.ui \
gs-shell-category.ui \
@@ -120,6 +122,12 @@ gnome_software_SOURCES = \
gs-feature-tile.h \
gs-category-tile.c \
gs-category-tile.h \
+ gs-app-review.c \
+ gs-app-review.h \
+ gs-app-review-dialog.c \
+ gs-app-review-dialog.h \
+ gs-app-review-row.c \
+ gs-app-review-row.h \
gs-app-tile.c \
gs-app-tile.h \
gs-app-folder-dialog.c \
diff --git a/src/gnome-software.gresource.xml b/src/gnome-software.gresource.xml
index dbeb2af..33faab2 100644
--- a/src/gnome-software.gresource.xml
+++ b/src/gnome-software.gresource.xml
@@ -10,6 +10,8 @@
<file preprocess="xml-stripblanks">app-folder-dialog.ui</file>
<file preprocess="xml-stripblanks">screenshot-image.ui</file>
<file preprocess="xml-stripblanks">gs-app-addon-row.ui</file>
+ <file preprocess="xml-stripblanks">gs-app-review-dialog.ui</file>
+ <file preprocess="xml-stripblanks">gs-app-review-row.ui</file>
<file preprocess="xml-stripblanks">gs-app-row.ui</file>
<file preprocess="xml-stripblanks">gs-first-run-dialog.ui</file>
<file preprocess="xml-stripblanks">gs-history-dialog.ui</file>
diff --git a/src/gs-app-review-dialog.c b/src/gs-app-review-dialog.c
new file mode 100644
index 0000000..8114930
--- /dev/null
+++ b/src/gs-app-review-dialog.c
@@ -0,0 +1,99 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2015 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include "gs-app-review-dialog.h"
+#include "gs-star-widget.h"
+
+struct _GsAppReviewDialog
+{
+ GtkDialog parent_instance;
+
+ GtkWidget *star;
+ GtkWidget *summary_entry;
+ GtkWidget *text_view;
+ GtkWidget *post_button;
+};
+
+G_DEFINE_TYPE (GsAppReviewDialog, gs_app_review_dialog, GTK_TYPE_DIALOG)
+
+gint
+gs_app_review_dialog_get_rating (GsAppReviewDialog *dialog)
+{
+ return gs_star_widget_get_rating (GS_STAR_WIDGET (dialog->star));
+}
+
+void
+gs_app_review_dialog_set_rating (GsAppReviewDialog *dialog, gint rating)
+{
+ gs_star_widget_set_rating (GS_STAR_WIDGET (dialog->star), GS_APP_RATING_KIND_USER, rating);
+}
+
+const gchar *
+gs_app_review_dialog_get_summary (GsAppReviewDialog *dialog)
+{
+ return gtk_entry_get_text (GTK_ENTRY (dialog->summary_entry));
+}
+
+gchar *
+gs_app_review_dialog_get_text (GsAppReviewDialog *dialog)
+{
+ GtkTextBuffer *buffer;
+ GtkTextIter start, end;
+
+ buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (dialog->text_view));
+ gtk_text_buffer_get_start_iter (buffer, &start);
+ gtk_text_buffer_get_end_iter (buffer, &end);
+ return gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
+}
+
+static void
+gs_app_review_dialog_init (GsAppReviewDialog *dialog)
+{
+ gtk_widget_init_template (GTK_WIDGET (dialog));
+}
+
+static void
+gs_app_review_dialog_class_init (GsAppReviewDialogClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
"/org/gnome/Software/gs-app-review-dialog.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GsAppReviewDialog, star);
+ gtk_widget_class_bind_template_child (widget_class, GsAppReviewDialog, summary_entry);
+ gtk_widget_class_bind_template_child (widget_class, GsAppReviewDialog, text_view);
+ gtk_widget_class_bind_template_child (widget_class, GsAppReviewDialog, post_button);
+}
+
+GtkWidget *
+gs_app_review_dialog_new (void)
+{
+ return GTK_WIDGET (g_object_new (GS_TYPE_APP_REVIEW_DIALOG,
+ "use-header-bar", TRUE,
+ NULL));
+}
+
+/* vim: set noexpandtab: */
diff --git a/src/gs-app-review-dialog.h b/src/gs-app-review-dialog.h
new file mode 100644
index 0000000..59ff2ba
--- /dev/null
+++ b/src/gs-app-review-dialog.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2015 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef GS_APP_REVIEW_DIALOG_H
+#define GS_APP_REVIEW_DIALOG_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_APP_REVIEW_DIALOG (gs_app_review_dialog_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsAppReviewDialog, gs_app_review_dialog, GS, APP_REVIEW_DIALOG, GtkDialog)
+
+GtkWidget *gs_app_review_dialog_new (void);
+gint gs_app_review_dialog_get_rating (GsAppReviewDialog *dialog);
+void gs_app_review_dialog_set_rating (GsAppReviewDialog *dialog,
+ gint rating);
+const gchar *gs_app_review_dialog_get_summary (GsAppReviewDialog *dialog);
+gchar *gs_app_review_dialog_get_text (GsAppReviewDialog *dialog);
+
+G_END_DECLS
+
+#endif /* GS_APP_REVIEW_DIALOG_H */
+
+/* vim: set noexpandtab: */
diff --git a/src/gs-app-review-dialog.ui b/src/gs-app-review-dialog.ui
new file mode 100644
index 0000000..356705c
--- /dev/null
+++ b/src/gs-app-review-dialog.ui
@@ -0,0 +1,236 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.18.3 -->
+<interface>
+ <requires lib="gtk+" version="3.10"/>
+ <template class="GsAppReviewDialog" parent="GtkDialog">
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Review</property>
+ <property name="modal">True</property>
+ <property name="default_width">600</property>
+ <property name="default_height">300</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="type_hint">dialog</property>
+ <property name="use_header_bar">1</property>
+ <child internal-child="headerbar">
+ <object class="GtkHeaderBar">
+ <child>
+ <object class="GtkButton" id="post_button">
+ <property name="label" translatable="yes">_Post Review</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_underline">True</property>
+ </object>
+ <packing>
+ <property name="pack-type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox" id="dialog-vbox">
+ <property name="can_focus">False</property>
+ <property name="margin_start">6</property>
+ <property name="margin_end">6</property>
+ <property name="margin_top">6</property>
+ <property name="margin_bottom">6</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">9</property>
+ <child internal-child="action_area">
+ <object class="GtkButtonBox" id="dialog-action_area1">
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkBox" id="box4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkLabel" id="label4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Rating</property>
+ <property name="xalign">0</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Rate between one star (bad) and five stars
(great)</property>
+ <property name="wrap">True</property>
+ <property name="xalign">0</property>
+ <attributes>
+ <attribute name="style" value="italic"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GsStarWidget" id="star">
+ <property name="visible">True</property>
+ <property name="halign">start</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Summary</property>
+ <property name="xalign">0</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">A single line summarising your review, e.g.
"Useful tool"</property>
+ <property name="wrap">True</property>
+ <property name="xalign">0</property>
+ <attributes>
+ <attribute name="style" value="italic"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="summary_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkLabel" id="label3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Review</property>
+ <property name="xalign">0</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">A few sentences describing how you find this
application. e.g. "This application is great, it does does X very well. It would be improved if it could do
Y."</property>
+ <property name="wrap">True</property>
+ <property name="xalign">0</property>
+ <attributes>
+ <attribute name="style" value="italic"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTextView" id="text_view">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/gs-app-review-row.c b/src/gs-app-review-row.c
new file mode 100644
index 0000000..6fca7a5
--- /dev/null
+++ b/src/gs-app-review-row.c
@@ -0,0 +1,154 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2015 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include "gs-app-review-row.h"
+#include "gs-star-widget.h"
+
+struct _GsAppReviewRow
+{
+ GtkListBoxRow parent_instance;
+
+ GsAppReview *review;
+ GtkWidget *stars;
+ GtkWidget *summary_label;
+ GtkWidget *author_label;
+ GtkWidget *text_label;
+};
+
+G_DEFINE_TYPE (GsAppReviewRow, gs_app_review_row, GTK_TYPE_LIST_BOX_ROW)
+
+static void
+gs_app_review_row_refresh (GsAppReviewRow *row)
+{
+ const gchar *reviewer;
+ GDateTime *date;
+ gchar *text;
+
+ gs_star_widget_set_rating (GS_STAR_WIDGET (row->stars), GS_APP_RATING_KIND_SYSTEM,
gs_app_review_get_rating (row->review));
+ reviewer = gs_app_review_get_reviewer (row->review);
+ date = gs_app_review_get_date (row->review);
+ if (reviewer && date) {
+ gchar *date_text = g_date_time_format (date, "%e %B %Y");
+ text = g_strdup_printf ("%s, %s", reviewer, date_text);
+ g_free (date_text);
+ }
+ else if (reviewer)
+ text = g_strdup (reviewer);
+ else if (date)
+ text = g_date_time_format (date, "%e %B %Y");
+ else
+ text = g_strdup ("");
+ gtk_label_set_text (GTK_LABEL (row->author_label), text);
+ g_free (text);
+ gtk_label_set_text (GTK_LABEL (row->summary_label), gs_app_review_get_summary (row->review));
+ gtk_label_set_text (GTK_LABEL (row->text_label), gs_app_review_get_text (row->review));
+}
+
+static gboolean
+gs_app_review_row_refresh_idle (gpointer user_data)
+{
+ GsAppReviewRow *row = GS_APP_REVIEW_ROW (user_data);
+
+ gs_app_review_row_refresh (row);
+
+ g_object_unref (row);
+ return G_SOURCE_REMOVE;
+}
+
+static void
+gs_app_review_row_notify_props_changed_cb (GsApp *app,
+ GParamSpec *pspec,
+ GsAppReviewRow *row)
+{
+ g_idle_add (gs_app_review_row_refresh_idle, g_object_ref (row));
+}
+
+static void
+gs_app_review_row_set_review (GsAppReviewRow *row, GsAppReview *review)
+{
+ row->review = g_object_ref (review);
+
+ g_signal_connect_object (row->review, "notify::state",
+ G_CALLBACK (gs_app_review_row_notify_props_changed_cb),
+ row, 0);
+ gs_app_review_row_refresh (row);
+}
+
+static void
+gs_app_review_row_init (GsAppReviewRow *row)
+{
+ gtk_widget_set_has_window (GTK_WIDGET (row), FALSE);
+ gtk_widget_init_template (GTK_WIDGET (row));
+}
+
+static void
+gs_app_review_row_dispose (GObject *object)
+{
+ GsAppReviewRow *row = GS_APP_REVIEW_ROW (object);
+
+ g_clear_object (&row->review);
+
+ G_OBJECT_CLASS (gs_app_review_row_parent_class)->dispose (object);
+}
+
+static void
+gs_app_review_row_class_init (GsAppReviewRowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = gs_app_review_row_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class,
"/org/gnome/Software/gs-app-review-row.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GsAppReviewRow, stars);
+ gtk_widget_class_bind_template_child (widget_class, GsAppReviewRow, summary_label);
+ gtk_widget_class_bind_template_child (widget_class, GsAppReviewRow, author_label);
+ gtk_widget_class_bind_template_child (widget_class, GsAppReviewRow, text_label);
+}
+
+/**
+ * gs_app_review_row_new:
+ * @review: The review to show
+ *
+ * Create a widget suitable for showing an application review.
+ *
+ * Return value: A new @GsAppReviewRow.
+ **/
+GtkWidget *
+gs_app_review_row_new (GsAppReview *review)
+{
+ GtkWidget *row;
+
+ g_return_val_if_fail (GS_IS_APP_REVIEW (review), NULL);
+
+ row = g_object_new (GS_TYPE_APP_REVIEW_ROW, NULL);
+ gs_app_review_row_set_review (GS_APP_REVIEW_ROW (row), review);
+
+ return row;
+}
+
+/* vim: set noexpandtab: */
diff --git a/src/gs-app-review-row.h b/src/gs-app-review-row.h
new file mode 100644
index 0000000..abff670
--- /dev/null
+++ b/src/gs-app-review-row.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2015 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef GS_APP_REVIEW_ROW_H
+#define GS_APP_REVIEW_ROW_H
+
+#include <gtk/gtk.h>
+
+#include "gs-app-review.h"
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_APP_REVIEW_ROW (gs_app_review_row_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsAppReviewRow, gs_app_review_row, GS, APP_REVIEW_ROW, GtkListBoxRow)
+
+GtkWidget *gs_app_review_row_new (GsAppReview *review);
+
+G_END_DECLS
+
+#endif /* GS_APP_REVIEW_ROW_H */
+
+/* vim: set noexpandtab: */
diff --git a/src/gs-app-review-row.ui b/src/gs-app-review-row.ui
new file mode 100644
index 0000000..6032f2d
--- /dev/null
+++ b/src/gs-app-review-row.ui
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.10 -->
+ <template class="GsAppReviewRow" parent="GtkListBoxRow">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid" id="grid">
+ <property name="visible">True</property>
+ <property name="row-spacing">12</property>
+ <property name="column-spacing">12</property>
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <child>
+ <object class="GsStarWidget" id="stars">
+ <property name="visible">True</property>
+ <property name="halign">start</property>
+ <property name="sensitive">False</property>
+ </object>
+ <packing>
+ <property name="left-attach">0</property>
+ <property name="top-attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="summary_label">
+ <property name="visible">True</property>
+ <property name="expand">True</property>
+ <property name="halign">start</property>
+ <property name="ellipsize">end</property>
+ <style>
+ <class name="review-summary"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left-attach">1</property>
+ <property name="top-attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="author_label">
+ <property name="visible">True</property>
+ <property name="halign">end</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left-attach">2</property>
+ <property name="top-attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="text_label">
+ <property name="visible">True</property>
+ <property name="halign">start</property>
+ <property name="wrap">True</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left-attach">0</property>
+ <property name="top-attach">1</property>
+ <property name="width">3</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/gs-app-review.c b/src/gs-app-review.c
new file mode 100644
index 0000000..720b23f
--- /dev/null
+++ b/src/gs-app-review.c
@@ -0,0 +1,337 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2015 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+
+#include "gs-app-review.h"
+
+struct _GsAppReview
+{
+ GObject parent_instance;
+
+ gchar *summary;
+ gchar *text;
+ gint rating;
+ gchar *version;
+ gchar *reviewer;
+ GDateTime *date;
+};
+
+enum {
+ PROP_0,
+ PROP_SUMMARY,
+ PROP_TEXT,
+ PROP_RATING,
+ PROP_VERSION,
+ PROP_REVIEWER,
+ PROP_DATE,
+ PROP_LAST
+};
+
+G_DEFINE_TYPE (GsAppReview, gs_app_review, G_TYPE_OBJECT)
+
+/**
+ * gs_app_review_get_summary:
+ */
+const gchar *
+gs_app_review_get_summary (GsAppReview *review)
+{
+ g_return_val_if_fail (GS_IS_APP_REVIEW (review), NULL);
+ return review->summary;
+}
+
+/**
+ * gs_app_review_set_summary:
+ */
+void
+gs_app_review_set_summary (GsAppReview *review, const gchar *summary)
+{
+ g_return_if_fail (GS_IS_APP_REVIEW (review));
+ g_free (review->summary);
+ review->summary = g_strdup (summary);
+}
+
+/**
+ * gs_app_review_get_text:
+ **/
+const gchar *
+gs_app_review_get_text (GsAppReview *review)
+{
+ g_return_val_if_fail (GS_IS_APP_REVIEW (review), NULL);
+ return review->text;
+}
+
+/**
+ * gs_app_review_set_text:
+ */
+void
+gs_app_review_set_text (GsAppReview *review, const gchar *text)
+{
+ g_return_if_fail (GS_IS_APP_REVIEW (review));
+ g_free (review->text);
+ review->text = g_strdup (text);
+}
+
+/**
+ * gs_app_review_get_rating:
+ */
+gint
+gs_app_review_get_rating (GsAppReview *review)
+{
+ g_return_val_if_fail (GS_IS_APP_REVIEW (review), -1);
+ return review->rating;
+}
+
+/**
+ * gs_app_review_set_rating:
+ */
+void
+gs_app_review_set_rating (GsAppReview *review, gint rating)
+{
+ g_return_if_fail (GS_IS_APP_REVIEW (review));
+ review->rating = rating;
+}
+
+/**
+ * gs_app_review_get_reviewer:
+ **/
+const gchar *
+gs_app_review_get_reviewer (GsAppReview *review)
+{
+ g_return_val_if_fail (GS_IS_APP_REVIEW (review), NULL);
+ return review->reviewer;
+}
+
+/**
+ * gs_app_review_set_version:
+ */
+void
+gs_app_review_set_version (GsAppReview *review, const gchar *version)
+{
+ g_return_if_fail (GS_IS_APP_REVIEW (review));
+ g_free (review->version);
+ review->version = g_strdup (version);
+}
+
+/**
+ * gs_app_review_get_version:
+ **/
+const gchar *
+gs_app_review_get_version (GsAppReview *review)
+{
+ g_return_val_if_fail (GS_IS_APP_REVIEW (review), NULL);
+ return review->version;
+}
+
+/**
+ * gs_app_review_set_reviewer:
+ */
+void
+gs_app_review_set_reviewer (GsAppReview *review, const gchar *reviewer)
+{
+ g_return_if_fail (GS_IS_APP_REVIEW (review));
+ g_free (review->reviewer);
+ review->reviewer = g_strdup (reviewer);
+}
+
+/**
+ * gs_app_review_get_date:
+ **/
+GDateTime *
+gs_app_review_get_date (GsAppReview *review)
+{
+ g_return_val_if_fail (GS_IS_APP_REVIEW (review), NULL);
+ return review->date;
+}
+
+/**
+ * gs_app_review_set_date:
+ */
+void
+gs_app_review_set_date (GsAppReview *review, GDateTime *date)
+{
+ g_return_if_fail (GS_IS_APP_REVIEW (review));
+ g_clear_pointer (&review->date, g_date_time_unref);
+ if (date)
+ review->date = g_date_time_ref (date);
+}
+
+static void
+gs_app_review_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+ GsAppReview *review = GS_APP_REVIEW (object);
+
+ switch (prop_id) {
+ case PROP_SUMMARY:
+ g_value_set_string (value, review->summary);
+ break;
+ case PROP_TEXT:
+ g_value_set_string (value, review->text);
+ break;
+ case PROP_RATING:
+ g_value_set_int (value, review->rating);
+ break;
+ case PROP_VERSION:
+ g_value_set_string (value, review->version);
+ break;
+ case PROP_REVIEWER:
+ g_value_set_string (value, review->reviewer);
+ break;
+ case PROP_DATE:
+ g_value_set_object (value, review->date);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_app_review_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+ GsAppReview *review = GS_APP_REVIEW (object);
+
+ switch (prop_id) {
+ case PROP_SUMMARY:
+ gs_app_review_set_summary (review, g_value_get_string (value));
+ break;
+ case PROP_TEXT:
+ gs_app_review_set_text (review, g_value_get_string (value));
+ break;
+ case PROP_RATING:
+ gs_app_review_set_rating (review, g_value_get_int (value));
+ break;
+ case PROP_VERSION:
+ gs_app_review_set_version (review, g_value_get_string (value));
+ break;
+ case PROP_REVIEWER:
+ gs_app_review_set_reviewer (review, g_value_get_string (value));
+ break;
+ case PROP_DATE:
+ gs_app_review_set_date (review, g_value_get_object (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_app_review_dispose (GObject *object)
+{
+ GsAppReview *review = GS_APP_REVIEW (object);
+
+ g_clear_pointer (&review->date, g_date_time_unref);
+
+ G_OBJECT_CLASS (gs_app_review_parent_class)->dispose (object);
+}
+
+static void
+gs_app_review_finalize (GObject *object)
+{
+ GsAppReview *review = GS_APP_REVIEW (object);
+
+ g_free (review->summary);
+ g_free (review->text);
+ g_free (review->reviewer);
+
+ G_OBJECT_CLASS (gs_app_review_parent_class)->finalize (object);
+}
+
+static void
+gs_app_review_class_init (GsAppReviewClass *klass)
+{
+ GParamSpec *pspec;
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->dispose = gs_app_review_dispose;
+ object_class->finalize = gs_app_review_finalize;
+ object_class->get_property = gs_app_review_get_property;
+ object_class->set_property = gs_app_review_set_property;
+
+ /**
+ * GsApp:summary:
+ */
+ pspec = g_param_spec_string ("summary", NULL, NULL,
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+ g_object_class_install_property (object_class, PROP_SUMMARY, pspec);
+
+ /**
+ * GsApp:text:
+ */
+ pspec = g_param_spec_string ("text", NULL, NULL,
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+ g_object_class_install_property (object_class, PROP_TEXT, pspec);
+
+ /**
+ * GsApp:rating:
+ */
+ pspec = g_param_spec_int ("rating", NULL, NULL,
+ -1, 100, -1,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+ g_object_class_install_property (object_class, PROP_RATING, pspec);
+
+ /**
+ * GsApp:version:
+ */
+ pspec = g_param_spec_string ("version", NULL, NULL,
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+ g_object_class_install_property (object_class, PROP_VERSION, pspec);
+
+ /**
+ * GsApp:reviewer:
+ */
+ pspec = g_param_spec_string ("reviewer", NULL, NULL,
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+ g_object_class_install_property (object_class, PROP_REVIEWER, pspec);
+
+ /**
+ * GsApp:date:
+ */
+ pspec = g_param_spec_object ("date", NULL, NULL,
+ GS_TYPE_APP_REVIEW,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+ g_object_class_install_property (object_class, PROP_DATE, pspec);
+}
+
+static void
+gs_app_review_init (GsAppReview *review)
+{
+ review->rating = -1;
+}
+
+/**
+ * gs_app_review_new:
+ *
+ * Return value: a new #GsAppReview object.
+ **/
+GsAppReview *
+gs_app_review_new (void)
+{
+ GsAppReview *review;
+ review = g_object_new (GS_TYPE_APP_REVIEW, NULL);
+ return GS_APP_REVIEW (review);
+}
+
+/* vim: set noexpandtab: */
diff --git a/src/gs-app-review.h b/src/gs-app-review.h
new file mode 100644
index 0000000..0f3c4c7
--- /dev/null
+++ b/src/gs-app-review.h
@@ -0,0 +1,64 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2015 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef __GS_APP_REVIEW_H
+#define __GS_APP_REVIEW_H
+
+#include <glib-object.h>
+#include "gs-app-review.h"
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_APP_REVIEW (gs_app_review_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsAppReview, gs_app_review, GS, APP_REVIEW, GObject)
+
+GsAppReview *gs_app_review_new (void);
+
+const gchar *gs_app_review_get_summary (GsAppReview *review);
+void gs_app_review_set_summary (GsAppReview *review,
+ const gchar *summary);
+
+const gchar *gs_app_review_get_text (GsAppReview *review);
+void gs_app_review_set_text (GsAppReview *review,
+ const gchar *text);
+
+gint gs_app_review_get_rating (GsAppReview *review);
+void gs_app_review_set_rating (GsAppReview *review,
+ gint rating);
+
+const gchar *gs_app_review_get_version (GsAppReview *review);
+void gs_app_review_set_version (GsAppReview *review,
+ const gchar *version);
+
+const gchar *gs_app_review_get_reviewer (GsAppReview *review);
+void gs_app_review_set_reviewer (GsAppReview *review,
+ const gchar *reviewer);
+
+GDateTime *gs_app_review_get_date (GsAppReview *review);
+void gs_app_review_set_date (GsAppReview *review,
+ GDateTime *date);
+
+G_END_DECLS
+
+#endif /* __GS_APP_REVIEW_H */
+
+/* vim: set noexpandtab: */
diff --git a/src/gs-app.c b/src/gs-app.c
index 4c60582..00cac08 100644
--- a/src/gs-app.c
+++ b/src/gs-app.c
@@ -82,6 +82,8 @@ struct _GsApp
gint rating;
gint rating_confidence;
GsAppRatingKind rating_kind;
+ GsAppReview *self_review;
+ GPtrArray *reviews; /* of GsAppReview */
guint64 size;
GsAppKind kind;
AsIdKind id_kind;
@@ -1595,6 +1597,49 @@ gs_app_set_rating_kind (GsApp *app, GsAppRatingKind rating_kind)
}
/**
+ * gs_app_get_self_review:
+ */
+GsAppReview *
+gs_app_get_self_review (GsApp *app)
+{
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return app->self_review;
+}
+
+/**
+ * gs_app_set_self_review:
+ */
+void
+gs_app_set_self_review (GsApp *app, GsAppReview *review)
+{
+ g_return_if_fail (GS_IS_APP (app));
+
+ g_clear_object (&app->self_review);
+ if (review != NULL)
+ app->self_review = g_object_ref (review);
+}
+
+/**
+ * gs_app_get_reviews:
+ */
+GPtrArray *
+gs_app_get_reviews (GsApp *app)
+{
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return app->reviews;
+}
+
+/**
+ * gs_app_add_review:
+ */
+void
+gs_app_add_review (GsApp *app, GsAppReview *review)
+{
+ g_return_if_fail (GS_IS_APP (app));
+ g_ptr_array_add (app->reviews, g_object_ref (review));
+}
+
+/**
* gs_app_get_size:
*/
guint64
@@ -2163,12 +2208,14 @@ gs_app_dispose (GObject *object)
g_clear_object (&app->bundle);
g_clear_object (&app->featured_pixbuf);
g_clear_object (&app->icon);
+ g_clear_object (&app->reviews);
g_clear_object (&app->pixbuf);
g_clear_pointer (&app->addons, g_ptr_array_unref);
g_clear_pointer (&app->history, g_ptr_array_unref);
g_clear_pointer (&app->related, g_ptr_array_unref);
g_clear_pointer (&app->screenshots, g_ptr_array_unref);
+ g_clear_pointer (&app->reviews, g_ptr_array_unref);
G_OBJECT_CLASS (gs_app_parent_class)->dispose (object);
}
@@ -2318,6 +2365,7 @@ gs_app_init (GsApp *app)
app->related = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
app->history = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
app->screenshots = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+ app->reviews = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
app->metadata = g_hash_table_new_full (g_str_hash,
g_str_equal,
g_free,
diff --git a/src/gs-app.h b/src/gs-app.h
index af985bc..4f4d2de 100644
--- a/src/gs-app.h
+++ b/src/gs-app.h
@@ -26,6 +26,8 @@
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <appstream-glib.h>
+#include "gs-app-review.h"
+
G_BEGIN_DECLS
#define GS_TYPE_APP (gs_app_get_type ())
@@ -203,6 +205,12 @@ void gs_app_set_rating_confidence (GsApp *app,
GsAppRatingKind gs_app_get_rating_kind (GsApp *app);
void gs_app_set_rating_kind (GsApp *app,
GsAppRatingKind rating_kind);
+GsAppReview *gs_app_get_self_review (GsApp *app);
+void gs_app_set_self_review (GsApp *app,
+ GsAppReview *review);
+GPtrArray *gs_app_get_reviews (GsApp *app);
+void gs_app_add_review (GsApp *app,
+ GsAppReview *review);
guint64 gs_app_get_size (GsApp *app);
void gs_app_set_size (GsApp *app,
guint64 size);
diff --git a/src/gs-plugin-loader.c b/src/gs-plugin-loader.c
index 84f35e9..66c1303 100644
--- a/src/gs-plugin-loader.c
+++ b/src/gs-plugin-loader.c
@@ -49,6 +49,9 @@ typedef struct
guint updates_changed_id;
gboolean online;
+
+ gboolean supports_reviews;
+ gchar **review_auths;
} GsPluginLoaderPrivate;
G_DEFINE_TYPE_WITH_PRIVATE (GsPluginLoader, gs_plugin_loader, G_TYPE_OBJECT)
@@ -2587,6 +2590,11 @@ gs_plugin_loader_app_action_async (GsPluginLoader *plugin_loader,
state->state_success = AS_APP_STATE_UNKNOWN;
state->state_failure = AS_APP_STATE_UNKNOWN;
break;
+ case GS_PLUGIN_LOADER_ACTION_SET_REVIEW:
+ state->function_name = "gs_plugin_app_set_review";
+ state->state_success = AS_APP_STATE_UNKNOWN;
+ state->state_failure = AS_APP_STATE_UNKNOWN;
+ break;
default:
g_assert_not_reached ();
break;
@@ -2798,13 +2806,17 @@ gs_plugin_loader_updates_changed_cb (GsPlugin *plugin, gpointer user_data)
*/
static GsPlugin *
gs_plugin_loader_open_plugin (GsPluginLoader *plugin_loader,
- const gchar *filename)
+ const gchar *filename,
+ GPtrArray *review_auths)
{
GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
gboolean ret;
GModule *module;
GsPluginGetNameFunc plugin_name = NULL;
GsPluginGetDepsFunc plugin_deps = NULL;
+ GsPluginGetSupportsReviewsFunc plugin_supports_reviews = NULL;
+ GsPluginGetReviewAuthFunc plugin_review_auth = NULL;
+ const gchar *review_auth;
GsPlugin *plugin = NULL;
module = g_module_open (filename, 0);
@@ -2829,6 +2841,21 @@ gs_plugin_loader_open_plugin (GsPluginLoader *plugin_loader,
"gs_plugin_get_deps",
(gpointer *) &plugin_deps);
+ /* Check if this plugin can do reviews */
+ (void) g_module_symbol (module,
+ "gs_plugin_get_supports_reviews",
+ (gpointer *) &plugin_supports_reviews);
+ if (plugin_supports_reviews && plugin_supports_reviews (plugin))
+ priv->supports_reviews = TRUE;
+
+ /* Check if this plugin requires any authorization for reviews */
+ (void) g_module_symbol (module,
+ "gs_plugin_get_review_auth",
+ (gpointer *) &plugin_review_auth);
+ review_auth = plugin_review_auth != NULL ? plugin_review_auth (plugin) : NULL;
+ if (review_auth)
+ g_ptr_array_add (review_auths, g_strdup (review_auth));
+
/* print what we know */
plugin = g_slice_new0 (GsPlugin);
plugin->enabled = TRUE;
@@ -2951,6 +2978,7 @@ gs_plugin_loader_setup (GsPluginLoader *plugin_loader, GError **error)
guint i;
guint j;
g_autoptr(GDir) dir = NULL;
+ GPtrArray *review_auths;
g_return_val_if_fail (priv->location != NULL, FALSE);
@@ -2964,6 +2992,7 @@ gs_plugin_loader_setup (GsPluginLoader *plugin_loader, GError **error)
/* try to open each plugin */
g_debug ("searching for plugins in %s", priv->location);
+ review_auths = g_ptr_array_new ();
do {
g_autofree gchar *filename_plugin = NULL;
filename_tmp = g_dir_read_name (dir);
@@ -2974,9 +3003,12 @@ gs_plugin_loader_setup (GsPluginLoader *plugin_loader, GError **error)
filename_plugin = g_build_filename (priv->location,
filename_tmp,
NULL);
- gs_plugin_loader_open_plugin (plugin_loader, filename_plugin);
+ gs_plugin_loader_open_plugin (plugin_loader, filename_plugin, review_auths);
} while (TRUE);
+ g_ptr_array_add (review_auths, NULL);
+ priv->review_auths = (char **) g_ptr_array_free (review_auths, FALSE);
+
/* order by deps */
do {
changes = FALSE;
@@ -3108,6 +3140,7 @@ gs_plugin_loader_finalize (GObject *object)
g_strfreev (priv->compatible_projects);
g_free (priv->location);
+ g_strfreev (priv->review_auths);
g_mutex_clear (&priv->pending_apps_mutex);
g_mutex_clear (&priv->app_cache_mutex);
@@ -3684,6 +3717,26 @@ gs_plugin_loader_offline_update_finish (GsPluginLoader *plugin_loader,
return g_task_propagate_boolean (G_TASK (res), error);
}
+/**
+ * gs_plugin_loader_get_supports_reviews:
+ */
+gboolean
+gs_plugin_loader_get_supports_reviews (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ return priv->supports_reviews;
+}
+
+/**
+ * gs_plugin_loader_get_review_auths:
+ */
+gchar **
+gs_plugin_loader_get_review_auths (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ return priv->review_auths;
+}
+
/******************************************************************************/
/* vim: set noexpandtab: */
diff --git a/src/gs-plugin-loader.h b/src/gs-plugin-loader.h
index b823efd..6edc759 100644
--- a/src/gs-plugin-loader.h
+++ b/src/gs-plugin-loader.h
@@ -56,6 +56,7 @@ typedef enum {
GS_PLUGIN_LOADER_ACTION_INSTALL,
GS_PLUGIN_LOADER_ACTION_REMOVE,
GS_PLUGIN_LOADER_ACTION_SET_RATING,
+ GS_PLUGIN_LOADER_ACTION_SET_REVIEW,
GS_PLUGIN_LOADER_ACTION_LAST
} GsPluginLoaderAction;
@@ -212,6 +213,8 @@ GsApp *gs_plugin_loader_dedupe (GsPluginLoader
*plugin_loader,
GsApp *app);
void gs_plugin_loader_set_network_status (GsPluginLoader *plugin_loader,
gboolean online);
+gboolean gs_plugin_loader_get_supports_reviews (GsPluginLoader *plugin_loader);
+gchar **gs_plugin_loader_get_review_auths (GsPluginLoader *plugin_loader);
G_END_DECLS
diff --git a/src/gs-plugin.h b/src/gs-plugin.h
index 78971b4..e5341e9 100644
--- a/src/gs-plugin.h
+++ b/src/gs-plugin.h
@@ -101,6 +101,7 @@ typedef enum {
GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS = 1 << 13,
GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES = 1 << 14,
GS_PLUGIN_REFINE_FLAGS_ALLOW_NO_APPDATA = 1 << 15,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS = 1 << 16,
GS_PLUGIN_REFINE_FLAGS_LAST
} GsPluginRefineFlags;
@@ -116,6 +117,8 @@ typedef enum {
typedef const gchar *(*GsPluginGetNameFunc) (void);
typedef const gchar **(*GsPluginGetDepsFunc) (GsPlugin *plugin);
+typedef gboolean *(*GsPluginGetSupportsReviewsFunc) (GsPlugin *plugin);
+typedef const gchar *(*GsPluginGetReviewAuthFunc) (GsPlugin *plugin);
typedef void (*GsPluginFunc) (GsPlugin *plugin);
typedef gboolean (*GsPluginSearchFunc) (GsPlugin *plugin,
gchar **value,
@@ -246,6 +249,12 @@ gboolean gs_plugin_app_set_rating (GsPlugin *plugin,
GsApp *app,
GCancellable *cancellable,
GError **error);
+gboolean gs_plugin_get_supports_review (GsPlugin *plugin);
+const gchar *gs_plugin_get_review_auth (GsPlugin *plugin);
+gboolean gs_plugin_app_set_review (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error);
gboolean gs_plugin_refresh (GsPlugin *plugin,
guint cache_age,
GsPluginRefreshFlags flags,
diff --git a/src/gs-shell-details.c b/src/gs-shell-details.c
index f86d7c4..0fd0877 100644
--- a/src/gs-shell-details.c
+++ b/src/gs-shell-details.c
@@ -35,6 +35,8 @@
#include "gs-screenshot-image.h"
#include "gs-progress-button.h"
#include "gs-star-widget.h"
+#include "gs-app-review-dialog.h"
+#include "gs-app-review-row.h"
typedef enum {
GS_SHELL_DETAILS_STATE_LOADING,
@@ -85,6 +87,8 @@ struct _GsShellDetails
GtkWidget *label_details_version_value;
GtkWidget *label_pending;
GtkWidget *list_box_addons;
+ GtkWidget *box_reviews;
+ GtkWidget *list_box_reviews;
GtkWidget *scrolledwindow_details;
GtkWidget *spinner_details;
GtkWidget *spinner_install_remove;
@@ -863,6 +867,31 @@ gs_shell_details_refresh_addons (GsShellDetails *self)
}
}
+static void
+gs_shell_details_refresh_reviews (GsShellDetails *self)
+{
+ GPtrArray *reviews;
+ guint i;
+
+ if (!gs_plugin_loader_get_supports_reviews (self->plugin_loader))
+ return;
+
+ gs_container_remove_all (GTK_CONTAINER (self->list_box_reviews));
+
+ reviews = gs_app_get_reviews (self->app);
+ for (i = 0; i < reviews->len; i++) {
+ GsAppReview *review;
+ GtkWidget *row;
+
+ review = g_ptr_array_index (reviews, i);
+
+ row = gs_app_review_row_new (review);
+
+ gtk_container_add (GTK_CONTAINER (self->list_box_reviews), row);
+ gtk_widget_show (row);
+ }
+}
+
/**
* gs_shell_details_app_refine_cb:
**/
@@ -893,6 +922,7 @@ gs_shell_details_app_refine_cb (GObject *source,
gs_shell_details_refresh_screenshots (self);
gs_shell_details_refresh_addons (self);
+ gs_shell_details_refresh_reviews (self);
gs_shell_details_refresh_all (self);
gs_shell_details_set_state (self, GS_SHELL_DETAILS_STATE_READY);
}
@@ -963,6 +993,7 @@ gs_shell_details_filename_to_app_cb (GObject *source,
gs_shell_details_switch_to (self);
gs_shell_details_refresh_screenshots (self);
gs_shell_details_refresh_addons (self);
+ gs_shell_details_refresh_reviews (self);
gs_shell_details_refresh_all (self);
gs_shell_details_set_state (self, GS_SHELL_DETAILS_STATE_READY);
}
@@ -977,7 +1008,8 @@ gs_shell_details_set_filename (GsShellDetails *self, const gchar *filename)
gs_plugin_loader_filename_to_app_async (self->plugin_loader,
filename,
GS_PLUGIN_REFINE_FLAGS_DEFAULT |
- GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS,
self->cancellable,
gs_shell_details_filename_to_app_cb,
self);
@@ -999,7 +1031,8 @@ gs_shell_details_load (GsShellDetails *self)
GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN |
GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH |
GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL |
- GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS,
self->cancellable,
gs_shell_details_app_refine_cb,
self);
@@ -1185,6 +1218,25 @@ gs_shell_details_app_set_ratings_cb (GObject *source,
}
}
+
+/**
+ * gs_shell_details_app_set_review_cb:
+ **/
+static void
+gs_shell_details_app_set_review_cb (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
+ GsShellDetails *self = GS_SHELL_DETAILS (user_data);
+ g_autoptr(GError) error = NULL;
+
+ if (!gs_plugin_loader_app_action_finish (plugin_loader, res, &error)) {
+ g_warning ("failed to set review %s: %s",
+ gs_app_get_id (self->app), error->message);
+ }
+}
+
/**
* gs_shell_details_rating_changed_cb:
**/
@@ -1193,6 +1245,43 @@ gs_shell_details_rating_changed_cb (GsStarWidget *star,
guint rating,
GsShellDetails *self)
{
+ GtkWidget *dialog;
+ GtkResponseType response;
+ gchar **review_auths;
+
+ dialog = gs_app_review_dialog_new ();
+ gs_app_review_dialog_set_rating (GS_APP_REVIEW_DIALOG (dialog), rating);
+
+ review_auths = gs_plugin_loader_get_review_auths (self->plugin_loader);
+ // FIXME: Use these
+
+ gtk_window_set_transient_for (GTK_WINDOW (dialog), gs_shell_get_window (self->shell));
+ response = gtk_dialog_run (GTK_DIALOG (dialog));
+ if (response == GTK_RESPONSE_OK) {
+ g_autoptr(GsAppReview) review = NULL;
+ g_autoptr(GDateTime) now = NULL;
+ g_autofree gchar *text = NULL;
+
+ review = gs_app_review_new ();
+ gs_app_review_set_summary (review, gs_app_review_dialog_get_summary (GS_APP_REVIEW_DIALOG
(dialog)));
+ text = gs_app_review_dialog_get_text (GS_APP_REVIEW_DIALOG (dialog));
+ gs_app_review_set_text (review, text);
+ gs_app_review_set_rating (review, gs_app_review_dialog_get_rating (GS_APP_REVIEW_DIALOG
(dialog)));
+ gs_app_review_set_version (review, gs_app_get_version (self->app));
+ gs_app_review_set_reviewer (review, "Joe Bloggs"); // FIXME
+ now = g_date_time_new_now_local ();
+ gs_app_review_set_date (review, now);
+
+ /* call into the plugins to set the new value */
+ gs_app_set_self_review (self->app, review);
+ gs_plugin_loader_app_action_async (self->plugin_loader, self->app,
+ GS_PLUGIN_LOADER_ACTION_SET_REVIEW,
+ self->cancellable,
+ gs_shell_details_app_set_review_cb,
+ self);
+ }
+ gtk_widget_destroy (dialog);
+#if 0
g_debug ("%s rating changed from %i%% to %i%%",
gs_app_get_id (self->app),
gs_app_get_rating (self->app),
@@ -1206,6 +1295,7 @@ gs_shell_details_rating_changed_cb (GsStarWidget *star,
self->cancellable,
gs_shell_details_app_set_ratings_cb,
self);
+#endif
}
static void
@@ -1240,6 +1330,10 @@ gs_shell_details_setup (GsShellDetails *self,
self->builder = g_object_ref (builder);
self->cancellable = g_object_ref (cancellable);
+ /* Show review widgets if we have plugins that provide them */
+ if (gs_plugin_loader_get_supports_reviews (plugin_loader))
+ gtk_widget_set_visible (self->box_reviews, TRUE);
+
/* set up star ratings */
self->star = gs_star_widget_new ();
g_signal_connect (self->star, "rating-changed",
@@ -1345,6 +1439,8 @@ gs_shell_details_class_init (GsShellDetailsClass *klass)
gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_details_version_value);
gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_pending);
gtk_widget_class_bind_template_child (widget_class, GsShellDetails, list_box_addons);
+ gtk_widget_class_bind_template_child (widget_class, GsShellDetails, box_reviews);
+ gtk_widget_class_bind_template_child (widget_class, GsShellDetails, list_box_reviews);
gtk_widget_class_bind_template_child (widget_class, GsShellDetails, scrolledwindow_details);
gtk_widget_class_bind_template_child (widget_class, GsShellDetails, spinner_details);
gtk_widget_class_bind_template_child (widget_class, GsShellDetails, spinner_install_remove);
diff --git a/src/gs-shell-details.ui b/src/gs-shell-details.ui
index 0e1511f..c4c1571 100644
--- a/src/gs-shell-details.ui
+++ b/src/gs-shell-details.ui
@@ -1022,7 +1022,39 @@
<property name="position">11</property>
</packing>
</child>
-
+ <child>
+ <object class="GtkBox" id="box_reviews">
+ <property name="orientation">vertical</property>
+ <property name="margin_top">28</property>
+ <child>
+ <object class="GtkLabel" id="application_details_reviews_title">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_bottom">12</property>
+ <property name="halign">start</property>
+ <property name="valign">start</property>
+ <property name="hexpand">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Reviews</property>
+ <style>
+ <class name="application-reviews-title"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBox" id="list_box_reviews">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="selection_mode">none</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">12</property>
+ </packing>
+ </child>
</object>
</child>
</object>
diff --git a/src/gtk-style-hc.css b/src/gtk-style-hc.css
index e222160..ceebcb3 100644
--- a/src/gtk-style-hc.css
+++ b/src/gtk-style-hc.css
@@ -91,6 +91,15 @@
.application-details-summary {
}
+.application-reviews-title {
+ font-weight: bold;
+ font-size: 14px;
+}
+
+.review-summary {
+ font-weight: bold;
+}
+
.application-details-description {
}
diff --git a/src/gtk-style.css b/src/gtk-style.css
index 093f812..c8d0489 100644
--- a/src/gtk-style.css
+++ b/src/gtk-style.css
@@ -98,6 +98,15 @@
.application-details-description {
}
+.application-reviews-title {
+ font-weight: bold;
+ font-size: 14px;
+}
+
+.review-summary {
+ font-weight: bold;
+}
+
.error-label {
text-shadow: none;
}
diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am
index 3c187f5..cf21b8a 100644
--- a/src/plugins/Makefile.am
+++ b/src/plugins/Makefile.am
@@ -11,6 +11,8 @@ AM_CPPFLAGS = \
$(SQLITE_CFLAGS) \
$(FWUPD_CFLAGS) \
$(LIMBA_CFLAGS) \
+ $(JSON_GLIB_CFLAGS) \
+ $(OAUTH_CFLAGS) \
-DBINDIR=\"$(bindir)\" \
-DDATADIR=\"$(datadir)\" \
-DGS_MODULESETDIR=\"$(datadir)/gnome-software/modulesets.d\" \
@@ -34,6 +36,7 @@ plugin_LTLIBRARIES = \
libgs_plugin_menu-spec-categories.la \
libgs_plugin_menu-spec-refine.la \
libgs_plugin_local-ratings.la \
+ libgs_plugin_ubuntu-reviews.la \
libgs_plugin_fedora_tagger_ratings.la \
libgs_plugin_fedora_tagger_usage.la \
libgs_plugin_epiphany.la \
@@ -130,6 +133,11 @@ libgs_plugin_local_ratings_la_LIBADD = $(GS_PLUGIN_LIBS) $(SQLITE_LIBS)
libgs_plugin_local_ratings_la_LDFLAGS = -module -avoid-version
libgs_plugin_local_ratings_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
+libgs_plugin_ubuntu_reviews_la_SOURCES = gs-plugin-ubuntu-reviews.c
+libgs_plugin_ubuntu_reviews_la_LIBADD = $(GS_PLUGIN_LIBS) $(SOUP_LIBS) $(JSON_GLIB_LIBS) $(OAUTH_LIBS)
$(SQLITE_LIBS)
+libgs_plugin_ubuntu_reviews_la_LDFLAGS = -module -avoid-version
+libgs_plugin_ubuntu_reviews_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARNINGFLAGS_C)
+
libgs_plugin_packagekit_la_SOURCES = \
gs-plugin-packagekit.c \
packagekit-common.c \
diff --git a/src/plugins/gs-plugin-local-ratings.c b/src/plugins/gs-plugin-local-ratings.c
index ef8c199..cf18649 100644
--- a/src/plugins/gs-plugin-local-ratings.c
+++ b/src/plugins/gs-plugin-local-ratings.c
@@ -50,6 +50,14 @@ gs_plugin_initialize (GsPlugin *plugin)
{
/* create private area */
plugin->priv = GS_PLUGIN_GET_PRIVATE (GsPluginPrivate);
+
+ /* Don't run on Ubuntu - it has its own review plugin */
+ if (gs_plugin_check_distro_id (plugin, "ubuntu")) {
+ gs_plugin_set_enabled (plugin, FALSE);
+ g_debug ("disabling '%s' as we're on Ubuntu", plugin->name);
+ return;
+ }
+
plugin->priv->db_path = g_build_filename (g_get_user_data_dir (),
"gnome-software",
"hardcoded-ratings.db",
diff --git a/src/plugins/gs-plugin-ubuntu-reviews.c b/src/plugins/gs-plugin-ubuntu-reviews.c
new file mode 100644
index 0000000..feda832
--- /dev/null
+++ b/src/plugins/gs-plugin-ubuntu-reviews.c
@@ -0,0 +1,860 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2015 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <config.h>
+
+#include <math.h>
+#include <libsoup/soup.h>
+#include <json-glib/json-glib.h>
+#include <oauth.h>
+#include <sqlite3.h>
+
+#include <gs-plugin.h>
+#include <gs-utils.h>
+
+struct GsPluginPrivate {
+ gchar *db_path;
+ sqlite3 *db;
+ gsize db_loaded;
+ SoupSession *session;
+};
+
+typedef struct {
+ gint64 one_star_count;
+ gint64 two_star_count;
+ gint64 three_star_count;
+ gint64 four_star_count;
+ gint64 five_star_count;
+} Histogram;
+
+const gchar *
+gs_plugin_get_name (void)
+{
+ return "ubuntu-reviews";
+}
+
+#define UBUNTU_REVIEWS_SERVER "https://reviews.ubuntu.com/reviews"
+
+/* Download new stats every three months */
+// FIXME: Much shorter time?
+#define REVIEW_STATS_AGE_MAX (60 * 60 * 24 * 7 * 4 * 3)
+
+void
+gs_plugin_initialize (GsPlugin *plugin)
+{
+ /* create private area */
+ plugin->priv = GS_PLUGIN_GET_PRIVATE (GsPluginPrivate);
+
+ /* check that we are running on Ubuntu */
+ if (!gs_plugin_check_distro_id (plugin, "ubuntu")) {
+ gs_plugin_set_enabled (plugin, FALSE);
+ g_debug ("disabling '%s' as we're not Ubuntu", plugin->name);
+ return;
+ }
+
+ plugin->priv->db_path = g_build_filename (g_get_user_data_dir (),
+ "gnome-software",
+ "ubuntu-reviews.db",
+ NULL);
+}
+
+const gchar **
+gs_plugin_get_deps (GsPlugin *plugin)
+{
+ static const gchar *deps[] = { NULL };
+ return deps;
+}
+
+void
+gs_plugin_destroy (GsPlugin *plugin)
+{
+ if (plugin->priv->db != NULL)
+ sqlite3_close (plugin->priv->db);
+ if (plugin->priv->session != NULL)
+ g_object_unref (plugin->priv->session);
+}
+
+static gboolean
+setup_networking (GsPlugin *plugin, GError **error)
+{
+ /* already set up */
+ if (plugin->priv->session != NULL)
+ return TRUE;
+
+ /* set up a session */
+ plugin->priv->session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT,
+ "gnome-software",
+ NULL);
+ if (plugin->priv->session == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "%s: failed to setup networking",
+ plugin->name);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static gint
+get_timestamp_sqlite_cb (void *data, gint argc,
+ gchar **argv, gchar **col_name)
+{
+ gint64 *timestamp = (gint64 *) data;
+ *timestamp = g_ascii_strtoll (argv[0], NULL, 10);
+ return 0;
+}
+
+static gboolean
+set_package_stats (GsPlugin *plugin,
+ const gchar *package_name,
+ Histogram *histogram,
+ GError **error)
+{
+ char *error_msg = NULL;
+ gint result;
+ g_autofree gchar *statement = NULL;
+
+ statement = g_strdup_printf ("INSERT OR REPLACE INTO review_stats (package_name, "
+ "one_star_count, two_star_count, three_star_count, "
+ "four_star_count, five_star_count) "
+ "VALUES ('%s', '%" G_GINT64_FORMAT "', '%" G_GINT64_FORMAT"', '%"
G_GINT64_FORMAT "', '%" G_GINT64_FORMAT "', '%" G_GINT64_FORMAT "');",
+ package_name, histogram->one_star_count, histogram->two_star_count,
+ histogram->three_star_count, histogram->four_star_count,
histogram->five_star_count);
+ result = sqlite3_exec (plugin->priv->db, statement, NULL, NULL, &error_msg);
+ if (result != SQLITE_OK) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "SQL error: %s", error_msg);
+ sqlite3_free (error_msg);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+set_timestamp (GsPlugin *plugin,
+ const gchar *type,
+ GError **error)
+{
+ char *error_msg = NULL;
+ gint result;
+ g_autofree gchar *statement = NULL;
+
+ statement = g_strdup_printf ("INSERT OR REPLACE INTO timestamps (key, value) "
+ "VALUES ('%s', '%" G_GINT64_FORMAT "');",
+ type,
+ g_get_real_time () / G_USEC_PER_SEC);
+ result = sqlite3_exec (plugin->priv->db, statement, NULL, NULL, &error_msg);
+ if (result != SQLITE_OK) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "SQL error: %s", error_msg);
+ sqlite3_free (error_msg);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static gint
+get_rating_sqlite_cb (void *data,
+ gint argc,
+ gchar **argv,
+ gchar **col_name)
+{
+ Histogram *histogram = (Histogram *) data;
+ histogram->one_star_count = g_ascii_strtoll (argv[0], NULL, 10);
+ histogram->two_star_count = g_ascii_strtoll (argv[1], NULL, 10);
+ histogram->three_star_count = g_ascii_strtoll (argv[2], NULL, 10);
+ histogram->four_star_count = g_ascii_strtoll (argv[3], NULL, 10);
+ histogram->five_star_count = g_ascii_strtoll (argv[4], NULL, 10);
+ return 0;
+}
+
+static gboolean
+get_rating (GsPlugin *plugin,
+ const gchar *package_name,
+ gint *rating,
+ GError **error)
+{
+ Histogram histogram = { 0, 0, 0, 0, 0 };
+ gchar *error_msg = NULL;
+ gint result, n_ratings;
+ g_autofree gchar *statement = NULL;
+
+ /* Get histogram from the database */
+ statement = g_strdup_printf ("SELECT one_star_count, two_star_count, three_star_count,
four_star_count, five_star_count FROM review_stats "
+ "WHERE package_name = '%s'", package_name);
+ result = sqlite3_exec (plugin->priv->db,
+ statement,
+ get_rating_sqlite_cb,
+ &histogram,
+ &error_msg);
+ if (result != SQLITE_OK) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "SQL error: %s", error_msg);
+ sqlite3_free (error_msg);
+ return FALSE;
+ }
+
+ /* Convert to a rating */
+ // FIXME: Convert to a Wilson score
+ n_ratings = histogram.one_star_count + histogram.two_star_count + histogram.three_star_count +
histogram.four_star_count + histogram.five_star_count;
+ if (n_ratings == 0)
+ *rating = -1;
+ else
+ *rating = ((histogram.one_star_count * 20) + (histogram.two_star_count * 40) +
(histogram.three_star_count * 60) + (histogram.four_star_count * 80) + (histogram.five_star_count * 100)) /
n_ratings;
+g_warning ("%s %zi %zi %zi %zi %zi / %d -> %d", package_name, histogram.one_star_count,
histogram.two_star_count, histogram.three_star_count, histogram.four_star_count, histogram.five_star_count,
n_ratings, *rating);
+
+ return TRUE;
+}
+
+static gboolean
+parse_histogram (const gchar *text, Histogram *histogram)
+{
+ JsonParser *parser = NULL;
+ JsonArray *array;
+ gboolean result = FALSE;
+
+ /* Histogram is a five element JSON array, e.g. "[1, 3, 5, 8, 4]" */
+ parser = json_parser_new ();
+ if (!json_parser_load_from_data (parser, text, -1, NULL))
+ goto out;
+ if (!JSON_NODE_HOLDS_ARRAY (json_parser_get_root (parser)))
+ goto out;
+ array = json_node_get_array (json_parser_get_root (parser));
+ if (json_array_get_length (array) != 5)
+ goto out;
+ histogram->one_star_count = json_array_get_int_element (array, 0);
+ histogram->two_star_count = json_array_get_int_element (array, 1);
+ histogram->three_star_count = json_array_get_int_element (array, 2);
+ histogram->four_star_count = json_array_get_int_element (array, 3);
+ histogram->five_star_count = json_array_get_int_element (array, 4);
+ result = TRUE;
+
+out:
+ g_clear_object (&parser);
+
+ return result;
+}
+
+static gboolean
+parse_review_entry (JsonNode *node, const gchar **package_name, Histogram *histogram)
+{
+ JsonObject *object;
+ const gchar *name = NULL, *histogram_text = NULL;
+
+ if (!JSON_NODE_HOLDS_OBJECT (node))
+ return FALSE;
+
+ object = json_node_get_object (node);
+
+ name = json_object_get_string_member (object, "package_name");
+ histogram_text = json_object_get_string_member (object, "histogram");
+ if (!name || !histogram_text)
+ return FALSE;
+
+ if (!parse_histogram (histogram_text, histogram))
+ return FALSE;
+ *package_name = name;
+
+ return TRUE;
+}
+
+static gboolean
+parse_review_entries (GsPlugin *plugin, const gchar *text, GError **error)
+{
+ JsonParser *parser = NULL;
+ JsonArray *array;
+ gint i;
+ gboolean result = FALSE;
+
+ parser = json_parser_new ();
+ if (!json_parser_load_from_data (parser, text, -1, error))
+ goto out;
+ if (!JSON_NODE_HOLDS_ARRAY (json_parser_get_root (parser)))
+ goto out;
+ array = json_node_get_array (json_parser_get_root (parser));
+ for (i = 0; i < json_array_get_length (array); i++) {
+ const gchar *package_name;
+ Histogram histogram;
+
+ /* Read in from JSON... (skip bad entries) */
+ if (!parse_review_entry (json_array_get_element (array, i), &package_name, &histogram))
+ continue;
+
+ /* ...write into the database (abort everything if can't write) */
+ if (!set_package_stats (plugin, package_name, &histogram, error))
+ goto out;
+ }
+ result = TRUE;
+
+out:
+ g_clear_object (&parser);
+
+ return result;
+}
+
+static gboolean
+download_review_stats (GsPlugin *plugin, GError **error)
+{
+ guint status_code;
+ g_autofree gchar *uri = NULL;
+ g_autoptr(SoupMessage) msg = NULL;
+ g_auto(GStrv) split = NULL;
+
+ /* Get the review stats using HTTP */
+ uri = g_strdup_printf ("%s/api/1.0/review-stats/any/any/",
+ UBUNTU_REVIEWS_SERVER);
+ msg = soup_message_new (SOUP_METHOD_GET, uri);
+ if (!setup_networking (plugin, error))
+ return FALSE;
+ status_code = soup_session_send_message (plugin->priv->session, msg);
+ if (status_code != SOUP_STATUS_OK) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Failed to download Ubuntu reviews dump: %s",
+ soup_status_get_phrase (status_code));
+ return FALSE;
+ }
+
+ /* Extract the stats from the data */
+ if (!parse_review_entries (plugin, msg->response_body->data, error))
+ return FALSE;
+
+ /* Record the time we downloaded it */
+ return set_timestamp (plugin, "stats_mtime", error);
+}
+
+static gboolean
+load_database (GsPlugin *plugin, GError **error)
+{
+ const gchar *statement;
+ gboolean rebuild_ratings = FALSE;
+ char *error_msg = NULL;
+ gint result;
+ gint64 stats_mtime = 0;
+ gint64 now;
+ g_autoptr(GError) error_local = NULL;
+
+ g_debug ("trying to open database '%s'", plugin->priv->db_path);
+ if (!gs_mkdir_parent (plugin->priv->db_path, error))
+ return FALSE;
+ result = sqlite3_open (plugin->priv->db_path, &plugin->priv->db);
+ if (result != SQLITE_OK) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Can't open Ubuntu review statistics database: %s",
+ sqlite3_errmsg (plugin->priv->db));
+ return FALSE;
+ }
+
+ /* We don't need to keep doing fsync */
+ sqlite3_exec (plugin->priv->db, "PRAGMA synchronous=OFF",
+ NULL, NULL, NULL);
+
+ /* Create a table to store the stats */
+ result = sqlite3_exec (plugin->priv->db, "SELECT * FROM review_stats LIMIT 1", NULL, NULL,
&error_msg);
+ if (result != SQLITE_OK) {
+ g_debug ("creating table to repair: %s", error_msg);
+ sqlite3_free (error_msg);
+ statement = "CREATE TABLE review_stats ("
+ "package_name TEXT PRIMARY KEY,"
+ "one_star_count INTEGER DEFAULT 0,"
+ "two_star_count INTEGER DEFAULT 0,"
+ "three_star_count INTEGER DEFAULT 0,"
+ "four_star_count INTEGER DEFAULT 0,"
+ "five_star_count INTEGER DEFAULT 0);";
+ sqlite3_exec (plugin->priv->db, statement, NULL, NULL, NULL);
+ rebuild_ratings = TRUE;
+ }
+
+ /* Create a table to store local reviews */
+ result = sqlite3_exec (plugin->priv->db, "SELECT * FROM reviews LIMIT 1", NULL, NULL, &error_msg);
+ if (result != SQLITE_OK) {
+ g_debug ("creating table to repair: %s", error_msg);
+ sqlite3_free (error_msg);
+ statement = "CREATE TABLE reviews ("
+ "package_name TEXT PRIMARY KEY,"
+ "id TEXT,"
+ "version TEXT,"
+ "date TEXT,"
+ "rating INTEGER,"
+ "summary TEXT,"
+ "text TEXT);";
+ sqlite3_exec (plugin->priv->db, statement, NULL, NULL, NULL);
+ rebuild_ratings = TRUE;
+ }
+
+ /* Create a table to store timestamps */
+ result = sqlite3_exec (plugin->priv->db,
+ "SELECT value FROM timestamps WHERE key = 'stats_mtime' LIMIT 1",
+ get_timestamp_sqlite_cb, &stats_mtime,
+ &error_msg);
+ if (result != SQLITE_OK) {
+ g_debug ("creating table to repair: %s", error_msg);
+ sqlite3_free (error_msg);
+ statement = "CREATE TABLE timestamps ("
+ "key TEXT PRIMARY KEY,"
+ "value INTEGER DEFAULT 0);";
+ sqlite3_exec (plugin->priv->db, statement, NULL, NULL, NULL);
+
+ /* Set the time of database creation */
+ if (!set_timestamp (plugin, "stats_ctime", error))
+ return FALSE;
+ }
+
+ /* Download data if we have none or it is out of date */
+ now = g_get_real_time () / G_USEC_PER_SEC;
+ if (stats_mtime == 0 || rebuild_ratings) {
+ g_debug ("No Ubuntu review statistics");
+ if (!download_review_stats (plugin, &error_local)) {
+ g_warning ("Failed to get Ubuntu review statistics: %s",
+ error_local->message);
+ return TRUE;
+ }
+ } else if (now - stats_mtime > REVIEW_STATS_AGE_MAX) {
+ g_debug ("Ubuntu review statistics was %" G_GINT64_FORMAT
+ " days old, so regetting",
+ (now - stats_mtime) / ( 60 * 60 * 24));
+ if (!download_review_stats (plugin, error))
+ return FALSE;
+ } else {
+ g_debug ("Ubuntu review statistics %" G_GINT64_FORMAT
+ " days old, so no need to redownload",
+ (now - stats_mtime) / ( 60 * 60 * 24));
+ }
+ return TRUE;
+}
+
+static GDateTime *
+parse_date_time (const gchar *text)
+{
+ const gchar *format = "YYYY-MM-DD HH:MM:SS";
+ int i, value_index, values[6] = { 0, 0, 0, 0, 0, 0 };
+
+ if (!text)
+ return NULL;
+
+ /* Extract the numbers as shown in the format */
+ for (i = 0, value_index = 0; text[i] && format[i] && value_index < 6; i++) {
+ char c = text[i];
+
+ if (c == '-' || c == ' ' || c == ':') {
+ if (format[i] != c)
+ return NULL;
+ value_index++;
+ } else {
+ int d = c - '0';
+ if (d < 0 || d > 9)
+ return NULL;
+ values[value_index] = values[value_index] * 10 + d;
+ }
+ }
+
+ /* We didn't match the format */
+ if (format[i] != '\0' || text[i] != '\0' || value_index != 5)
+ return NULL;
+
+ /* Use the numbers to create a GDateTime object */
+ return g_date_time_new_utc (values[0], values[1], values[2], values[3], values[4], values[5]);
+}
+
+static GsAppReview *
+parse_review (JsonNode *node)
+{
+ GsAppReview *review;
+ JsonObject *object;
+ gint64 star_rating;
+
+ if (!JSON_NODE_HOLDS_OBJECT (node))
+ return NULL;
+
+ object = json_node_get_object (node);
+
+ review = gs_app_review_new ();
+ gs_app_review_set_reviewer (review, json_object_get_string_member (object, "reviewer_displayname"));
+ gs_app_review_set_summary (review, json_object_get_string_member (object, "summary"));
+ gs_app_review_set_text (review, json_object_get_string_member (object, "review_text"));
+ gs_app_review_set_version (review, json_object_get_string_member (object, "version"));
+ star_rating = json_object_get_int_member (object, "rating");
+ if (star_rating > 0)
+ gs_app_review_set_rating (review, star_rating * 20);
+ gs_app_review_set_date (review, parse_date_time (json_object_get_string_member (object,
"date_created")));
+
+ return review;
+}
+
+static gboolean
+parse_reviews (GsPlugin *plugin, const gchar *text, GsApp *app, GError **error)
+{
+ JsonParser *parser = NULL;
+ JsonArray *array;
+ gint i;
+ gboolean result = FALSE;
+
+ parser = json_parser_new ();
+ if (!json_parser_load_from_data (parser, text, -1, error))
+ goto out;
+ if (!JSON_NODE_HOLDS_ARRAY (json_parser_get_root (parser)))
+ goto out;
+ array = json_node_get_array (json_parser_get_root (parser));
+ for (i = 0; i < json_array_get_length (array); i++) {
+ GsAppReview *review;
+
+ /* Read in from JSON... (skip bad entries) */
+ review = parse_review (json_array_get_element (array, i));
+ if (!review)
+ continue;
+
+ gs_app_add_review (app, review);
+ }
+ result = TRUE;
+
+out:
+ g_clear_object (&parser);
+
+ return result;
+}
+
+static gboolean
+download_reviews (GsPlugin *plugin, GsApp *app, const gchar *package_name, GError **error)
+{
+ guint status_code;
+ g_autofree gchar *uri = NULL;
+ g_autoptr(SoupMessage) msg = NULL;
+ g_auto(GStrv) split = NULL;
+
+ /* Get the review stats using HTTP */
+ // FIXME: This will only get the first page of reviews
+ uri = g_strdup_printf ("%s/api/1.0/reviews/filter/any/any/any/any/%s/",
+ UBUNTU_REVIEWS_SERVER, package_name);
+ msg = soup_message_new (SOUP_METHOD_GET, uri);
+ if (!setup_networking (plugin, error))
+ return FALSE;
+ status_code = soup_session_send_message (plugin->priv->session, msg);
+ if (status_code != SOUP_STATUS_OK) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Failed to download Ubuntu reviews for %s: %s",
+ package_name, soup_status_get_phrase (status_code));
+ return FALSE;
+ }
+
+ /* Extract the stats from the data */
+ if (!parse_reviews (plugin, msg->response_body->data, app, error))
+ return FALSE;
+
+ return TRUE;
+}
+
+static gboolean
+refine_rating (GsPlugin *plugin, GsApp *app, GError **error)
+{
+ GPtrArray *sources;
+ guint i;
+
+ /* Load database once */
+ if (g_once_init_enter (&plugin->priv->db_loaded)) {
+ gboolean ret = load_database (plugin, error);
+ g_once_init_leave (&plugin->priv->db_loaded, TRUE);
+ if (!ret)
+ return FALSE;
+ }
+
+ /* Skip if already has a rating */
+ if (gs_app_get_rating (app) != -1)
+ return TRUE;
+
+ sources = gs_app_get_sources (app);
+ for (i = 0; i < sources->len; i++) {
+ const gchar *package_name;
+ gint rating;
+ gboolean ret;
+
+ /* If we have a local review, use that as the rating */
+ // FIXME
+
+ /* Otherwise use the statistics */
+ package_name = g_ptr_array_index (sources, i);
+ ret = get_rating (plugin, package_name, &rating, error);
+ if (!ret)
+ return FALSE;
+ if (rating != -1) {
+ g_debug ("ubuntu-reviews setting rating on %s to %i%%",
+ package_name, rating);
+ gs_app_set_rating (app, rating);
+ gs_app_set_rating_confidence (app, 100);
+ gs_app_set_rating_kind (app, GS_APP_RATING_KIND_SYSTEM);
+ if (rating > 80)
+ gs_app_add_kudo (app, GS_APP_KUDO_POPULAR);
+ }
+ }
+
+ return TRUE;
+}
+
+static gboolean
+refine_reviews (GsPlugin *plugin, GsApp *app, GError **error)
+{
+ GPtrArray *sources;
+ guint i;
+
+ /* Skip if already has reviews */
+ if (gs_app_get_reviews (app)->len > 0)
+ return TRUE;
+
+ sources = gs_app_get_sources (app);
+ for (i = 0; i < sources->len; i++) {
+ const gchar *package_name;
+ gboolean ret;
+
+ package_name = g_ptr_array_index (sources, i);
+ ret = download_reviews (plugin, app, package_name, error);
+ if (!ret)
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+gs_plugin_refine (GsPlugin *plugin,
+ GList **list,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GList *l;
+ gboolean ret = TRUE;
+
+ for (l = *list; l != NULL; l = l->next) {
+ GsApp *app = GS_APP (l->data);
+
+ if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING) != 0) {
+ if (!refine_rating (plugin, app, error))
+ return FALSE;
+ }
+ if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS) != 0) {
+ if (!refine_reviews (plugin, app, error))
+ return FALSE;
+ }
+ }
+
+ return ret;
+}
+
+static void
+add_string_member (JsonBuilder *builder, const gchar *name, const gchar *value)
+{
+ json_builder_set_member_name (builder, name);
+ json_builder_add_string_value (builder, value);
+}
+
+static void
+add_int_member (JsonBuilder *builder, const gchar *name, gint64 value)
+{
+ json_builder_set_member_name (builder, name);
+ json_builder_add_int_value (builder, value);
+}
+
+static void
+sign_message (SoupMessage *message, OAuthMethod method,
+ const gchar *oauth_consumer_key, const gchar *oauth_consumer_secret,
+ const gchar *oauth_token, const gchar *oauth_token_secret)
+{
+ g_autofree gchar *url = NULL, *oauth_authorization_parameters = NULL, *authorization_text = NULL;
+ gchar **url_parameters = NULL;
+ int url_parameters_length;
+
+ url = soup_uri_to_string (soup_message_get_uri (message), FALSE);
+
+ url_parameters_length = oauth_split_url_parameters(url, &url_parameters);
+ oauth_sign_array2_process (&url_parameters_length, &url_parameters,
+ NULL,
+ method,
+ message->method,
+ oauth_consumer_key, oauth_consumer_secret,
+ oauth_token, oauth_token_secret);
+ oauth_authorization_parameters = oauth_serialize_url_sep (url_parameters_length, 1, url_parameters,
", ", 6);
+ oauth_free_array (&url_parameters_length, &url_parameters);
+ authorization_text = g_strdup_printf ("OAuth realm=\"Ratings and Reviews\", %s",
oauth_authorization_parameters);
+ soup_message_headers_append (message->request_headers, "Authorization", authorization_text);
+}
+
+static void
+set_request (SoupMessage *message, JsonBuilder *builder)
+{
+ JsonGenerator *generator = json_generator_new ();
+ json_generator_set_root (generator, json_builder_get_root (builder));
+ gsize length;
+ gchar *data = json_generator_to_data (generator, &length);
+ soup_message_set_request (message, "application/json", SOUP_MEMORY_TAKE, data, length);
+ g_object_unref (generator);
+}
+
+gboolean
+gs_plugin_get_supports_reviews (GsPlugin *plugin)
+{
+ return TRUE;
+}
+
+const gchar *
+gs_plugin_get_review_auth (GsPlugin *plugin)
+{
+ return "ubuntuone";
+}
+
+static gboolean
+set_package_review (GsPlugin *plugin,
+ GsAppReview *review,
+ const gchar *package_name,
+ GError **error)
+{
+ g_autofree gchar *uri = NULL, *path = NULL;
+ g_autofree gchar *oauth_consumer_key = NULL, *oauth_consumer_secret = NULL, *oauth_token = NULL,
*oauth_token_secret = NULL;
+ g_autoptr(GKeyFile) config = NULL;
+ gint rating, n_stars = 0;
+ g_autoptr(SoupMessage) msg = NULL;
+ JsonBuilder *builder;
+ guint status_code;
+
+ /* Ubuntu reviews require a summary and description - just make one up for now */
+ rating = gs_app_review_get_rating (review);
+ if (rating > 80)
+ n_stars = 5;
+ else if (rating > 60)
+ n_stars = 4;
+ else if (rating > 40)
+ n_stars = 3;
+ else if (rating > 20)
+ n_stars = 2;
+ else
+ n_stars = 1;
+
+ /* Write review into database so we can easily access it */
+ // FIXME
+
+ /* Load OAuth token */
+ // FIXME needs to integrate with GNOME Online Accounts / libaccounts
+ config = g_key_file_new ();
+ path = g_build_filename (g_get_user_config_dir (), "gnome-software", "ubuntu-one-credentials", NULL);
+ g_key_file_load_from_file (config, path, G_KEY_FILE_NONE, NULL);
+ oauth_consumer_key = g_key_file_get_string (config, "gnome-software", "consumer-key", NULL);
+ oauth_consumer_secret = g_key_file_get_string (config, "gnome-software", "consumer-secret", NULL);
+ oauth_token = g_key_file_get_string (config, "gnome-software", "token", NULL);
+ oauth_token_secret = g_key_file_get_string (config, "gnome-software", "token-secret", NULL);
+ if (!oauth_consumer_key || !oauth_consumer_secret || !oauth_token || !oauth_token_secret) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "%s", "No Ubuntu One OAuth tokens");
+ return FALSE;
+ }
+
+ /* Create message for reviews.ubuntu.com */
+ uri = g_strdup_printf ("%s/api/1.0/reviews/", UBUNTU_REVIEWS_SERVER);
+ msg = soup_message_new (SOUP_METHOD_POST, uri);
+ builder = json_builder_new ();
+ json_builder_begin_object (builder);
+ add_string_member (builder, "package_name", package_name);
+ add_string_member (builder, "summary", gs_app_review_get_summary (review));
+ add_string_member (builder, "review_text", gs_app_review_get_text (review));
+ add_string_member (builder, "language", "en"); // FIXME
+ add_string_member (builder, "origin", "ubuntu"); // FIXME gs_app_get_origin (app));
+ add_string_member (builder, "distroseries", "xenial"); // FIXME
+ add_string_member (builder, "version", gs_app_review_get_version (review));
+ add_int_member (builder, "rating", n_stars);
+ add_string_member (builder, "arch_tag", "amd64"); // FIXME
+ json_builder_end_object (builder);
+ set_request (msg, builder);
+ g_object_unref (builder);
+ sign_message (msg, OA_HMAC, oauth_consumer_key, oauth_consumer_secret, oauth_token,
oauth_token_secret);
+
+ /* Send to the server */
+ status_code = soup_session_send_message (plugin->priv->session, msg);
+ if (status_code != SOUP_STATUS_OK) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Failed to post review: %s",
+ soup_status_get_phrase (status_code));
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+gs_plugin_app_set_review (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsAppReview *review;
+ GPtrArray *sources;
+ const gchar *package_name;
+ gboolean ret;
+ guint i;
+ g_autoptr(SoupMessage) msg = NULL;
+
+ review = gs_app_get_self_review (app);
+ g_return_val_if_fail (review != NULL, FALSE);
+
+ /* Load database once */
+ if (g_once_init_enter (&plugin->priv->db_loaded)) {
+ gboolean ret = load_database (plugin, error);
+ g_once_init_leave (&plugin->priv->db_loaded, TRUE);
+ if (!ret)
+ return FALSE;
+ }
+
+ /* get the package name */
+ sources = gs_app_get_sources (app);
+ if (sources->len == 0) {
+ g_warning ("no package name for %s", gs_app_get_id (app));
+ return TRUE;
+ }
+
+ if (!setup_networking (plugin, error))
+ return FALSE;
+
+ /* set rating for each package */
+ for (i = 0; i < sources->len; i++) {
+ package_name = g_ptr_array_index (sources, i);
+ ret = set_package_review (plugin,
+ review,
+ package_name,
+ error);
+ if (!ret)
+ return FALSE;
+ }
+
+ return TRUE;
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]