[gnome-software/1364-implement-other-apps-by-author-section-in-app-details-page] Proof of concept



commit 4cc0f77faedbd3867de2c04cdadfed16f6fa7e90
Author: Milan Crha <mcrha redhat com>
Date:   Wed Mar 9 18:23:01 2022 +0100

    Proof of concept

 lib/gs-appstream.c                  | 74 +++++++++++++++++++--------
 lib/gs-appstream.h                  |  6 +++
 lib/gs-plugin-loader.c              |  6 +++
 lib/gs-plugin-types.h               |  2 +
 lib/gs-plugin-vfuncs.h              | 21 ++++++++
 lib/gs-plugin.c                     |  6 +++
 plugins/core/gs-plugin-appstream.c  | 22 +++++++++
 plugins/flatpak/gs-flatpak.c        | 74 +++++++++++++++++++++++++++
 plugins/flatpak/gs-flatpak.h        |  6 +++
 plugins/flatpak/gs-plugin-flatpak.c | 21 ++++++++
 src/gs-details-page.c               | 99 ++++++++++++++++++++++++++++++++++++-
 src/gs-details-page.ui              | 42 ++++++++++++++++
 12 files changed, 357 insertions(+), 22 deletions(-)
---
diff --git a/lib/gs-appstream.c b/lib/gs-appstream.c
index 14cf8887f..70d7aa472 100644
--- a/lib/gs-appstream.c
+++ b/lib/gs-appstream.c
@@ -1342,32 +1342,24 @@ gs_appstream_silo_search_component (GPtrArray *array, XbNode *component, const g
        return matches_sum;
 }
 
