diff options
Diffstat (limited to 'src/gs-overview-page.c')
-rw-r--r-- | src/gs-overview-page.c | 1061 |
1 files changed, 1061 insertions, 0 deletions
diff --git a/src/gs-overview-page.c b/src/gs-overview-page.c new file mode 100644 index 0000000..36b6610 --- /dev/null +++ b/src/gs-overview-page.c @@ -0,0 +1,1061 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> +#include <math.h> + +#include "gs-shell.h" +#include "gs-overview-page.h" +#include "gs-app-list-private.h" +#include "gs-popular-tile.h" +#include "gs-feature-tile.h" +#include "gs-category-tile.h" +#include "gs-hiding-box.h" +#include "gs-common.h" + +#define N_TILES 9 +#define FEATURED_ROTATE_TIME 30 /* seconds */ + +typedef struct +{ + GsPluginLoader *plugin_loader; + GtkBuilder *builder; + GCancellable *cancellable; + gboolean cache_valid; + GsShell *shell; + gint action_cnt; + gboolean loading_featured; + gboolean loading_popular; + gboolean loading_recent; + gboolean loading_popular_rotating; + gboolean loading_categories; + gboolean empty; + gchar *category_of_day; + GHashTable *category_hash; /* id : GsCategory */ + GSettings *settings; + GsApp *third_party_repo; + guint featured_rotate_timer_id; + + GtkWidget *infobar_third_party; + GtkWidget *label_third_party; + GtkWidget *overlay; + GtkWidget *stack_featured; + GtkWidget *button_featured_back; + GtkWidget *button_featured_forwards; + GtkWidget *box_overview; + GtkWidget *box_popular; + GtkWidget *box_popular_rotating; + GtkWidget *box_recent; + GtkWidget *category_heading; + GtkWidget *flowbox_categories; + GtkWidget *popular_heading; + GtkWidget *recent_heading; + GtkWidget *scrolledwindow_overview; + GtkWidget *stack_overview; +} GsOverviewPagePrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GsOverviewPage, gs_overview_page, GS_TYPE_PAGE) + +enum { + SIGNAL_REFRESHED, + SIGNAL_LAST +}; + +static guint signals [SIGNAL_LAST] = { 0 }; + +typedef struct { + GsCategory *category; + GsOverviewPage *self; + const gchar *title; +} LoadData; + +static void +load_data_free (LoadData *data) +{ + if (data->category != NULL) + g_object_unref (data->category); + if (data->self != NULL) + g_object_unref (data->self); + g_slice_free (LoadData, data); +} + +static void +gs_overview_page_invalidate (GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + + priv->cache_valid = FALSE; +} + +static void +app_tile_clicked (GsAppTile *tile, gpointer data) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (data); + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GsApp *app; + + app = gs_app_tile_get_app (tile); + gs_shell_show_app (priv->shell, app); +} + +static gboolean +filter_category (GsApp *app, gpointer user_data) +{ + const gchar *category = (const gchar *) user_data; + + return !gs_app_has_category (app, category); +} + +static void +gs_overview_page_decrement_action_cnt (GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + + /* every job increments this */ + if (priv->action_cnt == 0) { + g_warning ("action_cnt already zero!"); + return; + } + if (--priv->action_cnt > 0) + return; + + /* all done */ + priv->cache_valid = TRUE; + g_signal_emit (self, signals[SIGNAL_REFRESHED], 0); + priv->loading_categories = FALSE; + priv->loading_featured = FALSE; + priv->loading_popular = FALSE; + priv->loading_recent = FALSE; + priv->loading_popular_rotating = FALSE; +} + +static void +gs_overview_page_get_popular_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (user_data); + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + guint i; + GsApp *app; + GtkWidget *tile; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + /* get popular apps */ + list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error); + if (list == NULL) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("failed to get popular apps: %s", error->message); + goto out; + } + + /* not enough to show */ + if (gs_app_list_length (list) < N_TILES) { + g_warning ("Only %u apps for popular list, hiding", + gs_app_list_length (list)); + gtk_widget_set_visible (priv->box_popular, FALSE); + gtk_widget_set_visible (priv->popular_heading, FALSE); + goto out; + } + + /* Don't show apps from the category that's currently featured as the category of the day */ + gs_app_list_filter (list, filter_category, priv->category_of_day); + gs_app_list_randomize (list); + + gs_container_remove_all (GTK_CONTAINER (priv->box_popular)); + + for (i = 0; i < gs_app_list_length (list) && i < N_TILES; i++) { + app = gs_app_list_index (list, i); + tile = gs_popular_tile_new (app); + g_signal_connect (tile, "clicked", + G_CALLBACK (app_tile_clicked), self); + gtk_container_add (GTK_CONTAINER (priv->box_popular), tile); + } + gtk_widget_set_visible (priv->box_popular, TRUE); + gtk_widget_set_visible (priv->popular_heading, TRUE); + + priv->empty = FALSE; + +out: + gs_overview_page_decrement_action_cnt (self); +} + +static void +gs_overview_page_get_recent_cb (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (user_data); + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + guint i; + GsApp *app; + GtkWidget *tile; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + /* get recent apps */ + list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error); + if (list == NULL) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("failed to get recent apps: %s", error->message); + goto out; + } + + /* not enough to show */ + if (gs_app_list_length (list) < N_TILES) { + g_warning ("Only %u apps for recent list, hiding", + gs_app_list_length (list)); + gtk_widget_set_visible (priv->box_recent, FALSE); + gtk_widget_set_visible (priv->recent_heading, FALSE); + goto out; + } + + /* Don't show apps from the category that's currently featured as the category of the day */ + gs_app_list_filter (list, filter_category, priv->category_of_day); + gs_app_list_randomize (list); + + gs_container_remove_all (GTK_CONTAINER (priv->box_recent)); + + for (i = 0; i < gs_app_list_length (list) && i < N_TILES; i++) { + app = gs_app_list_index (list, i); + tile = gs_popular_tile_new (app); + g_signal_connect (tile, "clicked", + G_CALLBACK (app_tile_clicked), self); + gtk_container_add (GTK_CONTAINER (priv->box_recent), tile); + } + gtk_widget_set_visible (priv->box_recent, TRUE); + gtk_widget_set_visible (priv->recent_heading, TRUE); + + priv->empty = FALSE; + +out: + gs_overview_page_decrement_action_cnt (self); +} + +static void +gs_overview_page_category_more_cb (GtkButton *button, GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GsCategory *cat; + const gchar *id; + + id = g_object_get_data (G_OBJECT (button), "GnomeSoftware::CategoryId"); + if (id == NULL) + return; + cat = g_hash_table_lookup (priv->category_hash, id); + if (cat == NULL) + return; + gs_shell_show_category (priv->shell, cat); +} + +static void +gs_overview_page_get_category_apps_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + LoadData *load_data = (LoadData *) user_data; + GsOverviewPage *self = load_data->self; + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + guint i; + GsApp *app; + GtkWidget *box; + GtkWidget *button; + GtkWidget *headerbox; + GtkWidget *label; + GtkWidget *tile; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + /* get popular apps */ + list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error); + if (list == NULL) { + if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + goto out; + g_warning ("failed to get category %s featured applications: %s", + gs_category_get_id (load_data->category), + error->message); + goto out; + } else if (gs_app_list_length (list) < N_TILES) { + g_warning ("hiding category %s featured applications: " + "found only %u to show, need at least %d", + gs_category_get_id (load_data->category), + gs_app_list_length (list), N_TILES); + goto out; + } + gs_app_list_randomize (list); + + /* add header */ + headerbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 9); + gtk_widget_set_visible (headerbox, TRUE); + + /* add label */ + label = gtk_label_new (load_data->title); + gtk_widget_set_visible (label, TRUE); + gtk_label_set_xalign (GTK_LABEL (label), 0.f); + gtk_widget_set_margin_top (label, 24); + gtk_widget_set_margin_bottom (label, 6); + gtk_widget_set_hexpand (label, TRUE); + gtk_style_context_add_class (gtk_widget_get_style_context (label), + "index-title-alignment-software"); + gtk_container_add (GTK_CONTAINER (headerbox), label); + + /* add button */ + button = gtk_button_new_with_label (_("More…")); + gtk_style_context_add_class (gtk_widget_get_style_context (button), + "overview-more-button"); + g_object_set_data_full (G_OBJECT (button), "GnomeSoftware::CategoryId", + g_strdup (gs_category_get_id (load_data->category)), + g_free); + gtk_widget_set_visible (button, TRUE); + gtk_widget_set_valign (button, GTK_ALIGN_END); + gtk_widget_set_margin_bottom (button, 9); + g_signal_connect (button, "clicked", + G_CALLBACK (gs_overview_page_category_more_cb), self); + gtk_container_add (GTK_CONTAINER (headerbox), button); + gtk_container_add (GTK_CONTAINER (priv->box_popular_rotating), headerbox); + + /* add hiding box */ + box = gs_hiding_box_new (); + gs_hiding_box_set_spacing (GS_HIDING_BOX (box), 14); + gtk_widget_set_visible (box, TRUE); + gtk_widget_set_valign (box, GTK_ALIGN_START); + gtk_container_add (GTK_CONTAINER (priv->box_popular_rotating), box); + + /* add all the apps */ + for (i = 0; i < gs_app_list_length (list) && i < N_TILES; i++) { + app = gs_app_list_index (list, i); + tile = gs_popular_tile_new (app); + g_signal_connect (tile, "clicked", + G_CALLBACK (app_tile_clicked), self); + gtk_container_add (GTK_CONTAINER (box), tile); + } + + priv->empty = FALSE; + +out: + load_data_free (load_data); + gs_overview_page_decrement_action_cnt (self); +} + +static void +_feature_banner_forward (GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GtkWidget *visible_child; + GtkWidget *next_child = NULL; + GList *banner_link; + g_autoptr(GList) banners = NULL; + + visible_child = gtk_stack_get_visible_child (GTK_STACK (priv->stack_featured)); + banners = gtk_container_get_children (GTK_CONTAINER (priv->stack_featured)); + if (banners == NULL) + return; + + /* find banner after the currently visible one */ + for (banner_link = banners; banner_link != NULL; banner_link = banner_link->next) { + GtkWidget *child = banner_link->data; + if (child == visible_child) { + if (banner_link->next != NULL) + next_child = banner_link->next->data; + break; + } + } + if (next_child == NULL) + next_child = g_list_first(banners)->data; + gtk_stack_set_visible_child (GTK_STACK (priv->stack_featured), next_child); +} + +static void +_feature_banner_back (GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GtkWidget *visible_child; + GtkWidget *next_child = NULL; + GList *banner_link; + g_autoptr(GList) banners = NULL; + + visible_child = gtk_stack_get_visible_child (GTK_STACK (priv->stack_featured)); + banners = gtk_container_get_children (GTK_CONTAINER (priv->stack_featured)); + if (banners == NULL) + return; + + /* find banner before the currently visible one */ + for (banner_link = banners; banner_link != NULL; banner_link = banner_link->next) { + GtkWidget *child = banner_link->data; + if (child == visible_child) { + if (banner_link->prev != NULL) + next_child = banner_link->prev->data; + break; + } + } + if (next_child == NULL) + next_child = g_list_last(banners)->data; + gtk_stack_set_visible_child (GTK_STACK (priv->stack_featured), next_child); +} + +static gboolean +gs_overview_page_featured_rotate_cb (gpointer user_data) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (user_data); + _feature_banner_forward (self); + return G_SOURCE_CONTINUE; +} + +static void +featured_reset_rotate_timer (GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + if (priv->featured_rotate_timer_id != 0) + g_source_remove (priv->featured_rotate_timer_id); + priv->featured_rotate_timer_id = g_timeout_add_seconds (FEATURED_ROTATE_TIME, + gs_overview_page_featured_rotate_cb, + self); +} + +static void +_featured_back_clicked_cb (GsCategoryTile *tile, gpointer data) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (data); + _feature_banner_back (self); +} + +static void +_featured_forward_clicked_cb (GsCategoryTile *tile, gpointer data) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (data); + _feature_banner_forward (self); +} + +static void +gs_overview_page_get_featured_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (user_data); + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error); + if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + goto out; + + if (priv->featured_rotate_timer_id != 0) { + g_source_remove (priv->featured_rotate_timer_id); + priv->featured_rotate_timer_id = 0; + } + + gs_container_remove_all (GTK_CONTAINER (priv->stack_featured)); + gtk_widget_set_visible (priv->overlay, gs_app_list_length (list) > 0); + gtk_widget_set_visible (priv->button_featured_back, gs_app_list_length (list) > 1); + gtk_widget_set_visible (priv->button_featured_forwards, gs_app_list_length (list) > 1); + if (list == NULL) { + g_warning ("failed to get featured apps: %s", + error->message); + goto out; + } + if (gs_app_list_length (list) == 0) { + g_warning ("failed to get featured apps: " + "no apps to show"); + goto out; + } + + if (g_getenv ("GNOME_SOFTWARE_FEATURED") == NULL) { + /* Don't show apps from the category that's currently featured as the category of the day */ + gs_app_list_filter (list, filter_category, priv->category_of_day); + gs_app_list_filter_duplicates (list, GS_APP_LIST_FILTER_FLAG_KEY_ID); + gs_app_list_randomize (list); + } + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + GtkWidget *tile = gs_feature_tile_new (app); + g_signal_connect (tile, "clicked", + G_CALLBACK (app_tile_clicked), self); + gtk_container_add (GTK_CONTAINER (priv->stack_featured), tile); + } + + priv->empty = FALSE; + featured_reset_rotate_timer (self); + +out: + gs_overview_page_decrement_action_cnt (self); +} + +static void +category_tile_clicked (GsCategoryTile *tile, gpointer data) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (data); + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GsCategory *category; + + category = gs_category_tile_get_category (tile); + gs_shell_show_category (priv->shell, category); +} + +static void +gs_overview_page_get_categories_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (user_data); + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + guint i; + GsCategory *cat; + GtkFlowBox *flowbox; + GtkWidget *tile; + guint added_cnt = 0; + g_autoptr(GError) error = NULL; + g_autoptr(GPtrArray) list = NULL; + + list = gs_plugin_loader_job_get_categories_finish (plugin_loader, res, &error); + if (list == NULL) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("failed to get categories: %s", error->message); + goto out; + } + gs_container_remove_all (GTK_CONTAINER (priv->flowbox_categories)); + + /* add categories to the correct flowboxes, the second being hidden */ + for (i = 0; i < list->len; i++) { + cat = GS_CATEGORY (g_ptr_array_index (list, i)); + if (gs_category_get_size (cat) == 0) + continue; + tile = gs_category_tile_new (cat); + g_signal_connect (tile, "clicked", + G_CALLBACK (category_tile_clicked), self); + flowbox = GTK_FLOW_BOX (priv->flowbox_categories); + gtk_flow_box_insert (flowbox, tile, -1); + gtk_widget_set_can_focus (gtk_widget_get_parent (tile), FALSE); + added_cnt++; + + /* we save these for the 'More...' buttons */ + g_hash_table_insert (priv->category_hash, + g_strdup (gs_category_get_id (cat)), + g_object_ref (cat)); + } + +out: + if (added_cnt > 0) + priv->empty = FALSE; + gtk_widget_set_visible (priv->category_heading, added_cnt > 0); + + gs_overview_page_decrement_action_cnt (self); +} + +static const gchar * +gs_overview_page_get_category_label (const gchar *id) +{ + if (g_strcmp0 (id, "audio-video") == 0) { + /* TRANSLATORS: this is a heading for audio applications which + * have been featured ('recommended') by the distribution */ + return _("Recommended Audio & Video Applications"); + } + if (g_strcmp0 (id, "games") == 0) { + /* TRANSLATORS: this is a heading for games which have been + * featured ('recommended') by the distribution */ + return _("Recommended Games"); + } + if (g_strcmp0 (id, "graphics") == 0) { + /* TRANSLATORS: this is a heading for graphics applications + * which have been featured ('recommended') by the distribution */ + return _("Recommended Graphics Applications"); + } + if (g_strcmp0 (id, "productivity") == 0) { + /* TRANSLATORS: this is a heading for office applications which + * have been featured ('recommended') by the distribution */ + return _("Recommended Productivity Applications"); + } + return NULL; +} + +static GPtrArray * +gs_overview_page_get_random_categories (void) +{ + GPtrArray *cats; + guint i; + g_autoptr(GDateTime) date = NULL; + g_autoptr(GRand) rand = NULL; + const gchar *ids[] = { "audio-video", + "games", + "graphics", + "productivity", + NULL }; + + date = g_date_time_new_now_utc (); + rand = g_rand_new_with_seed ((guint32) g_date_time_get_day_of_year (date)); + cats = g_ptr_array_new_with_free_func (g_free); + for (i = 0; ids[i] != NULL; i++) + g_ptr_array_add (cats, g_strdup (ids[i])); + for (i = 0; i < powl (cats->len + 1, 2); i++) { + gpointer tmp; + guint rnd1 = (guint) g_rand_int_range (rand, 0, (gint32) cats->len); + guint rnd2 = (guint) g_rand_int_range (rand, 0, (gint32) cats->len); + if (rnd1 == rnd2) + continue; + tmp = cats->pdata[rnd1]; + cats->pdata[rnd1] = cats->pdata[rnd2]; + cats->pdata[rnd2] = tmp; + } + for (i = 0; i < cats->len; i++) { + const gchar *tmp = g_ptr_array_index (cats, i); + g_debug ("%u = %s", i + 1, tmp); + } + return cats; +} + +static void +refresh_third_party_repo (GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + + /* only show if never prompted and third party repo is available */ + if (g_settings_get_boolean (priv->settings, "show-nonfree-prompt") && + priv->third_party_repo != NULL && + gs_app_get_state (priv->third_party_repo) == AS_APP_STATE_AVAILABLE) { + gtk_widget_set_visible (priv->infobar_third_party, TRUE); + } else { + gtk_widget_set_visible (priv->infobar_third_party, FALSE); + } +} + +static void +resolve_third_party_repo_cb (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + /* get the results */ + list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error); + if (list == NULL) { + if (g_error_matches (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_CANCELLED)) { + g_debug ("resolve third party repo cancelled"); + return; + } else { + g_warning ("failed to resolve third party repo: %s", error->message); + return; + } + } + + /* save results for later */ + g_clear_object (&priv->third_party_repo); + if (gs_app_list_length (list) > 0) + priv->third_party_repo = g_object_ref (gs_app_list_index (list, 0)); + + /* refresh widget */ + refresh_third_party_repo (self); +} + +static gboolean +is_fedora (void) +{ + const gchar *id = NULL; + g_autoptr(GsOsRelease) os_release = NULL; + + os_release = gs_os_release_new (NULL); + if (os_release == NULL) + return FALSE; + + id = gs_os_release_get_id (os_release); + if (g_strcmp0 (id, "fedora") == 0) + return TRUE; + + return FALSE; +} + +static void +reload_third_party_repo (GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + const gchar *third_party_repo_package = "fedora-workstation-repositories"; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* only show if never prompted */ + if (!g_settings_get_boolean (priv->settings, "show-nonfree-prompt")) + return; + + /* Fedora-specific functionality */ + if (!is_fedora ()) + return; + + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH_PROVIDES, + "search", third_party_repo_package, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION | + GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES, + NULL); + gs_plugin_loader_job_process_async (priv->plugin_loader, plugin_job, + priv->cancellable, + (GAsyncReadyCallback) resolve_third_party_repo_cb, + self); +} + +static void +gs_overview_page_load (GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + guint i; + + priv->empty = TRUE; + + if (!priv->loading_featured) { + g_autoptr(GsPluginJob) plugin_job = NULL; + + priv->loading_featured = TRUE; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_FEATURED, + "max-results", 5, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + "dedupe-flags", GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED | + GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES, + NULL); + gs_plugin_loader_job_process_async (priv->plugin_loader, + plugin_job, + priv->cancellable, + gs_overview_page_get_featured_cb, + self); + priv->action_cnt++; + } + + if (!priv->loading_popular) { + g_autoptr(GsPluginJob) plugin_job = NULL; + + priv->loading_popular = TRUE; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_POPULAR, + "max-results", 20, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + "dedupe-flags", GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED | + GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES, + NULL); + gs_plugin_loader_job_process_async (priv->plugin_loader, + plugin_job, + priv->cancellable, + gs_overview_page_get_popular_cb, + self); + priv->action_cnt++; + } + + if (!priv->loading_recent) { + g_autoptr(GsPluginJob) plugin_job = NULL; + + priv->loading_recent = TRUE; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_RECENT, + "age", (guint64) (60 * 60 * 24 * 60), + "max-results", 20, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + "dedupe-flags", GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED | + GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES, + NULL); + gs_plugin_loader_job_process_async (priv->plugin_loader, + plugin_job, + priv->cancellable, + gs_overview_page_get_recent_cb, + self); + priv->action_cnt++; + } + + if (!priv->loading_popular_rotating) { + const guint MAX_CATS = 2; + g_autoptr(GPtrArray) cats_random = NULL; + cats_random = gs_overview_page_get_random_categories (); + + /* remove existing widgets, if any */ + gs_container_remove_all (GTK_CONTAINER (priv->box_popular_rotating)); + + /* load all the categories */ + for (i = 0; i < cats_random->len && i < MAX_CATS; i++) { + LoadData *load_data; + const gchar *cat_id; + g_autoptr(GsCategory) category = NULL; + g_autoptr(GsCategory) featured_category = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + cat_id = g_ptr_array_index (cats_random, i); + if (i == 0) { + g_free (priv->category_of_day); + priv->category_of_day = g_strdup (cat_id); + } + category = gs_category_new (cat_id); + featured_category = gs_category_new ("featured"); + gs_category_add_child (category, featured_category); + + load_data = g_slice_new0 (LoadData); + load_data->category = g_object_ref (category); + load_data->self = g_object_ref (self); + load_data->title = gs_overview_page_get_category_label (cat_id); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_CATEGORY_APPS, + "max-results", 20, + "category", featured_category, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + "dedupe-flags", GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED | + GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES, + NULL); + gs_plugin_loader_job_process_async (priv->plugin_loader, + plugin_job, + priv->cancellable, + gs_overview_page_get_category_apps_cb, + load_data); + priv->action_cnt++; + } + priv->loading_popular_rotating = TRUE; + } + + if (!priv->loading_categories) { + g_autoptr(GsPluginJob) plugin_job = NULL; + priv->loading_categories = TRUE; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_CATEGORIES, NULL); + gs_plugin_loader_job_get_categories_async (priv->plugin_loader, plugin_job, + priv->cancellable, + gs_overview_page_get_categories_cb, + self); + priv->action_cnt++; + } + + reload_third_party_repo (self); +} + +static void +gs_overview_page_reload (GsPage *page) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (page); + gs_overview_page_invalidate (self); + gs_overview_page_load (self); +} + +static void +gs_overview_page_switch_to (GsPage *page, gboolean scroll_up) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (page); + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GtkWidget *widget; + GtkAdjustment *adj; + + if (gs_shell_get_mode (priv->shell) != GS_SHELL_MODE_OVERVIEW) { + g_warning ("Called switch_to(overview) when in mode %s", + gs_shell_get_mode_string (priv->shell)); + return; + } + + /* we hid the search bar */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "search_button")); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), FALSE); + + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "buttonbox_main")); + gtk_widget_show (widget); + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "menu_button")); + gtk_widget_show (widget); + + if (scroll_up) { + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (priv->scrolledwindow_overview)); + gtk_adjustment_set_value (adj, gtk_adjustment_get_lower (adj)); + } + + gs_grab_focus_when_mapped (priv->scrolledwindow_overview); + + if (priv->cache_valid || priv->action_cnt > 0) + return; + gs_overview_page_load (self); +} + +static void +third_party_response_cb (GtkInfoBar *info_bar, + gint response_id, + GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + g_settings_set_boolean (priv->settings, "show-nonfree-prompt", FALSE); + if (response_id == GTK_RESPONSE_CLOSE) { + gtk_widget_hide (priv->infobar_third_party); + return; + } + if (response_id != GTK_RESPONSE_YES) + return; + + if (gs_app_get_state (priv->third_party_repo) == AS_APP_STATE_AVAILABLE) { + gs_page_install_app (GS_PAGE (self), priv->third_party_repo, + GS_SHELL_INTERACTION_FULL, + priv->cancellable); + } + + refresh_third_party_repo (self); +} + +static gboolean +gs_overview_page_setup (GsPage *page, + GsShell *shell, + GsPluginLoader *plugin_loader, + GtkBuilder *builder, + GCancellable *cancellable, + GError **error) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (page); + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GtkAdjustment *adj; + GtkWidget *tile; + gint i; + g_autoptr(GString) str = g_string_new (NULL); + + g_return_val_if_fail (GS_IS_OVERVIEW_PAGE (self), TRUE); + + priv->plugin_loader = g_object_ref (plugin_loader); + priv->builder = g_object_ref (builder); + priv->cancellable = g_object_ref (cancellable); + priv->category_hash = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) g_object_unref); + + g_string_append (str, + /* TRANSLATORS: this is the third party repositories info bar. */ + _("Access additional software from selected third party sources.")); + g_string_append (str, " "); + g_string_append (str, + /* TRANSLATORS: this is the third party repositories info bar. */ + _("Some of this software is proprietary and therefore has restrictions on use, sharing, and access to source code.")); + g_string_append_printf (str, " <a href=\"%s\">%s</a>", + "https://fedoraproject.org/wiki/Workstation/Third_Party_Software_Repositories", + /* TRANSLATORS: this is the clickable + * link on the third party repositories info bar */ + _("Find out more…")); + gtk_label_set_markup (GTK_LABEL (priv->label_third_party), str->str); + + /* create info bar if not already dismissed in initial-setup */ + refresh_third_party_repo (self); + reload_third_party_repo (self); + gtk_info_bar_add_button (GTK_INFO_BAR (priv->infobar_third_party), + /* TRANSLATORS: button to turn on third party software repositories */ + _("Enable"), GTK_RESPONSE_YES); + g_signal_connect (priv->infobar_third_party, "response", + G_CALLBACK (third_party_response_cb), self); + + /* avoid a ref cycle */ + priv->shell = shell; + + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (priv->scrolledwindow_overview)); + gtk_container_set_focus_vadjustment (GTK_CONTAINER (priv->box_overview), adj); + + tile = gs_feature_tile_new (NULL); + gtk_container_add (GTK_CONTAINER (priv->stack_featured), tile); + + for (i = 0; i < N_TILES; i++) { + tile = gs_popular_tile_new (NULL); + gtk_container_add (GTK_CONTAINER (priv->box_popular), tile); + } + + for (i = 0; i < N_TILES; i++) { + tile = gs_popular_tile_new (NULL); + gtk_container_add (GTK_CONTAINER (priv->box_recent), tile); + } + + return TRUE; +} + +static void +gs_overview_page_init (GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + gtk_widget_init_template (GTK_WIDGET (self)); + + g_signal_connect (priv->button_featured_back, "clicked", + G_CALLBACK (_featured_back_clicked_cb), self); + g_signal_connect (priv->button_featured_forwards, "clicked", + G_CALLBACK (_featured_forward_clicked_cb), self); + + priv->settings = g_settings_new ("org.gnome.software"); +} + +static void +gs_overview_page_dispose (GObject *object) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (object); + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + + g_clear_object (&priv->builder); + g_clear_object (&priv->plugin_loader); + g_clear_object (&priv->cancellable); + g_clear_object (&priv->settings); + g_clear_object (&priv->third_party_repo); + g_clear_pointer (&priv->category_of_day, g_free); + g_clear_pointer (&priv->category_hash, g_hash_table_unref); + + if (priv->featured_rotate_timer_id != 0) { + g_source_remove (priv->featured_rotate_timer_id); + priv->featured_rotate_timer_id = 0; + } + + G_OBJECT_CLASS (gs_overview_page_parent_class)->dispose (object); +} + +static void +gs_overview_page_refreshed (GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + + if (priv->empty) { + gtk_stack_set_visible_child_name (GTK_STACK (priv->stack_overview), "no-results"); + } else { + gtk_stack_set_visible_child_name (GTK_STACK (priv->stack_overview), "overview"); + } +} + +static void +gs_overview_page_class_init (GsOverviewPageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPageClass *page_class = GS_PAGE_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gs_overview_page_dispose; + page_class->switch_to = gs_overview_page_switch_to; + page_class->reload = gs_overview_page_reload; + page_class->setup = gs_overview_page_setup; + klass->refreshed = gs_overview_page_refreshed; + + signals [SIGNAL_REFRESHED] = + g_signal_new ("refreshed", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsOverviewPageClass, refreshed), + NULL, NULL, g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-overview-page.ui"); + + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, infobar_third_party); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, label_third_party); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, overlay); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, stack_featured); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, button_featured_back); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, button_featured_forwards); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, box_overview); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, box_popular); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, box_popular_rotating); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, box_recent); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, category_heading); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, flowbox_categories); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, popular_heading); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, recent_heading); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, scrolledwindow_overview); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, stack_overview); +} + +GsOverviewPage * +gs_overview_page_new (void) +{ + return GS_OVERVIEW_PAGE (g_object_new (GS_TYPE_OVERVIEW_PAGE, NULL)); +} |