-gboolean
-gs_appstream_search (GsPlugin *plugin,
-                    XbSilo *silo,
-                    const gchar * const *values,
-                    GsAppList *list,
-                    GCancellable *cancellable,
-                    GError **error)
+typedef struct _Query {
+       AsSearchTokenMatch      match_value;
+       const gchar             *xpath;
+} Query;
+
+static gboolean
+gs_appstream_do_search (GsPlugin *plugin,
+                       XbSilo *silo,
+                       const gchar * const *values,
+                       const Query queries[],
+                       GsAppList *list,
+                       GCancellable *cancellable,
+                       GError **error)
 {
        g_autoptr(GError) error_local = NULL;
        g_autoptr(GPtrArray) array = g_ptr_array_new_with_free_func ((GDestroyNotify) 
gs_appstream_search_helper_free);
        g_autoptr(GPtrArray) components = NULL;
        g_autoptr(GTimer) timer = g_timer_new ();
-       const struct {
-               AsSearchTokenMatch      match_value;
-               const gchar             *xpath;
-       } queries[] = {
-               { AS_SEARCH_TOKEN_MATCH_MIMETYPE,       "mimetypes/mimetype[text()~=stem(?)]" },
-               { AS_SEARCH_TOKEN_MATCH_PKGNAME,        "pkgname[text()~=stem(?)]" },
-               { AS_SEARCH_TOKEN_MATCH_SUMMARY,        "summary[text()~=stem(?)]" },
-               { AS_SEARCH_TOKEN_MATCH_NAME,   "name[text()~=stem(?)]" },
-               { AS_SEARCH_TOKEN_MATCH_KEYWORD,        "keywords/keyword[text()~=stem(?)]" },
-               { AS_SEARCH_TOKEN_MATCH_ID,     "id[text()~=stem(?)]" },
-               { AS_SEARCH_TOKEN_MATCH_ID,     "launchable[text()~=stem(?)]" },
-               { AS_SEARCH_TOKEN_MATCH_ORIGIN, "../components[@origin~=stem(?)]" },
-               { AS_SEARCH_TOKEN_MATCH_NONE,   NULL }
-       };
 
        /* add some weighted queries */
        for (guint i = 0; queries[i].xpath != NULL; i++) {
@@ -1437,6 +1429,46 @@ gs_appstream_search (GsPlugin *plugin,
        return TRUE;
 }
 
+gboolean
+gs_appstream_search (GsPlugin *plugin,
+                    XbSilo *silo,
+                    const gchar * const *values,
+                    GsAppList *list,
+                    GCancellable *cancellable,
+                    GError **error)
+{
+       const Query queries[] = {
+               { AS_SEARCH_TOKEN_MATCH_MIMETYPE,       "mimetypes/mimetype[text()~=stem(?)]" },
+               { AS_SEARCH_TOKEN_MATCH_PKGNAME,        "pkgname[text()~=stem(?)]" },
+               { AS_SEARCH_TOKEN_MATCH_SUMMARY,        "summary[text()~=stem(?)]" },
+               { AS_SEARCH_TOKEN_MATCH_NAME,   "name[text()~=stem(?)]" },
+               { AS_SEARCH_TOKEN_MATCH_KEYWORD,        "keywords/keyword[text()~=stem(?)]" },
+               { AS_SEARCH_TOKEN_MATCH_ID,     "id[text()~=stem(?)]" },
+               { AS_SEARCH_TOKEN_MATCH_ID,     "launchable[text()~=stem(?)]" },
+               { AS_SEARCH_TOKEN_MATCH_ORIGIN, "../components[@origin~=stem(?)]" },
+               { AS_SEARCH_TOKEN_MATCH_NONE,   NULL }
+       };
+
+       return gs_appstream_do_search (plugin, silo, values, queries, list, cancellable, error);
+}
+
+gboolean
+gs_appstream_search_other_apps (GsPlugin *plugin,
+                               XbSilo *silo,
+                               const gchar * const *values,
+                               GsAppList *list,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       const Query queries[] = {
+               { AS_SEARCH_TOKEN_MATCH_PKGNAME,        "developer_name[text()~=stem(?)]" },
+               { AS_SEARCH_TOKEN_MATCH_SUMMARY,        "project_group[text()~=stem(?)]" },
+               { AS_SEARCH_TOKEN_MATCH_NONE,           NULL }
+       };
+
+       return gs_appstream_do_search (plugin, silo, values, queries, list, cancellable, error);
+}
+
 gboolean
 gs_appstream_add_category_apps (XbSilo *silo,
                                GsCategory *category,
diff --git a/lib/gs-appstream.h b/lib/gs-appstream.h
index fbc68a1a4..1264a07b2 100644
--- a/lib/gs-appstream.h
+++ b/lib/gs-appstream.h
@@ -29,6 +29,12 @@ gboolean      gs_appstream_search                    (GsPlugin       *plugin,
                                                         GsAppList      *list,
                                                         GCancellable   *cancellable,
                                                         GError         **error);
+gboolean        gs_appstream_search_other_apps         (GsPlugin       *plugin,
+                                                        XbSilo         *silo,
+                                                        const gchar * const *values,
+                                                        GsAppList      *list,
+                                                        GCancellable   *cancellable,
+                                                        GError         **error);
 gboolean        gs_appstream_add_categories            (XbSilo         *silo,
                                                         GPtrArray      *list,
                                                         GCancellable   *cancellable,
diff --git a/lib/gs-plugin-loader.c b/lib/gs-plugin-loader.c
index 6c72bb022..c40db1294 100644
--- a/lib/gs-plugin-loader.c
+++ b/lib/gs-plugin-loader.c
@@ -679,6 +679,7 @@ gs_plugin_loader_call_vfunc (GsPluginLoaderHelper *helper,
                break;
        case GS_PLUGIN_ACTION_SEARCH_FILES:
        case GS_PLUGIN_ACTION_SEARCH_PROVIDES:
+       case GS_PLUGIN_ACTION_SEARCH_OTHER_APPS:
                {
                        GsPluginSearchFunc plugin_func = func;
                        const gchar *search[2] = { gs_plugin_job_get_search (helper->plugin_job), NULL };
@@ -3202,6 +3203,7 @@ gs_plugin_loader_process_thread_cb (GTask *task,
        case GS_PLUGIN_ACTION_LAUNCH:
        case GS_PLUGIN_ACTION_REMOVE:
        case GS_PLUGIN_ACTION_SEARCH:
+       case GS_PLUGIN_ACTION_SEARCH_OTHER_APPS:
        case GS_PLUGIN_ACTION_UPDATE:
        case GS_PLUGIN_ACTION_INSTALL_REPO:
        case GS_PLUGIN_ACTION_REMOVE_REPO:
@@ -3383,6 +3385,7 @@ gs_plugin_loader_process_thread_cb (GTask *task,
                gs_app_list_filter (list, gs_plugin_loader_app_is_valid_updatable, helper);
                break;
        case GS_PLUGIN_ACTION_GET_RECENT:
+       case GS_PLUGIN_ACTION_SEARCH_OTHER_APPS:
                gs_app_list_filter (list, gs_plugin_loader_app_is_non_compulsory, NULL);
                gs_app_list_filter (list, gs_plugin_loader_app_is_desktop, NULL);
                gs_app_list_filter (list, gs_plugin_loader_app_is_valid_filter, helper);
@@ -3779,6 +3782,7 @@ gs_plugin_loader_job_process_async (GsPluginLoader *plugin_loader,
        case GS_PLUGIN_ACTION_SEARCH:
        case GS_PLUGIN_ACTION_SEARCH_FILES:
        case GS_PLUGIN_ACTION_SEARCH_PROVIDES:
+       case GS_PLUGIN_ACTION_SEARCH_OTHER_APPS:
        case GS_PLUGIN_ACTION_URL_TO_APP:
                if (gs_plugin_job_get_search (plugin_job) == NULL) {
                        g_task_return_new_error (task,
@@ -3795,6 +3799,7 @@ gs_plugin_loader_job_process_async (GsPluginLoader *plugin_loader,
        /* sorting fallbacks */
        switch (action) {
        case GS_PLUGIN_ACTION_SEARCH:
+       case GS_PLUGIN_ACTION_SEARCH_OTHER_APPS:
                if (gs_plugin_job_get_sort_func (plugin_job, NULL) == NULL) {
                        gs_plugin_job_set_sort_func (plugin_job,
                                                     gs_plugin_loader_app_sort_match_value_cb, NULL);
@@ -3868,6 +3873,7 @@ gs_plugin_loader_job_process_async (GsPluginLoader *plugin_loader,
        case GS_PLUGIN_ACTION_SEARCH:
        case GS_PLUGIN_ACTION_SEARCH_FILES:
        case GS_PLUGIN_ACTION_SEARCH_PROVIDES:
+       case GS_PLUGIN_ACTION_SEARCH_OTHER_APPS:
                if (gs_plugin_job_get_timeout (plugin_job) > 0) {
                        helper->timeout_id =
                                g_timeout_add_seconds (gs_plugin_job_get_timeout (plugin_job),
diff --git a/lib/gs-plugin-types.h b/lib/gs-plugin-types.h
index 5723dec72..10debd1b4 100644
--- a/lib/gs-plugin-types.h
+++ b/lib/gs-plugin-types.h
@@ -261,6 +261,7 @@ typedef enum {
  * @GS_PLUGIN_ACTION_REMOVE_REPO:              Remove a repository (Since: 41)
  * @GS_PLUGIN_ACTION_ENABLE_REPO:              Enable a repository (Since: 41)
  * @GS_PLUGIN_ACTION_DISABLE_REPO:             Disable a repository (Since: 41)
+ * @GS_PLUGIN_ACTION_SEARCH_OTHER_APPS:                Get the search results for a developer (Since: 43)
  *
  * The plugin action.
  **/
@@ -293,6 +294,7 @@ typedef enum {
        GS_PLUGIN_ACTION_REMOVE_REPO,
        GS_PLUGIN_ACTION_ENABLE_REPO,
        GS_PLUGIN_ACTION_DISABLE_REPO,
+       GS_PLUGIN_ACTION_SEARCH_OTHER_APPS,
        GS_PLUGIN_ACTION_LAST  /*< skip >*/
 } GsPluginAction;
 
diff --git a/lib/gs-plugin-vfuncs.h b/lib/gs-plugin-vfuncs.h
index 83364376c..9f38d6145 100644
--- a/lib/gs-plugin-vfuncs.h
+++ b/lib/gs-plugin-vfuncs.h
@@ -125,6 +125,27 @@ gboolean    gs_plugin_add_search_what_provides     (GsPlugin       *plugin,
                                                         GCancellable   *cancellable,
                                                         GError         **error);
 
+/**
+ * gs_plugin_add_search_other_apps:
+ * @plugin: a #GsPlugin
+ * @values: a NULL terminated list of search terms, e.g. [ "gnome" ]
+ * @list: a #GsAppList
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Called when searching for applications provided by certain developer.
+ *
+ * Plugins are expected to add new apps using gs_app_list_add().
+ *
+ * Returns: %TRUE for success or if not relevant
+ *
+ * Since: 43
+ **/
+gboolean        gs_plugin_add_search_other_apps        (GsPlugin       *plugin,
+                                                        gchar          **values,
+                                                        GsAppList      *list,
+                                                        GCancellable   *cancellable,
+                                                        GError         **error);
 /**
  * gs_plugin_add_alternates
  * @plugin: a #GsPlugin
diff --git a/lib/gs-plugin.c b/lib/gs-plugin.c
index b34bbd8dc..92e39bdc7 100644
--- a/lib/gs-plugin.c
+++ b/lib/gs-plugin.c
@@ -1444,6 +1444,8 @@ gs_plugin_action_to_function_name (GsPluginAction action)
                return "gs_plugin_add_search_files";
        if (action == GS_PLUGIN_ACTION_SEARCH_PROVIDES)
                return "gs_plugin_add_search_what_provides";
+       if (action == GS_PLUGIN_ACTION_SEARCH_OTHER_APPS)
+               return "gs_plugin_add_search_other_apps";
        if (action == GS_PLUGIN_ACTION_GET_CATEGORY_APPS)
                return "gs_plugin_add_category_apps";
        if (action == GS_PLUGIN_ACTION_GET_CATEGORIES)
@@ -1506,6 +1508,8 @@ gs_plugin_action_to_string (GsPluginAction action)
                return "search-files";
        if (action == GS_PLUGIN_ACTION_SEARCH_PROVIDES)
                return "search-provides";
+       if (action == GS_PLUGIN_ACTION_SEARCH_OTHER_APPS)
+               return "search-other-apps";
        if (action == GS_PLUGIN_ACTION_GET_CATEGORIES)
                return "get-categories";
        if (action == GS_PLUGIN_ACTION_GET_CATEGORY_APPS)
@@ -1576,6 +1580,8 @@ gs_plugin_action_from_string (const gchar *action)
                return GS_PLUGIN_ACTION_SEARCH_FILES;
        if (g_strcmp0 (action, "search-provides") == 0)
                return GS_PLUGIN_ACTION_SEARCH_PROVIDES;
+       if (g_strcmp0 (action, "search-other-apps") == 0)
+               return GS_PLUGIN_ACTION_SEARCH_OTHER_APPS;
        if (g_strcmp0 (action, "get-categories") == 0)
                return GS_PLUGIN_ACTION_GET_CATEGORIES;
        if (g_strcmp0 (action, "get-category-apps") == 0)
diff --git a/plugins/core/gs-plugin-appstream.c b/plugins/core/gs-plugin-appstream.c
index 60e206ff9..ae1fa6f3c 100644
--- a/plugins/core/gs-plugin-appstream.c
+++ b/plugins/core/gs-plugin-appstream.c
@@ -1311,6 +1311,28 @@ gs_plugin_add_search (GsPlugin *plugin,
                                    error);
 }
 
+gboolean
+gs_plugin_add_search_other_apps (GsPlugin *plugin,
+                                gchar **values,
+                                GsAppList *list,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (plugin);
+       g_autoptr(GRWLockReaderLocker) locker = NULL;
+
+       if (!gs_plugin_appstream_check_silo (self, cancellable, error))
+               return FALSE;
+
+       locker = g_rw_lock_reader_locker_new (&self->silo_lock);
+       return gs_appstream_search_other_apps (plugin,
+                                              self->silo,
+                                              (const gchar * const *) values,
+                                              list,
+                                              cancellable,
+                                              error);
+}
+
 static void list_installed_apps_thread_cb (GTask        *task,
                                            gpointer      source_object,
                                            gpointer      task_data,
diff --git a/plugins/flatpak/gs-flatpak.c b/plugins/flatpak/gs-flatpak.c
index 3b3b641fa..a27de5abc 100644
--- a/plugins/flatpak/gs-flatpak.c
+++ b/plugins/flatpak/gs-flatpak.c
@@ -3949,6 +3949,80 @@ gs_flatpak_search (GsFlatpak *self,
        return TRUE;
 }
 
+gboolean
+gs_flatpak_search_other_apps (GsFlatpak *self,
+                             const gchar * const *values,
+                             GsAppList *list,
+                             gboolean interactive,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       g_autoptr(GsAppList) list_tmp = gs_app_list_new ();
+       g_autoptr(GRWLockReaderLocker) locker = NULL;
+       g_autoptr(GMutexLocker) app_silo_locker = NULL;
+       g_autoptr(GPtrArray) silos_to_remove = g_ptr_array_new ();
+       GHashTableIter iter;
+       gpointer key, value;
+
+       if (!gs_flatpak_rescan_app_data (self, interactive, cancellable, error))
+               return FALSE;
+
+       locker = g_rw_lock_reader_locker_new (&self->silo_lock);
+       if (!gs_appstream_search_other_apps (self->plugin, self->silo, values, list_tmp,
+                                            cancellable, error))
+               return FALSE;
+
+       gs_flatpak_ensure_remote_title (self, interactive, cancellable);
+
+       gs_flatpak_claim_app_list (self, list_tmp, interactive);
+       gs_app_list_add_list (list, list_tmp);
+
+       /* Also search silos from installed apps which were missing from self->silo */
+       app_silo_locker = g_mutex_locker_new (&self->app_silos_mutex);
+       g_hash_table_iter_init (&iter, self->app_silos);
+       while (g_hash_table_iter_next (&iter, &key, &value)) {
+               g_autoptr(XbSilo) app_silo = g_object_ref (value);
+               g_autoptr(GsAppList) app_list_tmp = gs_app_list_new ();
+               const char *app_ref = (char *)key;
+               g_autoptr(FlatpakInstalledRef) installed_ref = NULL;
+               g_auto(GStrv) split = NULL;
+               FlatpakRefKind kind;
+
+               /* Ignore any silos of apps that have since been removed.
+                * FIXME: can we use self->installed_refs here? */
+               split = g_strsplit (app_ref, "/", -1);
+               g_assert (g_strv_length (split) == 4);
+               if (g_strcmp0 (split[0], "app") == 0)
+                       kind = FLATPAK_REF_KIND_APP;
+               else
+                       kind = FLATPAK_REF_KIND_RUNTIME;
+               installed_ref = flatpak_installation_get_installed_ref (gs_flatpak_get_installation (self, 
interactive),
+                                                                       kind,
+                                                                       split[1],
+                                                                       split[2],
+                                                                       split[3],
+                                                                       NULL, NULL);
+               if (installed_ref == NULL) {
+                       g_ptr_array_add (silos_to_remove, (gpointer) app_ref);
+                       continue;
+               }
+
+               if (!gs_appstream_search (self->plugin, app_silo, values, app_list_tmp,
+                                         cancellable, error))
+                       return FALSE;
+
+               gs_flatpak_claim_app_list (self, app_list_tmp, interactive);
+               gs_app_list_add_list (list, app_list_tmp);
+       }
+
+       for (guint i = 0; i < silos_to_remove->len; i++) {
+               const char *silo = g_ptr_array_index (silos_to_remove, i);
+               g_hash_table_remove (self->app_silos, silo);
+       }
+
+       return TRUE;
+}
+
 gboolean
 gs_flatpak_add_category_apps (GsFlatpak *self,
                              GsCategory *category,
diff --git a/plugins/flatpak/gs-flatpak.h b/plugins/flatpak/gs-flatpak.h
index 3c4724897..6d35c00a9 100644
--- a/plugins/flatpak/gs-flatpak.h
+++ b/plugins/flatpak/gs-flatpak.h
@@ -125,6 +125,12 @@ gboolean   gs_flatpak_search               (GsFlatpak              *self,
                                                 gboolean                interactive,
                                                 GCancellable           *cancellable,
                                                 GError                 **error);
+gboolean       gs_flatpak_search_other_apps    (GsFlatpak              *self,
+                                                const gchar * const    *values,
+                                                GsAppList              *list,
+                                                gboolean                interactive,
+                                                GCancellable           *cancellable,
+                                                GError                 **error);
 gboolean       gs_flatpak_add_categories       (GsFlatpak              *self,
                                                 GPtrArray              *list,
                                                 gboolean                interactive,
diff --git a/plugins/flatpak/gs-plugin-flatpak.c b/plugins/flatpak/gs-plugin-flatpak.c
index 15e60404d..131313380 100644
--- a/plugins/flatpak/gs-plugin-flatpak.c
+++ b/plugins/flatpak/gs-plugin-flatpak.c
@@ -1811,6 +1811,27 @@ gs_plugin_add_search_what_provides (GsPlugin *plugin,
        return gs_plugin_flatpak_do_search (plugin, search, list, cancellable, error);
 }
 
+gboolean
+gs_plugin_add_search_other_apps (GsPlugin *plugin,
+                                gchar **values,
+                                GsAppList *list,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin);
+       gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE);
+
+       for (guint i = 0; i < self->installations->len; i++) {
+               GsFlatpak *flatpak = g_ptr_array_index (self->installations, i);
+               if (!gs_flatpak_search_other_apps (flatpak, (const gchar * const *) values, list,
+                                                  interactive, cancellable, error)) {
+                       return FALSE;
+               }
+       }
+
+       return TRUE;
+}
+
 gboolean
 gs_plugin_add_categories (GsPlugin *plugin,
                          GPtrArray *list,
diff --git a/src/gs-details-page.c b/src/gs-details-page.c
index 9c539482c..9d69428e7 100644
--- a/src/gs-details-page.c
+++ b/src/gs-details-page.c
@@ -32,6 +32,7 @@
 #include "gs-progress-button.h"
 #include "gs-screenshot-carousel.h"
 #include "gs-star-widget.h"
+#include "gs-summary-tile.h"
 #include "gs-review-histogram.h"
 #include "gs-review-dialog.h"
 #include "gs-review-row.h"
@@ -147,6 +148,9 @@ struct _GsDetailsPage
        GsLicenseTile           *license_tile;
        GtkInfoBar              *translation_infobar;
        GtkButton               *translation_infobar_button;
+       GtkWidget               *other_apps_heading;
+       GtkWidget               *box_other_apps;
+       gchar                   *last_developer_name;
 };
 
 G_DEFINE_TYPE (GsDetailsPage, gs_details_page, GS_TYPE_PAGE)
@@ -944,6 +948,55 @@ update_action_row_from_link (AdwActionRow *row,
        return (url != NULL);
 }
 
+static void
+gs_details_page_app_tile_clicked (GsAppTile *tile,
+                                 gpointer user_data)
+{
+       GsDetailsPage *self = GS_DETAILS_PAGE (user_data);
+       GsApp *app;
+
+       app = gs_app_tile_get_app (tile);
+       gs_details_page_set_app (self, app);
+}
+
+static void
+gs_details_page_search_other_apps_cb (GObject *source_object,
+                                     GAsyncResult *result,
+                                     gpointer user_data)
+{
+       GsDetailsPage *self = GS_DETAILS_PAGE (user_data);
+       g_autoptr(GsAppList) list = NULL;
+       g_autoptr(GError) local_error = NULL;
+       gboolean any_added = FALSE;
+
+       list = gs_plugin_loader_job_process_finish (GS_PLUGIN_LOADER (source_object), result, &local_error);
+       if (list == NULL) {
+               if (g_error_matches (local_error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) ||
+                   g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+                       g_debug ("search cancelled");
+                       return;
+               }
+               g_warning ("failed to get other apps: %s", local_error->message);
+               return;
+       }
+
+       if (!self->app || !gs_page_is_active (GS_PAGE (self)))
+               return;
+
+       for (guint i = 0; i < gs_app_list_length (list); i++) {
+               GsApp *app = gs_app_list_index (list, i);
+               if (g_strcmp0 (gs_app_get_id (app), gs_app_get_id (self->app)) != 0) {
+                       GtkWidget *tile = gs_summary_tile_new (app);
+                       g_signal_connect (tile, "clicked", G_CALLBACK (gs_details_page_app_tile_clicked), 
self);
+                       gtk_flow_box_insert (GTK_FLOW_BOX (self->box_other_apps), tile, -1);
+
+                       any_added = TRUE;
+               }
+       }
+
+       gtk_widget_set_visible (self->box_other_apps, any_added);
+}
+
 static void
 gs_details_page_refresh_all (GsDetailsPage *self)
 {
@@ -1024,8 +1077,49 @@ gs_details_page_refresh_all (GsDetailsPage *self)
        tmp = gs_app_get_developer_name (self->app);
        if (tmp == NULL)
                tmp = gs_app_get_project_group (self->app);
-       if (tmp != NULL)
+       if (tmp != NULL) {
                gtk_label_set_label (GTK_LABEL (self->developer_name_label), tmp);
+
+               if (g_strcmp0 (tmp, self->last_developer_name) != 0) {
+                       g_autoptr(GsPluginJob) plugin_job = NULL;
+                       g_autofree gchar *heading = NULL;
+
+                       /* Hide the section, it will be shown only if any other app had been found */
+                       gtk_widget_set_visible (self->box_other_apps, FALSE);
+
+                       g_clear_pointer (&self->last_developer_name, g_free);
+                       self->last_developer_name = g_strdup (tmp);
+
+                       /* Translators: the '%s' is replaced with a developer name or a project group */
+                       heading = g_strdup_printf (_("Other Apps by %s"), self->last_developer_name);
+                       gtk_label_set_label (GTK_LABEL (self->other_apps_heading), heading);
+                       gs_widget_remove_all (self->box_other_apps, (GsRemoveFunc) gtk_flow_box_remove);
+
+                       plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH_OTHER_APPS,
+                                                        "search", self->last_developer_name,
+                                                        "max-results", 20,
+                                                        "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON |
+                                                                        
GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION |
+                                                                        
GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY |
+                                                                        
GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION |
+                                                                        
GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS |
+                                                                        
GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION |
+                                                                        
GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE |
+                                                                        
GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS |
+                                                                        
GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING,
+                                                        "dedupe-flags", 
GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES,
+                                                        NULL);
+                       gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
+                                                           self->cancellable,
+                                                           gs_details_page_search_other_apps_cb,
+                                                           self);
+               }
+       } else if (tmp == NULL) {
+               g_clear_pointer (&self->last_developer_name, g_free);
+               gs_widget_remove_all (self->box_other_apps, (GsRemoveFunc) gtk_flow_box_remove);
+               gtk_widget_set_visible (self->box_other_apps, FALSE);
+       }
+
        gtk_widget_set_visible (GTK_WIDGET (self->developer_name_label), tmp != NULL);
        gtk_widget_set_visible (GTK_WIDGET (self->developer_verified_image), gs_app_has_quirk (self->app, 
GS_APP_QUIRK_DEVELOPER_VERIFIED));
 
@@ -2145,6 +2239,7 @@ gs_details_page_dispose (GObject *object)
        g_clear_object (&self->size_group_origin_popover);
        g_clear_object (&self->odrs_provider);
        g_clear_object (&self->app_info_monitor);
+       g_clear_pointer (&self->last_developer_name, g_free);
 
        G_OBJECT_CLASS (gs_details_page_parent_class)->dispose (object);
 }
@@ -2273,6 +2368,8 @@ gs_details_page_class_init (GsDetailsPageClass *klass)
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, license_tile);
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, translation_infobar);
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, translation_infobar_button);
+       gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, other_apps_heading);
+       gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_other_apps);
 
        gtk_widget_class_bind_template_callback (widget_class, gs_details_page_link_row_activated_cb);
        gtk_widget_class_bind_template_callback (widget_class, 
gs_details_page_license_tile_get_involved_activated_cb);
diff --git a/src/gs-details-page.ui b/src/gs-details-page.ui
index cda8392a1..5fb88fd0e 100644
--- a/src/gs-details-page.ui
+++ b/src/gs-details-page.ui
@@ -899,6 +899,48 @@
                                 </child>
                               </object>
                             </child>
+                            <child>
+                              <object class="AdwClamp">
+                                <property name="visible" bind-source="box_other_apps" 
bind-property="visible" bind-flags="sync-create"/>
+                                <property name="maximum-size">860</property>
+                                <!-- ~⅔ of the maximum size. -->
+                                <property name="tightening-threshold">576</property>
+                                <property name="margin-start">12</property>
+                                <property name="margin-end">12</property>
+                                <child>
+                                  <object class="GtkBox">
+                                    <property name="halign">center</property>
+                                    <property name="hexpand">False</property>
+                                    <property name="orientation">vertical</property>
+                                    <property name="valign">start</property>
+                                    <property name="spacing">6</property>
+                                    <child>
+                                      <object class="GtkLabel" id="other_apps_heading">
+                                        <property name="xalign">0</property>
+                                        <!-- the label is set in the code -->
+                                        <property name="label">Other Apps by ...</property>
+                                        <property name="margin-top">21</property>
+                                        <property name="margin-bottom">6</property>
+                                        <style>
+                                          <class name="heading"/>
+                                        </style>
+                                      </object>
+                                    </child>
+                                    <child>
+                                      <object class="GtkFlowBox" id="box_other_apps">
+                                        <property name="homogeneous">True</property>
+                                        <property name="column-spacing">14</property>
+                                        <property name="row-spacing">14</property>
+                                        <property name="valign">start</property>
+                                        <accessibility>
+                                          <relation name="labelled-by">other_apps_heading</relation>
+                                        </accessibility>
+                                      </object>
+                                    </child>
+                                  </object>
+                                </child>
+                              </object>
+                            </child>
                           </object>
                         </child>
                       </object>


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